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.
- package/CHANGELOG.md +28 -0
- package/README.md +6 -6
- package/dist/core/assetDragSession.d.ts +4 -0
- package/dist/core/assetDragSession.js +8 -0
- package/dist/core/assetTree.d.ts +39 -0
- package/dist/core/assetTree.js +112 -0
- package/dist/core/assets.d.ts +56 -0
- package/dist/core/assets.js +207 -0
- package/dist/core/binding.d.ts +6 -1
- package/dist/core/binding.js +7 -0
- package/dist/core/collision.d.ts +4 -5
- package/dist/core/collision.js +4 -5
- package/dist/core/colors.d.ts +1 -1
- package/dist/core/defaultEffect.d.ts +4 -5
- package/dist/core/defaultEffect.js +5 -14
- package/dist/core/effectTree.d.ts +1 -1
- package/dist/core/effectTree.js +18 -4
- package/dist/core/emitterShapeWireframe.d.ts +2 -1
- package/dist/core/emitterShapeWireframe.js +2 -1
- package/dist/core/galleryDropMarker.js +1 -1
- package/dist/core/galleryPlayback.d.ts +7 -7
- package/dist/core/galleryPlayback.js +2 -2
- package/dist/core/galleryScene.d.ts +8 -13
- package/dist/core/galleryScene.js +14 -42
- package/dist/core/gallerySelectionMarker.d.ts +1 -0
- package/dist/core/gallerySelectionMarker.js +1 -0
- package/dist/core/history.d.ts +12 -1
- package/dist/core/history.js +16 -3
- package/dist/core/indexedDbProjectStore.d.ts +35 -0
- package/dist/core/indexedDbProjectStore.js +299 -0
- package/dist/core/lineShaders.d.ts +2 -0
- package/dist/core/lineShaders.js +7 -0
- package/dist/core/loadEffect.d.ts +2 -2
- package/dist/core/loadEffect.js +1 -1
- package/dist/core/pickAssetFiles.d.ts +6 -0
- package/dist/core/{pickGalleryFiles.js → pickAssetFiles.js} +13 -8
- package/dist/core/projectStore.d.ts +61 -0
- package/dist/core/projectStore.js +14 -0
- package/dist/core/sceneEntries.d.ts +35 -0
- package/dist/core/sceneEntries.js +62 -0
- package/dist/core/shapes.d.ts +1 -1
- package/dist/core/shapes.js +35 -8
- package/dist/core/systems.d.ts +1 -1
- package/dist/core/systems.js +3 -1
- package/dist/core/timeline.d.ts +1 -2
- package/dist/core/transport.d.ts +12 -0
- package/dist/core/transport.js +36 -0
- package/dist/core/values.d.ts +1 -1
- package/dist/core/viewportPicker.d.ts +4 -4
- package/dist/core/viewportPicker.js +3 -3
- package/dist/index.d.ts +15 -11
- package/dist/index.js +8 -6
- package/dist/react/AssetsPanel.d.ts +22 -0
- package/dist/react/AssetsPanel.js +252 -0
- package/dist/react/ChoiceDialog.d.ts +18 -0
- package/dist/react/ChoiceDialog.js +48 -0
- package/dist/react/ContextMenu.d.ts +16 -0
- package/dist/react/ContextMenu.js +70 -0
- package/dist/react/CurveEditor.d.ts +1 -2
- package/dist/react/CurveEditor.js +14 -3
- package/dist/react/EditorWorkspace.d.ts +21 -0
- package/dist/react/EditorWorkspace.js +63 -0
- package/dist/react/EffectEditor.d.ts +3 -3
- package/dist/react/EffectEditor.js +9 -6
- package/dist/react/EffectEditorHost.d.ts +20 -3
- package/dist/react/EffectEditorHost.js +1426 -558
- package/dist/react/EffectTreeView.d.ts +17 -0
- package/dist/react/EffectTreeView.js +52 -0
- package/dist/react/GradientEditor.d.ts +1 -2
- package/dist/react/GradientEditor.js +8 -1
- package/dist/react/MessageDialog.js +7 -1
- package/dist/react/ModuleSection.js +10 -3
- package/dist/react/ObjectModule.d.ts +5 -2
- package/dist/react/ObjectModule.js +8 -11
- package/dist/react/ProjectsPage.d.ts +9 -0
- package/dist/react/ProjectsPage.js +140 -0
- package/dist/react/ScenePanel.d.ts +26 -0
- package/dist/react/ScenePanel.js +134 -0
- package/dist/react/SubEmittersModule.d.ts +1 -2
- package/dist/react/TimelinePanel.d.ts +11 -4
- package/dist/react/TimelinePanel.js +65 -40
- package/dist/react/TimelineTrackRow.d.ts +1 -1
- package/dist/react/TimelineTrackRow.js +20 -6
- package/dist/react/TransportBar.d.ts +24 -0
- package/dist/react/TransportBar.js +43 -0
- package/dist/react/ValueField.d.ts +1 -2
- package/dist/react/behaviorModules.js +24 -12
- package/dist/react/index.d.ts +31 -17
- package/dist/react/index.js +18 -11
- package/dist/react/inspectorHints.js +1 -2
- package/dist/react/modules.js +55 -25
- package/package.json +68 -68
- package/src/core/assetDragSession.ts +12 -0
- package/src/core/assetTree.ts +148 -0
- package/src/core/assets.ts +262 -0
- package/src/core/binding.ts +10 -2
- package/src/core/collision.ts +4 -5
- package/src/core/colors.ts +1 -1
- package/src/core/defaultEffect.ts +10 -23
- package/src/core/effectTree.ts +22 -7
- package/src/core/emitterShapeWireframe.ts +9 -4
- package/src/core/galleryDropMarker.ts +6 -2
- package/src/core/galleryPlayback.ts +9 -9
- package/src/core/galleryScene.ts +15 -63
- package/src/core/gallerySelectionMarker.ts +6 -1
- package/src/core/history.ts +24 -3
- package/src/core/indexedDbProjectStore.ts +357 -0
- package/src/core/lineShaders.ts +7 -0
- package/src/core/loadEffect.ts +3 -3
- package/src/core/{pickGalleryFiles.ts → pickAssetFiles.ts} +16 -8
- package/src/core/projectStore.ts +81 -0
- package/src/core/sceneEntries.ts +96 -0
- package/src/core/shapes.ts +48 -10
- package/src/core/systems.ts +9 -3
- package/src/core/timeline.ts +8 -8
- package/src/core/transport.ts +53 -0
- package/src/core/values.ts +1 -1
- package/src/core/viewportPicker.ts +5 -5
- package/src/index.ts +29 -18
- package/src/react/AssetsPanel.tsx +559 -0
- package/src/react/ChoiceDialog.tsx +93 -0
- package/src/react/ContextMenu.tsx +107 -0
- package/src/react/CurveEditor.tsx +40 -10
- package/src/react/EditorWorkspace.tsx +95 -0
- package/src/react/EffectEditor.tsx +28 -16
- package/src/react/EffectEditorHost.tsx +2005 -721
- package/src/react/EffectTreeView.tsx +117 -0
- package/src/react/GradientEditor.tsx +21 -4
- package/src/react/InspectorInfoIcon.tsx +6 -2
- package/src/react/MessageDialog.tsx +9 -1
- package/src/react/ModuleSection.tsx +12 -3
- package/src/react/ObjectModule.tsx +41 -16
- package/src/react/ProjectsPage.tsx +306 -0
- package/src/react/PromptDialog.tsx +5 -1
- package/src/react/ScenePanel.tsx +318 -0
- package/src/react/SubEmittersModule.tsx +1 -2
- package/src/react/TimelinePanel.tsx +171 -99
- package/src/react/TimelineTrackRow.tsx +71 -18
- package/src/react/TransportBar.tsx +114 -0
- package/src/react/ValueField.tsx +0 -1
- package/src/react/behaviorModules.tsx +148 -42
- package/src/react/index.ts +47 -33
- package/src/react/inspectorHints.ts +1 -2
- package/src/react/modules.tsx +173 -57
- package/dist/core/galleryDragSession.d.ts +0 -4
- package/dist/core/galleryDragSession.js +0 -8
- package/dist/core/galleryTree.d.ts +0 -49
- package/dist/core/galleryTree.js +0 -138
- package/dist/core/loadEffectGallery.d.ts +0 -36
- package/dist/core/loadEffectGallery.js +0 -97
- package/dist/core/pickGalleryFiles.d.ts +0 -5
- package/dist/react/GalleryPanel.d.ts +0 -20
- package/dist/react/GalleryPanel.js +0 -246
- package/src/core/galleryDragSession.ts +0 -12
- package/src/core/galleryTree.ts +0 -187
- package/src/core/loadEffectGallery.ts +0 -146
- 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
|
-
}):
|
|
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
|
|
74
|
-
|
|
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) => ({
|
|
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
|
|
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
|
-
/**
|
|
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
|
|
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;
|