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/systems.ts
CHANGED
|
@@ -1,98 +1,98 @@
|
|
|
1
|
-
import {
|
|
2
|
-
ConeEmitter,
|
|
3
|
-
ConstantColor,
|
|
4
|
-
ConstantValue,
|
|
5
|
-
IntervalValue,
|
|
6
|
-
ParticleSystem,
|
|
7
|
-
RenderMode,
|
|
8
|
-
Vector4,
|
|
9
|
-
} from 'babylon.quarks';
|
|
10
|
-
import type {TransformNode} from '@babylonjs/core/Meshes/transformNode';
|
|
11
|
-
import type {EffectBinding} from './binding';
|
|
12
|
-
|
|
13
|
-
export interface ChildSystemOptions {
|
|
14
|
-
name?: string;
|
|
15
|
-
/** One-shot burst (sub-emitter target) instead of a looping stream. */
|
|
16
|
-
oneShot?: boolean;
|
|
17
|
-
/** Attach point override — defaults to `parent.emitter`. Lets a new system land as a
|
|
18
|
-
* sibling under a group node instead of nesting under an existing system. */
|
|
19
|
-
parentNode?: TransformNode;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Creates a child system sharing the parent's scene/texture/blending, parented to the
|
|
24
|
-
* parent emitter (or `options.parentNode` when given) — the Unity pattern where an effect is
|
|
25
|
-
* a hierarchy of systems and groups.
|
|
26
|
-
*/
|
|
27
|
-
export function createChildSystem(parent: ParticleSystem, options: ChildSystemOptions = {}): ParticleSystem {
|
|
28
|
-
const oneShot = options.oneShot ?? false;
|
|
29
|
-
const child = new ParticleSystem({
|
|
30
|
-
scene: parent.emitter.getScene(),
|
|
31
|
-
duration: oneShot ? 1 : parent.duration,
|
|
32
|
-
looping: !oneShot,
|
|
33
|
-
startLife: new IntervalValue(0.3, oneShot ? 0.6 : 1.2),
|
|
34
|
-
startSpeed: new IntervalValue(1, 3),
|
|
35
|
-
startSize: new IntervalValue(0.05, 0.2),
|
|
36
|
-
startColor: new ConstantColor(new Vector4(1, 0.9, 0.6, 1)),
|
|
37
|
-
emissionOverTime: new ConstantValue(oneShot ? 0 : 30),
|
|
38
|
-
emissionBursts: oneShot ? [{time: 0, count: new ConstantValue(8), cycle: 1, interval: 0.01, probability: 1}] : [],
|
|
39
|
-
shape: new ConeEmitter({radius: 0.1, angle: Math.PI / 3}),
|
|
40
|
-
renderMode: RenderMode.BillBoard,
|
|
41
|
-
texture: parent.texture ?? undefined,
|
|
42
|
-
transparent: true,
|
|
43
|
-
blendMode: parent.blending,
|
|
44
|
-
worldSpace: true,
|
|
45
|
-
});
|
|
46
|
-
child.emitter.name = options.name ?? (oneShot ? 'sub-emitter' : 'child-system');
|
|
47
|
-
if (oneShot) {
|
|
48
|
-
child.onlyUsedByOther = true;
|
|
49
|
-
}
|
|
50
|
-
child.emitter.parent = options.parentNode ?? parent.emitter;
|
|
51
|
-
return child;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Strips EmitSubParticleSystem behaviors that point at any of the removed systems, so
|
|
56
|
-
* deleting a sub-emitter target never leaves a dangling reference (which crashes the next
|
|
57
|
-
* update with `reading 'duration'` on the disposed emitter).
|
|
58
|
-
*/
|
|
59
|
-
export function unlinkSubEmitterReferences(systems: ParticleSystem[], removed: ParticleSystem[]): void {
|
|
60
|
-
const removedEmitters = new Set(removed.map((system) => system.emitter));
|
|
61
|
-
for (const system of systems) {
|
|
62
|
-
const behaviors = system.behaviors as Array<{type?: string; subParticleSystem?: unknown}>;
|
|
63
|
-
for (let i = behaviors.length - 1; i >= 0; i--) {
|
|
64
|
-
const behavior = behaviors[i];
|
|
65
|
-
if (behavior.type === 'EmitSubParticleSystem' && removedEmitters.has(behavior.subParticleSystem as never)) {
|
|
66
|
-
behaviors.splice(i, 1);
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/** Disposes the given systems, first unlinking any sub-emitter behaviors that target them. */
|
|
73
|
-
export function removeSystems(binding: EffectBinding, targets: ParticleSystem[], onSelect: (system: ParticleSystem) => void): void {
|
|
74
|
-
// binding.system is "removable" too as long as some other top-level system can take over
|
|
75
|
-
// as the new main — being main vs. sub is an internal bookkeeping detail, not something
|
|
76
|
-
// that should make one arbitrary track un-deletable in the UI.
|
|
77
|
-
if (targets.includes(binding.system)) {
|
|
78
|
-
const replacement = binding.subSystems.find((s) => !targets.includes(s) && !s.onlyUsedByOther);
|
|
79
|
-
if (replacement) {
|
|
80
|
-
binding.promoteToMain(replacement);
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
const removable = targets.filter((system) => binding.subSystems.includes(system));
|
|
84
|
-
if (removable.length === 0) {
|
|
85
|
-
return;
|
|
86
|
-
}
|
|
87
|
-
binding.apply(() => {
|
|
88
|
-
unlinkSubEmitterReferences([binding.system, ...binding.subSystems], removable);
|
|
89
|
-
for (const system of removable) {
|
|
90
|
-
system._renderer?.deleteSystem(system);
|
|
91
|
-
system.dispose();
|
|
92
|
-
}
|
|
93
|
-
});
|
|
94
|
-
for (const system of removable) {
|
|
95
|
-
binding.removeSubSystem(system);
|
|
96
|
-
}
|
|
97
|
-
onSelect(binding.system);
|
|
98
|
-
}
|
|
1
|
+
import {
|
|
2
|
+
ConeEmitter,
|
|
3
|
+
ConstantColor,
|
|
4
|
+
ConstantValue,
|
|
5
|
+
IntervalValue,
|
|
6
|
+
ParticleSystem,
|
|
7
|
+
RenderMode,
|
|
8
|
+
Vector4,
|
|
9
|
+
} from 'babylon.quarks';
|
|
10
|
+
import type {TransformNode} from '@babylonjs/core/Meshes/transformNode';
|
|
11
|
+
import type {EffectBinding} from './binding';
|
|
12
|
+
|
|
13
|
+
export interface ChildSystemOptions {
|
|
14
|
+
name?: string;
|
|
15
|
+
/** One-shot burst (sub-emitter target) instead of a looping stream. */
|
|
16
|
+
oneShot?: boolean;
|
|
17
|
+
/** Attach point override — defaults to `parent.emitter`. Lets a new system land as a
|
|
18
|
+
* sibling under a group node instead of nesting under an existing system. */
|
|
19
|
+
parentNode?: TransformNode;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Creates a child system sharing the parent's scene/texture/blending, parented to the
|
|
24
|
+
* parent emitter (or `options.parentNode` when given) — the Unity pattern where an effect is
|
|
25
|
+
* a hierarchy of systems and groups.
|
|
26
|
+
*/
|
|
27
|
+
export function createChildSystem(parent: ParticleSystem, options: ChildSystemOptions = {}): ParticleSystem {
|
|
28
|
+
const oneShot = options.oneShot ?? false;
|
|
29
|
+
const child = new ParticleSystem({
|
|
30
|
+
scene: parent.emitter.getScene(),
|
|
31
|
+
duration: oneShot ? 1 : parent.duration,
|
|
32
|
+
looping: !oneShot,
|
|
33
|
+
startLife: new IntervalValue(0.3, oneShot ? 0.6 : 1.2),
|
|
34
|
+
startSpeed: new IntervalValue(1, 3),
|
|
35
|
+
startSize: new IntervalValue(0.05, 0.2),
|
|
36
|
+
startColor: new ConstantColor(new Vector4(1, 0.9, 0.6, 1)),
|
|
37
|
+
emissionOverTime: new ConstantValue(oneShot ? 0 : 30),
|
|
38
|
+
emissionBursts: oneShot ? [{time: 0, count: new ConstantValue(8), cycle: 1, interval: 0.01, probability: 1}] : [],
|
|
39
|
+
shape: new ConeEmitter({radius: 0.1, angle: Math.PI / 3}),
|
|
40
|
+
renderMode: RenderMode.BillBoard,
|
|
41
|
+
texture: parent.texture ?? undefined,
|
|
42
|
+
transparent: true,
|
|
43
|
+
blendMode: parent.blending,
|
|
44
|
+
worldSpace: true,
|
|
45
|
+
});
|
|
46
|
+
child.emitter.name = options.name ?? (oneShot ? 'sub-emitter' : 'child-system');
|
|
47
|
+
if (oneShot) {
|
|
48
|
+
child.onlyUsedByOther = true;
|
|
49
|
+
}
|
|
50
|
+
child.emitter.parent = options.parentNode ?? parent.emitter;
|
|
51
|
+
return child;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Strips EmitSubParticleSystem behaviors that point at any of the removed systems, so
|
|
56
|
+
* deleting a sub-emitter target never leaves a dangling reference (which crashes the next
|
|
57
|
+
* update with `reading 'duration'` on the disposed emitter).
|
|
58
|
+
*/
|
|
59
|
+
export function unlinkSubEmitterReferences(systems: ParticleSystem[], removed: ParticleSystem[]): void {
|
|
60
|
+
const removedEmitters = new Set(removed.map((system) => system.emitter));
|
|
61
|
+
for (const system of systems) {
|
|
62
|
+
const behaviors = system.behaviors as Array<{type?: string; subParticleSystem?: unknown}>;
|
|
63
|
+
for (let i = behaviors.length - 1; i >= 0; i--) {
|
|
64
|
+
const behavior = behaviors[i];
|
|
65
|
+
if (behavior.type === 'EmitSubParticleSystem' && removedEmitters.has(behavior.subParticleSystem as never)) {
|
|
66
|
+
behaviors.splice(i, 1);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Disposes the given systems, first unlinking any sub-emitter behaviors that target them. */
|
|
73
|
+
export function removeSystems(binding: EffectBinding, targets: ParticleSystem[], onSelect: (system: ParticleSystem) => void): void {
|
|
74
|
+
// binding.system is "removable" too as long as some other top-level system can take over
|
|
75
|
+
// as the new main — being main vs. sub is an internal bookkeeping detail, not something
|
|
76
|
+
// that should make one arbitrary track un-deletable in the UI.
|
|
77
|
+
if (targets.includes(binding.system)) {
|
|
78
|
+
const replacement = binding.subSystems.find((s) => !targets.includes(s) && !s.onlyUsedByOther);
|
|
79
|
+
if (replacement) {
|
|
80
|
+
binding.promoteToMain(replacement);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
const removable = targets.filter((system) => binding.subSystems.includes(system));
|
|
84
|
+
if (removable.length === 0) {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
binding.apply(() => {
|
|
88
|
+
unlinkSubEmitterReferences([binding.system, ...binding.subSystems], removable);
|
|
89
|
+
for (const system of removable) {
|
|
90
|
+
system._renderer?.deleteSystem(system);
|
|
91
|
+
system.dispose();
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
for (const system of removable) {
|
|
95
|
+
binding.removeSubSystem(system);
|
|
96
|
+
}
|
|
97
|
+
onSelect(binding.system);
|
|
98
|
+
}
|
package/src/core/timeline.ts
CHANGED
|
@@ -1,94 +1,95 @@
|
|
|
1
|
-
import type {ParticleSystem} from 'babylon.quarks';
|
|
2
|
-
import {ConstantValue, IntervalValue} from 'babylon.quarks';
|
|
3
|
-
import type {FunctionValueGenerator, ValueGenerator} from 'babylon.quarks';
|
|
4
|
-
import type {TransformNode} from '@babylonjs/core/Meshes/transformNode';
|
|
5
|
-
import type {EffectBinding} from './binding';
|
|
6
|
-
import type {EffectTreeNode} from './effectTree';
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
| {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
1
|
+
import type {ParticleSystem} from 'babylon.quarks';
|
|
2
|
+
import {ConstantValue, IntervalValue} from 'babylon.quarks';
|
|
3
|
+
import type {FunctionValueGenerator, ValueGenerator} from 'babylon.quarks';
|
|
4
|
+
import type {TransformNode} from '@babylonjs/core/Meshes/transformNode';
|
|
5
|
+
import type {EffectBinding} from './binding';
|
|
6
|
+
import type {EffectTreeNode} from './effectTree';
|
|
7
|
+
|
|
8
|
+
/** Guards against a literally zero-width ruler (e.g. no tracks yet) — not a usability stretch. */
|
|
9
|
+
const MIN_TIMELINE_SPAN = 0.01;
|
|
10
|
+
|
|
11
|
+
export type TimelineRow =
|
|
12
|
+
| {kind: 'group'; node: TransformNode; name: string; depth: number}
|
|
13
|
+
| {
|
|
14
|
+
kind: 'track';
|
|
15
|
+
node: TransformNode;
|
|
16
|
+
system: ParticleSystem;
|
|
17
|
+
name: string;
|
|
18
|
+
depth: number;
|
|
19
|
+
isRoot: boolean;
|
|
20
|
+
removable: boolean;
|
|
21
|
+
startOffset: number;
|
|
22
|
+
startOffsetVariable: boolean;
|
|
23
|
+
duration: number;
|
|
24
|
+
looping: boolean;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Reads a representative start-delay value for display without invoking the generator's
|
|
29
|
+
* stateful `genValue` (IntervalValue caches its sampled slot on first call and returns NaN
|
|
30
|
+
* on a later call with a fresh memory array, so it can't be safely re-read on every render).
|
|
31
|
+
*/
|
|
32
|
+
export function computeStartOffset(gen: ValueGenerator | FunctionValueGenerator): {value: number; startOffsetVariable: boolean} {
|
|
33
|
+
if (gen instanceof ConstantValue) {
|
|
34
|
+
return {value: gen.value, startOffsetVariable: false};
|
|
35
|
+
}
|
|
36
|
+
if (gen instanceof IntervalValue) {
|
|
37
|
+
return {value: Math.min(gen.a, gen.b), startOffsetVariable: false};
|
|
38
|
+
}
|
|
39
|
+
return {value: 0, startOffsetVariable: true};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Whether a track can be removed. Sub-systems always can; the designated "main" system can
|
|
44
|
+
* too as long as another top-level system remains to take over that role — being main vs.
|
|
45
|
+
* sub is an internal bookkeeping detail (see EffectBinding.promoteToMain), not something that
|
|
46
|
+
* should make one arbitrary track permanently un-deletable in the UI.
|
|
47
|
+
*/
|
|
48
|
+
function isRemovable(binding: EffectBinding, system: ParticleSystem): boolean {
|
|
49
|
+
if (binding.subSystems.includes(system)) {
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
if (system === binding.system) {
|
|
53
|
+
return binding.subSystems.some((s) => !s.onlyUsedByOther);
|
|
54
|
+
}
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Flattens the effect's group/emitter tree into display rows, preserving tree-walk order. */
|
|
59
|
+
export function computeTimelineRows(binding: EffectBinding): TimelineRow[] {
|
|
60
|
+
const rows: TimelineRow[] = [];
|
|
61
|
+
const walk = (node: EffectTreeNode) => {
|
|
62
|
+
if (node.system) {
|
|
63
|
+
const {value, startOffsetVariable} = computeStartOffset(node.system.startDelay);
|
|
64
|
+
rows.push({
|
|
65
|
+
kind: 'track',
|
|
66
|
+
node: node.node,
|
|
67
|
+
system: node.system,
|
|
68
|
+
name: node.name,
|
|
69
|
+
depth: node.depth,
|
|
70
|
+
isRoot: node.node === binding.root,
|
|
71
|
+
removable: isRemovable(binding, node.system),
|
|
72
|
+
startOffset: value,
|
|
73
|
+
startOffsetVariable,
|
|
74
|
+
duration: node.system.duration,
|
|
75
|
+
looping: node.system.looping,
|
|
76
|
+
});
|
|
77
|
+
} else {
|
|
78
|
+
rows.push({kind: 'group', node: node.node, name: node.name, depth: node.depth});
|
|
79
|
+
}
|
|
80
|
+
node.children.forEach(walk);
|
|
81
|
+
};
|
|
82
|
+
walk(binding.getTree());
|
|
83
|
+
return rows;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Overall timeline span: the furthest track extent (start offset + duration). */
|
|
87
|
+
export function computeTimelineSpan(rows: TimelineRow[]): number {
|
|
88
|
+
let span = 0;
|
|
89
|
+
for (const row of rows) {
|
|
90
|
+
if (row.kind === 'track') {
|
|
91
|
+
span = Math.max(span, row.startOffset + row.duration);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return Math.max(span, MIN_TIMELINE_SPAN);
|
|
95
|
+
}
|
package/src/core/values.ts
CHANGED
|
@@ -1,113 +1,113 @@
|
|
|
1
|
-
import {Bezier, ConstantValue, IntervalValue, PiecewiseBezier} from 'babylon.quarks';
|
|
2
|
-
import type {FunctionValueGenerator, ValueGenerator} from 'babylon.quarks';
|
|
3
|
-
|
|
4
|
-
export type ScalarGenerator = ValueGenerator | FunctionValueGenerator;
|
|
5
|
-
export type ScalarMode = 'constant' | 'random' | 'curve';
|
|
6
|
-
|
|
7
|
-
/** Editable snapshot of a scalar value generator. */
|
|
8
|
-
export interface ScalarValueState {
|
|
9
|
-
mode: ScalarMode;
|
|
10
|
-
/** Constant value (mode: constant). */
|
|
11
|
-
value: number;
|
|
12
|
-
/** Random range (mode: random). */
|
|
13
|
-
min: number;
|
|
14
|
-
max: number;
|
|
15
|
-
/** Cubic bezier control values over normalized lifetime (mode: curve). */
|
|
16
|
-
curve: [number, number, number, number];
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export function readScalar(generator: ScalarGenerator | undefined): ScalarValueState {
|
|
20
|
-
const state: ScalarValueState = {mode: 'constant', value: 1, min: 0.5, max: 1.5, curve: [0, 1, 1, 0]};
|
|
21
|
-
if (generator instanceof ConstantValue) {
|
|
22
|
-
state.mode = 'constant';
|
|
23
|
-
state.value = generator.value;
|
|
24
|
-
state.min = generator.value * 0.5;
|
|
25
|
-
state.max = generator.value * 1.5;
|
|
26
|
-
} else if (generator instanceof IntervalValue) {
|
|
27
|
-
state.mode = 'random';
|
|
28
|
-
state.min = generator.a;
|
|
29
|
-
state.max = generator.b;
|
|
30
|
-
state.value = (generator.a + generator.b) / 2;
|
|
31
|
-
} else if (generator instanceof PiecewiseBezier) {
|
|
32
|
-
state.mode = 'curve';
|
|
33
|
-
const first = generator.functions[0]?.[0];
|
|
34
|
-
if (first) {
|
|
35
|
-
state.curve = [first.p[0], first.p[1], first.p[2], first.p[3]];
|
|
36
|
-
state.value = first.p[0];
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
return state;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export function buildScalar(state: ScalarValueState): ScalarGenerator {
|
|
43
|
-
switch (state.mode) {
|
|
44
|
-
case 'constant':
|
|
45
|
-
return new ConstantValue(state.value);
|
|
46
|
-
case 'random':
|
|
47
|
-
return new IntervalValue(Math.min(state.min, state.max), Math.max(state.min, state.max));
|
|
48
|
-
case 'curve':
|
|
49
|
-
return new PiecewiseBezier([[new Bezier(...state.curve), 0]]);
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/** One segment of a piecewise bezier curve: start x (0..1) + four control values. */
|
|
54
|
-
export interface CurvePiece {
|
|
55
|
-
start: number;
|
|
56
|
-
p: [number, number, number, number];
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/** Reads pieces from a PiecewiseBezier (or synthesizes one segment for other generators). */
|
|
60
|
-
export function readPieces(generator: ScalarGenerator | undefined): CurvePiece[] {
|
|
61
|
-
if (generator instanceof PiecewiseBezier && generator.functions.length > 0) {
|
|
62
|
-
return generator.functions.map(([bezier, start]) => ({
|
|
63
|
-
start,
|
|
64
|
-
p: [bezier.p[0], bezier.p[1], bezier.p[2], bezier.p[3]] as [number, number, number, number],
|
|
65
|
-
}));
|
|
66
|
-
}
|
|
67
|
-
const fallback = readScalar(generator);
|
|
68
|
-
return [{start: 0, p: fallback.curve}];
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
export function buildCurve(pieces: CurvePiece[]): PiecewiseBezier {
|
|
72
|
-
const sorted = [...pieces].sort((a, b) => a.start - b.start);
|
|
73
|
-
return new PiecewiseBezier(sorted.map((piece) => [new Bezier(...piece.p), piece.start] as [Bezier, number]));
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/** Splits the piece containing x into two exact halves using Bezier.split. */
|
|
77
|
-
export function splitPieces(pieces: CurvePiece[], x: number): CurvePiece[] {
|
|
78
|
-
const sorted = [...pieces].sort((a, b) => a.start - b.start);
|
|
79
|
-
for (let i = 0; i < sorted.length; i++) {
|
|
80
|
-
const end = i + 1 < sorted.length ? sorted[i + 1].start : 1;
|
|
81
|
-
if (x > sorted[i].start + 0.02 && x < end - 0.02) {
|
|
82
|
-
const t = (x - sorted[i].start) / (end - sorted[i].start);
|
|
83
|
-
const {left, right} = new Bezier(...sorted[i].p).split(t);
|
|
84
|
-
sorted.splice(
|
|
85
|
-
i,
|
|
86
|
-
1,
|
|
87
|
-
{start: sorted[i].start, p: [left.p[0], left.p[1], left.p[2], left.p[3]]},
|
|
88
|
-
{start: x, p: [right.p[0], right.p[1], right.p[2], right.p[3]]}
|
|
89
|
-
);
|
|
90
|
-
return sorted;
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
return sorted;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/** Removes the boundary at pieces[index].start by merging with the previous piece. */
|
|
97
|
-
export function mergePieces(pieces: CurvePiece[], index: number): CurvePiece[] {
|
|
98
|
-
if (index <= 0 || index >= pieces.length) {
|
|
99
|
-
return pieces;
|
|
100
|
-
}
|
|
101
|
-
const sorted = [...pieces].sort((a, b) => a.start - b.start);
|
|
102
|
-
const a = sorted[index - 1];
|
|
103
|
-
const b = sorted[index];
|
|
104
|
-
sorted.splice(index - 1, 2, {start: a.start, p: [a.p[0], a.p[1], b.p[2], b.p[3]]});
|
|
105
|
-
return sorted;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/** Samples a cubic bezier defined by four control values at normalized t. */
|
|
109
|
-
export function sampleCurve(curve: [number, number, number, number], t: number): number {
|
|
110
|
-
const [p0, p1, p2, p3] = curve;
|
|
111
|
-
const u = 1 - t;
|
|
112
|
-
return u * u * u * p0 + 3 * u * u * t * p1 + 3 * u * t * t * p2 + t * t * t * p3;
|
|
113
|
-
}
|
|
1
|
+
import {Bezier, ConstantValue, IntervalValue, PiecewiseBezier} from 'babylon.quarks';
|
|
2
|
+
import type {FunctionValueGenerator, ValueGenerator} from 'babylon.quarks';
|
|
3
|
+
|
|
4
|
+
export type ScalarGenerator = ValueGenerator | FunctionValueGenerator;
|
|
5
|
+
export type ScalarMode = 'constant' | 'random' | 'curve';
|
|
6
|
+
|
|
7
|
+
/** Editable snapshot of a scalar value generator. */
|
|
8
|
+
export interface ScalarValueState {
|
|
9
|
+
mode: ScalarMode;
|
|
10
|
+
/** Constant value (mode: constant). */
|
|
11
|
+
value: number;
|
|
12
|
+
/** Random range (mode: random). */
|
|
13
|
+
min: number;
|
|
14
|
+
max: number;
|
|
15
|
+
/** Cubic bezier control values over normalized lifetime (mode: curve). */
|
|
16
|
+
curve: [number, number, number, number];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function readScalar(generator: ScalarGenerator | undefined): ScalarValueState {
|
|
20
|
+
const state: ScalarValueState = {mode: 'constant', value: 1, min: 0.5, max: 1.5, curve: [0, 1, 1, 0]};
|
|
21
|
+
if (generator instanceof ConstantValue) {
|
|
22
|
+
state.mode = 'constant';
|
|
23
|
+
state.value = generator.value;
|
|
24
|
+
state.min = generator.value * 0.5;
|
|
25
|
+
state.max = generator.value * 1.5;
|
|
26
|
+
} else if (generator instanceof IntervalValue) {
|
|
27
|
+
state.mode = 'random';
|
|
28
|
+
state.min = generator.a;
|
|
29
|
+
state.max = generator.b;
|
|
30
|
+
state.value = (generator.a + generator.b) / 2;
|
|
31
|
+
} else if (generator instanceof PiecewiseBezier) {
|
|
32
|
+
state.mode = 'curve';
|
|
33
|
+
const first = generator.functions[0]?.[0];
|
|
34
|
+
if (first) {
|
|
35
|
+
state.curve = [first.p[0], first.p[1], first.p[2], first.p[3]];
|
|
36
|
+
state.value = first.p[0];
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return state;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function buildScalar(state: ScalarValueState): ScalarGenerator {
|
|
43
|
+
switch (state.mode) {
|
|
44
|
+
case 'constant':
|
|
45
|
+
return new ConstantValue(state.value);
|
|
46
|
+
case 'random':
|
|
47
|
+
return new IntervalValue(Math.min(state.min, state.max), Math.max(state.min, state.max));
|
|
48
|
+
case 'curve':
|
|
49
|
+
return new PiecewiseBezier([[new Bezier(...state.curve), 0]]);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** One segment of a piecewise bezier curve: start x (0..1) + four control values. */
|
|
54
|
+
export interface CurvePiece {
|
|
55
|
+
start: number;
|
|
56
|
+
p: [number, number, number, number];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Reads pieces from a PiecewiseBezier (or synthesizes one segment for other generators). */
|
|
60
|
+
export function readPieces(generator: ScalarGenerator | undefined): CurvePiece[] {
|
|
61
|
+
if (generator instanceof PiecewiseBezier && generator.functions.length > 0) {
|
|
62
|
+
return generator.functions.map(([bezier, start]) => ({
|
|
63
|
+
start,
|
|
64
|
+
p: [bezier.p[0], bezier.p[1], bezier.p[2], bezier.p[3]] as [number, number, number, number],
|
|
65
|
+
}));
|
|
66
|
+
}
|
|
67
|
+
const fallback = readScalar(generator);
|
|
68
|
+
return [{start: 0, p: fallback.curve}];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function buildCurve(pieces: CurvePiece[]): PiecewiseBezier {
|
|
72
|
+
const sorted = [...pieces].sort((a, b) => a.start - b.start);
|
|
73
|
+
return new PiecewiseBezier(sorted.map((piece) => [new Bezier(...piece.p), piece.start] as [Bezier, number]));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Splits the piece containing x into two exact halves using Bezier.split. */
|
|
77
|
+
export function splitPieces(pieces: CurvePiece[], x: number): CurvePiece[] {
|
|
78
|
+
const sorted = [...pieces].sort((a, b) => a.start - b.start);
|
|
79
|
+
for (let i = 0; i < sorted.length; i++) {
|
|
80
|
+
const end = i + 1 < sorted.length ? sorted[i + 1].start : 1;
|
|
81
|
+
if (x > sorted[i].start + 0.02 && x < end - 0.02) {
|
|
82
|
+
const t = (x - sorted[i].start) / (end - sorted[i].start);
|
|
83
|
+
const {left, right} = new Bezier(...sorted[i].p).split(t);
|
|
84
|
+
sorted.splice(
|
|
85
|
+
i,
|
|
86
|
+
1,
|
|
87
|
+
{start: sorted[i].start, p: [left.p[0], left.p[1], left.p[2], left.p[3]]},
|
|
88
|
+
{start: x, p: [right.p[0], right.p[1], right.p[2], right.p[3]]}
|
|
89
|
+
);
|
|
90
|
+
return sorted;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return sorted;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Removes the boundary at pieces[index].start by merging with the previous piece. */
|
|
97
|
+
export function mergePieces(pieces: CurvePiece[], index: number): CurvePiece[] {
|
|
98
|
+
if (index <= 0 || index >= pieces.length) {
|
|
99
|
+
return pieces;
|
|
100
|
+
}
|
|
101
|
+
const sorted = [...pieces].sort((a, b) => a.start - b.start);
|
|
102
|
+
const a = sorted[index - 1];
|
|
103
|
+
const b = sorted[index];
|
|
104
|
+
sorted.splice(index - 1, 2, {start: a.start, p: [a.p[0], a.p[1], b.p[2], b.p[3]]});
|
|
105
|
+
return sorted;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Samples a cubic bezier defined by four control values at normalized t. */
|
|
109
|
+
export function sampleCurve(curve: [number, number, number, number], t: number): number {
|
|
110
|
+
const [p0, p1, p2, p3] = curve;
|
|
111
|
+
const u = 1 - t;
|
|
112
|
+
return u * u * u * p0 + 3 * u * u * t * p1 + 3 * u * t * t * p2 + t * t * t * p3;
|
|
113
|
+
}
|