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,252 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { ChevronDownIcon, ChevronRightIcon, ExclamationTriangleIcon, FolderIcon, FolderOpenIcon, SparklesIcon, } from '@heroicons/react/24/solid';
3
+ import React, { useCallback, useMemo, useRef, useState } from 'react';
4
+ import { setAssetDragSession } from '../core/assetDragSession';
5
+ import { buildAssetTree, collectFolderAssets, folderChildren, searchAssets, writeAssetDrag, } from '../core/assetTree';
6
+ import { assetIconUrl } from '../core/assets';
7
+ import { ContextMenu } from './ContextMenu';
8
+ import { iconStyle } from './icons';
9
+ import { buttonStyle, hiddenFileInputStyle, inputStyle, theme } from './theme';
10
+ const TREE_WIDTH = 200;
11
+ const TILE_WIDTH = 92;
12
+ function beginAssetDrag(e, payload) {
13
+ writeAssetDrag(e.dataTransfer, payload);
14
+ setAssetDragSession(payload);
15
+ }
16
+ function endAssetDrag() {
17
+ setAssetDragSession(null);
18
+ }
19
+ function folderName(path) {
20
+ return path.split('/').filter(Boolean).pop() ?? 'Assets';
21
+ }
22
+ /** Main texture of the effect, or a sparkle when it has none (or it fails to load). */
23
+ function AssetIcon(props) {
24
+ const url = assetIconUrl(props.asset);
25
+ const [failed, setFailed] = useState(false);
26
+ if (!url || failed) {
27
+ return _jsx(SparklesIcon, { style: { ...iconStyle(26), color: theme.accent, opacity: 0.8 } });
28
+ }
29
+ return (_jsx("img", { src: url, alt: "", loading: "lazy", decoding: "async", draggable: false, onError: () => setFailed(true), style: { maxWidth: '100%', maxHeight: '100%', objectFit: 'contain' } }));
30
+ }
31
+ function Tile(props) {
32
+ return (_jsxs("button", { type: "button", draggable: true, className: "qe-hover-bg", title: props.title, onClick: props.onSelect, onDoubleClick: props.onOpen, onContextMenu: props.onContextMenu, onDragStart: props.onDragStart, onDragEnd: endAssetDrag, style: {
33
+ position: 'relative',
34
+ display: 'flex',
35
+ flexDirection: 'column',
36
+ alignItems: 'center',
37
+ gap: 5,
38
+ width: '100%',
39
+ padding: '6px 4px',
40
+ border: `1px solid ${props.selected ? theme.borderActive : 'transparent'}`,
41
+ borderRadius: 8,
42
+ background: props.selected ? 'rgba(120, 165, 255, 0.18)' : 'transparent',
43
+ color: theme.text,
44
+ cursor: 'grab',
45
+ fontFamily: theme.font,
46
+ }, children: [_jsx("span", { style: {
47
+ width: 56,
48
+ height: 56,
49
+ display: 'flex',
50
+ alignItems: 'center',
51
+ justifyContent: 'center',
52
+ borderRadius: 6,
53
+ background: 'rgba(0, 0, 0, 0.35)',
54
+ overflow: 'hidden',
55
+ }, children: props.icon }), _jsx("span", { style: {
56
+ width: '100%',
57
+ fontSize: 11.5,
58
+ lineHeight: 1.25,
59
+ textAlign: 'center',
60
+ wordBreak: 'break-word',
61
+ display: '-webkit-box',
62
+ WebkitLineClamp: 2,
63
+ WebkitBoxOrient: 'vertical',
64
+ overflow: 'hidden',
65
+ }, children: props.label }), props.subtitle && (_jsx("span", { style: {
66
+ width: '100%',
67
+ fontSize: 10,
68
+ color: theme.textDim,
69
+ textAlign: 'center',
70
+ overflow: 'hidden',
71
+ textOverflow: 'ellipsis',
72
+ whiteSpace: 'nowrap',
73
+ }, children: props.subtitle })), props.badge && (_jsx("span", { style: { position: 'absolute', top: 4, right: 6, fontSize: 10.5, color: theme.accent }, children: props.badge })), props.warning && (_jsx("span", { title: props.warning, style: { position: 'absolute', top: 4, left: 5, color: '#ffcc66' }, children: _jsx(ExclamationTriangleIcon, { style: iconStyle(12) }) }))] }));
74
+ }
75
+ function FolderTreeRow(props) {
76
+ const { folder, depth, currentPath, collapsed, onToggle, onOpen, onContextMenu } = props;
77
+ const subfolders = folder.children.filter((node) => node.kind === 'folder');
78
+ const open = !collapsed.has(folder.path);
79
+ const current = currentPath === folder.path;
80
+ return (_jsxs(_Fragment, { children: [_jsxs("div", { className: "qe-hover-bg", title: folder.path, draggable: true, onClick: () => onOpen(folder.path), onContextMenu: (e) => onContextMenu(e, folder), onDragStart: (e) => beginAssetDrag(e, { kind: 'folder', path: folder.path }), onDragEnd: endAssetDrag, style: {
81
+ display: 'flex',
82
+ alignItems: 'center',
83
+ gap: 5,
84
+ height: 26,
85
+ padding: `0 8px 0 ${6 + depth * 12}px`,
86
+ fontSize: 12,
87
+ cursor: 'pointer',
88
+ color: current ? theme.text : theme.textDim,
89
+ background: current ? 'rgba(120, 165, 255, 0.2)' : undefined,
90
+ }, children: [_jsx("span", { style: {
91
+ width: 12,
92
+ display: 'inline-flex',
93
+ visibility: subfolders.length > 0 ? 'visible' : 'hidden',
94
+ }, onClick: (e) => {
95
+ e.stopPropagation();
96
+ onToggle(folder.path);
97
+ }, children: open ? _jsx(ChevronDownIcon, { style: iconStyle(11) }) : _jsx(ChevronRightIcon, { style: iconStyle(11) }) }), _jsx("span", { style: { display: 'inline-flex', color: theme.accent }, children: current ? _jsx(FolderOpenIcon, { style: iconStyle(13) }) : _jsx(FolderIcon, { style: iconStyle(13) }) }), _jsx("span", { style: { overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }, children: folder.name })] }), open &&
98
+ subfolders.map((child) => (_jsx(FolderTreeRow, { ...props, folder: child, depth: depth + 1 }, child.path)))] }));
99
+ }
100
+ /**
101
+ * Bottom-dock asset browser: folder tree on the left, the open folder's effects as tiles on the
102
+ * right. Drag a tile into the viewport (as many times as needed) or right-click it for actions.
103
+ */
104
+ export function AssetsPanel(props) {
105
+ const { assets, instanceCounts, loading, folderInputRef, onImportFiles, onImportFolder, onAddToScene, onDelete, onOpenPrefab, } = props;
106
+ const [query, setQuery] = useState('');
107
+ const [currentPath, setCurrentPath] = useState('');
108
+ const [collapsed, setCollapsed] = useState(() => new Set());
109
+ const [selectedKey, setSelectedKey] = useState(null);
110
+ const [menu, setMenu] = useState(null);
111
+ const fileInputRef = useRef(null);
112
+ const isLoading = loading !== null;
113
+ const searching = query.trim().length > 0;
114
+ const tree = useMemo(() => buildAssetTree(assets), [assets]);
115
+ const rootFolder = useMemo(() => ({ kind: 'folder', name: 'Assets', path: '', children: tree }), [tree]);
116
+ // A deleted folder falls back to the top level.
117
+ const children = folderChildren(tree, currentPath);
118
+ const openPath = children ? currentPath : '';
119
+ const content = children ?? tree;
120
+ const results = useMemo(() => (searching ? searchAssets(assets, query) : []), [assets, query, searching]);
121
+ const closeMenu = useCallback(() => setMenu(null), []);
122
+ const toggleFolder = useCallback((path) => {
123
+ setCollapsed((prev) => {
124
+ const next = new Set(prev);
125
+ if (next.has(path)) {
126
+ next.delete(path);
127
+ }
128
+ else {
129
+ next.add(path);
130
+ }
131
+ return next;
132
+ });
133
+ }, []);
134
+ const openFolder = useCallback((path) => {
135
+ setCurrentPath(path);
136
+ setQuery('');
137
+ setSelectedKey(null);
138
+ }, []);
139
+ const showMenu = (e, items) => {
140
+ e.preventDefault();
141
+ e.stopPropagation();
142
+ setMenu({ x: e.clientX, y: e.clientY, items });
143
+ };
144
+ const assetMenu = (e, asset) => {
145
+ setSelectedKey(`asset:${asset.id}`);
146
+ showMenu(e, [
147
+ ...(onOpenPrefab ? [{ label: 'Open prefab', onSelect: () => onOpenPrefab(asset) }] : []),
148
+ { label: 'Add to scene', onSelect: () => onAddToScene({ kind: 'asset', id: asset.id }) },
149
+ { label: 'Delete from Assets', danger: true, onSelect: () => onDelete({ kind: 'asset', id: asset.id }) },
150
+ ]);
151
+ };
152
+ const folderMenu = (e, folder) => {
153
+ const count = folder.path ? collectFolderAssets(folder).length : assets.length;
154
+ setSelectedKey(`folder:${folder.path}`);
155
+ const items = [
156
+ { label: 'Open', onSelect: () => openFolder(folder.path) },
157
+ {
158
+ label: `Add all to scene (${count})`,
159
+ disabled: count === 0 || !folder.path,
160
+ onSelect: () => onAddToScene({ kind: 'folder', path: folder.path }),
161
+ },
162
+ ];
163
+ if (folder.path) {
164
+ items.push({
165
+ label: 'Delete folder from Assets',
166
+ danger: true,
167
+ onSelect: () => onDelete({ kind: 'folder', path: folder.path }),
168
+ });
169
+ }
170
+ showMenu(e, items);
171
+ };
172
+ const backgroundMenu = (e) => showMenu(e, [
173
+ { label: 'Import effect…', disabled: isLoading, onSelect: () => fileInputRef.current?.click() },
174
+ { label: 'Import folder…', disabled: isLoading, onSelect: onImportFolder },
175
+ ]);
176
+ const assetTile = (asset, subtitle) => {
177
+ const key = `asset:${asset.id}`;
178
+ const instances = instanceCounts.get(asset.id) ?? 0;
179
+ return (_jsx(Tile, { label: asset.name, subtitle: subtitle, title: `${asset.path}\nDrag into the viewport · double-click ${onOpenPrefab ? 'opens the prefab' : 'adds to the scene'}`, selected: selectedKey === key, icon: _jsx(AssetIcon, { asset: asset }), badge: instances > 0 ? `×${instances}` : undefined, warning: asset.missingFiles.length > 0
180
+ ? `References files that were not imported — import the whole export folder:\n${asset.missingFiles.join('\n')}`
181
+ : undefined, onSelect: () => setSelectedKey(key), onOpen: () => (onOpenPrefab ? onOpenPrefab(asset) : onAddToScene({ kind: 'asset', id: asset.id })), onContextMenu: (e) => assetMenu(e, asset), onDragStart: (e) => beginAssetDrag(e, { kind: 'asset', id: asset.id }) }, key));
182
+ };
183
+ const folderTile = (folder) => {
184
+ const key = `folder:${folder.path}`;
185
+ const count = collectFolderAssets(folder).length;
186
+ return (_jsx(Tile, { label: folder.name, title: `${folder.path} — ${count} effect(s)\nDouble-click to open · drag to place them all`, selected: selectedKey === key, icon: _jsx(FolderIcon, { style: { ...iconStyle(34), color: theme.accent } }), badge: String(count), onSelect: () => setSelectedKey(key), onOpen: () => openFolder(folder.path), onContextMenu: (e) => folderMenu(e, folder), onDragStart: (e) => beginAssetDrag(e, { kind: 'folder', path: folder.path }) }, key));
187
+ };
188
+ const fileLabelStyle = {
189
+ ...buttonStyle,
190
+ position: 'relative',
191
+ display: 'inline-flex',
192
+ alignItems: 'center',
193
+ padding: '4px 10px',
194
+ cursor: isLoading ? 'not-allowed' : 'pointer',
195
+ opacity: isLoading ? 0.5 : 1,
196
+ };
197
+ const takeFiles = (e) => {
198
+ // Snapshot before clearing — FileList is live and empties with value=''.
199
+ const files = Array.from(e.target.files ?? []);
200
+ e.target.value = '';
201
+ if (files.length > 0) {
202
+ onImportFiles(files);
203
+ }
204
+ };
205
+ const crumbs = openPath ? ['', ...openPath.split('/').map((_, i, parts) => parts.slice(0, i + 1).join('/'))] : [''];
206
+ const gridItems = searching
207
+ ? results.map((asset) => assetTile(asset, asset.path.split('/').slice(0, -1).join('/') || undefined))
208
+ : content.map((node) => (node.kind === 'folder' ? folderTile(node) : assetTile(node.asset)));
209
+ return (_jsxs("div", { style: { display: 'flex', flexDirection: 'column', flex: 1, minHeight: 0 }, children: [_jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: 6, padding: '6px 10px' }, children: [_jsxs("label", { className: "qe-hover", style: fileLabelStyle, title: "Effect .json files (plus any texture files they use)", children: ["Import effect", _jsx("input", { ref: fileInputRef, type: "file", multiple: true, accept: ".json,image/*", disabled: isLoading, tabIndex: -1, style: hiddenFileInputStyle, onChange: takeFiles })] }), _jsxs("label", { className: "qe-hover", style: fileLabelStyle, title: "A folder of effects \u2014 subfolders and shared asset files are kept", onClick: (e) => {
210
+ if (typeof window.showDirectoryPicker ===
211
+ 'function') {
212
+ e.preventDefault();
213
+ onImportFolder();
214
+ }
215
+ }, children: ["Import folder", _jsx("input", { ref: (el) => {
216
+ folderInputRef.current = el;
217
+ if (el) {
218
+ el.setAttribute('webkitdirectory', '');
219
+ el.setAttribute('directory', '');
220
+ }
221
+ }, type: "file", multiple: true, disabled: isLoading, tabIndex: -1, style: hiddenFileInputStyle, onChange: takeFiles })] }), _jsx("input", { className: "qe-hover", style: { ...inputStyle, width: 220, padding: '4px 8px' }, placeholder: "Search all folders\u2026", value: query, onChange: (e) => setQuery(e.target.value) }), _jsx("span", { style: { fontSize: 12, color: theme.textDim, marginLeft: 'auto', whiteSpace: 'nowrap' }, children: isLoading
222
+ ? `Loading… ${loading.done} / ${loading.total || '…'} files`
223
+ : `${assets.length} asset(s) · drag into the viewport · right-click for actions` })] }), _jsxs("div", { style: { display: 'flex', flex: 1, minHeight: 0, borderTop: `1px solid ${theme.border}` }, children: [_jsx("div", { style: {
224
+ width: TREE_WIDTH,
225
+ flexShrink: 0,
226
+ overflowY: 'auto',
227
+ borderRight: `1px solid ${theme.border}`,
228
+ padding: '4px 0',
229
+ }, onContextMenu: backgroundMenu, children: _jsx(FolderTreeRow, { folder: rootFolder, depth: 0, currentPath: searching ? '\0' : openPath, collapsed: collapsed, onToggle: toggleFolder, onOpen: openFolder, onContextMenu: folderMenu }) }), _jsxs("div", { style: { flex: 1, minWidth: 0, display: 'flex', flexDirection: 'column' }, onClick: () => setSelectedKey(null), onContextMenu: backgroundMenu, children: [_jsx("div", { style: {
230
+ display: 'flex',
231
+ alignItems: 'center',
232
+ gap: 4,
233
+ padding: '5px 12px',
234
+ fontSize: 12,
235
+ color: theme.textDim,
236
+ borderBottom: `1px solid ${theme.border}`,
237
+ whiteSpace: 'nowrap',
238
+ overflow: 'hidden',
239
+ }, children: searching ? (_jsxs("span", { children: [results.length, " result(s) for \u201C", query.trim(), "\u201D"] })) : (crumbs.map((path, i) => (_jsxs(React.Fragment, { children: [i > 0 && _jsx("span", { style: { opacity: 0.5 }, children: "/" }), _jsx("span", { className: "qe-hover-bg", style: {
240
+ padding: '1px 4px',
241
+ borderRadius: 4,
242
+ cursor: 'pointer',
243
+ color: i === crumbs.length - 1 ? theme.text : theme.textDim,
244
+ }, onClick: (e) => {
245
+ e.stopPropagation();
246
+ openFolder(path);
247
+ }, children: folderName(path) })] }, path || 'root')))) }), _jsx("div", { style: { flex: 1, minHeight: 0, overflowY: 'auto', padding: 8 }, children: assets.length === 0 && !isLoading ? (_jsxs("div", { style: { padding: '4px 6px', fontSize: 12, color: theme.textDim, lineHeight: 1.5 }, children: ["No assets yet. ", _jsx("strong", { children: "Import effect" }), " or ", _jsx("strong", { children: "Import folder" }), " \u2014 then drag an effect into the viewport as many times as you need."] })) : gridItems.length === 0 ? (_jsx("div", { style: { padding: '4px 6px', fontSize: 12, color: theme.textDim }, children: searching ? 'No matches.' : 'This folder is empty.' })) : (_jsx("div", { style: {
248
+ display: 'grid',
249
+ gridTemplateColumns: `repeat(auto-fill, minmax(${TILE_WIDTH}px, 1fr))`,
250
+ gap: 6,
251
+ }, onClick: (e) => e.stopPropagation(), children: gridItems })) })] })] }), menu && _jsx(ContextMenu, { x: menu.x, y: menu.y, items: menu.items, onClose: closeMenu })] }));
252
+ }
@@ -0,0 +1,18 @@
1
+ import React from 'react';
2
+ export interface ChoiceDialogAction {
3
+ label: string;
4
+ onSelect: () => void;
5
+ /** Accent button — also what Enter picks. */
6
+ primary?: boolean;
7
+ danger?: boolean;
8
+ }
9
+ export interface ChoiceDialogProps {
10
+ title: string;
11
+ message: React.ReactNode;
12
+ /** Shown left to right; put the cancel action first. */
13
+ actions: ChoiceDialogAction[];
14
+ /** Escape / backdrop click. */
15
+ onCancel: () => void;
16
+ }
17
+ /** Confirmation modal with several outcomes (e.g. Save / Don't save / Cancel). */
18
+ export declare function ChoiceDialog(props: ChoiceDialogProps): React.ReactElement;
@@ -0,0 +1,48 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect } from 'react';
3
+ import { buttonStyle, theme } from './theme';
4
+ /** Confirmation modal with several outcomes (e.g. Save / Don't save / Cancel). */
5
+ export function ChoiceDialog(props) {
6
+ const { title, message, actions, onCancel } = props;
7
+ useEffect(() => {
8
+ const onKey = (e) => {
9
+ if (e.key === 'Escape') {
10
+ e.stopPropagation();
11
+ onCancel();
12
+ }
13
+ else if (e.key === 'Enter') {
14
+ const primary = actions.find((action) => action.primary);
15
+ if (primary) {
16
+ e.preventDefault();
17
+ primary.onSelect();
18
+ }
19
+ }
20
+ };
21
+ window.addEventListener('keydown', onKey, true);
22
+ return () => window.removeEventListener('keydown', onKey, true);
23
+ }, [actions, onCancel]);
24
+ return (_jsx("div", { style: {
25
+ position: 'fixed',
26
+ inset: 0,
27
+ background: 'rgba(3, 5, 12, 0.6)',
28
+ display: 'flex',
29
+ alignItems: 'center',
30
+ justifyContent: 'center',
31
+ zIndex: 1000,
32
+ fontFamily: theme.font,
33
+ }, onClick: onCancel, children: _jsxs("div", { role: "dialog", "aria-label": title, style: {
34
+ background: theme.panelBg,
35
+ border: `1px solid ${theme.border}`,
36
+ borderRadius: 10,
37
+ padding: 16,
38
+ width: 380,
39
+ maxWidth: 'calc(100vw - 32px)',
40
+ boxShadow: '0 12px 32px rgba(0, 0, 0, 0.5)',
41
+ }, onClick: (e) => e.stopPropagation(), children: [_jsx("div", { style: { fontSize: 14, fontWeight: 600, color: theme.text, marginBottom: 8 }, children: title }), _jsx("div", { style: { fontSize: 12.5, color: theme.textDim, lineHeight: 1.5, marginBottom: 16 }, children: message }), _jsx("div", { style: { display: 'flex', justifyContent: 'flex-end', gap: 8, flexWrap: 'wrap' }, children: actions.map((action) => (_jsx("button", { className: "qe-hover", style: {
42
+ ...buttonStyle,
43
+ ...(action.primary
44
+ ? { background: theme.accent, color: '#0a1128', border: `1px solid ${theme.accent}` }
45
+ : {}),
46
+ ...(action.danger ? { border: '1px solid #b04a4a', color: '#ffb0b0' } : {}),
47
+ }, onClick: action.onSelect, children: action.label }, action.label))) })] }) }));
48
+ }
@@ -0,0 +1,16 @@
1
+ import React from 'react';
2
+ export interface ContextMenuItem {
3
+ label: string;
4
+ onSelect: () => void;
5
+ danger?: boolean;
6
+ disabled?: boolean;
7
+ }
8
+ export interface ContextMenuProps {
9
+ /** Client coordinates of the right-click. */
10
+ x: number;
11
+ y: number;
12
+ items: ContextMenuItem[];
13
+ onClose: () => void;
14
+ }
15
+ /** Right-click menu at the pointer; closes on outside click, Escape, scroll or window blur. */
16
+ export declare function ContextMenu(props: ContextMenuProps): React.ReactElement;
@@ -0,0 +1,70 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useEffect, useLayoutEffect, useRef, useState } from 'react';
3
+ import { createPortal } from 'react-dom';
4
+ import { theme } from './theme';
5
+ /** Right-click menu at the pointer; closes on outside click, Escape, scroll or window blur. */
6
+ export function ContextMenu(props) {
7
+ const { x, y, items, onClose } = props;
8
+ const ref = useRef(null);
9
+ const [position, setPosition] = useState({ left: x, top: y });
10
+ // Keep the menu on screen near the right/bottom edges.
11
+ useLayoutEffect(() => {
12
+ const el = ref.current;
13
+ if (!el)
14
+ return;
15
+ const { width, height } = el.getBoundingClientRect();
16
+ setPosition({
17
+ left: Math.max(4, Math.min(x, window.innerWidth - width - 4)),
18
+ top: Math.max(4, Math.min(y, window.innerHeight - height - 4)),
19
+ });
20
+ }, [x, y]);
21
+ useEffect(() => {
22
+ const onPointerDown = (e) => {
23
+ if (!ref.current?.contains(e.target))
24
+ onClose();
25
+ };
26
+ const onKey = (e) => {
27
+ if (e.key === 'Escape')
28
+ onClose();
29
+ };
30
+ window.addEventListener('pointerdown', onPointerDown, true);
31
+ window.addEventListener('keydown', onKey);
32
+ window.addEventListener('wheel', onClose, true);
33
+ window.addEventListener('blur', onClose);
34
+ return () => {
35
+ window.removeEventListener('pointerdown', onPointerDown, true);
36
+ window.removeEventListener('keydown', onKey);
37
+ window.removeEventListener('wheel', onClose, true);
38
+ window.removeEventListener('blur', onClose);
39
+ };
40
+ }, [onClose]);
41
+ return createPortal(_jsx("div", { ref: ref, role: "menu", onContextMenu: (e) => e.preventDefault(), style: {
42
+ position: 'fixed',
43
+ left: position.left,
44
+ top: position.top,
45
+ zIndex: 1000,
46
+ minWidth: 170,
47
+ padding: 4,
48
+ background: '#0c1226',
49
+ border: `1px solid ${theme.border}`,
50
+ borderRadius: 8,
51
+ boxShadow: '0 8px 24px rgba(0, 0, 0, 0.45)',
52
+ fontFamily: theme.font,
53
+ }, children: items.map((item) => (_jsx("button", { type: "button", role: "menuitem", className: "qe-hover-bg", disabled: item.disabled, style: {
54
+ display: 'block',
55
+ width: '100%',
56
+ textAlign: 'left',
57
+ border: 'none',
58
+ borderRadius: 5,
59
+ background: 'transparent',
60
+ color: item.danger ? '#ff8a8a' : theme.text,
61
+ opacity: item.disabled ? 0.45 : 1,
62
+ padding: '6px 10px',
63
+ fontSize: 12.5,
64
+ cursor: item.disabled ? 'default' : 'pointer',
65
+ fontFamily: theme.font,
66
+ }, onClick: () => {
67
+ onClose();
68
+ item.onSelect();
69
+ }, children: item.label }, item.label))) }), document.body);
70
+ }
@@ -1,4 +1,3 @@
1
- import React from 'react';
2
1
  import { CurvePiece } from '../core/values';
3
2
  /**
4
3
  * Multi-segment piecewise-bezier curve editor. Drag keys/handles vertically,
@@ -9,4 +8,4 @@ export declare function CurveEditor(props: {
9
8
  pieces: CurvePiece[];
10
9
  onChange: (pieces: CurvePiece[]) => void;
11
10
  maxValue?: number;
12
- }): React.JSX.Element;
11
+ }): import("react").JSX.Element;
@@ -70,8 +70,16 @@ export function CurveEditor(props) {
70
70
  }
71
71
  props.onChange(next);
72
72
  };
73
- const selectedPoint = selected ? points.find((pt) => pt.ref.piece === selected.piece && pt.ref.idx === selected.idx) : null;
74
- return (_jsxs("div", { children: [_jsxs("svg", { ref: svgRef, width: WIDTH, height: HEIGHT, style: { background: 'rgba(5, 8, 18, 0.85)', border: `1px solid ${theme.border}`, borderRadius: 8, touchAction: 'none', display: 'block' }, onPointerMove: (e) => applyDrag(e.clientX, e.clientY), onPointerUp: (e) => {
73
+ const selectedPoint = selected
74
+ ? points.find((pt) => pt.ref.piece === selected.piece && pt.ref.idx === selected.idx)
75
+ : null;
76
+ return (_jsxs("div", { children: [_jsxs("svg", { ref: svgRef, width: WIDTH, height: HEIGHT, style: {
77
+ background: 'rgba(5, 8, 18, 0.85)',
78
+ border: `1px solid ${theme.border}`,
79
+ borderRadius: 8,
80
+ touchAction: 'none',
81
+ display: 'block',
82
+ }, onPointerMove: (e) => applyDrag(e.clientX, e.clientY), onPointerUp: (e) => {
75
83
  drag.current = null;
76
84
  e.target.releasePointerCapture?.(e.pointerId);
77
85
  }, onDoubleClick: (e) => {
@@ -95,7 +103,10 @@ export function CurveEditor(props) {
95
103
  if (Number.isNaN(parsed)) {
96
104
  return;
97
105
  }
98
- const next = pieces.map((piece) => ({ start: piece.start, p: [...piece.p] }));
106
+ const next = pieces.map((piece) => ({
107
+ start: piece.start,
108
+ p: [...piece.p],
109
+ }));
99
110
  const { piece, idx } = selectedPoint.ref;
100
111
  if (idx === 0 && piece > 0) {
101
112
  next[piece - 1].p[3] = parsed;
@@ -0,0 +1,21 @@
1
+ import React from 'react';
2
+ import type { ProjectStore } from '../core/projectStore';
3
+ import { type EffectEditorHostProps } from './EffectEditorHost';
4
+ export interface EditorWorkspaceProps extends Omit<EffectEditorHostProps, 'project' | 'onExitProject' | 'effectJson'> {
5
+ /** Where projects are kept; defaults to the browser's IndexedDB. */
6
+ store?: ProjectStore;
7
+ /** Project to open right away (e.g. from the url); the projects page is shown otherwise. */
8
+ projectId?: string | null;
9
+ /** Called when a project is opened or closed — e.g. to mirror it in the url. */
10
+ onProjectChange?: (projectId: string | null) => void;
11
+ }
12
+ /** Projects page → editor for the chosen project, with assets and scene saved in `store`. */
13
+ export declare function EditorWorkspace(props: EditorWorkspaceProps): React.ReactElement;
14
+ /** Imperative entry point, like QuarksEffectEditor.Show, for the project-based workspace. */
15
+ export declare const QuarksEditorWorkspace: {
16
+ Show(options: EditorWorkspaceProps & {
17
+ hostElement: HTMLElement;
18
+ }): {
19
+ dispose: () => void;
20
+ };
21
+ };
@@ -0,0 +1,63 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useEffect, useMemo, useState } from 'react';
3
+ import { createRoot } from 'react-dom/client';
4
+ import { IndexedDbProjectStore } from '../core/indexedDbProjectStore';
5
+ import { EffectEditorHost } from './EffectEditorHost';
6
+ import { ProjectsPage } from './ProjectsPage';
7
+ import { theme } from './theme';
8
+ /** Projects page → editor for the chosen project, with assets and scene saved in `store`. */
9
+ export function EditorWorkspace(props) {
10
+ const { store: storeProp, projectId, onProjectChange, ...hostProps } = props;
11
+ const store = useMemo(() => storeProp ?? new IndexedDbProjectStore(), [storeProp]);
12
+ const [project, setProject] = useState(null);
13
+ const [resolving, setResolving] = useState(Boolean(projectId));
14
+ useEffect(() => {
15
+ if (!projectId) {
16
+ setResolving(false);
17
+ return;
18
+ }
19
+ let cancelled = false;
20
+ void store.getProject(projectId).then((meta) => {
21
+ if (cancelled)
22
+ return;
23
+ setProject(meta);
24
+ setResolving(false);
25
+ if (!meta)
26
+ onProjectChange?.(null);
27
+ });
28
+ return () => {
29
+ cancelled = true;
30
+ };
31
+ // Only the initial project id is honoured; later navigation goes through onProjectChange.
32
+ // eslint-disable-next-line react-hooks/exhaustive-deps
33
+ }, [store]);
34
+ if (resolving) {
35
+ return (_jsx("div", { style: {
36
+ width: '100%',
37
+ height: '100%',
38
+ background: '#070b16',
39
+ color: theme.textDim,
40
+ fontFamily: theme.font,
41
+ padding: 24,
42
+ }, children: "Opening project\u2026" }));
43
+ }
44
+ if (!project) {
45
+ return (_jsx(ProjectsPage, { store: store, title: hostProps.title, onOpen: (meta) => {
46
+ setProject(meta);
47
+ onProjectChange?.(meta.id);
48
+ } }));
49
+ }
50
+ return (_jsx(EffectEditorHost, { ...hostProps, project: { store, id: project.id, name: project.name }, onExitProject: () => {
51
+ setProject(null);
52
+ onProjectChange?.(null);
53
+ } }, project.id));
54
+ }
55
+ /** Imperative entry point, like QuarksEffectEditor.Show, for the project-based workspace. */
56
+ export const QuarksEditorWorkspace = {
57
+ Show(options) {
58
+ const { hostElement, ...props } = options;
59
+ const root = createRoot(hostElement);
60
+ root.render(_jsx(EditorWorkspace, { ...props }));
61
+ return { dispose: () => root.unmount() };
62
+ },
63
+ };
@@ -1,8 +1,8 @@
1
- import React from 'react';
2
1
  import type { TransformNode } from '@babylonjs/core/Meshes/transformNode';
2
+ import type { ParticleSystem } from 'babylon.quarks';
3
+ import React from 'react';
3
4
  import { EffectBinding } from '../core/binding';
4
5
  import type { GeometryData, GeometryOption, TextureOption } from './modules';
5
- import type { ParticleSystem } from 'babylon.quarks';
6
6
  export interface EffectEditorProps {
7
7
  binding: EffectBinding;
8
8
  /** Currently selected system, lifted up so the timeline panel and this module stack agree. */
@@ -22,4 +22,4 @@ export interface EffectEditorProps {
22
22
  * Embed it anywhere React runs — the standalone examples page or a BabylonJS Editor plugin.
23
23
  * Module order mirrors Unity's Particle System inspector.
24
24
  */
25
- export declare function EffectEditor(props: EffectEditorProps): React.JSX.Element;
25
+ export declare const EffectEditor: React.NamedExoticComponent<EffectEditorProps>;
@@ -1,9 +1,9 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import React, { useSyncExternalStore } from 'react';
2
+ import React, { useEffect, useSyncExternalStore } from 'react';
3
3
  import { EffectBinding } from '../core/binding';
4
- import { ObjectModule } from './ObjectModule';
5
- import { ColorOverLifeModule, EmissionModule, MainModule, RendererModule, ShapeModule, SizeOverLifeModule, } from './modules';
6
4
  import { CollisionModule, ColorBySpeedModule, EmitDirectionModule, ForceOverLifeModule, GravityModule, InheritVelocityModule, LimitSpeedOverLifeModule, NoiseModule, OrbitOverLifeModule, RotationBySpeedModule, RotationOverLifeModule, SizeBySpeedModule, SpeedOverLifeModule, TextureSheetModule, TurbulenceModule, VelocityOverLifeModule, WidthOverTrailModule, } from './behaviorModules';
5
+ import { ColorOverLifeModule, EmissionModule, MainModule, RendererModule, ShapeModule, SizeOverLifeModule, } from './modules';
6
+ import { ObjectModule } from './ObjectModule';
7
7
  import { SubEmittersModule } from './SubEmittersModule';
8
8
  import { theme } from './theme';
9
9
  /**
@@ -11,7 +11,7 @@ import { theme } from './theme';
11
11
  * Embed it anywhere React runs — the standalone examples page or a BabylonJS Editor plugin.
12
12
  * Module order mirrors Unity's Particle System inspector.
13
13
  */
14
- export function EffectEditor(props) {
14
+ export const EffectEditor = React.memo(function EffectEditor(props) {
15
15
  useSyncExternalStore((onStoreChange) => props.binding.subscribe(onStoreChange), () => props.binding.getRevision());
16
16
  const allSystems = [props.binding.system, ...props.binding.subSystems];
17
17
  // Selection survives re-renders but falls back to root if the system was removed/replaced.
@@ -26,7 +26,10 @@ export function EffectEditor(props) {
26
26
  nested.onBeforeChange = () => props.binding.onBeforeChange?.();
27
27
  return nested;
28
28
  }, [props.binding, activeSystem]);
29
+ // A sub-system edit is a change of the whole effect: the host's undo buttons, timeline and
30
+ // wireframes all listen to the root binding.
31
+ useEffect(() => (binding === props.binding ? undefined : binding.subscribe(() => props.binding.markChanged())), [binding, props.binding]);
29
32
  useSyncExternalStore((onStoreChange) => binding.subscribe(onStoreChange), () => binding.getRevision());
30
33
  const gizmoTargetIsEmitter = allSystems.some((s) => s.emitter === props.gizmoTargetNode);
31
- return (_jsxs("div", { style: { fontFamily: theme.font, color: theme.text }, children: [_jsx(ObjectModule, { node: props.gizmoTargetNode, label: props.gizmoTargetLabel }), !gizmoTargetIsEmitter && (_jsxs("div", { style: { fontSize: 11, color: theme.textDim, margin: '0 0 10px', lineHeight: 1.45 }, children: ["Modules below edit ", _jsx("strong", { children: activeSystem.emitter.name || 'particle system' }), ". Select its emitter in the timeline to move it with the gizmo."] })), _jsx(MainModule, { binding: binding }), _jsx(EmissionModule, { binding: binding }), _jsx(ShapeModule, { binding: binding }), _jsx(EmitDirectionModule, { binding: binding }), _jsx(VelocityOverLifeModule, { binding: binding }), _jsx(SpeedOverLifeModule, { binding: binding }), _jsx(LimitSpeedOverLifeModule, { binding: binding }), _jsx(InheritVelocityModule, { binding: binding }), _jsx(ForceOverLifeModule, { binding: binding }), _jsx(GravityModule, { binding: binding }), _jsx(OrbitOverLifeModule, { binding: binding }), _jsx(ColorOverLifeModule, { binding: binding }), _jsx(ColorBySpeedModule, { binding: binding }), _jsx(SizeOverLifeModule, { binding: binding }), _jsx(SizeBySpeedModule, { binding: binding }), _jsx(RotationOverLifeModule, { binding: binding }), _jsx(RotationBySpeedModule, { binding: binding }), _jsx(NoiseModule, { binding: binding }), _jsx(TurbulenceModule, { binding: binding }), _jsx(CollisionModule, { binding: binding }), _jsx(WidthOverTrailModule, { binding: binding }), _jsx(SubEmittersModule, { binding: binding, rootBinding: props.binding }), _jsx(TextureSheetModule, { binding: binding }), _jsx(RendererModule, { binding: binding, textureOptions: props.textureOptions, geometryOptions: props.geometryOptions, resolveTexture: props.resolveTexture, resolveGeometry: props.resolveGeometry })] }));
32
- }
34
+ return (_jsxs("div", { style: { fontFamily: theme.font, color: theme.text }, children: [_jsx(ObjectModule, { node: props.gizmoTargetNode, label: props.gizmoTargetLabel, onBeforeChange: () => props.binding.onBeforeChange?.(), onChanged: () => props.binding.markChanged() }), !gizmoTargetIsEmitter && (_jsxs("div", { style: { fontSize: 11, color: theme.textDim, margin: '0 0 10px', lineHeight: 1.45 }, children: ["Modules below edit ", _jsx("strong", { children: activeSystem.emitter.name || 'particle system' }), ". Select its emitter in the timeline to move it with the gizmo."] })), _jsx(MainModule, { binding: binding }), _jsx(EmissionModule, { binding: binding }), _jsx(ShapeModule, { binding: binding }), _jsx(EmitDirectionModule, { binding: binding }), _jsx(VelocityOverLifeModule, { binding: binding }), _jsx(SpeedOverLifeModule, { binding: binding }), _jsx(LimitSpeedOverLifeModule, { binding: binding }), _jsx(InheritVelocityModule, { binding: binding }), _jsx(ForceOverLifeModule, { binding: binding }), _jsx(GravityModule, { binding: binding }), _jsx(OrbitOverLifeModule, { binding: binding }), _jsx(ColorOverLifeModule, { binding: binding }), _jsx(ColorBySpeedModule, { binding: binding }), _jsx(SizeOverLifeModule, { binding: binding }), _jsx(SizeBySpeedModule, { binding: binding }), _jsx(RotationOverLifeModule, { binding: binding }), _jsx(RotationBySpeedModule, { binding: binding }), _jsx(NoiseModule, { binding: binding }), _jsx(TurbulenceModule, { binding: binding }), _jsx(CollisionModule, { binding: binding }), _jsx(WidthOverTrailModule, { binding: binding }), _jsx(SubEmittersModule, { binding: binding, rootBinding: props.binding }), _jsx(TextureSheetModule, { binding: binding }), _jsx(RendererModule, { binding: binding, textureOptions: props.textureOptions, geometryOptions: props.geometryOptions, resolveTexture: props.resolveTexture, resolveGeometry: props.resolveGeometry })] }));
35
+ });
@@ -1,16 +1,20 @@
1
- import React from 'react';
2
1
  import { Scene } from '@babylonjs/core/scene';
3
2
  import { BatchedRenderer } from 'babylon.quarks';
3
+ import React from 'react';
4
4
  import { EffectBinding } from '../core/binding';
5
5
  import { EffectHistory } from '../core/history';
6
+ import type { ProjectStore } from '../core/projectStore';
6
7
  import type { GeometryData, GeometryOption, TextureOption } from './modules';
7
8
  export interface EffectEditorHostHandle {
8
9
  binding: EffectBinding;
9
10
  history: EffectHistory;
10
11
  scene: Scene;
11
12
  renderer: BatchedRenderer;
13
+ /** Replaces the scene with this effect (it is also added to Assets). */
12
14
  importJson: (json: unknown) => void;
13
- /** Loads many JSON effects at once and appends them to the gallery catalog. */
15
+ /** Imports effect files (and the texture/geometry files they reference) into Assets. */
16
+ importAssets: (files: FileList | File[]) => Promise<void>;
17
+ /** @deprecated Renamed to `importAssets`. */
14
18
  importGallery: (files: FileList | File[]) => Promise<void>;
15
19
  exportJson: () => string;
16
20
  }
@@ -33,9 +37,22 @@ export interface EffectEditorHostProps {
33
37
  href: string;
34
38
  label: string;
35
39
  };
40
+ /**
41
+ * Project to edit: its assets load from and save to `store`, and the scene is saved with
42
+ * Save / Ctrl+S. Without it the editor works in memory only.
43
+ */
44
+ project?: {
45
+ store: ProjectStore;
46
+ id: string;
47
+ name: string;
48
+ };
49
+ /** Shows a "← Projects" button; called once unsaved scene changes are dealt with. */
50
+ onExitProject?: () => void;
36
51
  }
37
52
  /**
38
- * Self-contained effect editor: live Babylon preview + hierarchy/inspector sidebar.
53
+ * Self-contained effect editor: live Babylon preview, a Scene list of the effects placed in it,
54
+ * an inspector, and a bottom dock with the timeline and the Assets browser. Assets are effect
55
+ * files; dragging one into the viewport instantiates an independent copy, as many times as needed.
39
56
  * Designed for embedding — see QuarksEffectEditor.Show for the NME-style entry point.
40
57
  */
41
58
  export declare function EffectEditorHost(props: EffectEditorHostProps): React.JSX.Element;