babylon.quarks-editor 0.17.7 → 0.17.9
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 +46 -0
- package/README.md +1 -1
- package/dist/core/binding.js +2 -5
- package/dist/core/defaultEffect.d.ts +12 -0
- package/dist/core/defaultEffect.js +43 -0
- package/dist/core/editorScene.d.ts +7 -0
- package/dist/core/editorScene.js +11 -0
- package/dist/core/effectTree.js +2 -1
- package/dist/core/emitterShapeWireframe.d.ts +20 -0
- package/dist/core/emitterShapeWireframe.js +329 -0
- package/dist/core/galleryDragSession.d.ts +4 -0
- package/dist/core/galleryDragSession.js +8 -0
- package/dist/core/galleryDropMarker.d.ts +13 -0
- package/dist/core/galleryDropMarker.js +42 -0
- package/dist/core/galleryLayout.d.ts +20 -0
- package/dist/core/galleryLayout.js +68 -0
- package/dist/core/galleryPlayback.d.ts +9 -0
- package/dist/core/galleryPlayback.js +43 -0
- package/dist/core/galleryScene.d.ts +12 -0
- package/dist/core/galleryScene.js +41 -0
- package/dist/core/gallerySelectionMarker.d.ts +25 -0
- package/dist/core/gallerySelectionMarker.js +69 -0
- package/dist/core/galleryTree.d.ts +49 -0
- package/dist/core/galleryTree.js +138 -0
- package/dist/core/loadEffect.d.ts +13 -0
- package/dist/core/loadEffect.js +31 -17
- package/dist/core/loadEffectGallery.d.ts +36 -0
- package/dist/core/loadEffectGallery.js +97 -0
- package/dist/core/material.d.ts +13 -0
- package/dist/core/material.js +68 -0
- package/dist/core/pickGalleryFiles.d.ts +5 -0
- package/dist/core/pickGalleryFiles.js +42 -0
- package/dist/core/playback.d.ts +23 -0
- package/dist/core/playback.js +32 -0
- package/dist/core/scrub.d.ts +5 -0
- package/dist/core/scrub.js +30 -0
- package/dist/core/timeline.d.ts +3 -1
- package/dist/core/timeline.js +8 -4
- package/dist/core/transformStore.d.ts +3 -0
- package/dist/core/transformStore.js +16 -0
- package/dist/core/viewportPicker.d.ts +25 -0
- package/dist/core/viewportPicker.js +86 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/react/EffectEditor.d.ts +3 -0
- package/dist/react/EffectEditor.js +3 -1
- package/dist/react/EffectEditorHost.d.ts +7 -0
- package/dist/react/EffectEditorHost.js +717 -102
- package/dist/react/GalleryPanel.d.ts +19 -0
- package/dist/react/GalleryPanel.js +215 -0
- package/dist/react/InspectorInfoIcon.d.ts +7 -0
- package/dist/react/InspectorInfoIcon.js +22 -0
- package/dist/react/InspectorLabel.d.ts +7 -0
- package/dist/react/InspectorLabel.js +12 -0
- package/dist/react/MessageDialog.d.ts +9 -0
- package/dist/react/MessageDialog.js +24 -0
- package/dist/react/ModuleSection.d.ts +2 -0
- package/dist/react/ModuleSection.js +8 -1
- package/dist/react/ObjectModule.d.ts +6 -0
- package/dist/react/ObjectModule.js +40 -0
- package/dist/react/TimelinePanel.d.ts +11 -15
- package/dist/react/TimelinePanel.js +129 -65
- package/dist/react/TimelineTrackRow.d.ts +2 -2
- package/dist/react/TimelineTrackRow.js +41 -23
- package/dist/react/Tooltip.d.ts +6 -0
- package/dist/react/Tooltip.js +65 -0
- package/dist/react/ValueField.d.ts +2 -0
- package/dist/react/ValueField.js +1 -1
- package/dist/react/behaviorModules.js +8 -8
- package/dist/react/fields.d.ts +2 -0
- package/dist/react/fields.js +14 -5
- package/dist/react/icons.d.ts +7 -0
- package/dist/react/icons.js +10 -0
- package/dist/react/index.d.ts +4 -0
- package/dist/react/index.js +2 -0
- package/dist/react/inspectorHints.d.ts +2 -0
- package/dist/react/inspectorHints.js +138 -0
- package/dist/react/modules.js +109 -81
- package/dist/react/theme.d.ts +2 -0
- package/dist/react/theme.js +97 -22
- package/package.json +72 -63
- package/src/core/binding.ts +102 -105
- package/src/core/collision.ts +40 -40
- package/src/core/defaultEffect.ts +74 -0
- package/src/core/editorScene.ts +14 -0
- package/src/core/effectTree.ts +2 -1
- package/src/core/emitterShapeWireframe.ts +373 -0
- package/src/core/galleryDragSession.ts +12 -0
- package/src/core/galleryDropMarker.ts +57 -0
- package/src/core/galleryLayout.ts +90 -0
- package/src/core/galleryPlayback.ts +55 -0
- package/src/core/galleryScene.ts +58 -0
- package/src/core/gallerySelectionMarker.ts +84 -0
- package/src/core/galleryTree.ts +187 -0
- package/src/core/groups.ts +29 -29
- package/src/core/loadEffect.ts +139 -102
- package/src/core/loadEffectGallery.ts +146 -0
- package/src/core/material.ts +84 -0
- package/src/core/pickGalleryFiles.ts +58 -0
- package/src/core/playback.ts +49 -0
- package/src/core/scrub.ts +60 -28
- package/src/core/shapes.ts +133 -133
- package/src/core/systems.ts +98 -98
- package/src/core/timeline.ts +98 -94
- package/src/core/transformStore.ts +19 -0
- package/src/core/values.ts +113 -113
- package/src/core/viewportPicker.ts +114 -0
- package/src/index.ts +34 -32
- package/src/react/CurveEditor.tsx +168 -168
- package/src/react/EffectEditor.tsx +123 -111
- package/src/react/EffectEditorHost.tsx +1162 -377
- package/src/react/GalleryPanel.tsx +453 -0
- package/src/react/GradientEditor.tsx +127 -127
- package/src/react/InspectorInfoIcon.tsx +36 -0
- package/src/react/InspectorLabel.tsx +18 -0
- package/src/react/MessageDialog.tsx +57 -0
- package/src/react/ModuleSection.tsx +69 -54
- package/src/react/ObjectModule.tsx +58 -0
- package/src/react/PromptDialog.tsx +85 -85
- package/src/react/TimelinePanel.tsx +448 -354
- package/src/react/TimelineTrackRow.tsx +326 -267
- package/src/react/Tooltip.tsx +93 -0
- package/src/react/ValueField.tsx +3 -1
- package/src/react/behaviorModules.tsx +663 -658
- package/src/react/fields.tsx +85 -76
- package/src/react/icons.ts +19 -0
- package/src/react/index.ts +47 -43
- package/src/react/inspectorHints.ts +150 -0
- package/src/react/modules.tsx +1049 -1034
- package/src/react/theme.ts +161 -85
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { TransformNode } from '@babylonjs/core/Meshes/transformNode';
|
|
3
|
+
import type { GalleryEntry, GalleryLoadProgress } from '../core/loadEffectGallery';
|
|
4
|
+
export interface GalleryPanelProps {
|
|
5
|
+
entries: GalleryEntry[];
|
|
6
|
+
/** Effect shown in the inspector and timeline. */
|
|
7
|
+
focusedRoot: TransformNode | null;
|
|
8
|
+
/** In-scene effects that keep simulating while transport plays. */
|
|
9
|
+
previewRootIds: ReadonlySet<number>;
|
|
10
|
+
loading: GalleryLoadProgress | null;
|
|
11
|
+
folderInputRef: React.RefObject<HTMLInputElement | null>;
|
|
12
|
+
onFocus: (entry: GalleryEntry) => void;
|
|
13
|
+
onTogglePreview: (entry: GalleryEntry) => void;
|
|
14
|
+
onImportFiles: (files: FileList | File[]) => void;
|
|
15
|
+
onImportFolder: () => void;
|
|
16
|
+
onCreateDefault: () => void;
|
|
17
|
+
}
|
|
18
|
+
/** Folder-tree catalog; import effects here and drag them into the viewport. */
|
|
19
|
+
export declare function GalleryPanel(props: GalleryPanelProps): React.ReactElement;
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useCallback, useMemo, useRef, useState } from 'react';
|
|
3
|
+
import { useVirtualizer } from '@tanstack/react-virtual';
|
|
4
|
+
import { CheckCircleIcon, ChevronDownIcon, ChevronRightIcon, FolderIcon, FolderOpenIcon, SparklesIcon, } from '@heroicons/react/24/solid';
|
|
5
|
+
import { setGalleryDragSession } from '../core/galleryDragSession';
|
|
6
|
+
import { buildGalleryTree, filterGalleryTree, flattenVisibleGalleryRows, writeGalleryDrag, } from '../core/galleryTree';
|
|
7
|
+
import { iconStyle } from './icons';
|
|
8
|
+
import { buttonStyle, hiddenFileInputStyle, inputStyle, theme } from './theme';
|
|
9
|
+
const GALLERY_ROW_HEIGHT = 34;
|
|
10
|
+
function beginGalleryDrag(e, payload) {
|
|
11
|
+
writeGalleryDrag(e.dataTransfer, payload);
|
|
12
|
+
setGalleryDragSession(payload);
|
|
13
|
+
}
|
|
14
|
+
function endGalleryDrag() {
|
|
15
|
+
setGalleryDragSession(null);
|
|
16
|
+
}
|
|
17
|
+
function EffectRow(props) {
|
|
18
|
+
const { entry, depth, focused, inPreview, onFocus, onTogglePreview } = props;
|
|
19
|
+
const isLooping = entry.systems.some((system) => system.looping);
|
|
20
|
+
return (_jsxs("div", { style: {
|
|
21
|
+
display: 'flex',
|
|
22
|
+
alignItems: 'stretch',
|
|
23
|
+
width: '100%',
|
|
24
|
+
height: GALLERY_ROW_HEIGHT,
|
|
25
|
+
borderBottom: `1px solid ${theme.border}`,
|
|
26
|
+
background: focused ? 'rgba(120, 165, 255, 0.22)' : inPreview ? 'rgba(120, 165, 255, 0.08)' : 'transparent',
|
|
27
|
+
boxSizing: 'border-box',
|
|
28
|
+
}, children: [entry.inScene ? (_jsx("button", { type: "button", className: "qe-hover", title: inPreview ? 'Remove from stage preview' : 'Preview on stage (keep playing)', style: {
|
|
29
|
+
flexShrink: 0,
|
|
30
|
+
width: 28,
|
|
31
|
+
border: 'none',
|
|
32
|
+
borderRight: `1px solid ${theme.border}`,
|
|
33
|
+
background: 'transparent',
|
|
34
|
+
color: inPreview ? theme.accent : theme.textDim,
|
|
35
|
+
cursor: 'pointer',
|
|
36
|
+
display: 'inline-flex',
|
|
37
|
+
alignItems: 'center',
|
|
38
|
+
justifyContent: 'center',
|
|
39
|
+
padding: 0,
|
|
40
|
+
}, onClick: (e) => {
|
|
41
|
+
e.stopPropagation();
|
|
42
|
+
onTogglePreview(entry);
|
|
43
|
+
}, children: inPreview ? _jsx(CheckCircleIcon, { style: iconStyle(14) }) : _jsx("span", { style: { width: 10, height: 10, border: `1px solid currentColor`, borderRadius: 2, boxSizing: 'border-box' } }) })) : (_jsx("span", { style: { width: 28, flexShrink: 0, borderRight: `1px solid ${theme.border}` } })), _jsxs("button", { type: "button", draggable: true, className: "qe-hover-bg", title: entry.inScene
|
|
44
|
+
? `${entry.path} — in scene · Ctrl+click toggles preview`
|
|
45
|
+
: `${entry.path} — drag into viewport`, style: {
|
|
46
|
+
display: 'flex',
|
|
47
|
+
alignItems: 'center',
|
|
48
|
+
gap: 8,
|
|
49
|
+
flex: 1,
|
|
50
|
+
minWidth: 0,
|
|
51
|
+
height: '100%',
|
|
52
|
+
textAlign: 'left',
|
|
53
|
+
border: 'none',
|
|
54
|
+
background: 'transparent',
|
|
55
|
+
color: focused ? theme.text : theme.textDim,
|
|
56
|
+
padding: `0 12px 0 ${8 + depth * 14}px`,
|
|
57
|
+
fontSize: 12.5,
|
|
58
|
+
cursor: 'grab',
|
|
59
|
+
fontFamily: theme.font,
|
|
60
|
+
}, onClick: (e) => {
|
|
61
|
+
if ((e.ctrlKey || e.metaKey) && entry.inScene) {
|
|
62
|
+
onTogglePreview(entry);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
onFocus(entry);
|
|
66
|
+
}, onDragStart: (e) => beginGalleryDrag(e, { kind: 'effect', id: entry.root.uniqueId }), onDragEnd: endGalleryDrag, children: [_jsx("span", { style: { width: 14, flexShrink: 0, opacity: entry.inScene ? 1 : 0.35, color: entry.inScene ? theme.accent : theme.textDim, display: 'inline-flex' }, children: entry.inScene ? _jsx(SparklesIcon, { style: iconStyle(14) }) : _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" }))] })] }));
|
|
67
|
+
}
|
|
68
|
+
function FolderRow(props) {
|
|
69
|
+
const { node, depth, effectCount, open, onToggle } = props;
|
|
70
|
+
return (_jsxs("button", { type: "button", draggable: effectCount > 0, className: "qe-hover-bg", title: effectCount > 0
|
|
71
|
+
? `${node.path} — drag ${effectCount} effect(s) into viewport`
|
|
72
|
+
: `${node.path} — empty folder`, style: {
|
|
73
|
+
display: 'flex',
|
|
74
|
+
alignItems: 'center',
|
|
75
|
+
gap: 6,
|
|
76
|
+
width: '100%',
|
|
77
|
+
height: GALLERY_ROW_HEIGHT,
|
|
78
|
+
boxSizing: 'border-box',
|
|
79
|
+
textAlign: 'left',
|
|
80
|
+
border: 'none',
|
|
81
|
+
borderBottom: `1px solid ${theme.border}`,
|
|
82
|
+
background: 'transparent',
|
|
83
|
+
color: theme.textDim,
|
|
84
|
+
padding: `0 12px 0 ${12 + depth * 14}px`,
|
|
85
|
+
fontSize: 12,
|
|
86
|
+
fontWeight: 600,
|
|
87
|
+
cursor: effectCount > 0 ? 'grab' : 'pointer',
|
|
88
|
+
fontFamily: theme.font,
|
|
89
|
+
}, onClick: () => onToggle(node.path), onDragStart: (e) => {
|
|
90
|
+
if (effectCount === 0) {
|
|
91
|
+
e.preventDefault();
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
beginGalleryDrag(e, { kind: 'folder', path: node.path });
|
|
95
|
+
}, onDragEnd: endGalleryDrag, children: [_jsx("span", { style: { width: 14, display: 'inline-flex', color: theme.textDim }, children: open ? _jsx(ChevronDownIcon, { style: iconStyle(12) }) : _jsx(ChevronRightIcon, { style: iconStyle(12) }) }), _jsx("span", { style: { width: 14, display: 'inline-flex', color: theme.accent }, children: open ? _jsx(FolderOpenIcon, { style: iconStyle(14) }) : _jsx(FolderIcon, { style: iconStyle(14) }) }), _jsx("span", { style: { flex: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }, children: node.name }), effectCount > 0 && (_jsx("span", { style: { fontSize: 11, fontWeight: 500, color: theme.textDim, flexShrink: 0 }, children: effectCount }))] }));
|
|
96
|
+
}
|
|
97
|
+
function flatRowKey(row) {
|
|
98
|
+
return row.kind === 'folder' ? `folder:${row.node.path}` : `effect:${row.node.entry.root.uniqueId}`;
|
|
99
|
+
}
|
|
100
|
+
function VirtualGalleryTree(props) {
|
|
101
|
+
const { rows, closedFolders, focusedRoot, previewRootIds, onFocus, onTogglePreview, onToggleFolder } = props;
|
|
102
|
+
const scrollRef = useRef(null);
|
|
103
|
+
const virtualizer = useVirtualizer({
|
|
104
|
+
count: rows.length,
|
|
105
|
+
getScrollElement: () => scrollRef.current,
|
|
106
|
+
estimateSize: () => GALLERY_ROW_HEIGHT,
|
|
107
|
+
overscan: 10,
|
|
108
|
+
});
|
|
109
|
+
return (_jsx("div", { ref: scrollRef, style: {
|
|
110
|
+
flex: 1,
|
|
111
|
+
minHeight: 120,
|
|
112
|
+
overflowY: 'auto',
|
|
113
|
+
border: `1px solid ${theme.border}`,
|
|
114
|
+
borderRadius: 8,
|
|
115
|
+
background: theme.sectionBg,
|
|
116
|
+
}, children: _jsx("div", { style: { height: virtualizer.getTotalSize(), position: 'relative', width: '100%' }, children: virtualizer.getVirtualItems().map((virtualRow) => {
|
|
117
|
+
const row = rows[virtualRow.index];
|
|
118
|
+
const open = row.kind === 'folder' && !closedFolders.has(row.node.path);
|
|
119
|
+
return (_jsx("div", { style: {
|
|
120
|
+
position: 'absolute',
|
|
121
|
+
top: 0,
|
|
122
|
+
left: 0,
|
|
123
|
+
width: '100%',
|
|
124
|
+
height: virtualRow.size,
|
|
125
|
+
transform: `translateY(${virtualRow.start}px)`,
|
|
126
|
+
}, children: row.kind === 'folder' ? (_jsx(FolderRow, { node: row.node, depth: row.depth, effectCount: row.effectCount, open: open, onToggle: onToggleFolder })) : (_jsx(EffectRow, { entry: row.node.entry, depth: row.depth, focused: focusedRoot === row.node.entry.root, inPreview: previewRootIds.has(row.node.entry.root.uniqueId), onFocus: onFocus, onTogglePreview: onTogglePreview })) }, flatRowKey(row)));
|
|
127
|
+
}) }) }));
|
|
128
|
+
}
|
|
129
|
+
/** Folder-tree catalog; import effects here and drag them into the viewport. */
|
|
130
|
+
export function GalleryPanel(props) {
|
|
131
|
+
const { entries, focusedRoot, previewRootIds, loading, folderInputRef, onFocus, onTogglePreview, onImportFiles, onImportFolder, onCreateDefault } = props;
|
|
132
|
+
const [query, setQuery] = useState('');
|
|
133
|
+
const [closedFolders, setClosedFolders] = useState(() => new Set());
|
|
134
|
+
const fileInputRef = useRef(null);
|
|
135
|
+
const tree = useMemo(() => buildGalleryTree(entries), [entries]);
|
|
136
|
+
const filtered = useMemo(() => filterGalleryTree(tree, query), [tree, query]);
|
|
137
|
+
const expandAll = query.trim().length > 0;
|
|
138
|
+
const flatRows = useMemo(() => flattenVisibleGalleryRows(filtered, closedFolders, expandAll), [filtered, closedFolders, expandAll]);
|
|
139
|
+
const inSceneCount = useMemo(() => entries.filter((entry) => entry.inScene).length, [entries]);
|
|
140
|
+
const previewCount = useMemo(() => entries.filter((entry) => entry.inScene && previewRootIds.has(entry.root.uniqueId)).length, [entries, previewRootIds]);
|
|
141
|
+
const isLoading = loading !== null;
|
|
142
|
+
const toggleFolder = useCallback((path) => {
|
|
143
|
+
setClosedFolders((prev) => {
|
|
144
|
+
const next = new Set(prev);
|
|
145
|
+
if (next.has(path)) {
|
|
146
|
+
next.delete(path);
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
next.add(path);
|
|
150
|
+
}
|
|
151
|
+
return next;
|
|
152
|
+
});
|
|
153
|
+
}, []);
|
|
154
|
+
return (_jsxs("div", { style: { display: 'flex', flexDirection: 'column', gap: 10, height: '100%', minHeight: 0 }, children: [_jsxs("div", { style: { display: 'flex', flexWrap: 'wrap', gap: 6 }, children: [_jsx("button", { type: "button", className: "qe-hover", style: buttonStyle, disabled: isLoading, onClick: onCreateDefault, children: "New effect" }), _jsxs("label", { className: "qe-hover", style: {
|
|
155
|
+
...buttonStyle,
|
|
156
|
+
position: 'relative',
|
|
157
|
+
display: 'inline-flex',
|
|
158
|
+
alignItems: 'center',
|
|
159
|
+
cursor: isLoading ? 'not-allowed' : 'pointer',
|
|
160
|
+
opacity: isLoading ? 0.5 : 1,
|
|
161
|
+
}, children: ["Import effect", _jsx("input", { ref: fileInputRef, type: "file", multiple: true, disabled: isLoading, tabIndex: -1, style: hiddenFileInputStyle, onChange: (e) => {
|
|
162
|
+
// Snapshot before clearing — FileList is live and empties with value=''.
|
|
163
|
+
const files = Array.from(e.target.files ?? []);
|
|
164
|
+
e.target.value = '';
|
|
165
|
+
if (files.length === 0)
|
|
166
|
+
return;
|
|
167
|
+
onImportFiles(files);
|
|
168
|
+
} })] }), _jsxs("label", { className: "qe-hover", style: {
|
|
169
|
+
...buttonStyle,
|
|
170
|
+
position: 'relative',
|
|
171
|
+
display: 'inline-flex',
|
|
172
|
+
alignItems: 'center',
|
|
173
|
+
cursor: isLoading ? 'not-allowed' : 'pointer',
|
|
174
|
+
opacity: isLoading ? 0.5 : 1,
|
|
175
|
+
}, onClick: (e) => {
|
|
176
|
+
if (typeof window.showDirectoryPicker === 'function') {
|
|
177
|
+
e.preventDefault();
|
|
178
|
+
onImportFolder();
|
|
179
|
+
}
|
|
180
|
+
}, children: ["Import folder", _jsx("input", { ref: (el) => {
|
|
181
|
+
folderInputRef.current = el;
|
|
182
|
+
if (el) {
|
|
183
|
+
el.setAttribute('webkitdirectory', '');
|
|
184
|
+
el.setAttribute('directory', '');
|
|
185
|
+
el.multiple = true;
|
|
186
|
+
}
|
|
187
|
+
}, type: "file", multiple: true, disabled: isLoading, tabIndex: -1, style: hiddenFileInputStyle, onChange: (e) => {
|
|
188
|
+
const files = Array.from(e.target.files ?? []);
|
|
189
|
+
e.target.value = '';
|
|
190
|
+
if (files.length === 0)
|
|
191
|
+
return;
|
|
192
|
+
onImportFiles(files);
|
|
193
|
+
} })] })] }), _jsx("div", { style: { fontSize: 12, color: theme.textDim, lineHeight: 1.45 }, children: isLoading
|
|
194
|
+
? `Loading… ${loading.done} / ${loading.total || '…'} files`
|
|
195
|
+
: `${previewCount} previewing · ${inSceneCount} in scene · ${entries.length} total` }), _jsx("div", { style: { fontSize: 11, color: theme.textDim, lineHeight: 1.4 }, children: "Preview plays on stage \u00B7 Click = edit \u00B7 Checkbox or Ctrl+click toggles preview." }), _jsx("input", { className: "qe-hover", style: inputStyle, placeholder: "Filter by name or path\u2026", value: query, onChange: (e) => setQuery(e.target.value) }), entries.length === 0 && !isLoading ? (_jsxs("div", { style: {
|
|
196
|
+
flex: 1,
|
|
197
|
+
minHeight: 120,
|
|
198
|
+
padding: '14px 12px',
|
|
199
|
+
fontSize: 12,
|
|
200
|
+
color: theme.textDim,
|
|
201
|
+
lineHeight: 1.5,
|
|
202
|
+
border: `1px solid ${theme.border}`,
|
|
203
|
+
borderRadius: 8,
|
|
204
|
+
background: theme.sectionBg,
|
|
205
|
+
}, children: ["Catalog is empty. Click ", _jsx("strong", { children: "New effect" }), ", or ", _jsx("strong", { children: "Import effect" }), " / ", _jsx("strong", { children: "Import folder" }), "."] })) : filtered.length === 0 ? (_jsx("div", { style: {
|
|
206
|
+
flex: 1,
|
|
207
|
+
minHeight: 120,
|
|
208
|
+
padding: '10px 12px',
|
|
209
|
+
fontSize: 12,
|
|
210
|
+
color: theme.textDim,
|
|
211
|
+
border: `1px solid ${theme.border}`,
|
|
212
|
+
borderRadius: 8,
|
|
213
|
+
background: theme.sectionBg,
|
|
214
|
+
}, children: "No matches." })) : (_jsx(VirtualGalleryTree, { rows: flatRows, closedFolders: closedFolders, focusedRoot: focusedRoot, previewRootIds: previewRootIds, onFocus: onFocus, onTogglePreview: onTogglePreview, onToggleFolder: toggleFolder }))] }));
|
|
215
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { InformationCircleIcon } from '@heroicons/react/24/solid';
|
|
3
|
+
import { iconStyle } from './icons';
|
|
4
|
+
import { theme } from './theme';
|
|
5
|
+
import { Tooltip } from './Tooltip';
|
|
6
|
+
/** Small info icon with a themed inspector tooltip. */
|
|
7
|
+
export function InspectorInfoIcon(props) {
|
|
8
|
+
const size = props.size ?? 13;
|
|
9
|
+
return (_jsx(Tooltip, { content: props.content, children: _jsx("span", { tabIndex: 0, role: "button", "aria-label": props.content, style: {
|
|
10
|
+
display: 'inline-flex',
|
|
11
|
+
flexShrink: 0,
|
|
12
|
+
color: theme.textDim,
|
|
13
|
+
cursor: 'help',
|
|
14
|
+
opacity: 0.85,
|
|
15
|
+
outline: 'none',
|
|
16
|
+
}, onClick: props.onClick, onKeyDown: (e) => {
|
|
17
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
18
|
+
e.preventDefault();
|
|
19
|
+
e.stopPropagation();
|
|
20
|
+
}
|
|
21
|
+
}, children: _jsx(InformationCircleIcon, { style: iconStyle(size) }) }) }));
|
|
22
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { getInspectorHint } from './inspectorHints';
|
|
3
|
+
import { InspectorInfoIcon } from './InspectorInfoIcon';
|
|
4
|
+
import { labelStyle } from './theme';
|
|
5
|
+
/** Inspector row label with an optional info icon tooltip. */
|
|
6
|
+
export function InspectorLabel(props) {
|
|
7
|
+
if (!props.label) {
|
|
8
|
+
return null;
|
|
9
|
+
}
|
|
10
|
+
const tooltip = props.hint ?? getInspectorHint(props.hintKey ?? props.label);
|
|
11
|
+
return (_jsxs("span", { style: { ...labelStyle, display: 'inline-flex', alignItems: 'center', gap: 4, minWidth: 0 }, children: [_jsx("span", { style: { overflow: 'hidden', textOverflow: 'ellipsis' }, children: props.label }), tooltip ? _jsx(InspectorInfoIcon, { content: tooltip }) : null] }));
|
|
12
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
export interface MessageDialogProps {
|
|
3
|
+
title: string;
|
|
4
|
+
message: string;
|
|
5
|
+
confirmLabel?: string;
|
|
6
|
+
onClose: () => void;
|
|
7
|
+
}
|
|
8
|
+
/** Modal for info and error messages — same chrome as PromptDialog, no text input. */
|
|
9
|
+
export declare function MessageDialog(props: MessageDialogProps): React.ReactElement;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { buttonStyle, theme } from './theme';
|
|
3
|
+
/** Modal for info and error messages — same chrome as PromptDialog, no text input. */
|
|
4
|
+
export function MessageDialog(props) {
|
|
5
|
+
const { title, message, confirmLabel = 'OK', onClose } = props;
|
|
6
|
+
return (_jsx("div", { style: {
|
|
7
|
+
position: 'fixed',
|
|
8
|
+
inset: 0,
|
|
9
|
+
background: 'rgba(3, 5, 12, 0.6)',
|
|
10
|
+
display: 'flex',
|
|
11
|
+
alignItems: 'center',
|
|
12
|
+
justifyContent: 'center',
|
|
13
|
+
zIndex: 1000,
|
|
14
|
+
fontFamily: theme.font,
|
|
15
|
+
}, onClick: onClose, children: _jsxs("div", { style: {
|
|
16
|
+
background: theme.panelBg,
|
|
17
|
+
border: `1px solid ${theme.border}`,
|
|
18
|
+
borderRadius: 10,
|
|
19
|
+
padding: 16,
|
|
20
|
+
minWidth: 320,
|
|
21
|
+
maxWidth: 480,
|
|
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 }) })] }) }));
|
|
24
|
+
}
|
|
@@ -2,6 +2,8 @@ import React from 'react';
|
|
|
2
2
|
/** Unity-style module foldout with an optional enable checkbox. */
|
|
3
3
|
export declare function ModuleSection(props: {
|
|
4
4
|
title: string;
|
|
5
|
+
hint?: string;
|
|
6
|
+
hintKey?: string;
|
|
5
7
|
children: React.ReactNode;
|
|
6
8
|
enabled?: boolean;
|
|
7
9
|
onToggle?: (enabled: boolean) => void;
|
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useState } from 'react';
|
|
3
|
+
import { ChevronDownIcon, ChevronRightIcon } from '@heroicons/react/24/solid';
|
|
4
|
+
import { getInspectorHint } from './inspectorHints';
|
|
5
|
+
import { InspectorInfoIcon } from './InspectorInfoIcon';
|
|
3
6
|
import { theme } from './theme';
|
|
7
|
+
import { iconStyle } from './icons';
|
|
4
8
|
/** Unity-style module foldout with an optional enable checkbox. */
|
|
5
9
|
export function ModuleSection(props) {
|
|
6
10
|
const [open, setOpen] = useState(props.defaultOpen ?? true);
|
|
7
11
|
const toggleable = props.onToggle !== undefined;
|
|
8
12
|
const dimmed = toggleable && props.enabled === false;
|
|
13
|
+
const titleHint = props.hint ?? getInspectorHint(props.hintKey ?? props.title);
|
|
14
|
+
const objectHint = props.title.startsWith('Object — ') ? getInspectorHint('Object') : undefined;
|
|
15
|
+
const tooltip = titleHint ?? objectHint;
|
|
9
16
|
return (_jsxs("section", { style: {
|
|
10
17
|
border: `1px solid ${theme.border}`,
|
|
11
18
|
borderRadius: 10,
|
|
@@ -20,5 +27,5 @@ export function ModuleSection(props) {
|
|
|
20
27
|
cursor: 'pointer',
|
|
21
28
|
userSelect: 'none',
|
|
22
29
|
background: 'rgba(20, 30, 60, 0.5)',
|
|
23
|
-
}, onClick: () => setOpen(!open), children: [_jsx("span", { style: { color: theme.accent,
|
|
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 })] }));
|
|
24
31
|
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useSyncExternalStore } from 'react';
|
|
3
|
+
import { Quaternion } from '@babylonjs/core/Maths/math.vector';
|
|
4
|
+
import { getTransformRevision, notifyTransformChanged, subscribeTransform } from '../core/transformStore';
|
|
5
|
+
import { ModuleSection } from './ModuleSection';
|
|
6
|
+
import { NumberField, Row } from './fields';
|
|
7
|
+
const RAD_TO_DEG = 180 / Math.PI;
|
|
8
|
+
const DEG_TO_RAD = Math.PI / 180;
|
|
9
|
+
function getEulerDegrees(node) {
|
|
10
|
+
const euler = node.rotationQuaternion ? node.rotationQuaternion.toEulerAngles() : node.rotation;
|
|
11
|
+
return { x: euler.x * RAD_TO_DEG, y: euler.y * RAD_TO_DEG, z: euler.z * RAD_TO_DEG };
|
|
12
|
+
}
|
|
13
|
+
function setEulerDegreeAxis(node, axis, degrees) {
|
|
14
|
+
const euler = node.rotationQuaternion ? node.rotationQuaternion.toEulerAngles() : node.rotation.clone();
|
|
15
|
+
euler[axis] = degrees * DEG_TO_RAD;
|
|
16
|
+
if (node.rotationQuaternion) {
|
|
17
|
+
node.rotationQuaternion = Quaternion.FromEulerVector(euler);
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
node.rotation = euler;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function AxisRow(props) {
|
|
24
|
+
return (_jsx(Row, { label: props.label, hintKey: props.hintKey, children: _jsxs("div", { style: { display: 'flex', gap: 4 }, children: [_jsx(NumberField, { value: props.values.x, step: props.step, onChange: (v) => props.onChange('x', v) }), _jsx(NumberField, { value: props.values.y, step: props.step, onChange: (v) => props.onChange('y', v) }), _jsx(NumberField, { value: props.values.z, step: props.step, onChange: (v) => props.onChange('z', v) })] }) }));
|
|
25
|
+
}
|
|
26
|
+
export function ObjectModule(props) {
|
|
27
|
+
useSyncExternalStore(subscribeTransform, getTransformRevision);
|
|
28
|
+
const { node } = props;
|
|
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
|
+
} })] }));
|
|
40
|
+
}
|
|
@@ -1,32 +1,28 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import type { ParticleSystem, BatchedRenderer } from 'babylon.quarks';
|
|
3
2
|
import type { Scene } from '@babylonjs/core/scene';
|
|
3
|
+
import type { TransformNode } from '@babylonjs/core/Meshes/transformNode';
|
|
4
4
|
import type { EffectBinding } from '../core/binding';
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
speed: number;
|
|
8
|
-
stepQueued: boolean;
|
|
9
|
-
elapsed: number;
|
|
10
|
-
scrubbing: boolean;
|
|
11
|
-
}
|
|
5
|
+
import { type PlaybackState } from '../core/playback';
|
|
6
|
+
export type { PlaybackState } from '../core/playback';
|
|
12
7
|
export interface TimelinePanelProps {
|
|
13
8
|
binding: EffectBinding;
|
|
14
|
-
|
|
15
|
-
|
|
9
|
+
selectedNode: TransformNode | null;
|
|
10
|
+
onSelectNode: (node: TransformNode | null) => void;
|
|
16
11
|
scene: Scene;
|
|
17
|
-
renderer: BatchedRenderer;
|
|
18
12
|
playbackRef: React.MutableRefObject<PlaybackState>;
|
|
19
13
|
playheadRef: React.RefObject<HTMLDivElement>;
|
|
20
14
|
counterRef: React.RefObject<HTMLSpanElement>;
|
|
21
|
-
|
|
22
|
-
|
|
15
|
+
stagePlaying: boolean;
|
|
16
|
+
setStagePlaying: (playing: boolean) => void;
|
|
23
17
|
speed: number;
|
|
24
18
|
setSpeed: (speed: number) => void;
|
|
19
|
+
/** False when a catalog entry is selected but not yet dropped into the viewport. */
|
|
20
|
+
playbackEnabled?: boolean;
|
|
25
21
|
}
|
|
26
22
|
/** Small gap between the label column and the first pixel of the time axis. */
|
|
27
23
|
export declare const TIMELINE_INSET = 10;
|
|
28
24
|
/**
|
|
29
|
-
* Bottom-docked
|
|
30
|
-
*
|
|
25
|
+
* Bottom-docked timeline: stage Play/Pause drives all preview effects; playhead/scrub/restart
|
|
26
|
+
* apply only to the focused effect without stopping other previews.
|
|
31
27
|
*/
|
|
32
28
|
export declare function TimelinePanel(props: TimelinePanelProps): React.ReactElement;
|