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
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import type { TransformNode } from '@babylonjs/core/Meshes/transformNode.js';
|
|
2
2
|
import React from 'react';
|
|
3
|
+
/** Props of {@link EffectTreeView}. */
|
|
3
4
|
export interface EffectTreeViewProps {
|
|
5
|
+
/** The effect's root. */
|
|
4
6
|
root: TransformNode;
|
|
5
7
|
/** Node shown in the inspector (and moved by the gizmo). */
|
|
6
8
|
selectedNode: TransformNode | null;
|
|
9
|
+
/** Selects a group or emitter. */
|
|
7
10
|
onSelect: (node: TransformNode) => void;
|
|
8
11
|
/** Show the root itself as the first row (default: only its descendants). */
|
|
9
12
|
includeRoot?: boolean;
|
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
+
/** Props of {@link MessageDialog}. */
|
|
2
3
|
export interface MessageDialogProps {
|
|
4
|
+
/** The dialog's title. */
|
|
3
5
|
title: string;
|
|
6
|
+
/** The message; line breaks are kept. */
|
|
4
7
|
message: string;
|
|
8
|
+
/** The button's text. Default `OK`. */
|
|
5
9
|
confirmLabel?: string;
|
|
10
|
+
/** Closes the dialog: the button, Enter, Escape or the backdrop. */
|
|
6
11
|
onClose: () => void;
|
|
7
12
|
}
|
|
8
13
|
/** Modal for info and error messages — same chrome as PromptDialog, no text input. */
|
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
+
import type { EffectBinding } from '../core/binding.js';
|
|
2
3
|
/** Unity-style module foldout with an optional enable checkbox. */
|
|
4
|
+
/** Props of every inspector module: the effect binding it edits, through `binding.apply`. */
|
|
5
|
+
export interface ModuleProps {
|
|
6
|
+
/** The focused effect; the module edits `binding.system`. */
|
|
7
|
+
binding: EffectBinding;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* An inspector module: a foldout with a title, a hint, and an enable checkbox when `onToggle` is
|
|
11
|
+
* given. Unity's Particle System inspector modules look the same way.
|
|
12
|
+
*/
|
|
3
13
|
export declare function ModuleSection(props: {
|
|
4
14
|
title: string;
|
|
5
15
|
hint?: string;
|
|
@@ -5,7 +5,10 @@ import { iconStyle } from './icons.js';
|
|
|
5
5
|
import { getInspectorHint } from './inspectorHints.js';
|
|
6
6
|
import { InspectorInfoIcon } from './InspectorInfoIcon.js';
|
|
7
7
|
import { theme } from './theme.js';
|
|
8
|
-
/**
|
|
8
|
+
/**
|
|
9
|
+
* An inspector module: a foldout with a title, a hint, and an enable checkbox when `onToggle` is
|
|
10
|
+
* given. Unity's Particle System inspector modules look the same way.
|
|
11
|
+
*/
|
|
9
12
|
export function ModuleSection(props) {
|
|
10
13
|
const [open, setOpen] = useState(props.defaultOpen ?? true);
|
|
11
14
|
const toggleable = props.onToggle !== undefined;
|
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { type ProjectMeta, type ProjectStore } from '../core/projectStore.js';
|
|
3
|
+
/** Props of {@link ProjectsPage}. */
|
|
3
4
|
export interface ProjectsPageProps {
|
|
5
|
+
/** Where the projects live. */
|
|
4
6
|
store: ProjectStore;
|
|
7
|
+
/** Opens a project in the editor. */
|
|
5
8
|
onOpen: (project: ProjectMeta) => void;
|
|
9
|
+
/** The page's heading. */
|
|
6
10
|
title?: string;
|
|
7
11
|
/** Optional link back to the page that hosts the editor (e.g. a gallery), shown above the title. */
|
|
8
12
|
backLink?: {
|
|
13
|
+
/** Where the link goes. */
|
|
9
14
|
href: string;
|
|
15
|
+
/** The link's text. */
|
|
10
16
|
label: string;
|
|
11
17
|
};
|
|
12
18
|
}
|
|
@@ -29,6 +29,15 @@ function StorageBar(props) {
|
|
|
29
29
|
background: theme.accent,
|
|
30
30
|
} }) }), _jsx("div", { style: { fontSize: 11.5, color: theme.textDim, lineHeight: 1.5 }, children: "Projects are saved in this browser only (IndexedDB). Clearing site data removes them, and Safari deletes data of sites not opened for 7 days \u2014 export projects you care about." })] }));
|
|
31
31
|
}
|
|
32
|
+
async function listProjects(store) {
|
|
33
|
+
try {
|
|
34
|
+
const [list, storage] = await Promise.all([store.listProjects(), store.estimate()]);
|
|
35
|
+
return { list, storage };
|
|
36
|
+
}
|
|
37
|
+
catch (err) {
|
|
38
|
+
return { error: err instanceof Error ? err.message : String(err) };
|
|
39
|
+
}
|
|
40
|
+
}
|
|
32
41
|
/** Start page: the projects saved in the store, plus storage usage. */
|
|
33
42
|
export function ProjectsPage(props) {
|
|
34
43
|
const { store, onOpen, title = 'Effect editor', backLink } = props;
|
|
@@ -37,20 +46,27 @@ export function ProjectsPage(props) {
|
|
|
37
46
|
const [dialog, setDialog] = useState(null);
|
|
38
47
|
const [menu, setMenu] = useState(null);
|
|
39
48
|
const [error, setError] = useState(null);
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
setProjects(list);
|
|
44
|
-
setEstimate(storage);
|
|
45
|
-
}
|
|
46
|
-
catch (err) {
|
|
47
|
-
setError(err instanceof Error ? err.message : String(err));
|
|
49
|
+
const show = useCallback((listing) => {
|
|
50
|
+
if ('error' in listing) {
|
|
51
|
+
setError(listing.error);
|
|
48
52
|
setProjects([]);
|
|
49
53
|
}
|
|
50
|
-
|
|
54
|
+
else {
|
|
55
|
+
setProjects(listing.list);
|
|
56
|
+
setEstimate(listing.storage);
|
|
57
|
+
}
|
|
58
|
+
}, []);
|
|
59
|
+
const reload = useCallback(async () => show(await listProjects(store)), [store, show]);
|
|
51
60
|
useEffect(() => {
|
|
52
|
-
|
|
53
|
-
|
|
61
|
+
let cancelled = false;
|
|
62
|
+
void listProjects(store).then((listing) => {
|
|
63
|
+
if (!cancelled)
|
|
64
|
+
show(listing);
|
|
65
|
+
});
|
|
66
|
+
return () => {
|
|
67
|
+
cancelled = true;
|
|
68
|
+
};
|
|
69
|
+
}, [store, show]);
|
|
54
70
|
const run = (action) => {
|
|
55
71
|
setDialog(null);
|
|
56
72
|
action()
|
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
+
/** Props of {@link PromptDialog}. */
|
|
2
3
|
export interface PromptDialogProps {
|
|
4
|
+
/** What to enter. */
|
|
3
5
|
title: string;
|
|
6
|
+
/** The text the field starts with, selected. */
|
|
4
7
|
defaultValue?: string;
|
|
8
|
+
/** Hint shown in the empty field. */
|
|
5
9
|
placeholder?: string;
|
|
10
|
+
/** The confirm button's text. Default `OK`. */
|
|
6
11
|
confirmLabel?: string;
|
|
7
12
|
/** Rejects empty/whitespace-only input by default (matches the old prompt() behavior). */
|
|
8
13
|
allowEmpty?: boolean;
|
|
14
|
+
/** Receives the entered text, trimmed. */
|
|
9
15
|
onSubmit: (value: string) => void;
|
|
16
|
+
/** Escape, the backdrop, Cancel, or an empty submit when empty is not allowed. */
|
|
10
17
|
onCancel: () => void;
|
|
11
18
|
}
|
|
12
19
|
/** Modal replacement for `window.prompt` — matches the editor's dark theme, submits on
|
|
@@ -1,20 +1,27 @@
|
|
|
1
1
|
import type { TransformNode } from '@babylonjs/core/Meshes/transformNode.js';
|
|
2
2
|
import React from 'react';
|
|
3
3
|
import type { SceneEntry } from '../core/sceneEntries.js';
|
|
4
|
+
/** Props of {@link ScenePanel}. */
|
|
4
5
|
export interface ScenePanelProps {
|
|
6
|
+
/** The effects in the scene, in list order. */
|
|
5
7
|
entries: SceneEntry[];
|
|
6
8
|
/** Effect shown in the inspector and timeline. */
|
|
7
9
|
focusedRoot: TransformNode | null;
|
|
8
10
|
/** Visible entries — they render and play with the stage transport. */
|
|
9
11
|
visibleRootIds: ReadonlySet<number>;
|
|
12
|
+
/** Focuses an effect in the inspector and timeline. */
|
|
10
13
|
onFocus: (entry: SceneEntry) => void;
|
|
14
|
+
/** Shows or hides an effect. */
|
|
11
15
|
onToggleVisible: (entry: SceneEntry) => void;
|
|
12
16
|
/** Root of the entry shown alone, or null. */
|
|
13
17
|
soloRootId: number | null;
|
|
18
|
+
/** Shows an effect alone, or ends solo. */
|
|
14
19
|
onToggleSolo: (entry: SceneEntry) => void;
|
|
20
|
+
/** Removes an effect from the scene. */
|
|
15
21
|
onDelete: (entry: SceneEntry) => void;
|
|
16
22
|
/** "Save as new asset…" in the row's right-click menu. */
|
|
17
23
|
onSaveAsAsset?: (entry: SceneEntry) => void;
|
|
24
|
+
/** Adds a new default effect. */
|
|
18
25
|
onCreateDefault: () => void;
|
|
19
26
|
/** Node shown in the inspector; highlighted in the expanded effect trees. */
|
|
20
27
|
selectedNode?: TransformNode | null;
|
package/dist/react/ScenePanel.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { ChevronDownIcon, ChevronRightIcon, EyeIcon, EyeSlashIcon, SparklesIcon, TrashIcon, ViewfinderCircleIcon, } from '@heroicons/react/24/solid';
|
|
3
|
-
import React, {
|
|
3
|
+
import React, { useState } from 'react';
|
|
4
4
|
import { buildEffectTree } from '../core/effectTree.js';
|
|
5
5
|
import { ContextMenu } from './ContextMenu.js';
|
|
6
6
|
import { EffectTreeView } from './EffectTreeView.js';
|
|
@@ -83,11 +83,13 @@ export function ScenePanel(props) {
|
|
|
83
83
|
const [menu, setMenu] = useState(null);
|
|
84
84
|
const [expanded, setExpanded] = useState(() => new Set());
|
|
85
85
|
// The focused effect opens its tree, so its sub-emitters are one click away.
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
86
|
+
const [expandedFocus, setExpandedFocus] = useState(null);
|
|
87
|
+
if (focusedRoot !== expandedFocus) {
|
|
88
|
+
setExpandedFocus(focusedRoot);
|
|
89
|
+
if (focusedRoot && !expanded.has(focusedRoot.uniqueId)) {
|
|
90
|
+
setExpanded(new Set(expanded).add(focusedRoot.uniqueId));
|
|
89
91
|
}
|
|
90
|
-
}
|
|
92
|
+
}
|
|
91
93
|
const toggleExpanded = (entry) => setExpanded((prev) => {
|
|
92
94
|
const next = new Set(prev);
|
|
93
95
|
if (next.has(entry.root.uniqueId)) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { Texture } from '@babylonjs/core/Materials/Textures/texture.js';
|
|
2
2
|
import type { TextureOption } from './modules.js';
|
|
3
3
|
/** The url a texture-like object was loaded from, or '' for none. */
|
|
4
4
|
export declare function textureUrlOf(texture: unknown): string;
|
|
@@ -11,10 +11,10 @@ export declare function TextureSelect(props: {
|
|
|
11
11
|
/** The slot's current texture (anything with a `url`), or null for none. */
|
|
12
12
|
value: unknown;
|
|
13
13
|
textureOptions?: TextureOption[];
|
|
14
|
-
/** Turns a URL or `data:` URI into
|
|
15
|
-
loadTexture: (url: string) =>
|
|
16
|
-
/** The
|
|
17
|
-
onChange: (texture:
|
|
14
|
+
/** Turns a URL or `data:` URI into a texture; null or undefined when it cannot. */
|
|
15
|
+
loadTexture: (url: string) => Texture | null | undefined;
|
|
16
|
+
/** The picked texture, or null for none. */
|
|
17
|
+
onChange: (texture: Texture | null) => void;
|
|
18
18
|
/** Label of the "none" entry. Default "(none)". */
|
|
19
19
|
noneLabel?: string;
|
|
20
20
|
/** Shows a thumbnail of the current texture under the select. Default false. */
|
|
@@ -5,18 +5,31 @@ import React from 'react';
|
|
|
5
5
|
import type { EffectBinding } from '../core/binding.js';
|
|
6
6
|
import { type PlaybackState } from '../core/playback.js';
|
|
7
7
|
export type { PlaybackState } from '../core/playback.js';
|
|
8
|
+
/** Props of {@link TimelinePanel}. */
|
|
8
9
|
export interface TimelinePanelProps {
|
|
10
|
+
/** The focused effect, whose systems are the tracks. */
|
|
9
11
|
binding: EffectBinding;
|
|
12
|
+
/** The node shown in the inspector; its track is highlighted. */
|
|
10
13
|
selectedNode: TransformNode | null;
|
|
14
|
+
/** Selects a track's or group's node. */
|
|
11
15
|
onSelectNode: (node: TransformNode | null) => void;
|
|
16
|
+
/** The preview scene, redrawn after a scrub. */
|
|
12
17
|
scene: Scene;
|
|
18
|
+
/** The renderer the effect draws through, refreshed after a scrub. */
|
|
13
19
|
renderer: BatchedRenderer;
|
|
20
|
+
/** Playback state shared with the host's render loop. */
|
|
14
21
|
playbackRef: React.MutableRefObject<PlaybackState>;
|
|
22
|
+
/** The playhead line, moved every frame without a re-render. */
|
|
15
23
|
playheadRef: React.RefObject<HTMLDivElement | null>;
|
|
24
|
+
/** The time and particle counter in the header, written by the host's render loop. */
|
|
16
25
|
counterRef: React.RefObject<HTMLSpanElement | null>;
|
|
26
|
+
/** The stage is playing. */
|
|
17
27
|
stagePlaying: boolean;
|
|
28
|
+
/** Plays or pauses the stage. */
|
|
18
29
|
setStagePlaying: (playing: boolean) => void;
|
|
30
|
+
/** Playback speed, 1 for real time. */
|
|
19
31
|
speed: number;
|
|
32
|
+
/** Sets the playback speed. */
|
|
20
33
|
setSpeed: (speed: number) => void;
|
|
21
34
|
/** False when the focused effect is hidden — it has nothing to play. */
|
|
22
35
|
playbackEnabled?: boolean;
|
|
@@ -38,10 +38,13 @@ export function TimelinePanel(props) {
|
|
|
38
38
|
const bodyRef = useRef(null);
|
|
39
39
|
const dragRef = useRef({ raf: null, pendingTime: null });
|
|
40
40
|
const focusRootId = binding.root.uniqueId;
|
|
41
|
-
|
|
41
|
+
// Another effect starts with every system shown and nothing selected.
|
|
42
|
+
const [shownBinding, setShownBinding] = useState(binding);
|
|
43
|
+
if (binding !== shownBinding) {
|
|
44
|
+
setShownBinding(binding);
|
|
42
45
|
setHiddenSystems(new Set());
|
|
43
46
|
setGroupSelection(new Set());
|
|
44
|
-
}
|
|
47
|
+
}
|
|
45
48
|
const toggleGroupSelect = (node) => {
|
|
46
49
|
setGroupSelection((prev) => {
|
|
47
50
|
const next = new Set(prev);
|
|
@@ -145,18 +148,19 @@ export function TimelinePanel(props) {
|
|
|
145
148
|
renderer,
|
|
146
149
|
]);
|
|
147
150
|
const visibleRows = useMemo(() => {
|
|
151
|
+
const visible = [];
|
|
148
152
|
let hideUntilDepth = null;
|
|
149
|
-
const
|
|
153
|
+
for (const row of rows) {
|
|
150
154
|
if (hideUntilDepth !== null) {
|
|
151
155
|
if (row.depth > hideUntilDepth)
|
|
152
|
-
|
|
156
|
+
continue;
|
|
153
157
|
hideUntilDepth = null;
|
|
154
158
|
}
|
|
155
159
|
if (row.kind === 'group' && collapsedGroups.has(row.node)) {
|
|
156
160
|
hideUntilDepth = row.depth;
|
|
157
161
|
}
|
|
158
|
-
|
|
159
|
-
}
|
|
162
|
+
visible.push(row);
|
|
163
|
+
}
|
|
160
164
|
return visible;
|
|
161
165
|
}, [rows, collapsedGroups]);
|
|
162
166
|
const toggleCollapse = (node) => {
|
|
@@ -2,21 +2,31 @@ import type { TransformNode } from '@babylonjs/core/Meshes/transformNode.js';
|
|
|
2
2
|
import React from 'react';
|
|
3
3
|
import type { EffectBinding } from '../core/binding.js';
|
|
4
4
|
import type { TimelineRow } from '../core/timeline.js';
|
|
5
|
+
/** Props of {@link TimelineTrackRow}. */
|
|
5
6
|
export interface TimelineTrackRowProps {
|
|
7
|
+
/** The group or track the row shows. */
|
|
6
8
|
row: TimelineRow;
|
|
9
|
+
/** The focused effect, edited by the row's actions. */
|
|
7
10
|
binding: EffectBinding;
|
|
11
|
+
/** The row's node is the one in the inspector. */
|
|
8
12
|
selected: boolean;
|
|
13
|
+
/** Selects the row's node. */
|
|
9
14
|
onSelectNode: (node: TransformNode | null) => void;
|
|
15
|
+
/** The ruler's scale, pixels per second. */
|
|
10
16
|
pxPerSecond: number;
|
|
11
17
|
/** Matches the ruler's left gutter so bars line up under the correct tick. */
|
|
12
18
|
offsetPx: number;
|
|
19
|
+
/** A group row's children are hidden. */
|
|
13
20
|
collapsed: boolean;
|
|
21
|
+
/** Collapses or expands a group row. */
|
|
14
22
|
onToggleCollapse?: () => void;
|
|
15
23
|
/** Preview-only visibility (toggles `system.emitter.visible`); lets you isolate one track. */
|
|
16
24
|
hidden?: boolean;
|
|
25
|
+
/** Shows or hides the track in the preview. */
|
|
17
26
|
onToggleVisible?: () => void;
|
|
18
27
|
/** Membership in the "group selected rows together" set (unrelated to inspector selection). */
|
|
19
28
|
groupSelected: boolean;
|
|
29
|
+
/** Adds the row to the set, or takes it out. */
|
|
20
30
|
onToggleGroupSelect: () => void;
|
|
21
31
|
}
|
|
22
32
|
/** One row of the timeline: a label cell (name + actions) and, for tracks, a bar cell. */
|
|
@@ -3,16 +3,25 @@ import type { BatchedRenderer } from 'babylon.quarks';
|
|
|
3
3
|
import React from 'react';
|
|
4
4
|
import type { EffectBinding } from '../core/binding.js';
|
|
5
5
|
import type { PlaybackState } from '../core/playback.js';
|
|
6
|
+
/** Props of {@link TransportBar}. */
|
|
6
7
|
export interface TransportBarProps {
|
|
8
|
+
/** The focused effect, which Restart and Step act on. */
|
|
7
9
|
binding: EffectBinding | null;
|
|
10
|
+
/** The preview scene, redrawn after a restart. */
|
|
8
11
|
scene: Scene;
|
|
12
|
+
/** The renderer the effects draw through. */
|
|
9
13
|
renderer: BatchedRenderer;
|
|
14
|
+
/** Playback state shared with the host's render loop. */
|
|
10
15
|
playbackRef: React.MutableRefObject<PlaybackState>;
|
|
11
16
|
/** The host's render loop writes time and particle count here. */
|
|
12
17
|
counterRef: React.RefObject<HTMLSpanElement | null>;
|
|
18
|
+
/** The stage is playing. */
|
|
13
19
|
stagePlaying: boolean;
|
|
20
|
+
/** Plays or pauses the stage. */
|
|
14
21
|
setStagePlaying: (playing: boolean) => void;
|
|
22
|
+
/** Playback speed, 1 for real time. */
|
|
15
23
|
speed: number;
|
|
24
|
+
/** Sets the playback speed. */
|
|
16
25
|
setSpeed: (speed: number) => void;
|
|
17
26
|
/** False when the focused effect is hidden. */
|
|
18
27
|
enabled: boolean;
|
|
@@ -1,11 +1,38 @@
|
|
|
1
|
+
import type { ValueGenerator } from 'babylon.quarks';
|
|
1
2
|
import React from 'react';
|
|
2
|
-
import { ScalarGenerator } from '../core/values.js';
|
|
3
|
-
|
|
3
|
+
import { AnyGenerator, ScalarGenerator } from '../core/values.js';
|
|
4
|
+
/** Props every {@link ValueField} takes. */
|
|
5
|
+
interface ValueFieldBaseProps {
|
|
6
|
+
/** The row's label. */
|
|
4
7
|
label: string;
|
|
8
|
+
/** Tooltip text of the label. */
|
|
5
9
|
hint?: string;
|
|
10
|
+
/** Key into the inspector hints, used when `hint` is not given. */
|
|
6
11
|
hintKey?: string;
|
|
7
|
-
generator
|
|
8
|
-
|
|
12
|
+
/** The generator shown; one that is not a scalar reads as the constant 1. */
|
|
13
|
+
generator: AnyGenerator | undefined;
|
|
14
|
+
/** Smallest value the number fields accept. */
|
|
9
15
|
min?: number;
|
|
16
|
+
/** Top of the curve editor's value range. */
|
|
10
17
|
curveMax?: number;
|
|
11
|
-
}
|
|
18
|
+
}
|
|
19
|
+
/** A value field for a setting that takes a curve over the particle's life. */
|
|
20
|
+
export interface CurveValueFieldProps extends ValueFieldBaseProps {
|
|
21
|
+
/** Offers Curve beside Constant and Random (the default). */
|
|
22
|
+
curve?: true;
|
|
23
|
+
/** Receives the edited generator. */
|
|
24
|
+
onChange: (generator: ScalarGenerator) => void;
|
|
25
|
+
}
|
|
26
|
+
/** A value field for a setting sampled once, without a curve: Constant or Random. */
|
|
27
|
+
export interface ConstantValueFieldProps extends ValueFieldBaseProps {
|
|
28
|
+
/** Hides Curve: the setting takes no curve. */
|
|
29
|
+
curve: false;
|
|
30
|
+
/** Receives the edited generator. */
|
|
31
|
+
onChange: (generator: ValueGenerator) => void;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Unity-like value field: Constant | Random Between Two Constants | Curve, bound to a quarks value
|
|
35
|
+
* generator. With `curve={false}` it offers only the first two, for settings sampled once.
|
|
36
|
+
*/
|
|
37
|
+
export declare function ValueField(props: CurveValueFieldProps | ConstantValueFieldProps): React.ReactElement;
|
|
38
|
+
export {};
|
package/dist/react/ValueField.js
CHANGED
|
@@ -1,20 +1,31 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
-
import { buildCurve, buildScalar, readPieces, readScalar } from '../core/values.js';
|
|
2
|
+
import { buildCurve, buildScalar, readPieces, readScalar, } from '../core/values.js';
|
|
3
3
|
import { CurveEditor } from './CurveEditor.js';
|
|
4
4
|
import { NumberField, Row, SelectField } from './fields.js';
|
|
5
|
+
const captionStyle = { fontSize: 10.5, opacity: 0.6, flex: 'none' };
|
|
6
|
+
function isValueGenerator(generator) {
|
|
7
|
+
return generator.type === 'value';
|
|
8
|
+
}
|
|
5
9
|
/**
|
|
6
|
-
* Unity-like value field: Constant | Random Between Two Constants | Curve,
|
|
7
|
-
*
|
|
10
|
+
* Unity-like value field: Constant | Random Between Two Constants | Curve, bound to a quarks value
|
|
11
|
+
* generator. With `curve={false}` it offers only the first two, for settings sampled once.
|
|
8
12
|
*/
|
|
9
|
-
const captionStyle = { fontSize: 10.5, opacity: 0.6, flex: 'none' };
|
|
10
13
|
export function ValueField(props) {
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
+
const read = readScalar(props.generator);
|
|
15
|
+
// A setting without curves shows a curve it was loaded with as a constant, so an edit replaces it.
|
|
16
|
+
const state = props.curve === false && read.mode === 'curve' ? { ...read, mode: 'constant' } : read;
|
|
17
|
+
const emit = (generator) => {
|
|
18
|
+
if (props.curve !== false) {
|
|
19
|
+
props.onChange(generator);
|
|
20
|
+
}
|
|
21
|
+
else if (isValueGenerator(generator)) {
|
|
22
|
+
props.onChange(generator);
|
|
23
|
+
}
|
|
14
24
|
};
|
|
25
|
+
const commit = (patch) => emit(buildScalar({ ...state, ...patch }));
|
|
15
26
|
return (_jsxs(_Fragment, { children: [_jsx(Row, { label: props.label, hint: props.hint, hintKey: props.hintKey, children: _jsx(SelectField, { value: state.mode, options: [
|
|
16
27
|
{ value: 'constant', label: 'Constant' },
|
|
17
28
|
{ value: 'random', label: 'Random between two' },
|
|
18
|
-
{ value: 'curve', label: 'Curve' },
|
|
19
|
-
], onChange: (mode) => commit({ mode }) }) }), state.mode === 'constant' && (_jsx(Row, { label: "", children: _jsx(NumberField, { value: state.value, min: props.min, onChange: (value) => commit({ value }) }) })), state.mode === 'random' && (_jsx(Row, { label: "", children: _jsxs("div", { style: { display: 'flex', gap: 6, alignItems: 'center' }, children: [_jsx("span", { style: captionStyle, children: "min" }), _jsx(NumberField, { value: state.min, min: props.min, onChange: (min) => commit({ min }) }), _jsx("span", { style: captionStyle, children: "max" }), _jsx(NumberField, { value: state.max, min: props.min, onChange: (max) => commit({ max }) })] }) })), state.mode === 'curve' && (_jsx("div", { style: { marginTop: 6 }, children: _jsx(CurveEditor, { pieces: readPieces(props.generator), maxValue: props.curveMax, onChange: (pieces) =>
|
|
29
|
+
...(props.curve !== false ? [{ value: 'curve', label: 'Curve' }] : []),
|
|
30
|
+
], onChange: (mode) => commit({ mode }) }) }), state.mode === 'constant' && (_jsx(Row, { label: "", children: _jsx(NumberField, { value: state.value, min: props.min, onChange: (value) => commit({ value }) }) })), state.mode === 'random' && (_jsx(Row, { label: "", children: _jsxs("div", { style: { display: 'flex', gap: 6, alignItems: 'center' }, children: [_jsx("span", { style: captionStyle, children: "min" }), _jsx(NumberField, { value: state.min, min: props.min, onChange: (min) => commit({ min }) }), _jsx("span", { style: captionStyle, children: "max" }), _jsx(NumberField, { value: state.max, min: props.min, onChange: (max) => commit({ max }) })] }) })), state.mode === 'curve' && (_jsx("div", { style: { marginTop: 6 }, children: _jsx(CurveEditor, { pieces: readPieces(props.generator), maxValue: props.curveMax, onChange: (pieces) => emit(buildCurve(pieces)) }) }))] }));
|
|
20
31
|
}
|
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import { EffectBinding } from '../core/binding.js';
|
|
3
2
|
import type { TextureOption } from './modules.js';
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
}
|
|
3
|
+
import { type ModuleProps } from './ModuleSection.js';
|
|
4
|
+
/** The Speed over Lifetime module (Unity's Speed Modifier): scales speed by a curve over the particle's life. */
|
|
7
5
|
export declare function SpeedOverLifeModule({ binding }: ModuleProps): React.JSX.Element;
|
|
6
|
+
/** The Limit Speed over Lifetime module (Unity's Limit Velocity over Lifetime): caps speed by a curve, with dampening. */
|
|
8
7
|
export declare function LimitSpeedOverLifeModule({ binding }: ModuleProps): React.JSX.Element;
|
|
8
|
+
/** The Force over Lifetime module (Unity's Force over Lifetime): a constant push per axis. */
|
|
9
9
|
export declare function ForceOverLifeModule({ binding }: ModuleProps): React.JSX.Element;
|
|
10
10
|
export declare function VelocityOverLifeModule({ binding }: ModuleProps): React.JSX.Element;
|
|
11
11
|
export declare function InheritVelocityModule({ binding }: ModuleProps): React.JSX.Element;
|
|
12
12
|
/** A point attractor (`GravityForce`): particles fall towards a point, harder the nearer they are. */
|
|
13
13
|
export declare function AttractorModule({ binding }: ModuleProps): React.JSX.Element;
|
|
14
|
+
/** The Gravity module: pulls particles towards a point. */
|
|
14
15
|
export declare function GravityModule({ binding }: ModuleProps): React.JSX.Element;
|
|
16
|
+
/** The Rotation over Lifetime module (Unity's Rotation over Lifetime): spin rate over the particle's life. */
|
|
15
17
|
export declare function RotationOverLifeModule({ binding }: ModuleProps): React.JSX.Element;
|
|
16
18
|
/** Turns mesh particles by a 3D angular velocity; billboards turn through Rotation over Lifetime. */
|
|
17
19
|
export declare function Rotation3DOverLifeModule({ binding }: ModuleProps): React.JSX.Element;
|
|
@@ -23,14 +25,23 @@ export declare function Rotation3DOverLifeModule({ binding }: ModuleProps): Reac
|
|
|
23
25
|
export declare function SequencesModule({ binding, textureOptions }: ModuleProps & {
|
|
24
26
|
textureOptions?: TextureOption[];
|
|
25
27
|
}): React.JSX.Element;
|
|
28
|
+
/** The Noise module (Unity's Noise): moves and turns particles by a noise field. */
|
|
26
29
|
export declare function NoiseModule({ binding }: ModuleProps): React.JSX.Element;
|
|
30
|
+
/** The Turbulence module: a curl noise force field. */
|
|
27
31
|
export declare function TurbulenceModule({ binding }: ModuleProps): React.JSX.Element;
|
|
32
|
+
/** The Randomize Direction module: tilts each particle's start direction by a random angle. */
|
|
28
33
|
export declare function EmitDirectionModule({ binding }: ModuleProps): React.JSX.Element;
|
|
34
|
+
/** The Collision module (Unity's Collision): particles bounce off or die on a ground plane. */
|
|
29
35
|
export declare function CollisionModule({ binding }: ModuleProps): React.JSX.Element;
|
|
36
|
+
/** The Color by Speed module (Unity's Color by Speed): a gradient over a speed range. */
|
|
30
37
|
export declare function ColorBySpeedModule({ binding }: ModuleProps): React.JSX.Element;
|
|
38
|
+
/** The Size by Speed module (Unity's Size by Speed): size over a speed range. */
|
|
31
39
|
export declare function SizeBySpeedModule({ binding }: ModuleProps): React.JSX.Element;
|
|
40
|
+
/** The Rotation by Speed module (Unity's Rotation by Speed): spin rate over a speed range. */
|
|
32
41
|
export declare function RotationBySpeedModule({ binding }: ModuleProps): React.JSX.Element;
|
|
42
|
+
/** The Orbit over Lifetime module (Unity's Velocity over Lifetime orbital): turns particles about an axis. */
|
|
33
43
|
export declare function OrbitOverLifeModule({ binding }: ModuleProps): React.JSX.Element;
|
|
44
|
+
/** The Width over Trail module (Unity's Trails width over trail): a trail's width along its length. */
|
|
34
45
|
export declare function WidthOverTrailModule({ binding }: ModuleProps): React.JSX.Element;
|
|
46
|
+
/** The Texture Sheet Animation module (Unity's Texture Sheet Animation): plays the sheet's frames by life or by frame rate. */
|
|
35
47
|
export declare function TextureSheetModule({ binding }: ModuleProps): React.JSX.Element;
|
|
36
|
-
export {};
|
|
@@ -28,12 +28,15 @@ function BehaviorModule(props) {
|
|
|
28
28
|
}
|
|
29
29
|
}), children: behavior ? props.children(behavior) : null }));
|
|
30
30
|
}
|
|
31
|
+
/** The Speed over Lifetime module (Unity's Speed Modifier): scales speed by a curve over the particle's life. */
|
|
31
32
|
export function SpeedOverLifeModule({ binding }) {
|
|
32
33
|
return (_jsx(BehaviorModule, { binding: binding, title: "Speed over Lifetime", type: "SpeedOverLife", create: () => new SpeedOverLife(buildScalar({ mode: 'curve', value: 1, min: 0, max: 1, curve: [1, 0.75, 0.5, 0] })), children: (behavior) => (_jsx("div", { style: { marginTop: 6 }, children: _jsx(CurveEditor, { pieces: readPieces(behavior.speed), maxValue: 2, onChange: (pieces) => binding.apply(() => (behavior.speed = buildCurve(pieces))) }) })) }));
|
|
33
34
|
}
|
|
35
|
+
/** The Limit Speed over Lifetime module (Unity's Limit Velocity over Lifetime): caps speed by a curve, with dampening. */
|
|
34
36
|
export function LimitSpeedOverLifeModule({ binding }) {
|
|
35
37
|
return (_jsx(BehaviorModule, { binding: binding, title: "Limit Speed over Lifetime", type: "LimitSpeedOverLife", create: () => new LimitSpeedOverLife(buildScalar({ mode: 'curve', value: 1, min: 0, max: 1, curve: [4, 2, 1, 1] }), 0.5), children: (behavior) => (_jsxs(_Fragment, { children: [_jsx("div", { style: { marginTop: 6 }, children: _jsx(CurveEditor, { pieces: readPieces(behavior.speed), maxValue: 8, onChange: (pieces) => binding.apply(() => (behavior.speed = buildCurve(pieces))) }) }), _jsx(Row, { label: "Dampen", children: _jsx(NumberField, { value: behavior.dampen, min: 0, step: 0.05, onChange: (v) => binding.apply(() => (behavior.dampen = Math.min(1, v))) }) })] })) }));
|
|
36
38
|
}
|
|
39
|
+
/** The Force over Lifetime module (Unity's Force over Lifetime): a constant push per axis. */
|
|
37
40
|
export function ForceOverLifeModule({ binding }) {
|
|
38
41
|
return (_jsx(BehaviorModule, { binding: binding, title: "Force over Lifetime", type: "ForceOverLife", create: () => new ForceOverLife(new ConstantValue(0), new ConstantValue(2), new ConstantValue(0)), children: (behavior) => (_jsxs(_Fragment, { children: [_jsx(Row, { label: "Space", children: _jsx(SelectField, { value: behavior.space, options: [
|
|
39
42
|
{ value: 'world', label: 'World' },
|
|
@@ -56,12 +59,14 @@ export function InheritVelocityModule({ binding }) {
|
|
|
56
59
|
export function AttractorModule({ binding }) {
|
|
57
60
|
return (_jsx(BehaviorModule, { binding: binding, title: "Attractor", type: "GravityForce", create: () => new GravityForce(new Vector3(0, 0, 0), 1), children: (behavior) => (_jsxs(_Fragment, { children: [_jsx(Row, { label: "Center", hintKey: "attractor.center", children: _jsx("div", { style: { display: 'flex', gap: 6 }, children: ['x', 'y', 'z'].map((axis) => (_jsx(NumberField, { value: behavior.center[axis], step: 0.1, onChange: (v) => binding.apply(() => (behavior.center[axis] = v)) }, axis))) }) }), _jsx(Row, { label: "Strength", hintKey: "attractor.magnitude", children: _jsx(NumberField, { value: behavior.magnitude, step: 0.1, onChange: (v) => binding.apply(() => (behavior.magnitude = v)) }) })] })) }));
|
|
58
61
|
}
|
|
62
|
+
/** The Gravity module: pulls particles towards a point. */
|
|
59
63
|
export function GravityModule({ binding }) {
|
|
60
64
|
return (_jsx(BehaviorModule, { binding: binding, title: "Gravity", type: "ApplyForce", create: () => new ApplyForce(new Vector3(0, -1, 0), new ConstantValue(9.81)), children: (behavior) => (_jsxs(_Fragment, { children: [_jsx(Row, { label: "Strength", hintKey: "gravity.strength", children: _jsx(NumberField, { value: readScalar(behavior.magnitude).value, step: 0.5, onChange: (v) => binding.apply(() => (behavior.magnitude = new ConstantValue(v))) }) }), _jsx(Row, { label: "Space", children: _jsx(SelectField, { value: behavior.space, options: [
|
|
61
65
|
{ value: 'world', label: 'World' },
|
|
62
66
|
{ value: 'local', label: 'Local' },
|
|
63
67
|
], onChange: (v) => binding.apply(() => (behavior.space = v)) }) })] })) }));
|
|
64
68
|
}
|
|
69
|
+
/** The Rotation over Lifetime module (Unity's Rotation over Lifetime): spin rate over the particle's life. */
|
|
65
70
|
export function RotationOverLifeModule({ binding }) {
|
|
66
71
|
return (_jsx(BehaviorModule, { binding: binding, title: "Rotation over Lifetime", type: "RotationOverLife", create: () => new RotationOverLife(new ConstantValue(Math.PI)), children: (behavior) => (_jsx(ValueField, { hintKey: "rotationOverLife.velocity", label: "Velocity (rad/s)", generator: behavior.angularVelocity, curveMax: Math.PI * 4, onChange: (g) => binding.apply(() => (behavior.angularVelocity = g)) })) }));
|
|
67
72
|
}
|
|
@@ -137,6 +142,7 @@ export function SequencesModule({ binding, textureOptions }) {
|
|
|
137
142
|
}, title: "Remove sequence", onClick: () => binding.apply(() => behavior.sequencers.splice(index, 1)), children: _jsx(XMarkIcon, { style: iconStyle(13) }) })] }), _jsx(Row, { label: "Time from / to (s)", children: pair(range.a, range.b, (v) => binding.apply(() => (range.a = v)), (v) => binding.apply(() => (range.b = v)), 0.1) }), texture.locations !== undefined && (_jsxs(_Fragment, { children: [_jsx(Row, { label: "Scale X / Y", children: pair(texture.scaleX, texture.scaleY, (v) => binding.apply(() => (texture.scaleX = v)), (v) => binding.apply(() => (texture.scaleY = v)), 0.01) }), _jsx(Vector3Row, { label: "Position", value: texture.position, step: 0.1, onChange: (v) => binding.apply(() => texture.position.copy(v)) })] }))] }, index));
|
|
138
143
|
}), _jsx(Row, { label: "Image", children: textureOptions && textureOptions.length > 0 ? (_jsx(SelectField, { value: imageUrl, options: textureOptions.map((option) => ({ value: option.url, label: option.label })), onChange: (url) => setImageUrl(url) })) : (_jsx("input", { className: "qe-hover", style: { flex: 1, minWidth: 0, fontSize: 12 }, placeholder: "Image URL", value: imageUrl, onChange: (event) => setImageUrl(event.target.value) })) }), _jsx(Row, { label: "Alpha threshold", children: _jsx(NumberField, { value: threshold, min: 0, step: 0.05, onChange: (v) => setThreshold(Math.min(1, v)) }) }), _jsxs("button", { className: "qe-hover", style: { display: 'flex', alignItems: 'center', gap: 4, marginTop: 6, fontSize: 12 }, onClick: () => void addFromImage(behavior), children: [_jsx(PlusIcon, { style: iconStyle(13) }), "Add sequence from image"] }), status && _jsx("p", { style: { margin: '4px 0 0', color: theme.textDim, fontSize: 11.5 }, children: status })] })) }));
|
|
139
144
|
}
|
|
145
|
+
/** The Noise module (Unity's Noise): moves and turns particles by a noise field. */
|
|
140
146
|
export function NoiseModule({ binding }) {
|
|
141
147
|
return (_jsx(BehaviorModule, { binding: binding, title: "Noise", type: "Noise", create: () => new Noise(new ConstantValue(1), new ConstantValue(1), new ConstantValue(1), new ConstantValue(0)), children: (behavior) => (_jsxs(_Fragment, { children: [_jsx(ValueField, { hintKey: "noise.frequency", label: "Frequency", generator: behavior.frequency, min: 0, curveMax: 10, onChange: (g) => binding.apply(() => (behavior.frequency = g)) }), _jsx(ValueField, { hintKey: "noise.strength", label: "Strength", generator: behavior.power, min: 0, curveMax: 10, onChange: (g) => binding.apply(() => (behavior.power = g)) }), _jsx(ValueField, { label: "Position amount", generator: behavior.positionAmount, min: 0, curveMax: 2, onChange: (g) => binding.apply(() => (behavior.positionAmount = g)) }), _jsx(ValueField, { label: "Rotation amount", generator: behavior.rotationAmount, min: 0, curveMax: Math.PI, onChange: (g) => binding.apply(() => (behavior.rotationAmount = g)) }), _jsx(ValueField, { label: "Scroll speed", generator: behavior.scrollSpeed, curveMax: 5, onChange: (g) => binding.apply(() => (behavior.scrollSpeed = g)) }), _jsx(Row, { label: "Remap", children: _jsx(CheckboxField, { value: behavior.remap !== undefined, onChange: (v) => binding.apply(() => (behavior.remap = v
|
|
142
148
|
? new PiecewiseBezier([[new Bezier(-1, -1 / 3, 1 / 3, 1), 0]])
|
|
@@ -151,6 +157,7 @@ function Vector3Row(props) {
|
|
|
151
157
|
};
|
|
152
158
|
return (_jsx(Row, { label: props.label, hintKey: props.hintKey, children: _jsxs("div", { style: { display: 'flex', gap: 6 }, children: [_jsx(NumberField, { value: props.value.x, min: props.min, step: props.step, onChange: (n) => set('x', n) }), _jsx(NumberField, { value: props.value.y, min: props.min, step: props.step, onChange: (n) => set('y', n) }), _jsx(NumberField, { value: props.value.z, min: props.min, step: props.step, onChange: (n) => set('z', n) })] }) }));
|
|
153
159
|
}
|
|
160
|
+
/** The Turbulence module: a curl noise force field. */
|
|
154
161
|
export function TurbulenceModule({ binding }) {
|
|
155
162
|
return (_jsx(BehaviorModule, { binding: binding, title: "Turbulence", type: "TurbulenceField", create: () => new TurbulenceField(new Vector3(1, 1, 1), 3, new Vector3(1, 1, 1), new Vector3(1, 1, 1)), children: (behavior) => (_jsxs(_Fragment, { children: [_jsx(Vector3Row, { hintKey: "turbulence.scale", label: "Scale", value: behavior.scale, min: 0.01, step: 0.1, onChange: (v) => binding.apply(() => (behavior.scale = v)) }), _jsx(Row, { label: "Octaves", children: _jsx(NumberField, { value: behavior.octaves, min: 1, step: 1, onChange: (v) => binding.apply(() => (behavior.octaves = Math.max(1, Math.round(v)))) }) }), _jsx(Vector3Row, { hintKey: "turbulence.strength", label: "Strength", value: behavior.velocityMultiplier, step: 0.5, onChange: (v) => binding.apply(() => (behavior.velocityMultiplier = v)) }), _jsx(Vector3Row, { label: "Time scale", value: behavior.timeScale, step: 0.1, onChange: (v) => binding.apply(() => (behavior.timeScale = v)) }), _jsx(Row, { label: "Seed", children: _jsx(NumberField, { value: behavior.seed, step: 1, onChange: (v) => binding.apply(() => {
|
|
156
163
|
const seed = Math.round(v);
|
|
@@ -161,9 +168,11 @@ export function TurbulenceModule({ binding }) {
|
|
|
161
168
|
behavior.timeOffset.copy(next.initialOffset);
|
|
162
169
|
}) }) })] })) }));
|
|
163
170
|
}
|
|
171
|
+
/** The Randomize Direction module: tilts each particle's start direction by a random angle. */
|
|
164
172
|
export function EmitDirectionModule({ binding }) {
|
|
165
|
-
return (_jsx(BehaviorModule, { binding: binding, title: "Randomize Direction", type: "ChangeEmitDirection", create: () => new ChangeEmitDirection(new IntervalValue(0, Math.PI / 6)), children: (behavior) => (_jsx(ValueField, { label: "Angle (rad)", generator: behavior.angle,
|
|
173
|
+
return (_jsx(BehaviorModule, { binding: binding, title: "Randomize Direction", type: "ChangeEmitDirection", create: () => new ChangeEmitDirection(new IntervalValue(0, Math.PI / 6)), children: (behavior) => (_jsx(ValueField, { label: "Angle (rad)", generator: behavior.angle, curve: false, min: 0, onChange: (g) => binding.apply(() => (behavior.angle = g)) })) }));
|
|
166
174
|
}
|
|
175
|
+
/** The Collision module (Unity's Collision): particles bounce off or die on a ground plane. */
|
|
167
176
|
export function CollisionModule({ binding }) {
|
|
168
177
|
// Like Unity: Collision stores bounce only. Colliders are host-provided via
|
|
169
178
|
// setPhysicsResolver, asked when a particle is tested — the editor never invents a ground plane.
|
|
@@ -173,20 +182,25 @@ export function CollisionModule({ binding }) {
|
|
|
173
182
|
function SpeedRangeRows({ binding, behavior }) {
|
|
174
183
|
return (_jsx(Row, { label: "Speed range", children: _jsxs("div", { style: { display: 'flex', gap: 6 }, children: [_jsx(NumberField, { value: behavior.speedRange.a, min: 0, onChange: (v) => binding.apply(() => (behavior.speedRange = new IntervalValue(v, behavior.speedRange.b))) }), _jsx(NumberField, { value: behavior.speedRange.b, min: 0, onChange: (v) => binding.apply(() => (behavior.speedRange = new IntervalValue(behavior.speedRange.a, v))) })] }) }));
|
|
175
184
|
}
|
|
185
|
+
/** The Color by Speed module (Unity's Color by Speed): a gradient over a speed range. */
|
|
176
186
|
export function ColorBySpeedModule({ binding }) {
|
|
177
187
|
return (_jsx(BehaviorModule, { binding: binding, title: "Color by Speed", type: "ColorBySpeed", create: () => new ColorBySpeed(buildGradient(DEFAULT_GRADIENT_STOPS), new IntervalValue(0, 5)), children: (behavior) => (_jsxs(_Fragment, { children: [_jsx("div", { style: { marginTop: 6 }, children: _jsx(GradientEditor, { stops: behavior.color instanceof Gradient
|
|
178
188
|
? readGradientStops(behavior.color)
|
|
179
189
|
: DEFAULT_GRADIENT_STOPS, onChange: (stops) => binding.apply(() => (behavior.color = buildGradient(stops))) }) }), _jsx(SpeedRangeRows, { binding: binding, behavior: behavior })] })) }));
|
|
180
190
|
}
|
|
191
|
+
/** The Size by Speed module (Unity's Size by Speed): size over a speed range. */
|
|
181
192
|
export function SizeBySpeedModule({ binding }) {
|
|
182
193
|
return (_jsx(BehaviorModule, { binding: binding, title: "Size by Speed", type: "SizeBySpeed", create: () => new SizeBySpeed(buildScalar({ mode: 'curve', value: 1, min: 0, max: 1, curve: [0.5, 0.8, 1.2, 1.5] }), new IntervalValue(0, 5)), children: (behavior) => (_jsxs(_Fragment, { children: [_jsx("div", { style: { marginTop: 6 }, children: _jsx(CurveEditor, { pieces: readPieces(behavior.size), maxValue: 2, onChange: (pieces) => binding.apply(() => (behavior.size = buildCurve(pieces))) }) }), _jsx(SpeedRangeRows, { binding: binding, behavior: behavior })] })) }));
|
|
183
194
|
}
|
|
195
|
+
/** The Rotation by Speed module (Unity's Rotation by Speed): spin rate over a speed range. */
|
|
184
196
|
export function RotationBySpeedModule({ binding }) {
|
|
185
197
|
return (_jsx(BehaviorModule, { binding: binding, title: "Rotation by Speed", type: "RotationBySpeed", create: () => new RotationBySpeed(new ConstantValue(Math.PI), new IntervalValue(0, 5)), children: (behavior) => (_jsxs(_Fragment, { children: [_jsx(ValueField, { hintKey: "rotationBySpeed.velocity", label: "Velocity (rad/s)", generator: behavior.angularVelocity, curveMax: Math.PI * 4, onChange: (g) => binding.apply(() => (behavior.angularVelocity = g)) }), _jsx(SpeedRangeRows, { binding: binding, behavior: behavior })] })) }));
|
|
186
198
|
}
|
|
199
|
+
/** The Orbit over Lifetime module (Unity's Velocity over Lifetime orbital): turns particles about an axis. */
|
|
187
200
|
export function OrbitOverLifeModule({ binding }) {
|
|
188
201
|
return (_jsx(BehaviorModule, { binding: binding, title: "Orbit over Lifetime", type: "OrbitOverLife", create: () => new OrbitOverLife(new ConstantValue(Math.PI), new Vector3(0, 1, 0)), children: (behavior) => (_jsx(ValueField, { label: "Orbit speed", generator: behavior.orbitSpeed, curveMax: Math.PI * 4, onChange: (g) => binding.apply(() => (behavior.orbitSpeed = g)) })) }));
|
|
189
202
|
}
|
|
203
|
+
/** The Width over Trail module (Unity's Trails width over trail): a trail's width along its length. */
|
|
190
204
|
export function WidthOverTrailModule({ binding }) {
|
|
191
205
|
return (_jsx(BehaviorModule, { binding: binding, title: "Width over Trail", type: "WidthOverLength", create: () => new WidthOverLength(buildScalar({ mode: 'curve', value: 1, min: 0, max: 1, curve: [1, 0.75, 0.4, 0] })), children: (behavior) => (_jsxs(_Fragment, { children: [_jsx("div", { style: { marginTop: 6 }, children: _jsx(CurveEditor, { pieces: readPieces(behavior.width), maxValue: 2, onChange: (pieces) => binding.apply(() => (behavior.width = buildCurve(pieces))) }) }), _jsx(Row, { label: "Absolute width", children: _jsx(CheckboxField, { value: behavior.absolute, onChange: (v) => binding.apply(() => (behavior.absolute = v)) }) })] })) }));
|
|
192
206
|
}
|
|
@@ -195,6 +209,7 @@ function startFrameCount(startFrame) {
|
|
|
195
209
|
const state = readScalar(startFrame);
|
|
196
210
|
return Math.round(state.mode === 'random' ? state.max : state.value);
|
|
197
211
|
}
|
|
212
|
+
/** The Texture Sheet Animation module (Unity's Texture Sheet Animation): plays the sheet's frames by life or by frame rate. */
|
|
198
213
|
export function TextureSheetModule({ binding }) {
|
|
199
214
|
const system = binding.system;
|
|
200
215
|
const frameBehavior = findBehavior(system.behaviors, 'FrameOverLife');
|