babylon.quarks-editor 0.27.0 → 0.28.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 +71 -0
- package/README.md +31 -4
- package/dist/core/assetTree.d.ts +5 -0
- package/dist/core/assets.d.ts +7 -0
- package/dist/core/assets.js +11 -7
- package/dist/core/binding.d.ts +9 -0
- package/dist/core/binding.js +8 -0
- package/dist/core/collision.d.ts +4 -0
- package/dist/core/collision.js +4 -0
- package/dist/core/colors.d.ts +16 -0
- package/dist/core/colors.js +8 -0
- package/dist/core/defaultEffect.d.ts +3 -2
- package/dist/core/defaultEffect.js +1 -1
- package/dist/core/editorSession.d.ts +229 -0
- package/dist/core/editorSession.js +983 -0
- package/dist/core/effectTree.d.ts +5 -0
- package/dist/core/galleryPlayback.js +2 -1
- package/dist/core/history.d.ts +6 -0
- package/dist/core/history.js +4 -0
- package/dist/core/indexedDbProjectStore.d.ts +13 -0
- package/dist/core/indexedDbProjectStore.js +13 -0
- package/dist/core/material.d.ts +12 -2
- package/dist/core/playback.d.ts +6 -0
- package/dist/core/projectStore.d.ts +26 -0
- package/dist/core/projectStore.js +1 -0
- package/dist/core/sceneEntries.d.ts +3 -0
- package/dist/core/scrub.d.ts +1 -1
- package/dist/core/scrub.js +2 -2
- package/dist/core/shapes.d.ts +24 -0
- package/dist/core/shapes.js +5 -0
- package/dist/core/systems.d.ts +2 -0
- package/dist/core/systems.js +3 -1
- package/dist/core/timeline.d.ts +23 -3
- package/dist/core/values.d.ts +21 -4
- package/dist/core/values.js +5 -0
- package/dist/index.d.ts +5 -1
- package/dist/react/AssetsPanel.d.ts +7 -0
- package/dist/react/ChoiceDialog.d.ts +7 -0
- package/dist/react/ContextMenu.d.ts +10 -1
- package/dist/react/EditorWorkspace.d.ts +7 -0
- package/dist/react/EditorWorkspace.js +6 -3
- package/dist/react/EffectEditor.d.ts +8 -2
- package/dist/react/EffectEditorHost.d.ts +32 -28
- package/dist/react/EffectEditorHost.js +241 -1665
- package/dist/react/EffectTreeView.d.ts +3 -0
- package/dist/react/MessageDialog.d.ts +5 -0
- package/dist/react/ModuleSection.d.ts +10 -0
- package/dist/react/ModuleSection.js +4 -1
- package/dist/react/ProjectsPage.d.ts +6 -0
- package/dist/react/ProjectsPage.js +27 -11
- package/dist/react/PromptDialog.d.ts +7 -0
- package/dist/react/ScenePanel.d.ts +7 -0
- package/dist/react/ScenePanel.js +7 -5
- package/dist/react/TextureSelect.d.ts +5 -5
- package/dist/react/TimelinePanel.d.ts +13 -0
- package/dist/react/TimelinePanel.js +10 -6
- package/dist/react/TimelineTrackRow.d.ts +10 -0
- package/dist/react/TransportBar.d.ts +9 -0
- package/dist/react/ValueField.d.ts +32 -5
- package/dist/react/ValueField.js +20 -9
- package/dist/react/behaviorModules.d.ts +16 -5
- package/dist/react/behaviorModules.js +16 -1
- package/dist/react/fields.d.ts +8 -0
- package/dist/react/fields.js +3 -0
- package/dist/react/host/BottomDocks.d.ts +36 -0
- package/dist/react/host/BottomDocks.js +63 -0
- package/dist/react/host/HostDialogs.d.ts +50 -0
- package/dist/react/host/HostDialogs.js +113 -0
- package/dist/react/host/InspectorSidebar.d.ts +32 -0
- package/dist/react/host/InspectorSidebar.js +59 -0
- package/dist/react/host/SceneSidebar.d.ts +53 -0
- package/dist/react/host/SceneSidebar.js +62 -0
- package/dist/react/host/ViewportPane.d.ts +56 -0
- package/dist/react/host/ViewportPane.js +91 -0
- package/dist/react/host/hostHooks.d.ts +44 -0
- package/dist/react/host/hostHooks.js +164 -0
- package/dist/react/host/hostStyles.d.ts +21 -0
- package/dist/react/host/hostStyles.js +57 -0
- package/dist/react/index.d.ts +3 -1
- package/dist/react/modules.d.ts +17 -6
- package/dist/react/modules.js +38 -20
- package/dist/react/theme.d.ts +15 -0
- package/dist/react/theme.js +15 -0
- package/dist/session.d.ts +9 -0
- package/dist/session.js +8 -0
- package/package.json +17 -11
- package/src/core/assetTree.ts +14 -1
- package/src/core/assets.ts +25 -10
- package/src/core/binding.ts +9 -0
- package/src/core/collision.ts +4 -0
- package/src/core/colors.ts +21 -1
- package/src/core/defaultEffect.ts +4 -3
- package/src/core/editorSession.ts +1299 -0
- package/src/core/effectTree.ts +5 -0
- package/src/core/galleryPlayback.ts +2 -1
- package/src/core/history.ts +6 -0
- package/src/core/indexedDbProjectStore.ts +13 -0
- package/src/core/loadEffect.ts +1 -1
- package/src/core/material.ts +14 -4
- package/src/core/playback.ts +6 -0
- package/src/core/projectStore.ts +26 -0
- package/src/core/sceneEntries.ts +3 -0
- package/src/core/scrub.ts +2 -2
- package/src/core/shapes.ts +28 -1
- package/src/core/systems.ts +9 -2
- package/src/core/timeline.ts +40 -15
- package/src/core/values.ts +20 -4
- package/src/index.ts +5 -1
- package/src/react/AssetsPanel.tsx +7 -0
- package/src/react/ChoiceDialog.tsx +7 -0
- package/src/react/ContextMenu.tsx +10 -1
- package/src/react/EditorWorkspace.tsx +11 -5
- package/src/react/EffectEditor.tsx +7 -1
- package/src/react/EffectEditorHost.tsx +406 -2340
- package/src/react/EffectTreeView.tsx +3 -0
- package/src/react/MessageDialog.tsx +5 -0
- package/src/react/ModuleSection.tsx +11 -0
- package/src/react/ProjectsPage.tsx +38 -11
- package/src/react/PromptDialog.tsx +7 -0
- package/src/react/ScenePanel.tsx +14 -5
- package/src/react/TextureSelect.tsx +6 -6
- package/src/react/TimelinePanel.tsx +23 -6
- package/src/react/TimelineTrackRow.tsx +10 -0
- package/src/react/TransportBar.tsx +9 -0
- package/src/react/ValueField.tsx +58 -14
- package/src/react/behaviorModules.tsx +34 -32
- package/src/react/fields.tsx +12 -1
- package/src/react/host/BottomDocks.tsx +178 -0
- package/src/react/host/HostDialogs.tsx +228 -0
- package/src/react/host/InspectorSidebar.tsx +147 -0
- package/src/react/host/SceneSidebar.tsx +227 -0
- package/src/react/host/ViewportPane.tsx +300 -0
- package/src/react/host/hostHooks.ts +198 -0
- package/src/react/host/hostStyles.ts +65 -0
- package/src/react/index.ts +3 -1
- package/src/react/modules.tsx +70 -44
- package/src/react/theme.ts +15 -0
- package/src/session.ts +17 -0
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type {EditorSession, PrefabInfo} from '../../core/editorSession.js';
|
|
3
|
+
import type {SceneEntry} from '../../core/sceneEntries.js';
|
|
4
|
+
import {ChoiceDialog} from '../ChoiceDialog.js';
|
|
5
|
+
import {MessageDialog} from '../MessageDialog.js';
|
|
6
|
+
import {PromptDialog} from '../PromptDialog.js';
|
|
7
|
+
import {theme} from '../theme.js';
|
|
8
|
+
|
|
9
|
+
/** Which prefab question is open: overwrite on save, save before leaving, or a new asset path. */
|
|
10
|
+
export type PrefabDialog = 'overwrite' | 'exit' | 'saveAs' | null;
|
|
11
|
+
|
|
12
|
+
/** The host's dialog and notice state, with its setters. */
|
|
13
|
+
export interface HostDialogState {
|
|
14
|
+
/** Name of the project being opened, while it loads. */
|
|
15
|
+
loadingProject: string | null;
|
|
16
|
+
/** The short notice shown, if any. */
|
|
17
|
+
toast: string | null;
|
|
18
|
+
/** Entry being saved as a new asset. */
|
|
19
|
+
saveAsTarget: SceneEntry | null;
|
|
20
|
+
setSaveAsTarget(entry: SceneEntry | null): void;
|
|
21
|
+
/** The open prefab, if any. */
|
|
22
|
+
prefabInfo: PrefabInfo | null;
|
|
23
|
+
/** The open prefab question. */
|
|
24
|
+
prefabDialog: PrefabDialog;
|
|
25
|
+
setPrefabDialog(dialog: PrefabDialog): void;
|
|
26
|
+
/** Asset waiting to replace an open prefab that has unsaved edits. */
|
|
27
|
+
prefabSwitch: {id: number; name: string} | null;
|
|
28
|
+
setPrefabSwitch(asset: {id: number; name: string} | null): void;
|
|
29
|
+
/** "Save changes to the scene?" before leaving the project. */
|
|
30
|
+
exitPrompt: boolean;
|
|
31
|
+
setExitPrompt(open: boolean): void;
|
|
32
|
+
/** The notice the user dismisses, if any. */
|
|
33
|
+
message: {title: string; message: string} | null;
|
|
34
|
+
setMessage(message: {title: string; message: string} | null): void;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Props of {@link HostDialogs}. */
|
|
38
|
+
export interface HostDialogsProps extends HostDialogState {
|
|
39
|
+
/** The session the dialogs act on. */
|
|
40
|
+
session: EditorSession | null;
|
|
41
|
+
/** Leaves the project, once the scene is dealt with. */
|
|
42
|
+
onExitProject?: () => void;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** The host's modal dialogs, the project loading screen and the toast. */
|
|
46
|
+
export function HostDialogs(props: HostDialogsProps): React.ReactElement {
|
|
47
|
+
const {session, prefabInfo, prefabDialog, setPrefabDialog, prefabSwitch, setPrefabSwitch} = props;
|
|
48
|
+
return (
|
|
49
|
+
<>
|
|
50
|
+
{props.loadingProject !== null && (
|
|
51
|
+
<div
|
|
52
|
+
style={{
|
|
53
|
+
position: 'fixed',
|
|
54
|
+
inset: 0,
|
|
55
|
+
display: 'flex',
|
|
56
|
+
alignItems: 'center',
|
|
57
|
+
justifyContent: 'center',
|
|
58
|
+
background: 'rgba(7, 11, 22, 0.8)',
|
|
59
|
+
zIndex: 900,
|
|
60
|
+
color: theme.text,
|
|
61
|
+
fontSize: 14,
|
|
62
|
+
}}
|
|
63
|
+
>
|
|
64
|
+
Opening {props.loadingProject}…
|
|
65
|
+
</div>
|
|
66
|
+
)}
|
|
67
|
+
{props.toast && (
|
|
68
|
+
<div
|
|
69
|
+
role="status"
|
|
70
|
+
style={{
|
|
71
|
+
position: 'fixed',
|
|
72
|
+
top: 16,
|
|
73
|
+
left: '50%',
|
|
74
|
+
transform: 'translateX(-50%)',
|
|
75
|
+
zIndex: 950,
|
|
76
|
+
background: '#13204a',
|
|
77
|
+
border: `1px solid ${theme.borderActive}`,
|
|
78
|
+
borderRadius: 8,
|
|
79
|
+
padding: '8px 14px',
|
|
80
|
+
fontSize: 12.5,
|
|
81
|
+
color: theme.text,
|
|
82
|
+
boxShadow: '0 8px 24px rgba(0, 0, 0, 0.4)',
|
|
83
|
+
pointerEvents: 'none',
|
|
84
|
+
}}
|
|
85
|
+
>
|
|
86
|
+
{props.toast}
|
|
87
|
+
</div>
|
|
88
|
+
)}
|
|
89
|
+
{props.saveAsTarget && (
|
|
90
|
+
<PromptDialog
|
|
91
|
+
title="Save as new asset — Folder/Name"
|
|
92
|
+
defaultValue={props.saveAsTarget.name}
|
|
93
|
+
placeholder="Fire/Big explosion"
|
|
94
|
+
confirmLabel="Save"
|
|
95
|
+
onSubmit={(path) => {
|
|
96
|
+
const entry = props.saveAsTarget!;
|
|
97
|
+
props.setSaveAsTarget(null);
|
|
98
|
+
session?.saveEntryAsAsset(entry, path);
|
|
99
|
+
}}
|
|
100
|
+
onCancel={() => props.setSaveAsTarget(null)}
|
|
101
|
+
/>
|
|
102
|
+
)}
|
|
103
|
+
{prefabDialog === 'overwrite' && prefabInfo && (
|
|
104
|
+
<ChoiceDialog
|
|
105
|
+
title={`Overwrite prefab “${prefabInfo.name}”?`}
|
|
106
|
+
message={(() => {
|
|
107
|
+
const count = session?.prefabInstanceCount() ?? 0;
|
|
108
|
+
return count > 0
|
|
109
|
+
? `The asset is replaced with your edits. The ${count} “${prefabInfo.name}” effect(s) already in the scene will not change.`
|
|
110
|
+
: 'The asset is replaced with your edits. Effects already in the scene will not change.';
|
|
111
|
+
})()}
|
|
112
|
+
actions={[
|
|
113
|
+
{label: 'Cancel', onSelect: () => setPrefabDialog(null)},
|
|
114
|
+
{label: 'Save as new…', onSelect: () => setPrefabDialog('saveAs')},
|
|
115
|
+
{
|
|
116
|
+
label: 'Overwrite',
|
|
117
|
+
primary: true,
|
|
118
|
+
onSelect: () => {
|
|
119
|
+
setPrefabDialog(null);
|
|
120
|
+
session?.overwritePrefab();
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
]}
|
|
124
|
+
onCancel={() => setPrefabDialog(null)}
|
|
125
|
+
/>
|
|
126
|
+
)}
|
|
127
|
+
{prefabSwitch && prefabInfo && (
|
|
128
|
+
<ChoiceDialog
|
|
129
|
+
title={`Save changes to prefab “${prefabInfo.name}”?`}
|
|
130
|
+
message={`Before opening “${prefabSwitch.name}”. Saving overwrites the prefab asset; effects already in the scene will not change.`}
|
|
131
|
+
actions={[
|
|
132
|
+
{label: 'Cancel', onSelect: () => setPrefabSwitch(null)},
|
|
133
|
+
{
|
|
134
|
+
label: "Don't save",
|
|
135
|
+
danger: true,
|
|
136
|
+
onSelect: () => {
|
|
137
|
+
setPrefabSwitch(null);
|
|
138
|
+
session?.openPrefab(prefabSwitch.id, true);
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
label: 'Save',
|
|
143
|
+
primary: true,
|
|
144
|
+
onSelect: () => {
|
|
145
|
+
setPrefabSwitch(null);
|
|
146
|
+
session?.overwritePrefab();
|
|
147
|
+
session?.openPrefab(prefabSwitch.id, true);
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
]}
|
|
151
|
+
onCancel={() => setPrefabSwitch(null)}
|
|
152
|
+
/>
|
|
153
|
+
)}
|
|
154
|
+
{prefabDialog === 'exit' && prefabInfo && (
|
|
155
|
+
<ChoiceDialog
|
|
156
|
+
title={`Save changes to prefab “${prefabInfo.name}”?`}
|
|
157
|
+
message="Saving overwrites the prefab asset. Effects already in the scene will not change."
|
|
158
|
+
actions={[
|
|
159
|
+
{label: 'Cancel', onSelect: () => setPrefabDialog(null)},
|
|
160
|
+
{
|
|
161
|
+
label: "Don't save",
|
|
162
|
+
danger: true,
|
|
163
|
+
onSelect: () => {
|
|
164
|
+
setPrefabDialog(null);
|
|
165
|
+
session?.closePrefab();
|
|
166
|
+
},
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
label: 'Save',
|
|
170
|
+
primary: true,
|
|
171
|
+
onSelect: () => {
|
|
172
|
+
setPrefabDialog(null);
|
|
173
|
+
session?.overwritePrefab();
|
|
174
|
+
session?.closePrefab();
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
]}
|
|
178
|
+
onCancel={() => setPrefabDialog(null)}
|
|
179
|
+
/>
|
|
180
|
+
)}
|
|
181
|
+
{prefabDialog === 'saveAs' && prefabInfo && (
|
|
182
|
+
<PromptDialog
|
|
183
|
+
title="Save prefab as new asset — Folder/Name"
|
|
184
|
+
defaultValue={prefabInfo.path.replace(/\.json$/i, '')}
|
|
185
|
+
confirmLabel="Save"
|
|
186
|
+
onSubmit={(path) => {
|
|
187
|
+
setPrefabDialog(null);
|
|
188
|
+
session?.savePrefabAsNew(path);
|
|
189
|
+
}}
|
|
190
|
+
onCancel={() => setPrefabDialog(null)}
|
|
191
|
+
/>
|
|
192
|
+
)}
|
|
193
|
+
{props.exitPrompt && (
|
|
194
|
+
<ChoiceDialog
|
|
195
|
+
title="Save changes to the scene?"
|
|
196
|
+
message="The scene has changes that are not saved. Assets are already saved."
|
|
197
|
+
actions={[
|
|
198
|
+
{label: 'Cancel', onSelect: () => props.setExitPrompt(false)},
|
|
199
|
+
{
|
|
200
|
+
label: "Don't save",
|
|
201
|
+
danger: true,
|
|
202
|
+
onSelect: () => {
|
|
203
|
+
props.setExitPrompt(false);
|
|
204
|
+
props.onExitProject?.();
|
|
205
|
+
},
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
label: 'Save',
|
|
209
|
+
primary: true,
|
|
210
|
+
onSelect: () => {
|
|
211
|
+
props.setExitPrompt(false);
|
|
212
|
+
void session?.saveScene().then((saved) => saved && props.onExitProject?.());
|
|
213
|
+
},
|
|
214
|
+
},
|
|
215
|
+
]}
|
|
216
|
+
onCancel={() => props.setExitPrompt(false)}
|
|
217
|
+
/>
|
|
218
|
+
)}
|
|
219
|
+
{props.message && (
|
|
220
|
+
<MessageDialog
|
|
221
|
+
title={props.message.title}
|
|
222
|
+
message={props.message.message}
|
|
223
|
+
onClose={() => props.setMessage(null)}
|
|
224
|
+
/>
|
|
225
|
+
)}
|
|
226
|
+
</>
|
|
227
|
+
);
|
|
228
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import type {TransformNode} from '@babylonjs/core/Meshes/transformNode.js';
|
|
2
|
+
import type {ParticleSystem} from 'babylon.quarks';
|
|
3
|
+
import React, {useState} from 'react';
|
|
4
|
+
import type {EffectBinding} from '../../core/binding.js';
|
|
5
|
+
import {EffectEditor, type EffectEditorProps} from '../EffectEditor.js';
|
|
6
|
+
import {PromptDialog} from '../PromptDialog.js';
|
|
7
|
+
import {buttonStyle, theme} from '../theme.js';
|
|
8
|
+
import {INSPECTOR_WIDTH} from './hostStyles.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The effect's JSON, with a warning when it points at object URLs: a texture
|
|
12
|
+
* dropped in from disk lives at a `blob:` URL that dies with the page, so the
|
|
13
|
+
* exported effect would load without it.
|
|
14
|
+
*/
|
|
15
|
+
export function exportEffectJSON(binding: EffectBinding, warn: (message: string) => void): string {
|
|
16
|
+
const json = binding.exportJSON('EditorEffect');
|
|
17
|
+
if (json.includes('"blob:')) {
|
|
18
|
+
warn(
|
|
19
|
+
'This effect references blob: textures, which will not load after a reload. Re-pick them from a hosted URL.'
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
return json;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Props of {@link InspectorSidebar}. */
|
|
26
|
+
export interface InspectorSidebarProps extends Pick<
|
|
27
|
+
EffectEditorProps,
|
|
28
|
+
'textureOptions' | 'geometryOptions' | 'resolveTexture' | 'resolveGeometry'
|
|
29
|
+
> {
|
|
30
|
+
/** The focused effect, for the header and its actions. */
|
|
31
|
+
binding: EffectBinding | null;
|
|
32
|
+
/** The binding, system and node the inspector shows (they may lag a selection change). */
|
|
33
|
+
inspector: {binding: EffectBinding; system: ParticleSystem; node: TransformNode} | null;
|
|
34
|
+
/** Header shown when the effect has no name. */
|
|
35
|
+
title?: string;
|
|
36
|
+
/** Called after the effect is renamed. */
|
|
37
|
+
onRenamed(name: string): void;
|
|
38
|
+
/** Shows a Save button that receives the effect's JSON. */
|
|
39
|
+
onSave?: (json: string) => void;
|
|
40
|
+
/** A short notice. */
|
|
41
|
+
toast(text: string): void;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** The editor's right column: the effect's name, Export and Save, and the module inspector. */
|
|
45
|
+
export function InspectorSidebar(props: InspectorSidebarProps): React.ReactElement {
|
|
46
|
+
const {binding, inspector, onSave} = props;
|
|
47
|
+
const [renaming, setRenaming] = useState(false);
|
|
48
|
+
return (
|
|
49
|
+
<aside
|
|
50
|
+
style={{
|
|
51
|
+
width: INSPECTOR_WIDTH,
|
|
52
|
+
flexShrink: 0,
|
|
53
|
+
borderLeft: `1px solid ${theme.border}`,
|
|
54
|
+
background: theme.panelBg,
|
|
55
|
+
display: 'flex',
|
|
56
|
+
flexDirection: 'column',
|
|
57
|
+
minHeight: 0,
|
|
58
|
+
}}
|
|
59
|
+
>
|
|
60
|
+
<div style={{padding: '12px 14px 10px', borderBottom: `1px solid ${theme.border}`}}>
|
|
61
|
+
<div
|
|
62
|
+
style={{
|
|
63
|
+
fontSize: 11,
|
|
64
|
+
fontWeight: 600,
|
|
65
|
+
color: theme.textDim,
|
|
66
|
+
textTransform: 'uppercase',
|
|
67
|
+
letterSpacing: 0.4,
|
|
68
|
+
marginBottom: 6,
|
|
69
|
+
}}
|
|
70
|
+
>
|
|
71
|
+
Inspector
|
|
72
|
+
</div>
|
|
73
|
+
<div
|
|
74
|
+
className="qe-hover-bg"
|
|
75
|
+
title={binding ? 'Rename effect' : undefined}
|
|
76
|
+
style={{
|
|
77
|
+
fontSize: 16,
|
|
78
|
+
fontWeight: 600,
|
|
79
|
+
cursor: binding ? 'pointer' : 'default',
|
|
80
|
+
borderRadius: 6,
|
|
81
|
+
padding: '2px 4px',
|
|
82
|
+
margin: '-2px -4px',
|
|
83
|
+
}}
|
|
84
|
+
onClick={() => binding && setRenaming(true)}
|
|
85
|
+
>
|
|
86
|
+
{(binding?.root.name || props.title) ?? 'Effect editor'}
|
|
87
|
+
</div>
|
|
88
|
+
{renaming && binding && (
|
|
89
|
+
<PromptDialog
|
|
90
|
+
title="Rename effect"
|
|
91
|
+
defaultValue={binding.root.name}
|
|
92
|
+
onSubmit={(name) => {
|
|
93
|
+
binding.apply(() => (binding.root.name = name));
|
|
94
|
+
props.onRenamed(name);
|
|
95
|
+
setRenaming(false);
|
|
96
|
+
}}
|
|
97
|
+
onCancel={() => setRenaming(false)}
|
|
98
|
+
/>
|
|
99
|
+
)}
|
|
100
|
+
<div style={{display: 'flex', flexWrap: 'wrap', gap: 6, marginTop: 10}}>
|
|
101
|
+
<button
|
|
102
|
+
className="qe-hover"
|
|
103
|
+
style={buttonStyle}
|
|
104
|
+
disabled={!binding}
|
|
105
|
+
onClick={() => {
|
|
106
|
+
if (!binding) return;
|
|
107
|
+
const blob = new Blob([exportEffectJSON(binding, props.toast)], {type: 'application/json'});
|
|
108
|
+
const link = document.createElement('a');
|
|
109
|
+
link.href = URL.createObjectURL(blob);
|
|
110
|
+
link.download = `${binding.root.name || 'effect'}.json`;
|
|
111
|
+
link.click();
|
|
112
|
+
URL.revokeObjectURL(link.href);
|
|
113
|
+
}}
|
|
114
|
+
>
|
|
115
|
+
Export
|
|
116
|
+
</button>
|
|
117
|
+
{onSave && (
|
|
118
|
+
<button
|
|
119
|
+
className="qe-hover"
|
|
120
|
+
style={buttonStyle}
|
|
121
|
+
disabled={!binding}
|
|
122
|
+
onClick={() => binding && onSave(exportEffectJSON(binding, props.toast))}
|
|
123
|
+
>
|
|
124
|
+
Save
|
|
125
|
+
</button>
|
|
126
|
+
)}
|
|
127
|
+
</div>
|
|
128
|
+
</div>
|
|
129
|
+
<div style={{flex: 1, minHeight: 0, overflowY: 'auto', padding: '6px 14px 20px'}}>
|
|
130
|
+
{inspector ? (
|
|
131
|
+
<EffectEditor
|
|
132
|
+
binding={inspector.binding}
|
|
133
|
+
selectedSystem={inspector.system}
|
|
134
|
+
gizmoTargetNode={inspector.node}
|
|
135
|
+
gizmoTargetLabel={inspector.node.name || 'Node'}
|
|
136
|
+
textureOptions={props.textureOptions}
|
|
137
|
+
geometryOptions={props.geometryOptions}
|
|
138
|
+
resolveTexture={props.resolveTexture}
|
|
139
|
+
resolveGeometry={props.resolveGeometry}
|
|
140
|
+
/>
|
|
141
|
+
) : (
|
|
142
|
+
<div style={{fontSize: 13, color: theme.textDim, lineHeight: 1.5}}>No effect selected.</div>
|
|
143
|
+
)}
|
|
144
|
+
</div>
|
|
145
|
+
</aside>
|
|
146
|
+
);
|
|
147
|
+
}
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import type {TransformNode} from '@babylonjs/core/Meshes/transformNode.js';
|
|
2
|
+
import {ArrowLeftIcon} from '@heroicons/react/24/solid';
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import type {PrefabInfo} from '../../core/editorSession.js';
|
|
5
|
+
import type {SceneEntry} from '../../core/sceneEntries.js';
|
|
6
|
+
import {EffectTreeView} from '../EffectTreeView.js';
|
|
7
|
+
import {iconStyle} from '../icons.js';
|
|
8
|
+
import {ScenePanel} from '../ScenePanel.js';
|
|
9
|
+
import {buttonStyle, theme} from '../theme.js';
|
|
10
|
+
import {dockIconButtonStyle, saveButtonStyle, SCENE_PANEL_WIDTH} from './hostStyles.js';
|
|
11
|
+
|
|
12
|
+
/** Props of {@link SceneSidebar}. */
|
|
13
|
+
export interface SceneSidebarProps {
|
|
14
|
+
/** The open project, for its header; none in memory-only mode. */
|
|
15
|
+
projectName?: string;
|
|
16
|
+
/** Shows the back button in the project header. */
|
|
17
|
+
canExitProject: boolean;
|
|
18
|
+
/** The scene has unsaved changes. */
|
|
19
|
+
sceneDirty: boolean;
|
|
20
|
+
/** The project's scene is still loading. */
|
|
21
|
+
projectLoading: boolean;
|
|
22
|
+
/** The open prefab, if any. */
|
|
23
|
+
prefabInfo: PrefabInfo | null;
|
|
24
|
+
/** The open prefab has unsaved edits. */
|
|
25
|
+
prefabDirty: boolean;
|
|
26
|
+
/** The scene's entries. */
|
|
27
|
+
entries: SceneEntry[];
|
|
28
|
+
/** The focused entry's root. */
|
|
29
|
+
focusedRoot: TransformNode | null;
|
|
30
|
+
/** Shown entries, by root `uniqueId`. */
|
|
31
|
+
visibleRootIds: ReadonlySet<number>;
|
|
32
|
+
/** The entry shown alone, if any. */
|
|
33
|
+
soloRootId: number | null;
|
|
34
|
+
/** The node selected in the focused effect. */
|
|
35
|
+
selectedNode: TransformNode | null;
|
|
36
|
+
/** Back to the projects list. */
|
|
37
|
+
onExitProject(): void;
|
|
38
|
+
/** Save (the scene, or the open prefab). */
|
|
39
|
+
onSave(): void;
|
|
40
|
+
/** Back from the prefab to the scene. */
|
|
41
|
+
onExitPrefab(): void;
|
|
42
|
+
/** Save the open prefab. */
|
|
43
|
+
onSavePrefab(): void;
|
|
44
|
+
/** Focus an entry. */
|
|
45
|
+
onFocus(entry: SceneEntry): void;
|
|
46
|
+
/** Show or hide an entry. */
|
|
47
|
+
onToggleVisible(entry: SceneEntry): void;
|
|
48
|
+
/** Show an entry alone. */
|
|
49
|
+
onToggleSolo(entry: SceneEntry): void;
|
|
50
|
+
/** Save an entry as a new asset. */
|
|
51
|
+
onSaveAsAsset(entry: SceneEntry): void;
|
|
52
|
+
/** Remove an entry. */
|
|
53
|
+
onDelete(entry: SceneEntry): void;
|
|
54
|
+
/** Add a default effect. */
|
|
55
|
+
onCreateDefault(): void;
|
|
56
|
+
/** Select a node, focusing its entry first when needed. */
|
|
57
|
+
onSelectNode(entry: SceneEntry | null, node: TransformNode): void;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** The editor's left column: the project header, then the Scene list or the open prefab. */
|
|
61
|
+
export function SceneSidebar(props: SceneSidebarProps): React.ReactElement {
|
|
62
|
+
const {prefabInfo, prefabDirty, entries} = props;
|
|
63
|
+
return (
|
|
64
|
+
<aside
|
|
65
|
+
style={{
|
|
66
|
+
width: SCENE_PANEL_WIDTH,
|
|
67
|
+
flexShrink: 0,
|
|
68
|
+
borderRight: `1px solid ${theme.border}`,
|
|
69
|
+
background: theme.panelBg,
|
|
70
|
+
display: 'flex',
|
|
71
|
+
flexDirection: 'column',
|
|
72
|
+
minHeight: 0,
|
|
73
|
+
}}
|
|
74
|
+
>
|
|
75
|
+
{props.projectName !== undefined && (
|
|
76
|
+
<div
|
|
77
|
+
style={{
|
|
78
|
+
display: 'flex',
|
|
79
|
+
alignItems: 'center',
|
|
80
|
+
gap: 6,
|
|
81
|
+
padding: '8px 10px',
|
|
82
|
+
borderBottom: `1px solid ${theme.border}`,
|
|
83
|
+
}}
|
|
84
|
+
>
|
|
85
|
+
{props.canExitProject && (
|
|
86
|
+
<button
|
|
87
|
+
type="button"
|
|
88
|
+
className="qe-hover"
|
|
89
|
+
title="Back to projects"
|
|
90
|
+
style={{...dockIconButtonStyle, width: 26, height: 26}}
|
|
91
|
+
onClick={props.onExitProject}
|
|
92
|
+
>
|
|
93
|
+
<ArrowLeftIcon style={iconStyle(13)} />
|
|
94
|
+
</button>
|
|
95
|
+
)}
|
|
96
|
+
<span
|
|
97
|
+
title={props.projectName}
|
|
98
|
+
style={{
|
|
99
|
+
flex: 1,
|
|
100
|
+
minWidth: 0,
|
|
101
|
+
fontSize: 13,
|
|
102
|
+
fontWeight: 600,
|
|
103
|
+
overflow: 'hidden',
|
|
104
|
+
textOverflow: 'ellipsis',
|
|
105
|
+
whiteSpace: 'nowrap',
|
|
106
|
+
}}
|
|
107
|
+
>
|
|
108
|
+
{props.projectName}
|
|
109
|
+
{props.sceneDirty ? ' *' : ''}
|
|
110
|
+
</span>
|
|
111
|
+
<button
|
|
112
|
+
type="button"
|
|
113
|
+
className="qe-hover"
|
|
114
|
+
title="Save the scene (Ctrl+S) — assets are not changed"
|
|
115
|
+
disabled={props.projectLoading}
|
|
116
|
+
style={saveButtonStyle(props.sceneDirty, '4px 12px')}
|
|
117
|
+
onClick={props.onSave}
|
|
118
|
+
>
|
|
119
|
+
Save
|
|
120
|
+
</button>
|
|
121
|
+
</div>
|
|
122
|
+
)}
|
|
123
|
+
<div style={{padding: '12px 14px 10px', borderBottom: `1px solid ${theme.border}`}}>
|
|
124
|
+
<div style={{fontSize: 14, fontWeight: 600}}>
|
|
125
|
+
{prefabInfo ? 'Prefab' : `Scene${entries.length > 0 ? ` · ${entries.length}` : ''}`}
|
|
126
|
+
</div>
|
|
127
|
+
<div style={{fontSize: 11, color: theme.textDim, marginTop: 4, lineHeight: 1.4}}>
|
|
128
|
+
{prefabInfo
|
|
129
|
+
? 'The scene is set aside while you edit this asset'
|
|
130
|
+
: 'Effects in the viewport · click to edit · eye shows/hides'}
|
|
131
|
+
</div>
|
|
132
|
+
</div>
|
|
133
|
+
<div
|
|
134
|
+
style={{
|
|
135
|
+
flex: 1,
|
|
136
|
+
minHeight: 0,
|
|
137
|
+
overflow: 'hidden',
|
|
138
|
+
padding: '8px 12px 14px',
|
|
139
|
+
display: 'flex',
|
|
140
|
+
flexDirection: 'column',
|
|
141
|
+
}}
|
|
142
|
+
>
|
|
143
|
+
{prefabInfo ? (
|
|
144
|
+
<div style={{display: 'flex', flexDirection: 'column', gap: 10, flex: 1, minHeight: 0}}>
|
|
145
|
+
<div
|
|
146
|
+
style={{
|
|
147
|
+
display: 'flex',
|
|
148
|
+
flexDirection: 'column',
|
|
149
|
+
gap: 10,
|
|
150
|
+
padding: 12,
|
|
151
|
+
border: `1px solid ${theme.borderActive}`,
|
|
152
|
+
borderRadius: 8,
|
|
153
|
+
background: 'rgba(120, 165, 255, 0.08)',
|
|
154
|
+
fontSize: 12,
|
|
155
|
+
color: theme.textDim,
|
|
156
|
+
lineHeight: 1.5,
|
|
157
|
+
}}
|
|
158
|
+
>
|
|
159
|
+
<div style={{fontSize: 13, fontWeight: 600, color: theme.text}}>
|
|
160
|
+
{prefabInfo.name}
|
|
161
|
+
{prefabDirty ? ' *' : ''}
|
|
162
|
+
</div>
|
|
163
|
+
<div style={{wordBreak: 'break-all'}}>{prefabInfo.path}</div>
|
|
164
|
+
<div>
|
|
165
|
+
Save (Ctrl+S) overwrites this asset. Effects already placed in the scene keep their own
|
|
166
|
+
copy and do not change.
|
|
167
|
+
</div>
|
|
168
|
+
<div style={{display: 'flex', gap: 6}}>
|
|
169
|
+
<button
|
|
170
|
+
type="button"
|
|
171
|
+
className="qe-hover"
|
|
172
|
+
style={buttonStyle}
|
|
173
|
+
onClick={props.onExitPrefab}
|
|
174
|
+
>
|
|
175
|
+
← Scene
|
|
176
|
+
</button>
|
|
177
|
+
<button
|
|
178
|
+
type="button"
|
|
179
|
+
className="qe-hover"
|
|
180
|
+
style={buttonStyle}
|
|
181
|
+
onClick={props.onSavePrefab}
|
|
182
|
+
>
|
|
183
|
+
Save prefab
|
|
184
|
+
</button>
|
|
185
|
+
</div>
|
|
186
|
+
</div>
|
|
187
|
+
{entries[0] && (
|
|
188
|
+
<div
|
|
189
|
+
style={{
|
|
190
|
+
flex: 1,
|
|
191
|
+
minHeight: 0,
|
|
192
|
+
overflowY: 'auto',
|
|
193
|
+
border: `1px solid ${theme.border}`,
|
|
194
|
+
borderRadius: 8,
|
|
195
|
+
background: theme.sectionBg,
|
|
196
|
+
padding: '4px 0',
|
|
197
|
+
}}
|
|
198
|
+
>
|
|
199
|
+
<EffectTreeView
|
|
200
|
+
includeRoot
|
|
201
|
+
root={entries[0].root}
|
|
202
|
+
selectedNode={props.selectedNode}
|
|
203
|
+
onSelect={(node) => props.onSelectNode(null, node)}
|
|
204
|
+
/>
|
|
205
|
+
</div>
|
|
206
|
+
)}
|
|
207
|
+
</div>
|
|
208
|
+
) : (
|
|
209
|
+
<ScenePanel
|
|
210
|
+
entries={entries}
|
|
211
|
+
focusedRoot={props.focusedRoot}
|
|
212
|
+
visibleRootIds={props.visibleRootIds}
|
|
213
|
+
onFocus={props.onFocus}
|
|
214
|
+
onToggleVisible={props.onToggleVisible}
|
|
215
|
+
soloRootId={props.soloRootId}
|
|
216
|
+
onToggleSolo={props.onToggleSolo}
|
|
217
|
+
onSaveAsAsset={props.onSaveAsAsset}
|
|
218
|
+
onDelete={props.onDelete}
|
|
219
|
+
onCreateDefault={props.onCreateDefault}
|
|
220
|
+
selectedNode={props.selectedNode}
|
|
221
|
+
onSelectNode={props.onSelectNode}
|
|
222
|
+
/>
|
|
223
|
+
)}
|
|
224
|
+
</div>
|
|
225
|
+
</aside>
|
|
226
|
+
);
|
|
227
|
+
}
|