babylon.quarks-editor 0.18.1 → 0.20.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.
Files changed (157) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/README.md +6 -6
  3. package/dist/core/assetDragSession.d.ts +4 -0
  4. package/dist/core/assetDragSession.js +8 -0
  5. package/dist/core/assetTree.d.ts +39 -0
  6. package/dist/core/assetTree.js +112 -0
  7. package/dist/core/assets.d.ts +56 -0
  8. package/dist/core/assets.js +207 -0
  9. package/dist/core/binding.d.ts +6 -1
  10. package/dist/core/binding.js +7 -0
  11. package/dist/core/collision.d.ts +4 -5
  12. package/dist/core/collision.js +4 -5
  13. package/dist/core/colors.d.ts +1 -1
  14. package/dist/core/defaultEffect.d.ts +4 -5
  15. package/dist/core/defaultEffect.js +5 -14
  16. package/dist/core/effectTree.d.ts +1 -1
  17. package/dist/core/effectTree.js +18 -4
  18. package/dist/core/emitterShapeWireframe.d.ts +2 -1
  19. package/dist/core/emitterShapeWireframe.js +2 -1
  20. package/dist/core/galleryDropMarker.js +1 -1
  21. package/dist/core/galleryPlayback.d.ts +7 -7
  22. package/dist/core/galleryPlayback.js +2 -2
  23. package/dist/core/galleryScene.d.ts +8 -13
  24. package/dist/core/galleryScene.js +14 -42
  25. package/dist/core/gallerySelectionMarker.d.ts +1 -0
  26. package/dist/core/gallerySelectionMarker.js +1 -0
  27. package/dist/core/history.d.ts +12 -1
  28. package/dist/core/history.js +16 -3
  29. package/dist/core/indexedDbProjectStore.d.ts +35 -0
  30. package/dist/core/indexedDbProjectStore.js +299 -0
  31. package/dist/core/lineShaders.d.ts +2 -0
  32. package/dist/core/lineShaders.js +7 -0
  33. package/dist/core/loadEffect.d.ts +2 -2
  34. package/dist/core/loadEffect.js +1 -1
  35. package/dist/core/pickAssetFiles.d.ts +6 -0
  36. package/dist/core/{pickGalleryFiles.js → pickAssetFiles.js} +13 -8
  37. package/dist/core/projectStore.d.ts +61 -0
  38. package/dist/core/projectStore.js +14 -0
  39. package/dist/core/sceneEntries.d.ts +35 -0
  40. package/dist/core/sceneEntries.js +62 -0
  41. package/dist/core/shapes.d.ts +1 -1
  42. package/dist/core/shapes.js +35 -8
  43. package/dist/core/systems.d.ts +1 -1
  44. package/dist/core/systems.js +3 -1
  45. package/dist/core/timeline.d.ts +1 -2
  46. package/dist/core/transport.d.ts +12 -0
  47. package/dist/core/transport.js +36 -0
  48. package/dist/core/values.d.ts +1 -1
  49. package/dist/core/viewportPicker.d.ts +4 -4
  50. package/dist/core/viewportPicker.js +3 -3
  51. package/dist/index.d.ts +15 -11
  52. package/dist/index.js +8 -6
  53. package/dist/react/AssetsPanel.d.ts +22 -0
  54. package/dist/react/AssetsPanel.js +252 -0
  55. package/dist/react/ChoiceDialog.d.ts +18 -0
  56. package/dist/react/ChoiceDialog.js +48 -0
  57. package/dist/react/ContextMenu.d.ts +16 -0
  58. package/dist/react/ContextMenu.js +70 -0
  59. package/dist/react/CurveEditor.d.ts +1 -2
  60. package/dist/react/CurveEditor.js +14 -3
  61. package/dist/react/EditorWorkspace.d.ts +21 -0
  62. package/dist/react/EditorWorkspace.js +63 -0
  63. package/dist/react/EffectEditor.d.ts +3 -3
  64. package/dist/react/EffectEditor.js +9 -6
  65. package/dist/react/EffectEditorHost.d.ts +20 -3
  66. package/dist/react/EffectEditorHost.js +1426 -558
  67. package/dist/react/EffectTreeView.d.ts +17 -0
  68. package/dist/react/EffectTreeView.js +52 -0
  69. package/dist/react/GradientEditor.d.ts +1 -2
  70. package/dist/react/GradientEditor.js +8 -1
  71. package/dist/react/MessageDialog.js +7 -1
  72. package/dist/react/ModuleSection.js +10 -3
  73. package/dist/react/ObjectModule.d.ts +5 -2
  74. package/dist/react/ObjectModule.js +8 -11
  75. package/dist/react/ProjectsPage.d.ts +9 -0
  76. package/dist/react/ProjectsPage.js +140 -0
  77. package/dist/react/ScenePanel.d.ts +26 -0
  78. package/dist/react/ScenePanel.js +134 -0
  79. package/dist/react/SubEmittersModule.d.ts +1 -2
  80. package/dist/react/TimelinePanel.d.ts +11 -4
  81. package/dist/react/TimelinePanel.js +65 -40
  82. package/dist/react/TimelineTrackRow.d.ts +1 -1
  83. package/dist/react/TimelineTrackRow.js +20 -6
  84. package/dist/react/TransportBar.d.ts +24 -0
  85. package/dist/react/TransportBar.js +43 -0
  86. package/dist/react/ValueField.d.ts +1 -2
  87. package/dist/react/behaviorModules.js +24 -12
  88. package/dist/react/index.d.ts +31 -17
  89. package/dist/react/index.js +18 -11
  90. package/dist/react/inspectorHints.js +1 -2
  91. package/dist/react/modules.js +55 -25
  92. package/package.json +68 -68
  93. package/src/core/assetDragSession.ts +12 -0
  94. package/src/core/assetTree.ts +148 -0
  95. package/src/core/assets.ts +262 -0
  96. package/src/core/binding.ts +10 -2
  97. package/src/core/collision.ts +4 -5
  98. package/src/core/colors.ts +1 -1
  99. package/src/core/defaultEffect.ts +10 -23
  100. package/src/core/effectTree.ts +22 -7
  101. package/src/core/emitterShapeWireframe.ts +9 -4
  102. package/src/core/galleryDropMarker.ts +6 -2
  103. package/src/core/galleryPlayback.ts +9 -9
  104. package/src/core/galleryScene.ts +15 -63
  105. package/src/core/gallerySelectionMarker.ts +6 -1
  106. package/src/core/history.ts +24 -3
  107. package/src/core/indexedDbProjectStore.ts +357 -0
  108. package/src/core/lineShaders.ts +7 -0
  109. package/src/core/loadEffect.ts +3 -3
  110. package/src/core/{pickGalleryFiles.ts → pickAssetFiles.ts} +16 -8
  111. package/src/core/projectStore.ts +81 -0
  112. package/src/core/sceneEntries.ts +96 -0
  113. package/src/core/shapes.ts +48 -10
  114. package/src/core/systems.ts +9 -3
  115. package/src/core/timeline.ts +8 -8
  116. package/src/core/transport.ts +53 -0
  117. package/src/core/values.ts +1 -1
  118. package/src/core/viewportPicker.ts +5 -5
  119. package/src/index.ts +29 -18
  120. package/src/react/AssetsPanel.tsx +559 -0
  121. package/src/react/ChoiceDialog.tsx +93 -0
  122. package/src/react/ContextMenu.tsx +107 -0
  123. package/src/react/CurveEditor.tsx +40 -10
  124. package/src/react/EditorWorkspace.tsx +95 -0
  125. package/src/react/EffectEditor.tsx +28 -16
  126. package/src/react/EffectEditorHost.tsx +2005 -721
  127. package/src/react/EffectTreeView.tsx +117 -0
  128. package/src/react/GradientEditor.tsx +21 -4
  129. package/src/react/InspectorInfoIcon.tsx +6 -2
  130. package/src/react/MessageDialog.tsx +9 -1
  131. package/src/react/ModuleSection.tsx +12 -3
  132. package/src/react/ObjectModule.tsx +41 -16
  133. package/src/react/ProjectsPage.tsx +306 -0
  134. package/src/react/PromptDialog.tsx +5 -1
  135. package/src/react/ScenePanel.tsx +318 -0
  136. package/src/react/SubEmittersModule.tsx +1 -2
  137. package/src/react/TimelinePanel.tsx +171 -99
  138. package/src/react/TimelineTrackRow.tsx +71 -18
  139. package/src/react/TransportBar.tsx +114 -0
  140. package/src/react/ValueField.tsx +0 -1
  141. package/src/react/behaviorModules.tsx +148 -42
  142. package/src/react/index.ts +47 -33
  143. package/src/react/inspectorHints.ts +1 -2
  144. package/src/react/modules.tsx +173 -57
  145. package/dist/core/galleryDragSession.d.ts +0 -4
  146. package/dist/core/galleryDragSession.js +0 -8
  147. package/dist/core/galleryTree.d.ts +0 -49
  148. package/dist/core/galleryTree.js +0 -138
  149. package/dist/core/loadEffectGallery.d.ts +0 -36
  150. package/dist/core/loadEffectGallery.js +0 -97
  151. package/dist/core/pickGalleryFiles.d.ts +0 -5
  152. package/dist/react/GalleryPanel.d.ts +0 -20
  153. package/dist/react/GalleryPanel.js +0 -246
  154. package/src/core/galleryDragSession.ts +0 -12
  155. package/src/core/galleryTree.ts +0 -187
  156. package/src/core/loadEffectGallery.ts +0 -146
  157. package/src/react/GalleryPanel.tsx +0 -517
@@ -0,0 +1,17 @@
1
+ import type { TransformNode } from '@babylonjs/core/Meshes/transformNode';
2
+ import React from 'react';
3
+ export interface EffectTreeViewProps {
4
+ root: TransformNode;
5
+ /** Node shown in the inspector (and moved by the gizmo). */
6
+ selectedNode: TransformNode | null;
7
+ onSelect: (node: TransformNode) => void;
8
+ /** Show the root itself as the first row (default: only its descendants). */
9
+ includeRoot?: boolean;
10
+ /** Left indent of the first level, in px. */
11
+ indent?: number;
12
+ }
13
+ /**
14
+ * The effect's group/emitter hierarchy — the same tree as the timeline's label column — so a
15
+ * sub-emitter can be picked for the inspector without opening the timeline.
16
+ */
17
+ export declare function EffectTreeView(props: EffectTreeViewProps): React.ReactElement | null;
@@ -0,0 +1,52 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { ChevronDownIcon, ChevronRightIcon, FolderIcon, SparklesIcon } from '@heroicons/react/24/solid';
3
+ import { useState } from 'react';
4
+ import { buildEffectTree } from '../core/effectTree';
5
+ import { iconStyle } from './icons';
6
+ import { theme } from './theme';
7
+ const ROW_HEIGHT = 26;
8
+ function TreeRow(props) {
9
+ const { item, level, indent, selectedNode, collapsed, onToggle, onSelect } = props;
10
+ const open = !collapsed.has(item.node);
11
+ const selected = item.node === selectedNode;
12
+ const hasChildren = item.children.length > 0;
13
+ return (_jsxs(_Fragment, { children: [_jsxs("div", { className: "qe-hover-bg", title: item.system ? `${item.name} — particle system` : `${item.name} — group`, onClick: () => onSelect(item.node), style: {
14
+ display: 'flex',
15
+ alignItems: 'center',
16
+ gap: 5,
17
+ height: ROW_HEIGHT,
18
+ padding: `0 8px 0 ${indent + level * 14}px`,
19
+ fontSize: 12,
20
+ cursor: 'pointer',
21
+ color: selected ? theme.text : theme.textDim,
22
+ background: selected ? 'rgba(120, 165, 255, 0.2)' : undefined,
23
+ }, children: [_jsx("span", { style: { width: 12, display: 'inline-flex', visibility: hasChildren ? 'visible' : 'hidden' }, onClick: (e) => {
24
+ e.stopPropagation();
25
+ onToggle(item.node);
26
+ }, children: open ? _jsx(ChevronDownIcon, { style: iconStyle(11) }) : _jsx(ChevronRightIcon, { style: iconStyle(11) }) }), _jsx("span", { style: { display: 'inline-flex', color: item.system ? theme.accent : theme.textDim }, children: item.system ? _jsx(SparklesIcon, { style: iconStyle(12) }) : _jsx(FolderIcon, { style: iconStyle(12) }) }), _jsx("span", { style: { overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }, children: item.name }), item.system?.looping && (_jsx("span", { title: "Looping", style: { flexShrink: 0, fontSize: 10.5 }, children: "\u27F3" }))] }), open &&
27
+ item.children.map((child) => (_jsx(TreeRow, { ...props, item: child, level: level + 1 }, child.node.uniqueId)))] }));
28
+ }
29
+ /**
30
+ * The effect's group/emitter hierarchy — the same tree as the timeline's label column — so a
31
+ * sub-emitter can be picked for the inspector without opening the timeline.
32
+ */
33
+ export function EffectTreeView(props) {
34
+ const { root, selectedNode, onSelect, includeRoot = false, indent = 8 } = props;
35
+ const [collapsed, setCollapsed] = useState(() => new Set());
36
+ if (root.isDisposed()) {
37
+ return null;
38
+ }
39
+ const tree = buildEffectTree(root);
40
+ const items = includeRoot ? [tree] : tree.children;
41
+ const toggle = (node) => setCollapsed((prev) => {
42
+ const next = new Set(prev);
43
+ if (next.has(node)) {
44
+ next.delete(node);
45
+ }
46
+ else {
47
+ next.add(node);
48
+ }
49
+ return next;
50
+ });
51
+ return (_jsx("div", { children: items.map((item) => (_jsx(TreeRow, { item: item, level: 0, indent: indent, selectedNode: selectedNode, collapsed: collapsed, onToggle: toggle, onSelect: onSelect }, item.node.uniqueId))) }));
52
+ }
@@ -1,7 +1,6 @@
1
- import React from 'react';
2
1
  import { GradientStop } from '../core/colors';
3
2
  /** Gradient editor: RGBA stops on a bar — click to add, drag to move, edit the selected stop. */
4
3
  export declare function GradientEditor(props: {
5
4
  stops: GradientStop[];
6
5
  onChange: (stops: GradientStop[]) => void;
7
- }): React.JSX.Element;
6
+ }): import("react").JSX.Element;
@@ -65,7 +65,14 @@ export function GradientEditor(props) {
65
65
  }, onPointerUp: (e) => {
66
66
  dragging.current = false;
67
67
  e.currentTarget.releasePointerCapture(e.pointerId);
68
- } }, i))) }), current && (_jsxs(_Fragment, { children: [_jsx(Row, { label: "Stop color", children: _jsx("input", { type: "color", className: "qe-hover", value: rgbToHex(current.r, current.g, current.b), onChange: (e) => updateStop(selected, hexToRgb(e.target.value)), style: { width: 42, height: 24, border: 'none', background: 'transparent', padding: 0, cursor: 'pointer' } }) }), _jsx(Row, { label: "Stop alpha", children: _jsx(NumberField, { value: current.a, min: 0, step: 0.05, onChange: (a) => updateStop(selected, { a: Math.min(1, a) }) }) }), _jsx(Row, { label: "Stop position", children: _jsx(NumberField, { value: current.pos, min: 0, step: 0.05, onChange: (pos) => updateStop(selected, { pos: Math.min(1, pos) }) }) }), stops.length > 2 && (_jsx("button", { className: "qe-hover", style: { ...buttonStyle, marginTop: 6 }, onClick: () => {
68
+ } }, i))) }), current && (_jsxs(_Fragment, { children: [_jsx(Row, { label: "Stop color", children: _jsx("input", { type: "color", className: "qe-hover", value: rgbToHex(current.r, current.g, current.b), onChange: (e) => updateStop(selected, hexToRgb(e.target.value)), style: {
69
+ width: 42,
70
+ height: 24,
71
+ border: 'none',
72
+ background: 'transparent',
73
+ padding: 0,
74
+ cursor: 'pointer',
75
+ } }) }), _jsx(Row, { label: "Stop alpha", children: _jsx(NumberField, { value: current.a, min: 0, step: 0.05, onChange: (a) => updateStop(selected, { a: Math.min(1, a) }) }) }), _jsx(Row, { label: "Stop position", children: _jsx(NumberField, { value: current.pos, min: 0, step: 0.05, onChange: (pos) => updateStop(selected, { pos: Math.min(1, pos) }) }) }), stops.length > 2 && (_jsx("button", { className: "qe-hover", style: { ...buttonStyle, marginTop: 6 }, onClick: () => {
69
76
  props.onChange(stops.filter((_, i) => i !== selected));
70
77
  setSelected(0);
71
78
  }, children: "Remove stop" }))] }))] }));
@@ -20,5 +20,11 @@ export function MessageDialog(props) {
20
20
  minWidth: 320,
21
21
  maxWidth: 480,
22
22
  boxShadow: '0 12px 32px rgba(0, 0, 0, 0.5)',
23
- }, onClick: (e) => e.stopPropagation(), children: [_jsx("div", { style: { fontSize: 14, fontWeight: 600, color: theme.text, marginBottom: 8 }, children: title }), _jsx("div", { style: { fontSize: 13, lineHeight: 1.5, color: theme.textDim, marginBottom: 14, whiteSpace: 'pre-wrap' }, children: message }), _jsx("div", { style: { display: 'flex', justifyContent: 'flex-end' }, children: _jsx("button", { className: "qe-hover", style: { ...buttonStyle, background: theme.accent, color: '#0a1128', borderColor: theme.accent }, onClick: onClose, children: confirmLabel }) })] }) }));
23
+ }, onClick: (e) => e.stopPropagation(), children: [_jsx("div", { style: { fontSize: 14, fontWeight: 600, color: theme.text, marginBottom: 8 }, children: title }), _jsx("div", { style: {
24
+ fontSize: 13,
25
+ lineHeight: 1.5,
26
+ color: theme.textDim,
27
+ marginBottom: 14,
28
+ whiteSpace: 'pre-wrap',
29
+ }, children: message }), _jsx("div", { style: { display: 'flex', justifyContent: 'flex-end' }, children: _jsx("button", { className: "qe-hover", style: { ...buttonStyle, background: theme.accent, color: '#0a1128', borderColor: theme.accent }, onClick: onClose, children: confirmLabel }) })] }) }));
24
30
  }
@@ -1,10 +1,10 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useState } from 'react';
3
2
  import { ChevronDownIcon, ChevronRightIcon } from '@heroicons/react/24/solid';
3
+ import { useState } from 'react';
4
+ import { iconStyle } from './icons';
4
5
  import { getInspectorHint } from './inspectorHints';
5
6
  import { InspectorInfoIcon } from './InspectorInfoIcon';
6
7
  import { theme } from './theme';
7
- import { iconStyle } from './icons';
8
8
  /** Unity-style module foldout with an optional enable checkbox. */
9
9
  export function ModuleSection(props) {
10
10
  const [open, setOpen] = useState(props.defaultOpen ?? true);
@@ -27,5 +27,12 @@ export function ModuleSection(props) {
27
27
  cursor: 'pointer',
28
28
  userSelect: 'none',
29
29
  background: 'rgba(20, 30, 60, 0.5)',
30
- }, onClick: () => setOpen(!open), children: [_jsx("span", { style: { color: theme.accent, display: 'inline-flex' }, children: open ? _jsx(ChevronDownIcon, { style: iconStyle(12) }) : _jsx(ChevronRightIcon, { style: iconStyle(12) }) }), toggleable && (_jsx("input", { type: "checkbox", className: "qe-hover", checked: props.enabled, onClick: (e) => e.stopPropagation(), onChange: (e) => props.onToggle?.(e.target.checked), style: { width: 14, height: 14, accentColor: '#78a5ff', cursor: 'pointer' } })), _jsxs("span", { style: { color: dimmed ? theme.textDim : theme.text, fontSize: 13, fontWeight: 600, display: 'inline-flex', alignItems: 'center', gap: 5 }, children: [props.title, tooltip ? _jsx(InspectorInfoIcon, { content: tooltip, onClick: (e) => e.stopPropagation() }) : null] })] }), open && !dimmed && _jsx("div", { style: { padding: '4px 10px 10px' }, children: props.children })] }));
30
+ }, onClick: () => setOpen(!open), children: [_jsx("span", { style: { color: theme.accent, display: 'inline-flex' }, children: open ? _jsx(ChevronDownIcon, { style: iconStyle(12) }) : _jsx(ChevronRightIcon, { style: iconStyle(12) }) }), toggleable && (_jsx("input", { type: "checkbox", className: "qe-hover", checked: props.enabled, onClick: (e) => e.stopPropagation(), onChange: (e) => props.onToggle?.(e.target.checked), style: { width: 14, height: 14, accentColor: '#78a5ff', cursor: 'pointer' } })), _jsxs("span", { style: {
31
+ color: dimmed ? theme.textDim : theme.text,
32
+ fontSize: 13,
33
+ fontWeight: 600,
34
+ display: 'inline-flex',
35
+ alignItems: 'center',
36
+ gap: 5,
37
+ }, children: [props.title, tooltip ? _jsx(InspectorInfoIcon, { content: tooltip, onClick: (e) => e.stopPropagation() }) : null] })] }), open && !dimmed && _jsx("div", { style: { padding: '4px 10px 10px' }, children: props.children })] }));
31
38
  }
@@ -1,6 +1,9 @@
1
- import React from 'react';
2
1
  import type { TransformNode } from '@babylonjs/core/Meshes/transformNode';
3
2
  export declare function ObjectModule(props: {
4
3
  node: TransformNode;
5
4
  label: string;
6
- }): React.JSX.Element;
5
+ /** Called before a field mutates the node — lets the host capture an undo snapshot. */
6
+ onBeforeChange?: () => void;
7
+ /** Called after a field mutated the node. */
8
+ onChanged?: () => void;
9
+ }): import("react").JSX.Element;
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useSyncExternalStore } from 'react';
3
2
  import { Quaternion } from '@babylonjs/core/Maths/math.vector';
3
+ import { useSyncExternalStore } from 'react';
4
4
  import { getTransformRevision, notifyTransformChanged, subscribeTransform } from '../core/transformStore';
5
5
  import { ModuleSection } from './ModuleSection';
6
6
  import { NumberField, Row } from './fields';
@@ -27,14 +27,11 @@ export function ObjectModule(props) {
27
27
  useSyncExternalStore(subscribeTransform, getTransformRevision);
28
28
  const { node } = props;
29
29
  const euler = getEulerDegrees(node);
30
- return (_jsxs(ModuleSection, { title: `Object — ${props.label}`, children: [_jsx(AxisRow, { label: "Position", values: { x: node.position.x, y: node.position.y, z: node.position.z }, step: 0.1, onChange: (axis, v) => {
31
- node.position[axis] = v;
32
- notifyTransformChanged();
33
- } }), _jsx(AxisRow, { label: "Rotation", values: euler, step: 1, onChange: (axis, v) => {
34
- setEulerDegreeAxis(node, axis, v);
35
- notifyTransformChanged();
36
- } }), _jsx(AxisRow, { label: "Scale", values: { x: node.scaling.x, y: node.scaling.y, z: node.scaling.z }, step: 0.1, onChange: (axis, v) => {
37
- node.scaling[axis] = v;
38
- notifyTransformChanged();
39
- } })] }));
30
+ const change = (mutate) => {
31
+ props.onBeforeChange?.();
32
+ mutate();
33
+ notifyTransformChanged();
34
+ props.onChanged?.();
35
+ };
36
+ return (_jsxs(ModuleSection, { title: `Object — ${props.label}`, children: [_jsx(AxisRow, { label: "Position", values: { x: node.position.x, y: node.position.y, z: node.position.z }, step: 0.1, onChange: (axis, v) => change(() => (node.position[axis] = v)) }), _jsx(AxisRow, { label: "Rotation", values: euler, step: 1, onChange: (axis, v) => change(() => setEulerDegreeAxis(node, axis, v)) }), _jsx(AxisRow, { label: "Scale", values: { x: node.scaling.x, y: node.scaling.y, z: node.scaling.z }, step: 0.1, onChange: (axis, v) => change(() => (node.scaling[axis] = v)) })] }));
40
37
  }
@@ -0,0 +1,9 @@
1
+ import React from 'react';
2
+ import { type ProjectMeta, type ProjectStore } from '../core/projectStore';
3
+ export interface ProjectsPageProps {
4
+ store: ProjectStore;
5
+ onOpen: (project: ProjectMeta) => void;
6
+ title?: string;
7
+ }
8
+ /** Start page: the projects saved in the store, plus storage usage. */
9
+ export declare function ProjectsPage(props: ProjectsPageProps): React.ReactElement;
@@ -0,0 +1,140 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { FolderIcon, PlusIcon } from '@heroicons/react/24/solid';
3
+ import { useCallback, useEffect, useState } from 'react';
4
+ import { formatBytes } from '../core/projectStore';
5
+ import { ChoiceDialog } from './ChoiceDialog';
6
+ import { ContextMenu } from './ContextMenu';
7
+ import { iconStyle } from './icons';
8
+ import { PromptDialog } from './PromptDialog';
9
+ import { buttonStyle, globalEditorStyles, theme } from './theme';
10
+ function formatDate(time) {
11
+ return new Date(time).toLocaleString(undefined, { dateStyle: 'medium', timeStyle: 'short' });
12
+ }
13
+ function StorageBar(props) {
14
+ const { estimate, onPersist } = props;
15
+ const ratio = estimate?.usage != null && estimate.quota ? Math.min(1, estimate.usage / estimate.quota) : 0;
16
+ return (_jsxs("div", { style: {
17
+ border: `1px solid ${theme.border}`,
18
+ borderRadius: 10,
19
+ background: theme.sectionBg,
20
+ padding: '12px 14px',
21
+ display: 'flex',
22
+ flexDirection: 'column',
23
+ gap: 8,
24
+ }, children: [_jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: 10, fontSize: 12.5 }, children: [_jsx("span", { style: { fontWeight: 600 }, children: "Browser storage" }), _jsx("span", { style: { color: theme.textDim }, children: estimate
25
+ ? `${formatBytes(estimate.usage)} used of ~${formatBytes(estimate.quota)} available`
26
+ : 'Checking…' }), _jsx("span", { style: { flex: 1 } }), estimate?.persisted ? (_jsx("span", { style: { color: '#8fe3a8', fontSize: 12 }, children: "\u2713 Persistent \u2014 the browser will not clear it" })) : (_jsx("button", { className: "qe-hover", style: { ...buttonStyle, padding: '4px 10px' }, title: "Ask the browser not to clear project data when the disk runs low", onClick: onPersist, children: "Make storage persistent" }))] }), _jsx("div", { style: { height: 6, borderRadius: 3, background: 'rgba(255, 255, 255, 0.08)', overflow: 'hidden' }, children: _jsx("div", { style: {
27
+ width: `${Math.max(ratio * 100, ratio > 0 ? 1 : 0)}%`,
28
+ height: '100%',
29
+ background: theme.accent,
30
+ } }) }), _jsx("div", { style: { fontSize: 11.5, color: theme.textDim, lineHeight: 1.5 }, children: "Projects are saved in this browser only (IndexedDB). Clearing site data removes them, and Safari deletes data of sites not opened for 7 days \u2014 export projects you care about." })] }));
31
+ }
32
+ /** Start page: the projects saved in the store, plus storage usage. */
33
+ export function ProjectsPage(props) {
34
+ const { store, onOpen, title = 'Effect editor' } = props;
35
+ const [projects, setProjects] = useState(null);
36
+ const [estimate, setEstimate] = useState(null);
37
+ const [dialog, setDialog] = useState(null);
38
+ const [menu, setMenu] = useState(null);
39
+ const [error, setError] = useState(null);
40
+ const reload = useCallback(async () => {
41
+ try {
42
+ const [list, storage] = await Promise.all([store.listProjects(), store.estimate()]);
43
+ setProjects(list);
44
+ setEstimate(storage);
45
+ }
46
+ catch (err) {
47
+ setError(err instanceof Error ? err.message : String(err));
48
+ setProjects([]);
49
+ }
50
+ }, [store]);
51
+ useEffect(() => {
52
+ void reload();
53
+ }, [reload]);
54
+ const run = (action) => {
55
+ setDialog(null);
56
+ action()
57
+ .then(reload)
58
+ .catch((err) => setError(err instanceof Error ? err.message : String(err)));
59
+ };
60
+ const projectMenu = (e, project) => {
61
+ e.preventDefault();
62
+ setMenu({
63
+ x: e.clientX,
64
+ y: e.clientY,
65
+ items: [
66
+ { label: 'Open', onSelect: () => onOpen(project) },
67
+ { label: 'Rename…', onSelect: () => setDialog({ kind: 'rename', project }) },
68
+ { label: 'Duplicate…', onSelect: () => setDialog({ kind: 'duplicate', project }) },
69
+ { label: 'Delete…', danger: true, onSelect: () => setDialog({ kind: 'delete', project }) },
70
+ ],
71
+ });
72
+ };
73
+ return (_jsxs("div", { className: "qe-root", style: {
74
+ width: '100%',
75
+ height: '100%',
76
+ overflowY: 'auto',
77
+ background: '#070b16',
78
+ color: theme.text,
79
+ fontFamily: theme.font,
80
+ }, children: [_jsx("style", { children: globalEditorStyles }), _jsxs("div", { style: {
81
+ maxWidth: 960,
82
+ margin: '0 auto',
83
+ padding: '32px 24px',
84
+ display: 'flex',
85
+ flexDirection: 'column',
86
+ gap: 18,
87
+ }, children: [_jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: 12 }, children: [_jsxs("div", { children: [_jsx("div", { style: { fontSize: 22, fontWeight: 700 }, children: title }), _jsx("div", { style: { fontSize: 13, color: theme.textDim, marginTop: 4 }, children: "Projects keep their assets and scene between sessions." })] }), _jsx("span", { style: { flex: 1 } }), _jsxs("button", { className: "qe-hover", style: {
88
+ ...buttonStyle,
89
+ display: 'inline-flex',
90
+ alignItems: 'center',
91
+ gap: 6,
92
+ padding: '8px 14px',
93
+ }, onClick: () => setDialog({ kind: 'create' }), children: [_jsx(PlusIcon, { style: iconStyle(14) }), " New project"] })] }), _jsx(StorageBar, { estimate: estimate, onPersist: () => {
94
+ void store.requestPersistence().then((granted) => {
95
+ if (!granted) {
96
+ setError('The browser did not grant persistent storage. Chrome grants it more readily to sites you visit often or have bookmarked.');
97
+ }
98
+ return reload();
99
+ });
100
+ } }), projects === null ? (_jsx("div", { style: { color: theme.textDim, fontSize: 13 }, children: "Loading projects\u2026" })) : projects.length === 0 ? (_jsx("div", { style: {
101
+ border: `1px dashed ${theme.border}`,
102
+ borderRadius: 10,
103
+ padding: '36px 20px',
104
+ textAlign: 'center',
105
+ color: theme.textDim,
106
+ fontSize: 13,
107
+ }, children: "No projects yet. Create one to start building effects." })) : (_jsx("div", { style: { display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(210px, 1fr))', gap: 12 }, children: projects.map((project) => (_jsxs("button", { className: "qe-hover", title: "Open \u00B7 right-click for more", onClick: () => onOpen(project), onContextMenu: (e) => projectMenu(e, project), style: {
108
+ display: 'flex',
109
+ flexDirection: 'column',
110
+ alignItems: 'flex-start',
111
+ gap: 6,
112
+ textAlign: 'left',
113
+ padding: 14,
114
+ borderRadius: 10,
115
+ border: `1px solid ${theme.border}`,
116
+ background: theme.panelBg,
117
+ color: theme.text,
118
+ cursor: 'pointer',
119
+ fontFamily: theme.font,
120
+ }, children: [_jsxs("span", { style: {
121
+ display: 'flex',
122
+ alignItems: 'center',
123
+ gap: 8,
124
+ fontSize: 14,
125
+ fontWeight: 600,
126
+ }, children: [_jsx(FolderIcon, { style: { ...iconStyle(16), color: theme.accent } }), project.name] }), _jsxs("span", { style: { fontSize: 12, color: theme.textDim }, children: [project.assetCount, " asset(s) \u00B7 ", formatBytes(project.sizeBytes)] }), _jsxs("span", { style: { fontSize: 11.5, color: theme.textDim }, children: ["Edited ", formatDate(project.updatedAt)] })] }, project.id))) }))] }), menu && _jsx(ContextMenu, { x: menu.x, y: menu.y, items: menu.items, onClose: () => setMenu(null) }), dialog?.kind === 'create' && (_jsx(PromptDialog, { title: "New project", defaultValue: "My effects", confirmLabel: "Create", onSubmit: (name) => {
127
+ setDialog(null);
128
+ store
129
+ .createProject(name)
130
+ .then(onOpen)
131
+ .catch((err) => setError(err instanceof Error ? err.message : String(err)));
132
+ }, onCancel: () => setDialog(null) })), dialog?.kind === 'rename' && (_jsx(PromptDialog, { title: "Rename project", defaultValue: dialog.project.name, confirmLabel: "Rename", onSubmit: (name) => run(() => store.renameProject(dialog.project.id, name)), onCancel: () => setDialog(null) })), dialog?.kind === 'duplicate' && (_jsx(PromptDialog, { title: "Duplicate project", defaultValue: `${dialog.project.name} copy`, confirmLabel: "Duplicate", onSubmit: (name) => run(() => store.duplicateProject(dialog.project.id, name)), onCancel: () => setDialog(null) })), dialog?.kind === 'delete' && (_jsx(ChoiceDialog, { title: `Delete “${dialog.project.name}”?`, message: `Its ${dialog.project.assetCount} asset(s) and saved scene are removed from this browser. This cannot be undone.`, actions: [
133
+ { label: 'Cancel', onSelect: () => setDialog(null) },
134
+ {
135
+ label: 'Delete',
136
+ danger: true,
137
+ onSelect: () => run(() => store.deleteProject(dialog.project.id)),
138
+ },
139
+ ], onCancel: () => setDialog(null) })), error && (_jsx(ChoiceDialog, { title: "Projects", message: error, actions: [{ label: 'OK', primary: true, onSelect: () => setError(null) }], onCancel: () => setError(null) }))] }));
140
+ }
@@ -0,0 +1,26 @@
1
+ import type { TransformNode } from '@babylonjs/core/Meshes/transformNode';
2
+ import React from 'react';
3
+ import type { SceneEntry } from '../core/sceneEntries';
4
+ export interface ScenePanelProps {
5
+ entries: SceneEntry[];
6
+ /** Effect shown in the inspector and timeline. */
7
+ focusedRoot: TransformNode | null;
8
+ /** Visible entries — they render and play with the stage transport. */
9
+ visibleRootIds: ReadonlySet<number>;
10
+ onFocus: (entry: SceneEntry) => void;
11
+ onToggleVisible: (entry: SceneEntry) => void;
12
+ /** Root of the entry shown alone, or null. */
13
+ soloRootId: number | null;
14
+ onToggleSolo: (entry: SceneEntry) => void;
15
+ onDelete: (entry: SceneEntry) => void;
16
+ /** "Save as new asset…" in the row's right-click menu. */
17
+ onSaveAsAsset?: (entry: SceneEntry) => void;
18
+ onCreateDefault: () => void;
19
+ /** Node shown in the inspector; highlighted in the expanded effect trees. */
20
+ selectedNode?: TransformNode | null;
21
+ /** Picks a group/emitter inside an effect (focusing the effect first when needed). */
22
+ onSelectNode?: (entry: SceneEntry, node: TransformNode) => void;
23
+ }
24
+ export declare const rowIconButtonStyle: React.CSSProperties;
25
+ /** Lists the effect instances that are in the scene right now. */
26
+ export declare function ScenePanel(props: ScenePanelProps): React.ReactElement;
@@ -0,0 +1,134 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { ChevronDownIcon, ChevronRightIcon, EyeIcon, EyeSlashIcon, SparklesIcon, TrashIcon, ViewfinderCircleIcon, } from '@heroicons/react/24/solid';
3
+ import React, { useEffect, useState } from 'react';
4
+ import { buildEffectTree } from '../core/effectTree';
5
+ import { ContextMenu } from './ContextMenu';
6
+ import { EffectTreeView } from './EffectTreeView';
7
+ import { iconStyle } from './icons';
8
+ import { buttonStyle, theme } from './theme';
9
+ const ROW_HEIGHT = 34;
10
+ export const rowIconButtonStyle = {
11
+ flexShrink: 0,
12
+ width: 28,
13
+ border: 'none',
14
+ background: 'transparent',
15
+ color: theme.textDim,
16
+ cursor: 'pointer',
17
+ display: 'inline-flex',
18
+ alignItems: 'center',
19
+ justifyContent: 'center',
20
+ padding: 0,
21
+ };
22
+ function SceneRow(props) {
23
+ const { entry, focused, visible, solo, onFocus, onToggleVisible, onToggleSolo, onDelete, onContextMenu, hasTree, expanded, onToggleExpanded, } = props;
24
+ const isLooping = entry.systems.some((system) => system.looping);
25
+ return (_jsxs("div", { onContextMenu: (e) => onContextMenu(e, entry), style: {
26
+ display: 'flex',
27
+ alignItems: 'stretch',
28
+ height: ROW_HEIGHT,
29
+ borderBottom: `1px solid ${theme.border}`,
30
+ background: focused ? 'rgba(120, 165, 255, 0.22)' : 'transparent',
31
+ boxSizing: 'border-box',
32
+ }, children: [_jsx("button", { type: "button", className: "qe-hover", title: visible ? 'Hide — stops and clears this effect' : 'Show — plays with the stage transport', style: {
33
+ ...rowIconButtonStyle,
34
+ borderRight: `1px solid ${theme.border}`,
35
+ color: visible ? theme.accent : theme.textDim,
36
+ opacity: visible ? 1 : 0.6,
37
+ }, onClick: (e) => {
38
+ e.stopPropagation();
39
+ onToggleVisible(entry);
40
+ }, children: visible ? _jsx(EyeIcon, { style: iconStyle(14) }) : _jsx(EyeSlashIcon, { style: iconStyle(14) }) }), _jsxs("button", { type: "button", className: "qe-hover-bg", title: `${entry.name}${entry.assetId != null ? ` — from ${entry.path}` : ''} · Ctrl+click toggles visibility`, style: {
41
+ display: 'flex',
42
+ alignItems: 'center',
43
+ gap: 8,
44
+ flex: 1,
45
+ minWidth: 0,
46
+ textAlign: 'left',
47
+ border: 'none',
48
+ background: 'transparent',
49
+ color: focused ? theme.text : visible ? theme.textDim : 'rgba(183, 198, 234, 0.5)',
50
+ padding: '0 8px 0 4px',
51
+ fontSize: 12.5,
52
+ cursor: 'pointer',
53
+ fontFamily: theme.font,
54
+ }, onClick: (e) => {
55
+ if (e.ctrlKey || e.metaKey) {
56
+ onToggleVisible(entry);
57
+ return;
58
+ }
59
+ onFocus(entry);
60
+ }, children: [_jsx("span", { title: expanded ? 'Collapse' : 'Show its groups and emitters', style: {
61
+ width: 12,
62
+ flexShrink: 0,
63
+ display: 'inline-flex',
64
+ visibility: hasTree ? 'visible' : 'hidden',
65
+ }, onClick: (e) => {
66
+ e.stopPropagation();
67
+ onToggleExpanded(entry);
68
+ }, children: expanded ? _jsx(ChevronDownIcon, { style: iconStyle(11) }) : _jsx(ChevronRightIcon, { style: iconStyle(11) }) }), _jsx("span", { style: { width: 14, flexShrink: 0, display: 'inline-flex', opacity: visible ? 1 : 0.4 }, children: _jsx(SparklesIcon, { style: iconStyle(14) }) }), _jsx("span", { style: { overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }, children: entry.name }), isLooping && (_jsx("span", { title: "Looping effect", style: { flexShrink: 0, fontSize: 11, color: theme.textDim }, children: "\u27F3" }))] }), _jsx("button", { type: "button", className: "qe-hover", title: solo ? 'Exit solo — show the other effects again' : 'Solo — hide every other effect', style: {
69
+ ...rowIconButtonStyle,
70
+ borderLeft: `1px solid ${theme.border}`,
71
+ color: solo ? '#ffcc66' : theme.textDim,
72
+ }, onClick: (e) => {
73
+ e.stopPropagation();
74
+ onToggleSolo(entry);
75
+ }, children: _jsx(ViewfinderCircleIcon, { style: iconStyle(14) }) }), _jsx("button", { type: "button", className: "qe-hover", title: "Delete from scene (Delete)", style: { ...rowIconButtonStyle, borderLeft: `1px solid ${theme.border}` }, onClick: (e) => {
76
+ e.stopPropagation();
77
+ onDelete(entry);
78
+ }, children: _jsx(TrashIcon, { style: iconStyle(13) }) })] }));
79
+ }
80
+ /** Lists the effect instances that are in the scene right now. */
81
+ export function ScenePanel(props) {
82
+ const { entries, focusedRoot, visibleRootIds, soloRootId, onFocus, onToggleVisible, onToggleSolo, onDelete, onSaveAsAsset, onCreateDefault, selectedNode = null, onSelectNode, } = props;
83
+ const [menu, setMenu] = useState(null);
84
+ const [expanded, setExpanded] = useState(() => new Set());
85
+ // The focused effect opens its tree, so its sub-emitters are one click away.
86
+ useEffect(() => {
87
+ if (focusedRoot) {
88
+ setExpanded((prev) => (prev.has(focusedRoot.uniqueId) ? prev : new Set(prev).add(focusedRoot.uniqueId)));
89
+ }
90
+ }, [focusedRoot]);
91
+ const toggleExpanded = (entry) => setExpanded((prev) => {
92
+ const next = new Set(prev);
93
+ if (next.has(entry.root.uniqueId)) {
94
+ next.delete(entry.root.uniqueId);
95
+ }
96
+ else {
97
+ next.add(entry.root.uniqueId);
98
+ }
99
+ return next;
100
+ });
101
+ const openMenu = (e, entry) => {
102
+ e.preventDefault();
103
+ const visible = visibleRootIds.has(entry.root.uniqueId);
104
+ const items = [];
105
+ if (onSaveAsAsset) {
106
+ items.push({ label: 'Save as new asset…', onSelect: () => onSaveAsAsset(entry) });
107
+ }
108
+ items.push({ label: visible ? 'Hide' : 'Show', onSelect: () => onToggleVisible(entry) }, {
109
+ label: soloRootId === entry.root.uniqueId ? 'Exit solo' : 'Solo',
110
+ onSelect: () => onToggleSolo(entry),
111
+ }, { label: 'Delete', danger: true, onSelect: () => onDelete(entry) });
112
+ setMenu({ x: e.clientX, y: e.clientY, items });
113
+ };
114
+ return (_jsxs("div", { style: { display: 'flex', flexDirection: 'column', gap: 10, height: '100%', minHeight: 0 }, children: [_jsx("div", { style: { display: 'flex', gap: 6 }, children: _jsx("button", { type: "button", className: "qe-hover", style: buttonStyle, onClick: onCreateDefault, children: "New effect" }) }), entries.length === 0 ? (_jsxs("div", { style: {
115
+ padding: '14px 12px',
116
+ fontSize: 12,
117
+ color: theme.textDim,
118
+ lineHeight: 1.5,
119
+ border: `1px solid ${theme.border}`,
120
+ borderRadius: 8,
121
+ background: theme.sectionBg,
122
+ }, children: ["The scene is empty. Click ", _jsx("strong", { children: "New effect" }), ", or drag effects from the", ' ', _jsx("strong", { children: "Assets" }), " tab below into the viewport."] })) : (_jsx("div", { style: {
123
+ flex: 1,
124
+ minHeight: 0,
125
+ overflowY: 'auto',
126
+ border: `1px solid ${theme.border}`,
127
+ borderRadius: 8,
128
+ background: theme.sectionBg,
129
+ }, children: entries.map((entry) => {
130
+ const hasTree = onSelectNode !== undefined && buildEffectTree(entry.root).children.length > 0;
131
+ const open = hasTree && expanded.has(entry.root.uniqueId);
132
+ return (_jsxs(React.Fragment, { children: [_jsx(SceneRow, { entry: entry, hasTree: hasTree, expanded: open, onToggleExpanded: toggleExpanded, focused: focusedRoot === entry.root, visible: visibleRootIds.has(entry.root.uniqueId), solo: soloRootId === entry.root.uniqueId, onFocus: onFocus, onToggleVisible: onToggleVisible, onToggleSolo: onToggleSolo, onDelete: onDelete, onContextMenu: openMenu }), open && (_jsx("div", { style: { borderBottom: `1px solid ${theme.border}`, padding: '2px 0' }, children: _jsx(EffectTreeView, { root: entry.root, selectedNode: selectedNode, indent: 34, onSelect: (node) => onSelectNode(entry, node) }) }))] }, entry.root.uniqueId));
133
+ }) })), menu && _jsx(ContextMenu, { x: menu.x, y: menu.y, items: menu.items, onClose: () => setMenu(null) })] }));
134
+ }
@@ -1,4 +1,3 @@
1
- import React from 'react';
2
1
  import { EffectBinding } from '../core/binding';
3
2
  /**
4
3
  * Sub Emitters module for the currently selected system. The spawned target lives as a
@@ -7,4 +6,4 @@ import { EffectBinding } from '../core/binding';
7
6
  export declare function SubEmittersModule(props: {
8
7
  binding: EffectBinding;
9
8
  rootBinding: EffectBinding;
10
- }): React.JSX.Element;
9
+ }): import("react").JSX.Element;
@@ -1,7 +1,7 @@
1
- import React from 'react';
2
- import type { BatchedRenderer } from 'babylon.quarks';
3
- import type { Scene } from '@babylonjs/core/scene';
4
1
  import type { TransformNode } from '@babylonjs/core/Meshes/transformNode';
2
+ import type { Scene } from '@babylonjs/core/scene';
3
+ import type { BatchedRenderer } from 'babylon.quarks';
4
+ import React from 'react';
5
5
  import type { EffectBinding } from '../core/binding';
6
6
  import { type PlaybackState } from '../core/playback';
7
7
  export type { PlaybackState } from '../core/playback';
@@ -18,8 +18,15 @@ export interface TimelinePanelProps {
18
18
  setStagePlaying: (playing: boolean) => void;
19
19
  speed: number;
20
20
  setSpeed: (speed: number) => void;
21
- /** False when a catalog entry is selected but not yet dropped into the viewport. */
21
+ /** False when the focused effect is hidden — it has nothing to play. */
22
22
  playbackEnabled?: boolean;
23
+ /** Fills its parent (e.g. a bottom dock) instead of drawing its own fixed-height bar. */
24
+ docked?: boolean;
25
+ /**
26
+ * Play/step/restart/speed buttons and the counter in the timeline header (default true).
27
+ * Hosts that show a TransportBar in the viewport turn this off.
28
+ */
29
+ showTransport?: boolean;
23
30
  }
24
31
  /** Small gap between the label column and the first pixel of the time axis. */
25
32
  export declare const TIMELINE_INSET = 10;