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.
- package/CHANGELOG.md +134 -0
- package/README.md +69 -9
- package/dist/core/assetDragSession.d.ts +1 -1
- package/dist/core/assetTree.d.ts +6 -1
- package/dist/core/assets.d.ts +7 -0
- package/dist/core/assets.js +11 -7
- package/dist/core/binding.d.ts +11 -2
- package/dist/core/binding.js +9 -1
- 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 +5 -4
- package/dist/core/defaultEffect.js +4 -4
- package/dist/core/editorScene.d.ts +1 -1
- package/dist/core/editorSession.d.ts +229 -0
- package/dist/core/editorSession.js +983 -0
- package/dist/core/effectTree.d.ts +6 -1
- package/dist/core/effectTree.js +13 -88
- package/dist/core/emitterShapeWireframe.d.ts +2 -2
- package/dist/core/emitterShapeWireframe.js +45 -13
- package/dist/core/galleryDropMarker.d.ts +2 -2
- package/dist/core/galleryDropMarker.js +6 -6
- package/dist/core/galleryLayout.d.ts +3 -3
- package/dist/core/galleryLayout.js +1 -1
- package/dist/core/galleryPlayback.d.ts +1 -1
- package/dist/core/galleryPlayback.js +2 -1
- package/dist/core/galleryScene.d.ts +4 -4
- package/dist/core/galleryScene.js +3 -3
- package/dist/core/gallerySelectionMarker.d.ts +4 -4
- package/dist/core/gallerySelectionMarker.js +5 -5
- package/dist/core/groups.d.ts +1 -1
- package/dist/core/groups.js +1 -1
- package/dist/core/history.d.ts +7 -1
- package/dist/core/history.js +4 -0
- package/dist/core/indexedDbProjectStore.d.ts +14 -1
- package/dist/core/indexedDbProjectStore.js +13 -0
- package/dist/core/lineShaders.d.ts +2 -2
- package/dist/core/lineShaders.js +2 -2
- package/dist/core/loadEffect.d.ts +2 -2
- package/dist/core/loadEffect.js +1 -1
- package/dist/core/material.d.ts +12 -2
- package/dist/core/pickAssetFiles.js +1 -1
- 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 +7 -4
- package/dist/core/sceneEntries.js +2 -2
- package/dist/core/scrub.d.ts +2 -2
- package/dist/core/scrub.js +2 -2
- package/dist/core/shapes.d.ts +25 -1
- package/dist/core/shapes.js +5 -0
- package/dist/core/systems.d.ts +4 -2
- package/dist/core/systems.js +3 -1
- package/dist/core/textureExport.d.ts +1 -1
- package/dist/core/timeline.d.ts +25 -5
- package/dist/core/transport.d.ts +3 -3
- package/dist/core/transport.js +2 -2
- package/dist/core/values.d.ts +21 -4
- package/dist/core/values.js +5 -0
- package/dist/core/viewportPicker.d.ts +4 -4
- package/dist/core/viewportPicker.js +8 -7
- package/dist/index.d.ts +25 -21
- package/dist/index.js +12 -12
- package/dist/react/AssetsPanel.d.ts +9 -2
- package/dist/react/AssetsPanel.js +6 -6
- package/dist/react/ChoiceDialog.d.ts +7 -0
- package/dist/react/ChoiceDialog.js +1 -1
- package/dist/react/ContextMenu.d.ts +10 -1
- package/dist/react/ContextMenu.js +1 -1
- package/dist/react/CurveEditor.d.ts +1 -1
- package/dist/react/CurveEditor.js +2 -2
- package/dist/react/EditorWorkspace.d.ts +9 -2
- package/dist/react/EditorWorkspace.js +10 -7
- package/dist/react/EffectEditor.d.ts +11 -5
- package/dist/react/EffectEditor.js +7 -7
- package/dist/react/EffectEditorHost.d.ts +33 -29
- package/dist/react/EffectEditorHost.js +247 -1674
- package/dist/react/EffectTreeView.d.ts +4 -1
- package/dist/react/EffectTreeView.js +3 -3
- package/dist/react/GradientEditor.d.ts +1 -1
- package/dist/react/GradientEditor.js +3 -3
- package/dist/react/InspectorInfoIcon.js +3 -3
- package/dist/react/InspectorLabel.js +3 -3
- package/dist/react/MessageDialog.d.ts +5 -0
- package/dist/react/MessageDialog.js +1 -1
- package/dist/react/ModuleSection.d.ts +10 -0
- package/dist/react/ModuleSection.js +8 -5
- package/dist/react/ObjectModule.d.ts +1 -1
- package/dist/react/ObjectModule.js +4 -4
- package/dist/react/ProjectsPage.d.ts +7 -1
- package/dist/react/ProjectsPage.js +33 -17
- package/dist/react/PromptDialog.d.ts +7 -0
- package/dist/react/PromptDialog.js +1 -1
- package/dist/react/ScenePanel.d.ts +9 -2
- package/dist/react/ScenePanel.js +12 -10
- package/dist/react/SubEmittersModule.d.ts +1 -1
- package/dist/react/SubEmittersModule.js +5 -5
- package/dist/react/TextureSelect.d.ts +24 -0
- package/dist/react/TextureSelect.js +74 -0
- package/dist/react/TextureUVField.d.ts +14 -0
- package/dist/react/TextureUVField.js +23 -0
- package/dist/react/TimelinePanel.d.ts +20 -7
- package/dist/react/TimelinePanel.js +21 -15
- package/dist/react/TimelineTrackRow.d.ts +13 -3
- package/dist/react/TimelineTrackRow.js +6 -6
- package/dist/react/TransportBar.d.ts +13 -4
- package/dist/react/TransportBar.js +3 -3
- package/dist/react/ValueField.d.ts +32 -5
- package/dist/react/ValueField.js +22 -11
- package/dist/react/behaviorModules.d.ts +19 -6
- package/dist/react/behaviorModules.js +68 -17
- package/dist/react/fields.d.ts +9 -0
- package/dist/react/fields.js +6 -3
- 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 +42 -38
- package/dist/react/index.js +25 -23
- package/dist/react/inspectorHints.js +31 -0
- package/dist/react/modules.d.ts +19 -6
- package/dist/react/modules.js +134 -117
- 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 +18 -11
- package/src/core/assetDragSession.ts +1 -1
- package/src/core/assetTree.ts +15 -2
- package/src/core/assets.ts +25 -10
- package/src/core/binding.ts +12 -3
- package/src/core/collision.ts +4 -0
- package/src/core/colors.ts +21 -1
- package/src/core/defaultEffect.ts +8 -7
- package/src/core/editorScene.ts +1 -1
- package/src/core/editorSession.ts +1299 -0
- package/src/core/effectTree.ts +18 -101
- package/src/core/emitterShapeWireframe.ts +48 -16
- package/src/core/galleryDropMarker.ts +8 -8
- package/src/core/galleryLayout.ts +4 -4
- package/src/core/galleryPlayback.ts +3 -2
- package/src/core/galleryScene.ts +5 -5
- package/src/core/gallerySelectionMarker.ts +7 -7
- package/src/core/groups.ts +1 -1
- package/src/core/history.ts +7 -1
- package/src/core/indexedDbProjectStore.ts +14 -1
- package/src/core/lineShaders.ts +2 -2
- package/src/core/loadEffect.ts +4 -4
- package/src/core/material.ts +14 -4
- package/src/core/pickAssetFiles.ts +2 -2
- package/src/core/playback.ts +6 -0
- package/src/core/projectStore.ts +26 -0
- package/src/core/sceneEntries.ts +8 -5
- package/src/core/scrub.ts +3 -3
- package/src/core/shapes.ts +29 -2
- package/src/core/systems.ts +11 -4
- package/src/core/textureExport.ts +1 -1
- package/src/core/timeline.ts +43 -18
- package/src/core/transport.ts +4 -4
- package/src/core/values.ts +20 -4
- package/src/core/viewportPicker.ts +12 -11
- package/src/index.ts +25 -21
- package/src/react/AssetsPanel.tsx +13 -6
- package/src/react/ChoiceDialog.tsx +8 -1
- package/src/react/ContextMenu.tsx +11 -2
- package/src/react/CurveEditor.tsx +2 -2
- package/src/react/EditorWorkspace.tsx +16 -10
- package/src/react/EffectEditor.tsx +17 -9
- package/src/react/EffectEditorHost.tsx +415 -2354
- package/src/react/EffectTreeView.tsx +7 -4
- package/src/react/GradientEditor.tsx +3 -3
- package/src/react/InspectorInfoIcon.tsx +3 -3
- package/src/react/InspectorLabel.tsx +3 -3
- package/src/react/MessageDialog.tsx +6 -1
- package/src/react/ModuleSection.tsx +15 -4
- package/src/react/ObjectModule.tsx +5 -5
- package/src/react/ProjectsPage.tsx +44 -17
- package/src/react/PromptDialog.tsx +8 -1
- package/src/react/ScenePanel.tsx +21 -12
- package/src/react/SubEmittersModule.tsx +6 -6
- package/src/react/TextureSelect.tsx +118 -0
- package/src/react/TextureUVField.tsx +62 -0
- package/src/react/TimelinePanel.tsx +40 -21
- package/src/react/TimelineTrackRow.tsx +19 -9
- package/src/react/TransportBar.tsx +16 -7
- package/src/react/ValueField.tsx +60 -16
- package/src/react/behaviorModules.tsx +240 -55
- package/src/react/fields.tsx +22 -4
- 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 +43 -38
- package/src/react/inspectorHints.ts +40 -0
- package/src/react/modules.tsx +440 -170
- package/src/react/theme.ts +15 -0
- package/src/session.ts +17 -0
|
@@ -1,14 +1,17 @@
|
|
|
1
|
-
import type {TransformNode} from '@babylonjs/core/Meshes/transformNode';
|
|
1
|
+
import type {TransformNode} from '@babylonjs/core/Meshes/transformNode.js';
|
|
2
2
|
import {ChevronDownIcon, ChevronRightIcon, FolderIcon, SparklesIcon} from '@heroicons/react/24/solid';
|
|
3
3
|
import React, {useState} from 'react';
|
|
4
|
-
import {buildEffectTree, type EffectTreeNode} from '../core/effectTree';
|
|
5
|
-
import {iconStyle} from './icons';
|
|
6
|
-
import {theme} from './theme';
|
|
4
|
+
import {buildEffectTree, type EffectTreeNode} from '../core/effectTree.js';
|
|
5
|
+
import {iconStyle} from './icons.js';
|
|
6
|
+
import {theme} from './theme.js';
|
|
7
7
|
|
|
8
|
+
/** Props of {@link EffectTreeView}. */
|
|
8
9
|
export interface EffectTreeViewProps {
|
|
10
|
+
/** The effect's root. */
|
|
9
11
|
root: TransformNode;
|
|
10
12
|
/** Node shown in the inspector (and moved by the gizmo). */
|
|
11
13
|
selectedNode: TransformNode | null;
|
|
14
|
+
/** Selects a group or emitter. */
|
|
12
15
|
onSelect: (node: TransformNode) => void;
|
|
13
16
|
/** Show the root itself as the first row (default: only its descendants). */
|
|
14
17
|
includeRoot?: boolean;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {useRef, useState} from 'react';
|
|
2
|
-
import {GradientStop, hexToRgb, rgbToHex, stopsToCssGradient} from '../core/colors';
|
|
3
|
-
import {NumberField, Row} from './fields';
|
|
4
|
-
import {buttonStyle, theme} from './theme';
|
|
2
|
+
import {GradientStop, hexToRgb, rgbToHex, stopsToCssGradient} from '../core/colors.js';
|
|
3
|
+
import {NumberField, Row} from './fields.js';
|
|
4
|
+
import {buttonStyle, theme} from './theme.js';
|
|
5
5
|
|
|
6
6
|
const BAR_WIDTH = 218;
|
|
7
7
|
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import {InformationCircleIcon} from '@heroicons/react/24/solid';
|
|
2
2
|
import React from 'react';
|
|
3
|
-
import {iconStyle} from './icons';
|
|
4
|
-
import {theme} from './theme';
|
|
5
|
-
import {Tooltip} from './Tooltip';
|
|
3
|
+
import {iconStyle} from './icons.js';
|
|
4
|
+
import {theme} from './theme.js';
|
|
5
|
+
import {Tooltip} from './Tooltip.js';
|
|
6
6
|
|
|
7
7
|
/** Small info icon with a themed inspector tooltip. */
|
|
8
8
|
export function InspectorInfoIcon(props: {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import {getInspectorHint} from './inspectorHints';
|
|
3
|
-
import {InspectorInfoIcon} from './InspectorInfoIcon';
|
|
4
|
-
import {labelStyle} from './theme';
|
|
2
|
+
import {getInspectorHint} from './inspectorHints.js';
|
|
3
|
+
import {InspectorInfoIcon} from './InspectorInfoIcon.js';
|
|
4
|
+
import {labelStyle} from './theme.js';
|
|
5
5
|
|
|
6
6
|
/** Inspector row label with an optional info icon tooltip. */
|
|
7
7
|
export function InspectorLabel(props: {label: string; hint?: string; hintKey?: string}): React.ReactElement | null {
|
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import {buttonStyle, theme} from './theme';
|
|
2
|
+
import {buttonStyle, theme} from './theme.js';
|
|
3
3
|
|
|
4
|
+
/** Props of {@link MessageDialog}. */
|
|
4
5
|
export interface MessageDialogProps {
|
|
6
|
+
/** The dialog's title. */
|
|
5
7
|
title: string;
|
|
8
|
+
/** The message; line breaks are kept. */
|
|
6
9
|
message: string;
|
|
10
|
+
/** The button's text. Default `OK`. */
|
|
7
11
|
confirmLabel?: string;
|
|
12
|
+
/** Closes the dialog: the button, Enter, Escape or the backdrop. */
|
|
8
13
|
onClose: () => void;
|
|
9
14
|
}
|
|
10
15
|
|
|
@@ -1,11 +1,22 @@
|
|
|
1
1
|
import {ChevronDownIcon, ChevronRightIcon} from '@heroicons/react/24/solid';
|
|
2
2
|
import React, {useState} from 'react';
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
3
|
+
import type {EffectBinding} from '../core/binding.js';
|
|
4
|
+
import {iconStyle} from './icons.js';
|
|
5
|
+
import {getInspectorHint} from './inspectorHints.js';
|
|
6
|
+
import {InspectorInfoIcon} from './InspectorInfoIcon.js';
|
|
7
|
+
import {theme} from './theme.js';
|
|
7
8
|
|
|
8
9
|
/** Unity-style module foldout with an optional enable checkbox. */
|
|
10
|
+
/** Props of every inspector module: the effect binding it edits, through `binding.apply`. */
|
|
11
|
+
export interface ModuleProps {
|
|
12
|
+
/** The focused effect; the module edits `binding.system`. */
|
|
13
|
+
binding: EffectBinding;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* An inspector module: a foldout with a title, a hint, and an enable checkbox when `onToggle` is
|
|
18
|
+
* given. Unity's Particle System inspector modules look the same way.
|
|
19
|
+
*/
|
|
9
20
|
export function ModuleSection(props: {
|
|
10
21
|
title: string;
|
|
11
22
|
hint?: string;
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import {Quaternion} from '@babylonjs/core/Maths/math.vector';
|
|
2
|
-
import type {TransformNode} from '@babylonjs/core/Meshes/transformNode';
|
|
1
|
+
import {Quaternion} from '@babylonjs/core/Maths/math.vector.js';
|
|
2
|
+
import type {TransformNode} from '@babylonjs/core/Meshes/transformNode.js';
|
|
3
3
|
import {useSyncExternalStore} from 'react';
|
|
4
|
-
import {getTransformRevision, notifyTransformChanged, subscribeTransform} from '../core/transformStore';
|
|
5
|
-
import {ModuleSection} from './ModuleSection';
|
|
6
|
-
import {NumberField, Row} from './fields';
|
|
4
|
+
import {getTransformRevision, notifyTransformChanged, subscribeTransform} from '../core/transformStore.js';
|
|
5
|
+
import {ModuleSection} from './ModuleSection.js';
|
|
6
|
+
import {NumberField, Row} from './fields.js';
|
|
7
7
|
|
|
8
8
|
const RAD_TO_DEG = 180 / Math.PI;
|
|
9
9
|
const DEG_TO_RAD = Math.PI / 180;
|
|
@@ -1,18 +1,27 @@
|
|
|
1
1
|
import {ArrowLeftIcon, FolderIcon, PlusIcon} from '@heroicons/react/24/solid';
|
|
2
2
|
import React, {useCallback, useEffect, useState} from 'react';
|
|
3
|
-
import {formatBytes, type ProjectMeta, type ProjectStore, type StorageEstimate} from '../core/projectStore';
|
|
4
|
-
import {ChoiceDialog} from './ChoiceDialog';
|
|
5
|
-
import {ContextMenu, type ContextMenuItem} from './ContextMenu';
|
|
6
|
-
import {iconStyle} from './icons';
|
|
7
|
-
import {PromptDialog} from './PromptDialog';
|
|
8
|
-
import {buttonStyle, globalEditorStyles, theme} from './theme';
|
|
3
|
+
import {formatBytes, type ProjectMeta, type ProjectStore, type StorageEstimate} from '../core/projectStore.js';
|
|
4
|
+
import {ChoiceDialog} from './ChoiceDialog.js';
|
|
5
|
+
import {ContextMenu, type ContextMenuItem} from './ContextMenu.js';
|
|
6
|
+
import {iconStyle} from './icons.js';
|
|
7
|
+
import {PromptDialog} from './PromptDialog.js';
|
|
8
|
+
import {buttonStyle, globalEditorStyles, theme} from './theme.js';
|
|
9
9
|
|
|
10
|
+
/** Props of {@link ProjectsPage}. */
|
|
10
11
|
export interface ProjectsPageProps {
|
|
12
|
+
/** Where the projects live. */
|
|
11
13
|
store: ProjectStore;
|
|
14
|
+
/** Opens a project in the editor. */
|
|
12
15
|
onOpen: (project: ProjectMeta) => void;
|
|
16
|
+
/** The page's heading. */
|
|
13
17
|
title?: string;
|
|
14
18
|
/** Optional link back to the page that hosts the editor (e.g. a gallery), shown above the title. */
|
|
15
|
-
backLink?: {
|
|
19
|
+
backLink?: {
|
|
20
|
+
/** Where the link goes. */
|
|
21
|
+
href: string;
|
|
22
|
+
/** The link's text. */
|
|
23
|
+
label: string;
|
|
24
|
+
};
|
|
16
25
|
}
|
|
17
26
|
|
|
18
27
|
type Dialog =
|
|
@@ -79,6 +88,18 @@ function StorageBar(props: {estimate: StorageEstimate | null; onPersist: () => v
|
|
|
79
88
|
);
|
|
80
89
|
}
|
|
81
90
|
|
|
91
|
+
/** What the projects page shows: the store's projects and usage, or why it could not read them. */
|
|
92
|
+
type ProjectListing = {list: ProjectMeta[]; storage: StorageEstimate} | {error: string};
|
|
93
|
+
|
|
94
|
+
async function listProjects(store: ProjectsPageProps['store']): Promise<ProjectListing> {
|
|
95
|
+
try {
|
|
96
|
+
const [list, storage] = await Promise.all([store.listProjects(), store.estimate()]);
|
|
97
|
+
return {list, storage};
|
|
98
|
+
} catch (err) {
|
|
99
|
+
return {error: err instanceof Error ? err.message : String(err)};
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
82
103
|
/** Start page: the projects saved in the store, plus storage usage. */
|
|
83
104
|
export function ProjectsPage(props: ProjectsPageProps): React.ReactElement {
|
|
84
105
|
const {store, onOpen, title = 'Effect editor', backLink} = props;
|
|
@@ -88,20 +109,26 @@ export function ProjectsPage(props: ProjectsPageProps): React.ReactElement {
|
|
|
88
109
|
const [menu, setMenu] = useState<{x: number; y: number; items: ContextMenuItem[]} | null>(null);
|
|
89
110
|
const [error, setError] = useState<string | null>(null);
|
|
90
111
|
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
setProjects(list);
|
|
95
|
-
setEstimate(storage);
|
|
96
|
-
} catch (err) {
|
|
97
|
-
setError(err instanceof Error ? err.message : String(err));
|
|
112
|
+
const show = useCallback((listing: ProjectListing) => {
|
|
113
|
+
if ('error' in listing) {
|
|
114
|
+
setError(listing.error);
|
|
98
115
|
setProjects([]);
|
|
116
|
+
} else {
|
|
117
|
+
setProjects(listing.list);
|
|
118
|
+
setEstimate(listing.storage);
|
|
99
119
|
}
|
|
100
|
-
}, [
|
|
120
|
+
}, []);
|
|
121
|
+
const reload = useCallback(async () => show(await listProjects(store)), [store, show]);
|
|
101
122
|
|
|
102
123
|
useEffect(() => {
|
|
103
|
-
|
|
104
|
-
|
|
124
|
+
let cancelled = false;
|
|
125
|
+
void listProjects(store).then((listing) => {
|
|
126
|
+
if (!cancelled) show(listing);
|
|
127
|
+
});
|
|
128
|
+
return () => {
|
|
129
|
+
cancelled = true;
|
|
130
|
+
};
|
|
131
|
+
}, [store, show]);
|
|
105
132
|
|
|
106
133
|
const run = (action: () => Promise<unknown>) => {
|
|
107
134
|
setDialog(null);
|
|
@@ -1,14 +1,21 @@
|
|
|
1
1
|
import React, {useEffect, useRef, useState} from 'react';
|
|
2
|
-
import {buttonStyle, inputStyle, theme} from './theme';
|
|
2
|
+
import {buttonStyle, inputStyle, theme} from './theme.js';
|
|
3
3
|
|
|
4
|
+
/** Props of {@link PromptDialog}. */
|
|
4
5
|
export interface PromptDialogProps {
|
|
6
|
+
/** What to enter. */
|
|
5
7
|
title: string;
|
|
8
|
+
/** The text the field starts with, selected. */
|
|
6
9
|
defaultValue?: string;
|
|
10
|
+
/** Hint shown in the empty field. */
|
|
7
11
|
placeholder?: string;
|
|
12
|
+
/** The confirm button's text. Default `OK`. */
|
|
8
13
|
confirmLabel?: string;
|
|
9
14
|
/** Rejects empty/whitespace-only input by default (matches the old prompt() behavior). */
|
|
10
15
|
allowEmpty?: boolean;
|
|
16
|
+
/** Receives the entered text, trimmed. */
|
|
11
17
|
onSubmit: (value: string) => void;
|
|
18
|
+
/** Escape, the backdrop, Cancel, or an empty submit when empty is not allowed. */
|
|
12
19
|
onCancel: () => void;
|
|
13
20
|
}
|
|
14
21
|
|
package/src/react/ScenePanel.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {TransformNode} from '@babylonjs/core/Meshes/transformNode';
|
|
1
|
+
import type {TransformNode} from '@babylonjs/core/Meshes/transformNode.js';
|
|
2
2
|
import {
|
|
3
3
|
ChevronDownIcon,
|
|
4
4
|
ChevronRightIcon,
|
|
@@ -8,30 +8,37 @@ import {
|
|
|
8
8
|
TrashIcon,
|
|
9
9
|
ViewfinderCircleIcon,
|
|
10
10
|
} from '@heroicons/react/24/solid';
|
|
11
|
-
import React, {
|
|
12
|
-
import {buildEffectTree} from '../core/effectTree';
|
|
13
|
-
import type {SceneEntry} from '../core/sceneEntries';
|
|
14
|
-
import {ContextMenu, type ContextMenuItem} from './ContextMenu';
|
|
15
|
-
import {EffectTreeView} from './EffectTreeView';
|
|
16
|
-
import {iconStyle} from './icons';
|
|
17
|
-
import {buttonStyle, theme} from './theme';
|
|
11
|
+
import React, {useState} from 'react';
|
|
12
|
+
import {buildEffectTree} from '../core/effectTree.js';
|
|
13
|
+
import type {SceneEntry} from '../core/sceneEntries.js';
|
|
14
|
+
import {ContextMenu, type ContextMenuItem} from './ContextMenu.js';
|
|
15
|
+
import {EffectTreeView} from './EffectTreeView.js';
|
|
16
|
+
import {iconStyle} from './icons.js';
|
|
17
|
+
import {buttonStyle, theme} from './theme.js';
|
|
18
18
|
|
|
19
19
|
const ROW_HEIGHT = 34;
|
|
20
20
|
|
|
21
|
+
/** Props of {@link ScenePanel}. */
|
|
21
22
|
export interface ScenePanelProps {
|
|
23
|
+
/** The effects in the scene, in list order. */
|
|
22
24
|
entries: SceneEntry[];
|
|
23
25
|
/** Effect shown in the inspector and timeline. */
|
|
24
26
|
focusedRoot: TransformNode | null;
|
|
25
27
|
/** Visible entries — they render and play with the stage transport. */
|
|
26
28
|
visibleRootIds: ReadonlySet<number>;
|
|
29
|
+
/** Focuses an effect in the inspector and timeline. */
|
|
27
30
|
onFocus: (entry: SceneEntry) => void;
|
|
31
|
+
/** Shows or hides an effect. */
|
|
28
32
|
onToggleVisible: (entry: SceneEntry) => void;
|
|
29
33
|
/** Root of the entry shown alone, or null. */
|
|
30
34
|
soloRootId: number | null;
|
|
35
|
+
/** Shows an effect alone, or ends solo. */
|
|
31
36
|
onToggleSolo: (entry: SceneEntry) => void;
|
|
37
|
+
/** Removes an effect from the scene. */
|
|
32
38
|
onDelete: (entry: SceneEntry) => void;
|
|
33
39
|
/** "Save as new asset…" in the row's right-click menu. */
|
|
34
40
|
onSaveAsAsset?: (entry: SceneEntry) => void;
|
|
41
|
+
/** Adds a new default effect. */
|
|
35
42
|
onCreateDefault: () => void;
|
|
36
43
|
/** Node shown in the inspector; highlighted in the expanded effect trees. */
|
|
37
44
|
selectedNode?: TransformNode | null;
|
|
@@ -213,11 +220,13 @@ export function ScenePanel(props: ScenePanelProps): React.ReactElement {
|
|
|
213
220
|
const [menu, setMenu] = useState<{x: number; y: number; items: ContextMenuItem[]} | null>(null);
|
|
214
221
|
const [expanded, setExpanded] = useState<Set<number>>(() => new Set());
|
|
215
222
|
// The focused effect opens its tree, so its sub-emitters are one click away.
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
223
|
+
const [expandedFocus, setExpandedFocus] = useState<TransformNode | null>(null);
|
|
224
|
+
if (focusedRoot !== expandedFocus) {
|
|
225
|
+
setExpandedFocus(focusedRoot);
|
|
226
|
+
if (focusedRoot && !expanded.has(focusedRoot.uniqueId)) {
|
|
227
|
+
setExpanded(new Set(expanded).add(focusedRoot.uniqueId));
|
|
219
228
|
}
|
|
220
|
-
}
|
|
229
|
+
}
|
|
221
230
|
const toggleExpanded = (entry: SceneEntry) =>
|
|
222
231
|
setExpanded((prev) => {
|
|
223
232
|
const next = new Set(prev);
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import type {ParticleEmitter, ParticleSystem} from 'babylon.quarks';
|
|
2
2
|
import {EmitSubParticleSystem, SubParticleEmitMode} from 'babylon.quarks';
|
|
3
|
-
import {EffectBinding} from '../core/binding';
|
|
4
|
-
import {findBehavior} from '../core/colors';
|
|
5
|
-
import {createChildSystem} from '../core/systems';
|
|
6
|
-
import {ModuleSection} from './ModuleSection';
|
|
7
|
-
import {CheckboxField, NumberField, Row, SelectField} from './fields';
|
|
8
|
-
import {theme} from './theme';
|
|
3
|
+
import {EffectBinding} from '../core/binding.js';
|
|
4
|
+
import {findBehavior} from '../core/colors.js';
|
|
5
|
+
import {createChildSystem} from '../core/systems.js';
|
|
6
|
+
import {ModuleSection} from './ModuleSection.js';
|
|
7
|
+
import {CheckboxField, NumberField, Row, SelectField} from './fields.js';
|
|
8
|
+
import {theme} from './theme.js';
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* Sub Emitters module for the currently selected system. The spawned target lives as a
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import type {Texture} from '@babylonjs/core/Materials/Textures/texture.js';
|
|
2
|
+
import {useState} from 'react';
|
|
3
|
+
import {SelectField} from './fields.js';
|
|
4
|
+
import type {TextureOption} from './modules.js';
|
|
5
|
+
import {PromptDialog} from './PromptDialog.js';
|
|
6
|
+
|
|
7
|
+
/** A texture-like object the host's `resolveTexture` returned; only its `url` is read here. */
|
|
8
|
+
type TextureLike = {url?: string} | null | undefined;
|
|
9
|
+
|
|
10
|
+
/** The url a texture-like object was loaded from, or '' for none. */
|
|
11
|
+
export function textureUrlOf(texture: unknown): string {
|
|
12
|
+
return (texture as TextureLike)?.url ?? '';
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Picks a texture for one slot: a host preset, a custom URL, a file from disk, or none. The
|
|
17
|
+
* current texture stays selectable when it is not among the presets. Used by the Renderer
|
|
18
|
+
* module for the map and for the ramp, mask and noise of the texture effects.
|
|
19
|
+
*/
|
|
20
|
+
export function TextureSelect(props: {
|
|
21
|
+
/** The slot's current texture (anything with a `url`), or null for none. */
|
|
22
|
+
value: unknown;
|
|
23
|
+
textureOptions?: TextureOption[];
|
|
24
|
+
/** Turns a URL or `data:` URI into a texture; null or undefined when it cannot. */
|
|
25
|
+
loadTexture: (url: string) => Texture | null | undefined;
|
|
26
|
+
/** The picked texture, or null for none. */
|
|
27
|
+
onChange: (texture: Texture | null) => void;
|
|
28
|
+
/** Label of the "none" entry. Default "(none)". */
|
|
29
|
+
noneLabel?: string;
|
|
30
|
+
/** Shows a thumbnail of the current texture under the select. Default false. */
|
|
31
|
+
preview?: boolean;
|
|
32
|
+
/** Title of the custom URL prompt. Default "Texture URL". */
|
|
33
|
+
promptTitle?: string;
|
|
34
|
+
}) {
|
|
35
|
+
const currentUrl = textureUrlOf(props.value);
|
|
36
|
+
const options = props.textureOptions ?? [];
|
|
37
|
+
const isPreset = options.some((o) => o.url === currentUrl);
|
|
38
|
+
const [prompting, setPrompting] = useState(false);
|
|
39
|
+
const apply = (url: string) => {
|
|
40
|
+
const texture = props.loadTexture(url);
|
|
41
|
+
if (texture) {
|
|
42
|
+
props.onChange(texture);
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
return (
|
|
46
|
+
<>
|
|
47
|
+
<SelectField
|
|
48
|
+
value={!currentUrl ? '__none' : isPreset ? currentUrl : '__current'}
|
|
49
|
+
options={[
|
|
50
|
+
{value: '__none', label: props.noneLabel ?? '(none)'},
|
|
51
|
+
...(currentUrl && !isPreset
|
|
52
|
+
? [{value: '__current', label: `(current) ${currentUrl.slice(-24)}`}]
|
|
53
|
+
: []),
|
|
54
|
+
...options.map((o) => ({value: o.url, label: o.label})),
|
|
55
|
+
{value: '__url', label: 'Custom URL…'},
|
|
56
|
+
{value: '__file', label: 'Load from file…'},
|
|
57
|
+
]}
|
|
58
|
+
onChange={(url) => {
|
|
59
|
+
if (url === '__current') {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if (url === '__none') {
|
|
63
|
+
props.onChange(null);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (url === '__url') {
|
|
67
|
+
setPrompting(true);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
if (url === '__file') {
|
|
71
|
+
const input = document.createElement('input');
|
|
72
|
+
input.type = 'file';
|
|
73
|
+
input.accept = 'image/*';
|
|
74
|
+
input.onchange = () => {
|
|
75
|
+
const file = input.files?.[0];
|
|
76
|
+
if (!file) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
const reader = new FileReader();
|
|
80
|
+
reader.onload = () => apply(reader.result as string);
|
|
81
|
+
reader.readAsDataURL(file);
|
|
82
|
+
};
|
|
83
|
+
input.click();
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
apply(url);
|
|
87
|
+
}}
|
|
88
|
+
/>
|
|
89
|
+
{prompting && (
|
|
90
|
+
<PromptDialog
|
|
91
|
+
title={props.promptTitle ?? 'Texture URL'}
|
|
92
|
+
defaultValue={currentUrl}
|
|
93
|
+
placeholder="https://…"
|
|
94
|
+
onSubmit={(url) => {
|
|
95
|
+
apply(url);
|
|
96
|
+
setPrompting(false);
|
|
97
|
+
}}
|
|
98
|
+
onCancel={() => setPrompting(false)}
|
|
99
|
+
/>
|
|
100
|
+
)}
|
|
101
|
+
{props.preview && currentUrl && (
|
|
102
|
+
<img
|
|
103
|
+
src={currentUrl}
|
|
104
|
+
alt="texture preview"
|
|
105
|
+
style={{
|
|
106
|
+
width: 64,
|
|
107
|
+
height: 64,
|
|
108
|
+
marginTop: 6,
|
|
109
|
+
objectFit: 'contain',
|
|
110
|
+
borderRadius: 6,
|
|
111
|
+
border: '1px solid #2b3761',
|
|
112
|
+
background: 'repeating-conic-gradient(#2a3252 0% 25%, #171d33 0% 50%) 0 0 / 12px 12px',
|
|
113
|
+
}}
|
|
114
|
+
/>
|
|
115
|
+
)}
|
|
116
|
+
</>
|
|
117
|
+
);
|
|
118
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type {TextureUV, UVChannel} from 'babylon.quarks';
|
|
2
|
+
import {NumberField, Row, SelectField} from './fields.js';
|
|
3
|
+
|
|
4
|
+
/** A `[u, v]` pair of number inputs. */
|
|
5
|
+
function PairField(props: {value: [number, number]; step: number; onChange: (next: [number, number]) => void}) {
|
|
6
|
+
return (
|
|
7
|
+
<div style={{display: 'flex', gap: 6}}>
|
|
8
|
+
{[0, 1].map((i) => (
|
|
9
|
+
<NumberField
|
|
10
|
+
key={i}
|
|
11
|
+
value={props.value[i]}
|
|
12
|
+
step={props.step}
|
|
13
|
+
onChange={(v) => {
|
|
14
|
+
const next: [number, number] = [props.value[0], props.value[1]];
|
|
15
|
+
next[i] = v;
|
|
16
|
+
props.onChange(next);
|
|
17
|
+
}}
|
|
18
|
+
/>
|
|
19
|
+
))}
|
|
20
|
+
</div>
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Rows for how a texture is laid over the particle (`TextureUV`): its UV channel, then
|
|
26
|
+
* tiling and offset as Unity's material Tiling / Offset, then a scroll as Unreal's Panner.
|
|
27
|
+
* One field for the map, the erosion mask and the distortion noise alike.
|
|
28
|
+
*/
|
|
29
|
+
export function TextureUVField(props: {
|
|
30
|
+
/** The complete layout as the system holds it. */
|
|
31
|
+
value: TextureUV;
|
|
32
|
+
/** Called with the whole layout, one field changed. */
|
|
33
|
+
onChange: (next: TextureUV) => void;
|
|
34
|
+
/** Prefix of each row's label, so several layouts in one module stay apart. Default none. */
|
|
35
|
+
labelPrefix?: string;
|
|
36
|
+
}) {
|
|
37
|
+
const prefix = props.labelPrefix ?? '';
|
|
38
|
+
const patch = (fields: Partial<TextureUV>) => props.onChange({...props.value, ...fields});
|
|
39
|
+
return (
|
|
40
|
+
<>
|
|
41
|
+
<Row label={`${prefix}UV channel`} hintKey="UV channel">
|
|
42
|
+
<SelectField<UVChannel>
|
|
43
|
+
value={props.value.channel}
|
|
44
|
+
options={[
|
|
45
|
+
{value: 0, label: 'UV0 (sprite / mesh UV)'},
|
|
46
|
+
{value: 1, label: 'UV1 (mesh second set, along a trail)'},
|
|
47
|
+
]}
|
|
48
|
+
onChange={(channel) => patch({channel})}
|
|
49
|
+
/>
|
|
50
|
+
</Row>
|
|
51
|
+
<Row label={`${prefix}Tiling`} hintKey="Tiling">
|
|
52
|
+
<PairField value={props.value.tiling} step={0.5} onChange={(tiling) => patch({tiling})} />
|
|
53
|
+
</Row>
|
|
54
|
+
<Row label={`${prefix}Offset`} hintKey="Offset">
|
|
55
|
+
<PairField value={props.value.offset} step={0.05} onChange={(offset) => patch({offset})} />
|
|
56
|
+
</Row>
|
|
57
|
+
<Row label={`${prefix}Scroll`} hintKey="Scroll">
|
|
58
|
+
<PairField value={props.value.scroll} step={0.05} onChange={(scroll) => patch({scroll})} />
|
|
59
|
+
</Row>
|
|
60
|
+
</>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import type {TransformNode} from '@babylonjs/core/Meshes/transformNode';
|
|
2
|
-
import type {Scene} from '@babylonjs/core/scene';
|
|
1
|
+
import type {TransformNode} from '@babylonjs/core/Meshes/transformNode.js';
|
|
2
|
+
import type {Scene} from '@babylonjs/core/scene.js';
|
|
3
3
|
import {ArrowPathIcon, ForwardIcon, PauseIcon, PlayIcon, RectangleGroupIcon} from '@heroicons/react/24/solid';
|
|
4
4
|
import type {BatchedRenderer, ParticleSystem} from 'babylon.quarks';
|
|
5
5
|
import React, {useMemo, useRef, useState} from 'react';
|
|
6
|
-
import type {EffectBinding} from '../core/binding';
|
|
7
|
-
import {groupNodes} from '../core/groups';
|
|
6
|
+
import type {EffectBinding} from '../core/binding.js';
|
|
7
|
+
import {groupNodes} from '../core/groups.js';
|
|
8
8
|
import {
|
|
9
9
|
clearFocusFinished,
|
|
10
10
|
getFocusElapsed,
|
|
@@ -12,28 +12,41 @@ import {
|
|
|
12
12
|
markFocusFinished,
|
|
13
13
|
setFocusElapsed,
|
|
14
14
|
type PlaybackState,
|
|
15
|
-
} from '../core/playback';
|
|
16
|
-
import {pauseFocus, playFocus, scrubFocusTo, scrubFocusToSettled} from '../core/scrub';
|
|
17
|
-
import {computeTimelineRows, computeTimelineSpan} from '../core/timeline';
|
|
18
|
-
import {PLAYBACK_SPEEDS, restartFocusPlayback, toggleStagePlayback} from '../core/transport';
|
|
19
|
-
import {TimelineTrackRow} from './TimelineTrackRow';
|
|
20
|
-
import {iconStyle} from './icons';
|
|
21
|
-
import {buttonStyle, theme} from './theme';
|
|
15
|
+
} from '../core/playback.js';
|
|
16
|
+
import {pauseFocus, playFocus, scrubFocusTo, scrubFocusToSettled} from '../core/scrub.js';
|
|
17
|
+
import {computeTimelineRows, computeTimelineSpan} from '../core/timeline.js';
|
|
18
|
+
import {PLAYBACK_SPEEDS, restartFocusPlayback, toggleStagePlayback} from '../core/transport.js';
|
|
19
|
+
import {TimelineTrackRow} from './TimelineTrackRow.js';
|
|
20
|
+
import {iconStyle} from './icons.js';
|
|
21
|
+
import {buttonStyle, theme} from './theme.js';
|
|
22
22
|
|
|
23
|
-
export type {PlaybackState} from '../core/playback';
|
|
23
|
+
export type {PlaybackState} from '../core/playback.js';
|
|
24
24
|
|
|
25
|
+
/** Props of {@link TimelinePanel}. */
|
|
25
26
|
export interface TimelinePanelProps {
|
|
27
|
+
/** The focused effect, whose systems are the tracks. */
|
|
26
28
|
binding: EffectBinding;
|
|
29
|
+
/** The node shown in the inspector; its track is highlighted. */
|
|
27
30
|
selectedNode: TransformNode | null;
|
|
31
|
+
/** Selects a track's or group's node. */
|
|
28
32
|
onSelectNode: (node: TransformNode | null) => void;
|
|
33
|
+
/** The preview scene, redrawn after a scrub. */
|
|
29
34
|
scene: Scene;
|
|
35
|
+
/** The renderer the effect draws through, refreshed after a scrub. */
|
|
30
36
|
renderer: BatchedRenderer;
|
|
37
|
+
/** Playback state shared with the host's render loop. */
|
|
31
38
|
playbackRef: React.MutableRefObject<PlaybackState>;
|
|
32
|
-
|
|
33
|
-
|
|
39
|
+
/** The playhead line, moved every frame without a re-render. */
|
|
40
|
+
playheadRef: React.RefObject<HTMLDivElement | null>;
|
|
41
|
+
/** The time and particle counter in the header, written by the host's render loop. */
|
|
42
|
+
counterRef: React.RefObject<HTMLSpanElement | null>;
|
|
43
|
+
/** The stage is playing. */
|
|
34
44
|
stagePlaying: boolean;
|
|
45
|
+
/** Plays or pauses the stage. */
|
|
35
46
|
setStagePlaying: (playing: boolean) => void;
|
|
47
|
+
/** Playback speed, 1 for real time. */
|
|
36
48
|
speed: number;
|
|
49
|
+
/** Sets the playback speed. */
|
|
37
50
|
setSpeed: (speed: number) => void;
|
|
38
51
|
/** False when the focused effect is hidden — it has nothing to play. */
|
|
39
52
|
playbackEnabled?: boolean;
|
|
@@ -93,10 +106,13 @@ export function TimelinePanel(props: TimelinePanelProps): React.ReactElement {
|
|
|
93
106
|
|
|
94
107
|
const focusRootId = binding.root.uniqueId;
|
|
95
108
|
|
|
96
|
-
|
|
109
|
+
// Another effect starts with every system shown and nothing selected.
|
|
110
|
+
const [shownBinding, setShownBinding] = useState(binding);
|
|
111
|
+
if (binding !== shownBinding) {
|
|
112
|
+
setShownBinding(binding);
|
|
97
113
|
setHiddenSystems(new Set());
|
|
98
114
|
setGroupSelection(new Set());
|
|
99
|
-
}
|
|
115
|
+
}
|
|
100
116
|
|
|
101
117
|
const toggleGroupSelect = (node: TransformNode) => {
|
|
102
118
|
setGroupSelection((prev) => {
|
|
@@ -146,7 +162,9 @@ export function TimelinePanel(props: TimelinePanelProps): React.ReactElement {
|
|
|
146
162
|
return () => observer.disconnect();
|
|
147
163
|
}, []);
|
|
148
164
|
|
|
149
|
-
const
|
|
165
|
+
const revision = binding.getRevision();
|
|
166
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps -- the binding changes in place; its revision says when to recompute
|
|
167
|
+
const rows = useMemo(() => computeTimelineRows(binding), [binding, revision]);
|
|
150
168
|
const timelineSpan = useMemo(() => computeTimelineSpan(rows), [rows]);
|
|
151
169
|
const pxPerSecond = Math.max(1, bodyWidthPx - TIMELINE_INSET - RIGHT_PADDING) / timelineSpan;
|
|
152
170
|
const anyLooping = useMemo(() => rows.some((r) => r.kind === 'track' && r.looping), [rows]);
|
|
@@ -200,17 +218,18 @@ export function TimelinePanel(props: TimelinePanelProps): React.ReactElement {
|
|
|
200
218
|
]);
|
|
201
219
|
|
|
202
220
|
const visibleRows = useMemo(() => {
|
|
221
|
+
const visible: typeof rows = [];
|
|
203
222
|
let hideUntilDepth: number | null = null;
|
|
204
|
-
const
|
|
223
|
+
for (const row of rows) {
|
|
205
224
|
if (hideUntilDepth !== null) {
|
|
206
|
-
if (row.depth > hideUntilDepth)
|
|
225
|
+
if (row.depth > hideUntilDepth) continue;
|
|
207
226
|
hideUntilDepth = null;
|
|
208
227
|
}
|
|
209
228
|
if (row.kind === 'group' && collapsedGroups.has(row.node)) {
|
|
210
229
|
hideUntilDepth = row.depth;
|
|
211
230
|
}
|
|
212
|
-
|
|
213
|
-
}
|
|
231
|
+
visible.push(row);
|
|
232
|
+
}
|
|
214
233
|
return visible;
|
|
215
234
|
}, [rows, collapsedGroups]);
|
|
216
235
|
|