babylon.quarks-editor 0.26.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.
Files changed (211) hide show
  1. package/CHANGELOG.md +134 -0
  2. package/README.md +69 -9
  3. package/dist/core/assetDragSession.d.ts +1 -1
  4. package/dist/core/assetTree.d.ts +6 -1
  5. package/dist/core/assets.d.ts +7 -0
  6. package/dist/core/assets.js +11 -7
  7. package/dist/core/binding.d.ts +11 -2
  8. package/dist/core/binding.js +9 -1
  9. package/dist/core/collision.d.ts +4 -0
  10. package/dist/core/collision.js +4 -0
  11. package/dist/core/colors.d.ts +16 -0
  12. package/dist/core/colors.js +8 -0
  13. package/dist/core/defaultEffect.d.ts +5 -4
  14. package/dist/core/defaultEffect.js +4 -4
  15. package/dist/core/editorScene.d.ts +1 -1
  16. package/dist/core/editorSession.d.ts +229 -0
  17. package/dist/core/editorSession.js +983 -0
  18. package/dist/core/effectTree.d.ts +6 -1
  19. package/dist/core/effectTree.js +13 -88
  20. package/dist/core/emitterShapeWireframe.d.ts +2 -2
  21. package/dist/core/emitterShapeWireframe.js +45 -13
  22. package/dist/core/galleryDropMarker.d.ts +2 -2
  23. package/dist/core/galleryDropMarker.js +6 -6
  24. package/dist/core/galleryLayout.d.ts +3 -3
  25. package/dist/core/galleryLayout.js +1 -1
  26. package/dist/core/galleryPlayback.d.ts +1 -1
  27. package/dist/core/galleryPlayback.js +2 -1
  28. package/dist/core/galleryScene.d.ts +4 -4
  29. package/dist/core/galleryScene.js +3 -3
  30. package/dist/core/gallerySelectionMarker.d.ts +4 -4
  31. package/dist/core/gallerySelectionMarker.js +5 -5
  32. package/dist/core/groups.d.ts +1 -1
  33. package/dist/core/groups.js +1 -1
  34. package/dist/core/history.d.ts +7 -1
  35. package/dist/core/history.js +4 -0
  36. package/dist/core/indexedDbProjectStore.d.ts +14 -1
  37. package/dist/core/indexedDbProjectStore.js +13 -0
  38. package/dist/core/lineShaders.d.ts +2 -2
  39. package/dist/core/lineShaders.js +2 -2
  40. package/dist/core/loadEffect.d.ts +2 -2
  41. package/dist/core/loadEffect.js +1 -1
  42. package/dist/core/material.d.ts +12 -2
  43. package/dist/core/pickAssetFiles.js +1 -1
  44. package/dist/core/playback.d.ts +6 -0
  45. package/dist/core/projectStore.d.ts +26 -0
  46. package/dist/core/projectStore.js +1 -0
  47. package/dist/core/sceneEntries.d.ts +7 -4
  48. package/dist/core/sceneEntries.js +2 -2
  49. package/dist/core/scrub.d.ts +2 -2
  50. package/dist/core/scrub.js +2 -2
  51. package/dist/core/shapes.d.ts +25 -1
  52. package/dist/core/shapes.js +5 -0
  53. package/dist/core/systems.d.ts +4 -2
  54. package/dist/core/systems.js +3 -1
  55. package/dist/core/textureExport.d.ts +1 -1
  56. package/dist/core/timeline.d.ts +25 -5
  57. package/dist/core/transport.d.ts +3 -3
  58. package/dist/core/transport.js +2 -2
  59. package/dist/core/values.d.ts +21 -4
  60. package/dist/core/values.js +5 -0
  61. package/dist/core/viewportPicker.d.ts +4 -4
  62. package/dist/core/viewportPicker.js +8 -7
  63. package/dist/index.d.ts +25 -21
  64. package/dist/index.js +12 -12
  65. package/dist/react/AssetsPanel.d.ts +9 -2
  66. package/dist/react/AssetsPanel.js +6 -6
  67. package/dist/react/ChoiceDialog.d.ts +7 -0
  68. package/dist/react/ChoiceDialog.js +1 -1
  69. package/dist/react/ContextMenu.d.ts +10 -1
  70. package/dist/react/ContextMenu.js +1 -1
  71. package/dist/react/CurveEditor.d.ts +1 -1
  72. package/dist/react/CurveEditor.js +2 -2
  73. package/dist/react/EditorWorkspace.d.ts +9 -2
  74. package/dist/react/EditorWorkspace.js +10 -7
  75. package/dist/react/EffectEditor.d.ts +11 -5
  76. package/dist/react/EffectEditor.js +7 -7
  77. package/dist/react/EffectEditorHost.d.ts +33 -29
  78. package/dist/react/EffectEditorHost.js +247 -1674
  79. package/dist/react/EffectTreeView.d.ts +4 -1
  80. package/dist/react/EffectTreeView.js +3 -3
  81. package/dist/react/GradientEditor.d.ts +1 -1
  82. package/dist/react/GradientEditor.js +3 -3
  83. package/dist/react/InspectorInfoIcon.js +3 -3
  84. package/dist/react/InspectorLabel.js +3 -3
  85. package/dist/react/MessageDialog.d.ts +5 -0
  86. package/dist/react/MessageDialog.js +1 -1
  87. package/dist/react/ModuleSection.d.ts +10 -0
  88. package/dist/react/ModuleSection.js +8 -5
  89. package/dist/react/ObjectModule.d.ts +1 -1
  90. package/dist/react/ObjectModule.js +4 -4
  91. package/dist/react/ProjectsPage.d.ts +7 -1
  92. package/dist/react/ProjectsPage.js +33 -17
  93. package/dist/react/PromptDialog.d.ts +7 -0
  94. package/dist/react/PromptDialog.js +1 -1
  95. package/dist/react/ScenePanel.d.ts +9 -2
  96. package/dist/react/ScenePanel.js +12 -10
  97. package/dist/react/SubEmittersModule.d.ts +1 -1
  98. package/dist/react/SubEmittersModule.js +5 -5
  99. package/dist/react/TextureSelect.d.ts +24 -0
  100. package/dist/react/TextureSelect.js +74 -0
  101. package/dist/react/TextureUVField.d.ts +14 -0
  102. package/dist/react/TextureUVField.js +23 -0
  103. package/dist/react/TimelinePanel.d.ts +20 -7
  104. package/dist/react/TimelinePanel.js +21 -15
  105. package/dist/react/TimelineTrackRow.d.ts +13 -3
  106. package/dist/react/TimelineTrackRow.js +6 -6
  107. package/dist/react/TransportBar.d.ts +13 -4
  108. package/dist/react/TransportBar.js +3 -3
  109. package/dist/react/ValueField.d.ts +32 -5
  110. package/dist/react/ValueField.js +22 -11
  111. package/dist/react/behaviorModules.d.ts +19 -6
  112. package/dist/react/behaviorModules.js +68 -17
  113. package/dist/react/fields.d.ts +9 -0
  114. package/dist/react/fields.js +6 -3
  115. package/dist/react/host/BottomDocks.d.ts +36 -0
  116. package/dist/react/host/BottomDocks.js +63 -0
  117. package/dist/react/host/HostDialogs.d.ts +50 -0
  118. package/dist/react/host/HostDialogs.js +113 -0
  119. package/dist/react/host/InspectorSidebar.d.ts +32 -0
  120. package/dist/react/host/InspectorSidebar.js +59 -0
  121. package/dist/react/host/SceneSidebar.d.ts +53 -0
  122. package/dist/react/host/SceneSidebar.js +62 -0
  123. package/dist/react/host/ViewportPane.d.ts +56 -0
  124. package/dist/react/host/ViewportPane.js +91 -0
  125. package/dist/react/host/hostHooks.d.ts +44 -0
  126. package/dist/react/host/hostHooks.js +164 -0
  127. package/dist/react/host/hostStyles.d.ts +21 -0
  128. package/dist/react/host/hostStyles.js +57 -0
  129. package/dist/react/index.d.ts +42 -38
  130. package/dist/react/index.js +25 -23
  131. package/dist/react/inspectorHints.js +31 -0
  132. package/dist/react/modules.d.ts +19 -6
  133. package/dist/react/modules.js +134 -117
  134. package/dist/react/theme.d.ts +15 -0
  135. package/dist/react/theme.js +15 -0
  136. package/dist/session.d.ts +9 -0
  137. package/dist/session.js +8 -0
  138. package/package.json +18 -11
  139. package/src/core/assetDragSession.ts +1 -1
  140. package/src/core/assetTree.ts +15 -2
  141. package/src/core/assets.ts +25 -10
  142. package/src/core/binding.ts +12 -3
  143. package/src/core/collision.ts +4 -0
  144. package/src/core/colors.ts +21 -1
  145. package/src/core/defaultEffect.ts +8 -7
  146. package/src/core/editorScene.ts +1 -1
  147. package/src/core/editorSession.ts +1299 -0
  148. package/src/core/effectTree.ts +18 -101
  149. package/src/core/emitterShapeWireframe.ts +48 -16
  150. package/src/core/galleryDropMarker.ts +8 -8
  151. package/src/core/galleryLayout.ts +4 -4
  152. package/src/core/galleryPlayback.ts +3 -2
  153. package/src/core/galleryScene.ts +5 -5
  154. package/src/core/gallerySelectionMarker.ts +7 -7
  155. package/src/core/groups.ts +1 -1
  156. package/src/core/history.ts +7 -1
  157. package/src/core/indexedDbProjectStore.ts +14 -1
  158. package/src/core/lineShaders.ts +2 -2
  159. package/src/core/loadEffect.ts +4 -4
  160. package/src/core/material.ts +14 -4
  161. package/src/core/pickAssetFiles.ts +2 -2
  162. package/src/core/playback.ts +6 -0
  163. package/src/core/projectStore.ts +26 -0
  164. package/src/core/sceneEntries.ts +8 -5
  165. package/src/core/scrub.ts +3 -3
  166. package/src/core/shapes.ts +29 -2
  167. package/src/core/systems.ts +11 -4
  168. package/src/core/textureExport.ts +1 -1
  169. package/src/core/timeline.ts +43 -18
  170. package/src/core/transport.ts +4 -4
  171. package/src/core/values.ts +20 -4
  172. package/src/core/viewportPicker.ts +12 -11
  173. package/src/index.ts +25 -21
  174. package/src/react/AssetsPanel.tsx +13 -6
  175. package/src/react/ChoiceDialog.tsx +8 -1
  176. package/src/react/ContextMenu.tsx +11 -2
  177. package/src/react/CurveEditor.tsx +2 -2
  178. package/src/react/EditorWorkspace.tsx +16 -10
  179. package/src/react/EffectEditor.tsx +17 -9
  180. package/src/react/EffectEditorHost.tsx +415 -2354
  181. package/src/react/EffectTreeView.tsx +7 -4
  182. package/src/react/GradientEditor.tsx +3 -3
  183. package/src/react/InspectorInfoIcon.tsx +3 -3
  184. package/src/react/InspectorLabel.tsx +3 -3
  185. package/src/react/MessageDialog.tsx +6 -1
  186. package/src/react/ModuleSection.tsx +15 -4
  187. package/src/react/ObjectModule.tsx +5 -5
  188. package/src/react/ProjectsPage.tsx +44 -17
  189. package/src/react/PromptDialog.tsx +8 -1
  190. package/src/react/ScenePanel.tsx +21 -12
  191. package/src/react/SubEmittersModule.tsx +6 -6
  192. package/src/react/TextureSelect.tsx +118 -0
  193. package/src/react/TextureUVField.tsx +62 -0
  194. package/src/react/TimelinePanel.tsx +40 -21
  195. package/src/react/TimelineTrackRow.tsx +19 -9
  196. package/src/react/TransportBar.tsx +16 -7
  197. package/src/react/ValueField.tsx +60 -16
  198. package/src/react/behaviorModules.tsx +240 -55
  199. package/src/react/fields.tsx +22 -4
  200. package/src/react/host/BottomDocks.tsx +178 -0
  201. package/src/react/host/HostDialogs.tsx +228 -0
  202. package/src/react/host/InspectorSidebar.tsx +147 -0
  203. package/src/react/host/SceneSidebar.tsx +227 -0
  204. package/src/react/host/ViewportPane.tsx +300 -0
  205. package/src/react/host/hostHooks.ts +198 -0
  206. package/src/react/host/hostStyles.ts +65 -0
  207. package/src/react/index.ts +43 -38
  208. package/src/react/inspectorHints.ts +40 -0
  209. package/src/react/modules.tsx +440 -170
  210. package/src/react/theme.ts +15 -0
  211. package/src/session.ts +17 -0
@@ -0,0 +1,178 @@
1
+ import {ChevronDownIcon, ChevronUpIcon, MapPinIcon, XMarkIcon} from '@heroicons/react/24/solid';
2
+ import React from 'react';
3
+ import {AssetsPanel, type AssetsPanelProps} from '../AssetsPanel.js';
4
+ import {iconStyle} from '../icons.js';
5
+ import {buttonStyle, theme} from '../theme.js';
6
+ import {
7
+ activeSquareButtonStyle,
8
+ dockIconButtonStyle,
9
+ dockTitleStyle,
10
+ DRAWER_HEIGHT,
11
+ TIMELINE_HEIGHT,
12
+ } from './hostStyles.js';
13
+
14
+ /** Props of {@link AssetsDrawer}. */
15
+ export interface AssetsDrawerProps {
16
+ /** The drawer is shown. */
17
+ open: boolean;
18
+ /** The drawer is docked above the timeline instead of floating over the viewport. */
19
+ pinned: boolean;
20
+ /** Pin or unpin it (it stays open). */
21
+ onTogglePin(): void;
22
+ /** Close and unpin it. */
23
+ onClose(): void;
24
+ /** The Assets browser inside. */
25
+ panel: AssetsPanelProps;
26
+ }
27
+
28
+ /** The Assets browser, floating over the viewport or pinned above the timeline. */
29
+ export function AssetsDrawer(props: AssetsDrawerProps): React.ReactElement {
30
+ const {open, pinned} = props;
31
+ return (
32
+ <div
33
+ aria-hidden={!open && !pinned}
34
+ style={{
35
+ height: DRAWER_HEIGHT,
36
+ display: 'flex',
37
+ flexDirection: 'column',
38
+ background: theme.panelBg,
39
+ borderTop: `1px solid ${theme.border}`,
40
+ ...(pinned
41
+ ? {flexShrink: 0}
42
+ : {
43
+ position: 'absolute',
44
+ left: 0,
45
+ right: 0,
46
+ bottom: 0,
47
+ zIndex: 20,
48
+ background: '#0a0f20',
49
+ boxShadow: '0 -10px 30px rgba(0, 0, 0, 0.5)',
50
+ transform: open ? 'none' : 'translateY(100%)',
51
+ opacity: open ? 1 : 0,
52
+ pointerEvents: open ? 'auto' : 'none',
53
+ transition: 'transform 0.18s ease, opacity 0.18s ease',
54
+ }),
55
+ }}
56
+ >
57
+ <div
58
+ style={{
59
+ display: 'flex',
60
+ alignItems: 'center',
61
+ gap: 6,
62
+ padding: '4px 6px 4px 12px',
63
+ borderBottom: `1px solid ${theme.border}`,
64
+ }}
65
+ >
66
+ <span style={dockTitleStyle}>Assets</span>
67
+ <span style={{fontSize: 11, color: theme.textDim}}>Ctrl+Space</span>
68
+ <span style={{flex: 1}} />
69
+ <button
70
+ type="button"
71
+ className="qe-hover"
72
+ title={pinned ? 'Unpin — float over the viewport again' : 'Pin — keep it docked above the timeline'}
73
+ style={{...dockIconButtonStyle, ...(pinned ? activeSquareButtonStyle : {})}}
74
+ onClick={props.onTogglePin}
75
+ >
76
+ <MapPinIcon style={iconStyle(13)} />
77
+ </button>
78
+ <button
79
+ type="button"
80
+ className="qe-hover"
81
+ title="Close (Esc)"
82
+ style={dockIconButtonStyle}
83
+ onClick={props.onClose}
84
+ >
85
+ <XMarkIcon style={iconStyle(13)} />
86
+ </button>
87
+ </div>
88
+ <AssetsPanel {...props.panel} />
89
+ </div>
90
+ );
91
+ }
92
+
93
+ /** Props of {@link TimelineDock}. */
94
+ export interface TimelineDockProps {
95
+ /** The timeline is collapsed to its header. */
96
+ collapsed: boolean;
97
+ /** Collapse or expand it. */
98
+ onToggleCollapsed(): void;
99
+ /** Name of the focused effect, if any. */
100
+ effectName?: string;
101
+ /** Assets in the project, counted on the Assets button. */
102
+ assetCount: number;
103
+ /** The Assets button shows as switched on. */
104
+ assetsActive: boolean;
105
+ /** The Assets button. */
106
+ onToggleAssets(): void;
107
+ /** The timeline, or `null` when nothing is focused. */
108
+ children: React.ReactNode;
109
+ }
110
+
111
+ /** The bottom dock: the focused effect's timeline, and the Assets button. */
112
+ export function TimelineDock(props: TimelineDockProps): React.ReactElement {
113
+ const {collapsed} = props;
114
+ return (
115
+ <div
116
+ style={{
117
+ flexShrink: 0,
118
+ borderTop: `1px solid ${theme.border}`,
119
+ background: theme.panelBg,
120
+ display: 'flex',
121
+ flexDirection: 'column',
122
+ }}
123
+ >
124
+ <div style={{display: 'flex', alignItems: 'center', gap: 6, padding: '3px 6px'}}>
125
+ <button
126
+ type="button"
127
+ className="qe-hover-bg"
128
+ title={collapsed ? 'Expand timeline' : 'Collapse timeline'}
129
+ style={{...dockIconButtonStyle, border: 'none', background: 'transparent'}}
130
+ onClick={props.onToggleCollapsed}
131
+ >
132
+ {collapsed ? <ChevronUpIcon style={iconStyle(13)} /> : <ChevronDownIcon style={iconStyle(13)} />}
133
+ </button>
134
+ <span style={dockTitleStyle}>Timeline</span>
135
+ {props.effectName !== undefined && (
136
+ <span
137
+ style={{
138
+ fontSize: 12,
139
+ color: theme.textDim,
140
+ overflow: 'hidden',
141
+ textOverflow: 'ellipsis',
142
+ whiteSpace: 'nowrap',
143
+ }}
144
+ >
145
+ {props.effectName}
146
+ </span>
147
+ )}
148
+ <span style={{flex: 1}} />
149
+ <button
150
+ type="button"
151
+ className="qe-hover"
152
+ title="Assets (Ctrl+Space)"
153
+ style={{
154
+ ...buttonStyle,
155
+ padding: '3px 10px',
156
+ ...(props.assetsActive ? activeSquareButtonStyle : {}),
157
+ }}
158
+ onClick={props.onToggleAssets}
159
+ >
160
+ Assets{props.assetCount > 0 ? ` · ${props.assetCount}` : ''}
161
+ </button>
162
+ </div>
163
+ <div
164
+ style={{
165
+ height: TIMELINE_HEIGHT,
166
+ display: collapsed ? 'none' : 'flex',
167
+ borderTop: `1px solid ${theme.border}`,
168
+ }}
169
+ >
170
+ {props.children ?? (
171
+ <div style={{padding: '14px', fontSize: 12, color: theme.textDim}}>
172
+ Select an effect in the Scene to see its timeline.
173
+ </div>
174
+ )}
175
+ </div>
176
+ </div>
177
+ );
178
+ }
@@ -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
+ }