babylon.quarks-editor 0.17.7 → 0.17.8
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/dist/core/timeline.d.ts +1 -1
- package/dist/core/timeline.js +3 -2
- package/dist/react/EffectEditor.d.ts +3 -0
- package/dist/react/EffectEditor.js +6 -1
- package/dist/react/EffectEditorHost.js +72 -11
- package/dist/react/ObjectModule.d.ts +6 -0
- package/dist/react/ObjectModule.js +28 -0
- package/dist/react/TimelinePanel.d.ts +4 -3
- package/dist/react/TimelinePanel.js +60 -10
- package/dist/react/TimelineTrackRow.d.ts +2 -2
- package/dist/react/TimelineTrackRow.js +16 -12
- package/dist/react/theme.js +44 -21
- package/package.json +68 -63
- package/src/core/binding.ts +105 -105
- package/src/core/collision.ts +40 -40
- package/src/core/groups.ts +29 -29
- package/src/core/loadEffect.ts +102 -102
- package/src/core/scrub.ts +28 -28
- package/src/core/shapes.ts +133 -133
- package/src/core/systems.ts +98 -98
- package/src/core/timeline.ts +95 -94
- package/src/core/values.ts +113 -113
- package/src/index.ts +32 -32
- package/src/react/CurveEditor.tsx +168 -168
- package/src/react/EffectEditor.tsx +125 -111
- package/src/react/EffectEditorHost.tsx +466 -377
- package/src/react/GradientEditor.tsx +127 -127
- package/src/react/ModuleSection.tsx +54 -54
- package/src/react/ObjectModule.tsx +47 -0
- package/src/react/PromptDialog.tsx +85 -85
- package/src/react/TimelinePanel.tsx +412 -354
- package/src/react/TimelineTrackRow.tsx +291 -267
- package/src/react/behaviorModules.tsx +658 -658
- package/src/react/fields.tsx +76 -76
- package/src/react/index.ts +43 -43
- package/src/react/modules.tsx +1034 -1034
- package/src/react/theme.ts +108 -85
package/src/core/loadEffect.ts
CHANGED
|
@@ -1,102 +1,102 @@
|
|
|
1
|
-
import {ParticleEmitter, QuarksLoader, QuarksUtil} from 'babylon.quarks';
|
|
2
|
-
import type {BatchedRenderer, ParticleSystem} from 'babylon.quarks';
|
|
3
|
-
import type {Scene} from '@babylonjs/core/scene';
|
|
4
|
-
import {TransformNode} from '@babylonjs/core/Meshes/transformNode';
|
|
5
|
-
import type {Node} from '@babylonjs/core/node';
|
|
6
|
-
|
|
7
|
-
/** `ParticleEmitter`'s constructor default (see babylon.quarks/src/ParticleEmitter.ts) — the
|
|
8
|
-
* name every emitter gets when the source JSON didn't give it one of its own. */
|
|
9
|
-
const DEFAULT_EMITTER_NAME = 'particleEmitter';
|
|
10
|
-
|
|
11
|
-
function hasMeaningfulName(node: TransformNode): boolean {
|
|
12
|
-
return !!node.name && node.name !== DEFAULT_EMITTER_NAME;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
function groupChildren(node: TransformNode): TransformNode[] {
|
|
16
|
-
return node.getChildren().filter((c): c is TransformNode => c instanceof TransformNode);
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/** Reparents `only` onto `parent`, transferring the outgoing node's name if `only` doesn't
|
|
20
|
-
* already have a meaningful one of its own, then disposes the now-empty wrapper. World
|
|
21
|
-
* transform is preserved via `setParent`. */
|
|
22
|
-
function collapseInto(wrapper: TransformNode, only: TransformNode, parent: Node | null): void {
|
|
23
|
-
if (!hasMeaningfulName(only) && hasMeaningfulName(wrapper)) {
|
|
24
|
-
only.name = wrapper.name;
|
|
25
|
-
}
|
|
26
|
-
only.setParent(parent);
|
|
27
|
-
wrapper.dispose(true, false);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Collapses redundant organizational groups left over by importers — Unity's
|
|
32
|
-
* one-GameObject-per-component convention turns every visual layer into its own Group, so a
|
|
33
|
-
* single particle system becomes "Sparks" > unnamed emitter, and a component quarks doesn't
|
|
34
|
-
* understand (e.g. a Light) becomes an empty Group with nothing under it. Recurses
|
|
35
|
-
* bottom-up so nested wrapper chains collapse all the way down. The tree root itself is
|
|
36
|
-
* handled separately by `promoteIfSingleChildRoot`, since collapsing it means returning a
|
|
37
|
-
* different node as the effect's root.
|
|
38
|
-
*/
|
|
39
|
-
function flattenRedundantGroups(node: TransformNode): void {
|
|
40
|
-
for (const child of node.getChildren()) {
|
|
41
|
-
if (child instanceof TransformNode) {
|
|
42
|
-
flattenRedundantGroups(child);
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
if (node instanceof ParticleEmitter || !node.parent) {
|
|
46
|
-
return;
|
|
47
|
-
}
|
|
48
|
-
const children = groupChildren(node);
|
|
49
|
-
if (children.length === 0) {
|
|
50
|
-
node.dispose(true, false);
|
|
51
|
-
return;
|
|
52
|
-
}
|
|
53
|
-
if (children.length !== 1) {
|
|
54
|
-
return;
|
|
55
|
-
}
|
|
56
|
-
const only = children[0];
|
|
57
|
-
if (only instanceof ParticleEmitter && only.getChildren().length > 0) {
|
|
58
|
-
return;
|
|
59
|
-
}
|
|
60
|
-
collapseInto(node, only, node.parent);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/** If the tree root itself only ever wraps a single child (after `flattenRedundantGroups`
|
|
64
|
-
* has settled everything below it), that wrapper adds a tree level with nothing to organize —
|
|
65
|
-
* promote the child to be the new root instead. Loops in case that child is, in turn, also a
|
|
66
|
-
* single-child wrapper. */
|
|
67
|
-
function promoteIfSingleChildRoot(root: TransformNode): TransformNode {
|
|
68
|
-
let current = root;
|
|
69
|
-
while (!(current instanceof ParticleEmitter)) {
|
|
70
|
-
const children = groupChildren(current);
|
|
71
|
-
if (children.length !== 1) {
|
|
72
|
-
break;
|
|
73
|
-
}
|
|
74
|
-
const only = children[0];
|
|
75
|
-
const parent = current.parent;
|
|
76
|
-
collapseInto(current, only, parent);
|
|
77
|
-
current = only;
|
|
78
|
-
}
|
|
79
|
-
return current;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/** Parses a Quarks JSON export, registers all systems with the renderer and starts playback. */
|
|
83
|
-
export function loadEffectFromJson(
|
|
84
|
-
scene: Scene,
|
|
85
|
-
renderer: BatchedRenderer,
|
|
86
|
-
json: unknown,
|
|
87
|
-
baseUrl = ''
|
|
88
|
-
): {root: TransformNode; systems: ParticleSystem[]} {
|
|
89
|
-
const loader = new QuarksLoader(scene, {baseUrl});
|
|
90
|
-
let root = loader.parse(json as never, baseUrl);
|
|
91
|
-
flattenRedundantGroups(root);
|
|
92
|
-
root = promoteIfSingleChildRoot(root);
|
|
93
|
-
const systems: ParticleSystem[] = [];
|
|
94
|
-
QuarksUtil.runOnAllParticleEmitters(root, (emitter: ParticleEmitter) => {
|
|
95
|
-
const system = emitter.system as ParticleSystem;
|
|
96
|
-
renderer.addSystem(system);
|
|
97
|
-
systems.push(system);
|
|
98
|
-
});
|
|
99
|
-
QuarksUtil.restart(root);
|
|
100
|
-
QuarksUtil.play(root);
|
|
101
|
-
return {root, systems};
|
|
102
|
-
}
|
|
1
|
+
import {ParticleEmitter, QuarksLoader, QuarksUtil} from 'babylon.quarks';
|
|
2
|
+
import type {BatchedRenderer, ParticleSystem} from 'babylon.quarks';
|
|
3
|
+
import type {Scene} from '@babylonjs/core/scene';
|
|
4
|
+
import {TransformNode} from '@babylonjs/core/Meshes/transformNode';
|
|
5
|
+
import type {Node} from '@babylonjs/core/node';
|
|
6
|
+
|
|
7
|
+
/** `ParticleEmitter`'s constructor default (see babylon.quarks/src/ParticleEmitter.ts) — the
|
|
8
|
+
* name every emitter gets when the source JSON didn't give it one of its own. */
|
|
9
|
+
const DEFAULT_EMITTER_NAME = 'particleEmitter';
|
|
10
|
+
|
|
11
|
+
function hasMeaningfulName(node: TransformNode): boolean {
|
|
12
|
+
return !!node.name && node.name !== DEFAULT_EMITTER_NAME;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function groupChildren(node: TransformNode): TransformNode[] {
|
|
16
|
+
return node.getChildren().filter((c): c is TransformNode => c instanceof TransformNode);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Reparents `only` onto `parent`, transferring the outgoing node's name if `only` doesn't
|
|
20
|
+
* already have a meaningful one of its own, then disposes the now-empty wrapper. World
|
|
21
|
+
* transform is preserved via `setParent`. */
|
|
22
|
+
function collapseInto(wrapper: TransformNode, only: TransformNode, parent: Node | null): void {
|
|
23
|
+
if (!hasMeaningfulName(only) && hasMeaningfulName(wrapper)) {
|
|
24
|
+
only.name = wrapper.name;
|
|
25
|
+
}
|
|
26
|
+
only.setParent(parent);
|
|
27
|
+
wrapper.dispose(true, false);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Collapses redundant organizational groups left over by importers — Unity's
|
|
32
|
+
* one-GameObject-per-component convention turns every visual layer into its own Group, so a
|
|
33
|
+
* single particle system becomes "Sparks" > unnamed emitter, and a component quarks doesn't
|
|
34
|
+
* understand (e.g. a Light) becomes an empty Group with nothing under it. Recurses
|
|
35
|
+
* bottom-up so nested wrapper chains collapse all the way down. The tree root itself is
|
|
36
|
+
* handled separately by `promoteIfSingleChildRoot`, since collapsing it means returning a
|
|
37
|
+
* different node as the effect's root.
|
|
38
|
+
*/
|
|
39
|
+
function flattenRedundantGroups(node: TransformNode): void {
|
|
40
|
+
for (const child of node.getChildren()) {
|
|
41
|
+
if (child instanceof TransformNode) {
|
|
42
|
+
flattenRedundantGroups(child);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
if (node instanceof ParticleEmitter || !node.parent) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const children = groupChildren(node);
|
|
49
|
+
if (children.length === 0) {
|
|
50
|
+
node.dispose(true, false);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
if (children.length !== 1) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
const only = children[0];
|
|
57
|
+
if (only instanceof ParticleEmitter && only.getChildren().length > 0) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
collapseInto(node, only, node.parent);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** If the tree root itself only ever wraps a single child (after `flattenRedundantGroups`
|
|
64
|
+
* has settled everything below it), that wrapper adds a tree level with nothing to organize —
|
|
65
|
+
* promote the child to be the new root instead. Loops in case that child is, in turn, also a
|
|
66
|
+
* single-child wrapper. */
|
|
67
|
+
function promoteIfSingleChildRoot(root: TransformNode): TransformNode {
|
|
68
|
+
let current = root;
|
|
69
|
+
while (!(current instanceof ParticleEmitter)) {
|
|
70
|
+
const children = groupChildren(current);
|
|
71
|
+
if (children.length !== 1) {
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
const only = children[0];
|
|
75
|
+
const parent = current.parent;
|
|
76
|
+
collapseInto(current, only, parent);
|
|
77
|
+
current = only;
|
|
78
|
+
}
|
|
79
|
+
return current;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Parses a Quarks JSON export, registers all systems with the renderer and starts playback. */
|
|
83
|
+
export function loadEffectFromJson(
|
|
84
|
+
scene: Scene,
|
|
85
|
+
renderer: BatchedRenderer,
|
|
86
|
+
json: unknown,
|
|
87
|
+
baseUrl = ''
|
|
88
|
+
): {root: TransformNode; systems: ParticleSystem[]} {
|
|
89
|
+
const loader = new QuarksLoader(scene, {baseUrl});
|
|
90
|
+
let root = loader.parse(json as never, baseUrl);
|
|
91
|
+
flattenRedundantGroups(root);
|
|
92
|
+
root = promoteIfSingleChildRoot(root);
|
|
93
|
+
const systems: ParticleSystem[] = [];
|
|
94
|
+
QuarksUtil.runOnAllParticleEmitters(root, (emitter: ParticleEmitter) => {
|
|
95
|
+
const system = emitter.system as ParticleSystem;
|
|
96
|
+
renderer.addSystem(system);
|
|
97
|
+
systems.push(system);
|
|
98
|
+
});
|
|
99
|
+
QuarksUtil.restart(root);
|
|
100
|
+
QuarksUtil.play(root);
|
|
101
|
+
return {root, systems};
|
|
102
|
+
}
|
package/src/core/scrub.ts
CHANGED
|
@@ -1,28 +1,28 @@
|
|
|
1
|
-
import type {BatchedRenderer} from 'babylon.quarks';
|
|
2
|
-
import type {EffectBinding} from './binding';
|
|
3
|
-
|
|
4
|
-
const WIDE_STEP_THRESHOLD = 8;
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Approximate timeline scrub: replays every system from t=0 up to `time` using the existing
|
|
8
|
-
* simulation loop. Not frame-exact (spawn/burst randomization is unseeded), but reuses
|
|
9
|
-
* `EffectBinding.restart()` and `BatchedRenderer.update()` verbatim — no new engine primitive.
|
|
10
|
-
*
|
|
11
|
-
* `ParticleSystem.update()` clamps its own `delta` to 0.1s per call, so a single big step
|
|
12
|
-
* can't reach `time` directly; must step through in small increments instead.
|
|
13
|
-
*/
|
|
14
|
-
export function scrubTo(binding: EffectBinding, renderer: BatchedRenderer, time: number): void {
|
|
15
|
-
binding.restart();
|
|
16
|
-
if (time <= 0) {
|
|
17
|
-
return;
|
|
18
|
-
}
|
|
19
|
-
const fixedStep = time > WIDE_STEP_THRESHOLD ? 1 / 30 : 1 / 60;
|
|
20
|
-
const steps = Math.floor(time / fixedStep);
|
|
21
|
-
for (let i = 0; i < steps; i++) {
|
|
22
|
-
renderer.update(fixedStep);
|
|
23
|
-
}
|
|
24
|
-
const remainder = time - steps * fixedStep;
|
|
25
|
-
if (remainder > 0) {
|
|
26
|
-
renderer.update(remainder);
|
|
27
|
-
}
|
|
28
|
-
}
|
|
1
|
+
import type {BatchedRenderer} from 'babylon.quarks';
|
|
2
|
+
import type {EffectBinding} from './binding';
|
|
3
|
+
|
|
4
|
+
const WIDE_STEP_THRESHOLD = 8;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Approximate timeline scrub: replays every system from t=0 up to `time` using the existing
|
|
8
|
+
* simulation loop. Not frame-exact (spawn/burst randomization is unseeded), but reuses
|
|
9
|
+
* `EffectBinding.restart()` and `BatchedRenderer.update()` verbatim — no new engine primitive.
|
|
10
|
+
*
|
|
11
|
+
* `ParticleSystem.update()` clamps its own `delta` to 0.1s per call, so a single big step
|
|
12
|
+
* can't reach `time` directly; must step through in small increments instead.
|
|
13
|
+
*/
|
|
14
|
+
export function scrubTo(binding: EffectBinding, renderer: BatchedRenderer, time: number): void {
|
|
15
|
+
binding.restart();
|
|
16
|
+
if (time <= 0) {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
const fixedStep = time > WIDE_STEP_THRESHOLD ? 1 / 30 : 1 / 60;
|
|
20
|
+
const steps = Math.floor(time / fixedStep);
|
|
21
|
+
for (let i = 0; i < steps; i++) {
|
|
22
|
+
renderer.update(fixedStep);
|
|
23
|
+
}
|
|
24
|
+
const remainder = time - steps * fixedStep;
|
|
25
|
+
if (remainder > 0) {
|
|
26
|
+
renderer.update(remainder);
|
|
27
|
+
}
|
|
28
|
+
}
|
package/src/core/shapes.ts
CHANGED
|
@@ -1,133 +1,133 @@
|
|
|
1
|
-
import {
|
|
2
|
-
CircleEmitter,
|
|
3
|
-
ConeEmitter,
|
|
4
|
-
DonutEmitter,
|
|
5
|
-
GridEmitter,
|
|
6
|
-
HemisphereEmitter,
|
|
7
|
-
MeshSurfaceEmitter,
|
|
8
|
-
PointEmitter,
|
|
9
|
-
RectangleEmitter,
|
|
10
|
-
SphereEmitter,
|
|
11
|
-
} from 'babylon.quarks';
|
|
12
|
-
import type {EmitterShape} from 'babylon.quarks';
|
|
13
|
-
import {EmitterMode} from 'babylon.quarks';
|
|
14
|
-
import type {Mesh} from '@babylonjs/core/Meshes/mesh';
|
|
15
|
-
|
|
16
|
-
export type ShapeType = 'point' | 'sphere' | 'hemisphere' | 'cone' | 'circle' | 'donut' | 'rectangle' | 'grid' | 'mesh_surface';
|
|
17
|
-
|
|
18
|
-
export const SHAPE_TYPES: Array<{value: ShapeType; label: string}> = [
|
|
19
|
-
{value: 'cone', label: 'Cone'},
|
|
20
|
-
{value: 'sphere', label: 'Sphere'},
|
|
21
|
-
{value: 'hemisphere', label: 'Hemisphere'},
|
|
22
|
-
{value: 'circle', label: 'Circle'},
|
|
23
|
-
{value: 'donut', label: 'Donut'},
|
|
24
|
-
{value: 'rectangle', label: 'Rectangle'},
|
|
25
|
-
{value: 'grid', label: 'Grid'},
|
|
26
|
-
{value: 'mesh_surface', label: 'Mesh surface'},
|
|
27
|
-
{value: 'point', label: 'Point'},
|
|
28
|
-
];
|
|
29
|
-
|
|
30
|
-
export interface ShapeParams {
|
|
31
|
-
radius: number;
|
|
32
|
-
arc: number;
|
|
33
|
-
thickness: number;
|
|
34
|
-
angle: number;
|
|
35
|
-
donutRadius: number;
|
|
36
|
-
width: number;
|
|
37
|
-
height: number;
|
|
38
|
-
mode: EmitterMode;
|
|
39
|
-
spread: number;
|
|
40
|
-
column: number;
|
|
41
|
-
row: number;
|
|
42
|
-
mesh?: Mesh;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export const DEFAULT_SHAPE_PARAMS: ShapeParams = {
|
|
46
|
-
radius: 0.3,
|
|
47
|
-
arc: Math.PI * 2,
|
|
48
|
-
thickness: 1,
|
|
49
|
-
angle: 0.5,
|
|
50
|
-
donutRadius: 0.1,
|
|
51
|
-
width: 1,
|
|
52
|
-
height: 1,
|
|
53
|
-
mode: EmitterMode.Random,
|
|
54
|
-
spread: 0,
|
|
55
|
-
column: 10,
|
|
56
|
-
row: 10,
|
|
57
|
-
mesh: undefined,
|
|
58
|
-
};
|
|
59
|
-
|
|
60
|
-
export function getShapeType(shape: EmitterShape): ShapeType {
|
|
61
|
-
const type = (shape as {type?: string}).type ?? 'point';
|
|
62
|
-
return (SHAPE_TYPES.some((s) => s.value === type) ? type : 'point') as ShapeType;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
export function readShapeParams(shape: EmitterShape): ShapeParams {
|
|
66
|
-
const anyShape = shape as unknown as {[key: string]: unknown};
|
|
67
|
-
const num = (key: string, fallback: number) => (typeof anyShape[key] === 'number' ? (anyShape[key] as number) : fallback);
|
|
68
|
-
return {
|
|
69
|
-
radius: num('radius', DEFAULT_SHAPE_PARAMS.radius),
|
|
70
|
-
mode: num('mode', DEFAULT_SHAPE_PARAMS.mode) as EmitterMode,
|
|
71
|
-
spread: num('spread', DEFAULT_SHAPE_PARAMS.spread),
|
|
72
|
-
arc: num('arc', DEFAULT_SHAPE_PARAMS.arc),
|
|
73
|
-
thickness: num('thickness', DEFAULT_SHAPE_PARAMS.thickness),
|
|
74
|
-
angle: num('angle', DEFAULT_SHAPE_PARAMS.angle),
|
|
75
|
-
donutRadius: num('donutRadius', DEFAULT_SHAPE_PARAMS.donutRadius),
|
|
76
|
-
width: num('width', DEFAULT_SHAPE_PARAMS.width),
|
|
77
|
-
height: num('height', DEFAULT_SHAPE_PARAMS.height),
|
|
78
|
-
column: num('column', DEFAULT_SHAPE_PARAMS.column),
|
|
79
|
-
row: num('row', DEFAULT_SHAPE_PARAMS.row),
|
|
80
|
-
mesh: anyShape.mesh as Mesh | undefined,
|
|
81
|
-
};
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/** Which parameters are relevant per shape type (drives the UI). `mesh_surface`'s mesh
|
|
85
|
-
* reference isn't a NumberField, so it's handled separately by the Shape module. */
|
|
86
|
-
export const SHAPE_PARAM_KEYS: {[K in ShapeType]: Array<keyof ShapeParams>} = {
|
|
87
|
-
point: [],
|
|
88
|
-
sphere: ['radius', 'arc', 'thickness'],
|
|
89
|
-
hemisphere: ['radius', 'arc', 'thickness'],
|
|
90
|
-
cone: ['radius', 'arc', 'thickness', 'angle'],
|
|
91
|
-
circle: ['radius', 'arc', 'thickness'],
|
|
92
|
-
donut: ['radius', 'arc', 'thickness', 'donutRadius'],
|
|
93
|
-
rectangle: ['width', 'height'],
|
|
94
|
-
grid: ['width', 'height', 'column', 'row'],
|
|
95
|
-
mesh_surface: [],
|
|
96
|
-
};
|
|
97
|
-
|
|
98
|
-
export function createShape(type: ShapeType, params: ShapeParams): EmitterShape {
|
|
99
|
-
switch (type) {
|
|
100
|
-
case 'point':
|
|
101
|
-
return new PointEmitter();
|
|
102
|
-
case 'sphere':
|
|
103
|
-
return new SphereEmitter({radius: params.radius, arc: params.arc, thickness: params.thickness, mode: params.mode, spread: params.spread});
|
|
104
|
-
case 'hemisphere':
|
|
105
|
-
return new HemisphereEmitter({radius: params.radius, arc: params.arc, thickness: params.thickness, mode: params.mode, spread: params.spread});
|
|
106
|
-
case 'cone':
|
|
107
|
-
return new ConeEmitter({
|
|
108
|
-
radius: params.radius,
|
|
109
|
-
arc: params.arc,
|
|
110
|
-
thickness: params.thickness,
|
|
111
|
-
angle: params.angle,
|
|
112
|
-
mode: params.mode,
|
|
113
|
-
spread: params.spread,
|
|
114
|
-
});
|
|
115
|
-
case 'circle':
|
|
116
|
-
return new CircleEmitter({radius: params.radius, arc: params.arc, thickness: params.thickness, mode: params.mode, spread: params.spread});
|
|
117
|
-
case 'donut':
|
|
118
|
-
return new DonutEmitter({
|
|
119
|
-
radius: params.radius,
|
|
120
|
-
arc: params.arc,
|
|
121
|
-
thickness: params.thickness,
|
|
122
|
-
donutRadius: params.donutRadius,
|
|
123
|
-
mode: params.mode,
|
|
124
|
-
spread: params.spread,
|
|
125
|
-
});
|
|
126
|
-
case 'rectangle':
|
|
127
|
-
return new RectangleEmitter({width: params.width, height: params.height, mode: params.mode, spread: params.spread});
|
|
128
|
-
case 'grid':
|
|
129
|
-
return new GridEmitter({width: params.width, height: params.height, column: params.column, row: params.row});
|
|
130
|
-
case 'mesh_surface':
|
|
131
|
-
return new MeshSurfaceEmitter(params.mesh);
|
|
132
|
-
}
|
|
133
|
-
}
|
|
1
|
+
import {
|
|
2
|
+
CircleEmitter,
|
|
3
|
+
ConeEmitter,
|
|
4
|
+
DonutEmitter,
|
|
5
|
+
GridEmitter,
|
|
6
|
+
HemisphereEmitter,
|
|
7
|
+
MeshSurfaceEmitter,
|
|
8
|
+
PointEmitter,
|
|
9
|
+
RectangleEmitter,
|
|
10
|
+
SphereEmitter,
|
|
11
|
+
} from 'babylon.quarks';
|
|
12
|
+
import type {EmitterShape} from 'babylon.quarks';
|
|
13
|
+
import {EmitterMode} from 'babylon.quarks';
|
|
14
|
+
import type {Mesh} from '@babylonjs/core/Meshes/mesh';
|
|
15
|
+
|
|
16
|
+
export type ShapeType = 'point' | 'sphere' | 'hemisphere' | 'cone' | 'circle' | 'donut' | 'rectangle' | 'grid' | 'mesh_surface';
|
|
17
|
+
|
|
18
|
+
export const SHAPE_TYPES: Array<{value: ShapeType; label: string}> = [
|
|
19
|
+
{value: 'cone', label: 'Cone'},
|
|
20
|
+
{value: 'sphere', label: 'Sphere'},
|
|
21
|
+
{value: 'hemisphere', label: 'Hemisphere'},
|
|
22
|
+
{value: 'circle', label: 'Circle'},
|
|
23
|
+
{value: 'donut', label: 'Donut'},
|
|
24
|
+
{value: 'rectangle', label: 'Rectangle'},
|
|
25
|
+
{value: 'grid', label: 'Grid'},
|
|
26
|
+
{value: 'mesh_surface', label: 'Mesh surface'},
|
|
27
|
+
{value: 'point', label: 'Point'},
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
export interface ShapeParams {
|
|
31
|
+
radius: number;
|
|
32
|
+
arc: number;
|
|
33
|
+
thickness: number;
|
|
34
|
+
angle: number;
|
|
35
|
+
donutRadius: number;
|
|
36
|
+
width: number;
|
|
37
|
+
height: number;
|
|
38
|
+
mode: EmitterMode;
|
|
39
|
+
spread: number;
|
|
40
|
+
column: number;
|
|
41
|
+
row: number;
|
|
42
|
+
mesh?: Mesh;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export const DEFAULT_SHAPE_PARAMS: ShapeParams = {
|
|
46
|
+
radius: 0.3,
|
|
47
|
+
arc: Math.PI * 2,
|
|
48
|
+
thickness: 1,
|
|
49
|
+
angle: 0.5,
|
|
50
|
+
donutRadius: 0.1,
|
|
51
|
+
width: 1,
|
|
52
|
+
height: 1,
|
|
53
|
+
mode: EmitterMode.Random,
|
|
54
|
+
spread: 0,
|
|
55
|
+
column: 10,
|
|
56
|
+
row: 10,
|
|
57
|
+
mesh: undefined,
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export function getShapeType(shape: EmitterShape): ShapeType {
|
|
61
|
+
const type = (shape as {type?: string}).type ?? 'point';
|
|
62
|
+
return (SHAPE_TYPES.some((s) => s.value === type) ? type : 'point') as ShapeType;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function readShapeParams(shape: EmitterShape): ShapeParams {
|
|
66
|
+
const anyShape = shape as unknown as {[key: string]: unknown};
|
|
67
|
+
const num = (key: string, fallback: number) => (typeof anyShape[key] === 'number' ? (anyShape[key] as number) : fallback);
|
|
68
|
+
return {
|
|
69
|
+
radius: num('radius', DEFAULT_SHAPE_PARAMS.radius),
|
|
70
|
+
mode: num('mode', DEFAULT_SHAPE_PARAMS.mode) as EmitterMode,
|
|
71
|
+
spread: num('spread', DEFAULT_SHAPE_PARAMS.spread),
|
|
72
|
+
arc: num('arc', DEFAULT_SHAPE_PARAMS.arc),
|
|
73
|
+
thickness: num('thickness', DEFAULT_SHAPE_PARAMS.thickness),
|
|
74
|
+
angle: num('angle', DEFAULT_SHAPE_PARAMS.angle),
|
|
75
|
+
donutRadius: num('donutRadius', DEFAULT_SHAPE_PARAMS.donutRadius),
|
|
76
|
+
width: num('width', DEFAULT_SHAPE_PARAMS.width),
|
|
77
|
+
height: num('height', DEFAULT_SHAPE_PARAMS.height),
|
|
78
|
+
column: num('column', DEFAULT_SHAPE_PARAMS.column),
|
|
79
|
+
row: num('row', DEFAULT_SHAPE_PARAMS.row),
|
|
80
|
+
mesh: anyShape.mesh as Mesh | undefined,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Which parameters are relevant per shape type (drives the UI). `mesh_surface`'s mesh
|
|
85
|
+
* reference isn't a NumberField, so it's handled separately by the Shape module. */
|
|
86
|
+
export const SHAPE_PARAM_KEYS: {[K in ShapeType]: Array<keyof ShapeParams>} = {
|
|
87
|
+
point: [],
|
|
88
|
+
sphere: ['radius', 'arc', 'thickness'],
|
|
89
|
+
hemisphere: ['radius', 'arc', 'thickness'],
|
|
90
|
+
cone: ['radius', 'arc', 'thickness', 'angle'],
|
|
91
|
+
circle: ['radius', 'arc', 'thickness'],
|
|
92
|
+
donut: ['radius', 'arc', 'thickness', 'donutRadius'],
|
|
93
|
+
rectangle: ['width', 'height'],
|
|
94
|
+
grid: ['width', 'height', 'column', 'row'],
|
|
95
|
+
mesh_surface: [],
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
export function createShape(type: ShapeType, params: ShapeParams): EmitterShape {
|
|
99
|
+
switch (type) {
|
|
100
|
+
case 'point':
|
|
101
|
+
return new PointEmitter();
|
|
102
|
+
case 'sphere':
|
|
103
|
+
return new SphereEmitter({radius: params.radius, arc: params.arc, thickness: params.thickness, mode: params.mode, spread: params.spread});
|
|
104
|
+
case 'hemisphere':
|
|
105
|
+
return new HemisphereEmitter({radius: params.radius, arc: params.arc, thickness: params.thickness, mode: params.mode, spread: params.spread});
|
|
106
|
+
case 'cone':
|
|
107
|
+
return new ConeEmitter({
|
|
108
|
+
radius: params.radius,
|
|
109
|
+
arc: params.arc,
|
|
110
|
+
thickness: params.thickness,
|
|
111
|
+
angle: params.angle,
|
|
112
|
+
mode: params.mode,
|
|
113
|
+
spread: params.spread,
|
|
114
|
+
});
|
|
115
|
+
case 'circle':
|
|
116
|
+
return new CircleEmitter({radius: params.radius, arc: params.arc, thickness: params.thickness, mode: params.mode, spread: params.spread});
|
|
117
|
+
case 'donut':
|
|
118
|
+
return new DonutEmitter({
|
|
119
|
+
radius: params.radius,
|
|
120
|
+
arc: params.arc,
|
|
121
|
+
thickness: params.thickness,
|
|
122
|
+
donutRadius: params.donutRadius,
|
|
123
|
+
mode: params.mode,
|
|
124
|
+
spread: params.spread,
|
|
125
|
+
});
|
|
126
|
+
case 'rectangle':
|
|
127
|
+
return new RectangleEmitter({width: params.width, height: params.height, mode: params.mode, spread: params.spread});
|
|
128
|
+
case 'grid':
|
|
129
|
+
return new GridEmitter({width: params.width, height: params.height, column: params.column, row: params.row});
|
|
130
|
+
case 'mesh_surface':
|
|
131
|
+
return new MeshSurfaceEmitter(params.mesh);
|
|
132
|
+
}
|
|
133
|
+
}
|