babylon.quarks-editor 0.17.7
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 +13 -0
- package/LICENSE +21 -0
- package/README.md +84 -0
- package/dist/core/binding.d.ts +43 -0
- package/dist/core/binding.js +82 -0
- package/dist/core/collision.d.ts +20 -0
- package/dist/core/collision.js +39 -0
- package/dist/core/colors.d.ts +31 -0
- package/dist/core/colors.js +60 -0
- package/dist/core/effectTree.d.ts +25 -0
- package/dist/core/effectTree.js +115 -0
- package/dist/core/groups.d.ts +12 -0
- package/dist/core/groups.js +27 -0
- package/dist/core/history.d.ts +25 -0
- package/dist/core/history.js +69 -0
- package/dist/core/loadEffect.d.ts +8 -0
- package/dist/core/loadEffect.js +87 -0
- package/dist/core/scrub.d.ts +11 -0
- package/dist/core/scrub.js +24 -0
- package/dist/core/shapes.d.ts +31 -0
- package/dist/core/shapes.js +98 -0
- package/dist/core/systems.d.ts +25 -0
- package/dist/core/systems.js +76 -0
- package/dist/core/textureExport.d.ts +9 -0
- package/dist/core/textureExport.js +58 -0
- package/dist/core/timeline.d.ts +35 -0
- package/dist/core/timeline.js +69 -0
- package/dist/core/values.d.ts +31 -0
- package/dist/core/values.js +81 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +9 -0
- package/dist/react/CurveEditor.d.ts +12 -0
- package/dist/react/CurveEditor.js +106 -0
- package/dist/react/EffectEditor.d.ts +22 -0
- package/dist/react/EffectEditor.js +30 -0
- package/dist/react/EffectEditorHost.d.ts +42 -0
- package/dist/react/EffectEditorHost.js +247 -0
- package/dist/react/GradientEditor.d.ts +7 -0
- package/dist/react/GradientEditor.js +72 -0
- package/dist/react/ModuleSection.d.ts +9 -0
- package/dist/react/ModuleSection.js +24 -0
- package/dist/react/PromptDialog.d.ts +14 -0
- package/dist/react/PromptDialog.js +45 -0
- package/dist/react/SubEmittersModule.d.ts +10 -0
- package/dist/react/SubEmittersModule.js +39 -0
- package/dist/react/TimelinePanel.d.ts +32 -0
- package/dist/react/TimelinePanel.js +225 -0
- package/dist/react/TimelineTrackRow.d.ts +23 -0
- package/dist/react/TimelineTrackRow.js +107 -0
- package/dist/react/ValueField.d.ts +13 -0
- package/dist/react/ValueField.js +19 -0
- package/dist/react/behaviorModules.d.ts +23 -0
- package/dist/react/behaviorModules.js +139 -0
- package/dist/react/fields.d.ts +24 -0
- package/dist/react/fields.js +30 -0
- package/dist/react/index.d.ts +20 -0
- package/dist/react/index.js +14 -0
- package/dist/react/modules.d.ts +35 -0
- package/dist/react/modules.js +439 -0
- package/dist/react/theme.d.ts +26 -0
- package/dist/react/theme.js +78 -0
- package/package.json +63 -0
- package/src/core/binding.ts +105 -0
- package/src/core/collision.ts +40 -0
- package/src/core/colors.ts +84 -0
- package/src/core/effectTree.ts +143 -0
- package/src/core/groups.ts +29 -0
- package/src/core/history.ts +77 -0
- package/src/core/loadEffect.ts +102 -0
- package/src/core/scrub.ts +28 -0
- package/src/core/shapes.ts +133 -0
- package/src/core/systems.ts +98 -0
- package/src/core/textureExport.ts +63 -0
- package/src/core/timeline.ts +94 -0
- package/src/core/values.ts +113 -0
- package/src/index.ts +32 -0
- package/src/react/CurveEditor.tsx +168 -0
- package/src/react/EffectEditor.tsx +111 -0
- package/src/react/EffectEditorHost.tsx +377 -0
- package/src/react/GradientEditor.tsx +127 -0
- package/src/react/ModuleSection.tsx +54 -0
- package/src/react/PromptDialog.tsx +85 -0
- package/src/react/SubEmittersModule.tsx +80 -0
- package/src/react/TimelinePanel.tsx +354 -0
- package/src/react/TimelineTrackRow.tsx +267 -0
- package/src/react/ValueField.tsx +60 -0
- package/src/react/behaviorModules.tsx +658 -0
- package/src/react/fields.tsx +76 -0
- package/src/react/index.ts +43 -0
- package/src/react/modules.tsx +1034 -0
- package/src/react/theme.ts +85 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to the `babylon.quarks-editor` package are documented here.
|
|
4
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
5
|
+
and the project follows the `quarks.core` 0.x version line.
|
|
6
|
+
|
|
7
|
+
## [0.17.7] — 2026-07-09
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- First npm release. Shuriken-style effect editor for `babylon.quarks`: headless edit-model
|
|
12
|
+
core (`.` export) plus an embeddable React UI (`./react` export) — hierarchy panel, stacked
|
|
13
|
+
module inspector, undo/redo, JSON import/export round-tripping through `QuarksLoader`.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 soullnik
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# babylon.quarks-editor
|
|
2
|
+
|
|
3
|
+
Shuriken-style effect editor for [babylon.quarks](https://www.npmjs.com/package/babylon.quarks): a Unity-like hierarchy of particle systems with a stacked module inspector. Headless edit-model core plus embeddable React UI.
|
|
4
|
+
|
|
5
|
+
[Live editor](https://soullnik.github.io/babylon.quarks-standalone/editor.html) · [Source](https://github.com/Soullnik/babylon.quarks-standalone/tree/main/packages/babylon.quarks-editor)
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install babylon.quarks-editor babylon.quarks @babylonjs/core react
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
```tsx
|
|
16
|
+
import {EffectBinding, EffectHistory} from "babylon.quarks-editor";
|
|
17
|
+
import {EffectEditor} from "babylon.quarks-editor/react";
|
|
18
|
+
|
|
19
|
+
const binding = new EffectBinding(particleSystem); // your babylon.quarks ParticleSystem
|
|
20
|
+
const history = new EffectHistory();
|
|
21
|
+
history.attach(binding);
|
|
22
|
+
|
|
23
|
+
<EffectEditor binding={binding} />;
|
|
24
|
+
|
|
25
|
+
// Serialize to Quarks JSON (round-trips through QuarksLoader):
|
|
26
|
+
const json = binding.exportJSON("MyEffect");
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Modules: Main, Emission (bursts), Shape (7 emitter types), Speed/Limit Speed/Force/Rotation over Lifetime, Gravity, Color over Lifetime (gradient), Size over Lifetime (curve), Noise, Sub Emitters, Texture Sheet Animation, Renderer. Multi-system hierarchy with add/remove/rename, undo/redo via `EffectHistory`.
|
|
30
|
+
|
|
31
|
+
The `.` export is UI-free (edit model, serialization, value/shape/gradient helpers) — usable headless. The `./react` export ships the components; React is a peer dependency, so the same UI can mount in a standalone page or inside a [BabylonJS Editor](https://github.com/BabylonJS/Editor) plugin.
|
|
32
|
+
|
|
33
|
+
## Textures & meshes (`resolveTexture` / `resolveGeometry`)
|
|
34
|
+
|
|
35
|
+
The editor never loads assets itself — it defers to host-supplied callbacks so it doesn't need
|
|
36
|
+
an opinion on your CDN, asset pipeline, or loader (`@babylonjs/loaders`, your own backend, etc.):
|
|
37
|
+
|
|
38
|
+
- `resolveTexture(url) => Texture-like` — turns a picked URL or `data:` URI (from "Custom URL…" /
|
|
39
|
+
"Load from file…" in the Renderer module) into whatever object you assign as `system.texture`.
|
|
40
|
+
- `resolveGeometry(file) => Promise<GeometryData>` — turns a picked file (from "Load from file…"
|
|
41
|
+
in the Mesh render mode's geometry dropdown) into vertex buffers: `{positions, indices?, uvs?,
|
|
42
|
+
normals?}` (typed arrays or plain number arrays).
|
|
43
|
+
- `textureOptions?: {label, url}[]` / `geometryOptions?: {label, positions, indices?, uvs?,
|
|
44
|
+
normals?}[]` — preset lists shown alongside "Custom URL…" / "Load from file…".
|
|
45
|
+
|
|
46
|
+
**Signature differs by entry point** — a common source of "works in the demo, throws in my
|
|
47
|
+
host" bugs:
|
|
48
|
+
|
|
49
|
+
- `QuarksEffectEditor.Show(...)` / `<EffectEditorHost>` call your callbacks with the live
|
|
50
|
+
`Scene` as a second argument: `resolveTexture: (url, scene) => unknown`,
|
|
51
|
+
`resolveGeometry: (file, scene) => Promise<GeometryData>`.
|
|
52
|
+
- The lower-level `<EffectEditor>` (no Host wrapper) does **not** pass a scene:
|
|
53
|
+
`resolveTexture: (url) => unknown`, `resolveGeometry: (file) => Promise<GeometryData>`. If you
|
|
54
|
+
copy a `Show()`-style resolver that reads its `scene` parameter into a raw `<EffectEditor>`,
|
|
55
|
+
`scene` is `undefined` and your resolver throws.
|
|
56
|
+
|
|
57
|
+
Both callbacks are optional, and omitting them doesn't crash the editor — it just hides the
|
|
58
|
+
affected UI: no `resolveTexture` means the Renderer module's Texture row isn't rendered at all,
|
|
59
|
+
and no `resolveGeometry` means "Load from file…" isn't offered for Mesh geometry. The catch: a
|
|
60
|
+
freshly created effect (i.e. `EffectEditorHost`/`Show()` called without `effectJson`) is built
|
|
61
|
+
with `texture: undefined` unless you also pass `textureOptions` + `resolveTexture`, so it's easy
|
|
62
|
+
to end up with an effect that has no visible particles simply because texture loading was never
|
|
63
|
+
wired up. Always pass `resolveTexture` (and `textureOptions` with at least one entry) unless you
|
|
64
|
+
intend every new effect to start textureless.
|
|
65
|
+
|
|
66
|
+
Reference implementation (from the standalone [`editor.html`](https://github.com/Soullnik/babylon.quarks-standalone/blob/main/examples/src/editorApp.tsx)):
|
|
67
|
+
|
|
68
|
+
```ts
|
|
69
|
+
resolveTexture: (url, scene) => {
|
|
70
|
+
const texture = new Texture(url, scene);
|
|
71
|
+
texture.wrapU = Texture.CLAMP_ADDRESSMODE;
|
|
72
|
+
texture.wrapV = Texture.CLAMP_ADDRESSMODE;
|
|
73
|
+
return texture;
|
|
74
|
+
},
|
|
75
|
+
resolveGeometry: async (file, scene) => {
|
|
76
|
+
const result = await SceneLoader.ImportMeshAsync("", "", file, scene, null, ".glb");
|
|
77
|
+
const mesh = result.meshes.find((m) => m.getVerticesData(VertexBuffer.PositionKind));
|
|
78
|
+
// ...extract positions/indices/uvs/normals from `mesh`, dispose the imported nodes, return them
|
|
79
|
+
},
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## License
|
|
83
|
+
|
|
84
|
+
MIT
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { ParticleSystem } from 'babylon.quarks';
|
|
2
|
+
import type { TransformNode } from '@babylonjs/core/Meshes/transformNode';
|
|
3
|
+
import type { EffectTreeNode } from './effectTree';
|
|
4
|
+
export type EditorListener = () => void;
|
|
5
|
+
/**
|
|
6
|
+
* Headless edit-model over a live ParticleSystem: mutate through apply() so every
|
|
7
|
+
* change notifies subscribers (React UI, save indicators, hosts like BabylonJS Editor).
|
|
8
|
+
*/
|
|
9
|
+
export declare class EffectBinding {
|
|
10
|
+
/** Not `readonly` — `promoteToMain` reassigns this when the current main system is removed. */
|
|
11
|
+
system: ParticleSystem;
|
|
12
|
+
/** Systems spawned by sub-emitter behaviors; serialized as children of the root emitter. */
|
|
13
|
+
readonly subSystems: ParticleSystem[];
|
|
14
|
+
/** Loaded scene root of the whole effect (Group tree); defaults to the system's emitter. */
|
|
15
|
+
readonly root: TransformNode;
|
|
16
|
+
/** Invoked before any mutation — used by EffectHistory to capture undo snapshots. */
|
|
17
|
+
onBeforeChange?: () => void;
|
|
18
|
+
private listeners;
|
|
19
|
+
private revision;
|
|
20
|
+
constructor(system: ParticleSystem, subSystems?: ParticleSystem[], root?: TransformNode);
|
|
21
|
+
/** Editor hierarchy tree (groups + emitters) rooted at this effect's scene root. */
|
|
22
|
+
getTree(): EffectTreeNode;
|
|
23
|
+
subscribe(listener: EditorListener): () => void;
|
|
24
|
+
/** Monotonic change counter, usable as a React external-store snapshot. */
|
|
25
|
+
getRevision(): number;
|
|
26
|
+
apply(mutate: (system: ParticleSystem) => void): void;
|
|
27
|
+
addSubSystem(sub: ParticleSystem): void;
|
|
28
|
+
removeSubSystem(sub: ParticleSystem): void;
|
|
29
|
+
/**
|
|
30
|
+
* Swaps which system is "main" — used when the current `system` is about to be removed
|
|
31
|
+
* but other top-level systems remain. Being "main" vs. a sub-system is otherwise
|
|
32
|
+
* meaningless to the effect itself (both play/restart/export the same way); it only
|
|
33
|
+
* exists so there's always a designated fallback system to select.
|
|
34
|
+
*/
|
|
35
|
+
promoteToMain(next: ParticleSystem): void;
|
|
36
|
+
private notify;
|
|
37
|
+
restart(): void;
|
|
38
|
+
/**
|
|
39
|
+
* Serializes the effect into the Quarks JSON envelope understood by QuarksLoader,
|
|
40
|
+
* preserving the loaded group/emitter hierarchy.
|
|
41
|
+
*/
|
|
42
|
+
exportJSON(name?: string): string;
|
|
43
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { buildEffectTree, serializeEffectTree } from './effectTree';
|
|
2
|
+
/**
|
|
3
|
+
* Headless edit-model over a live ParticleSystem: mutate through apply() so every
|
|
4
|
+
* change notifies subscribers (React UI, save indicators, hosts like BabylonJS Editor).
|
|
5
|
+
*/
|
|
6
|
+
export class EffectBinding {
|
|
7
|
+
constructor(system, subSystems = [], root) {
|
|
8
|
+
/** Systems spawned by sub-emitter behaviors; serialized as children of the root emitter. */
|
|
9
|
+
this.subSystems = [];
|
|
10
|
+
this.listeners = new Set();
|
|
11
|
+
this.revision = 0;
|
|
12
|
+
this.system = system;
|
|
13
|
+
this.subSystems.push(...subSystems);
|
|
14
|
+
this.root = root ?? system.emitter;
|
|
15
|
+
}
|
|
16
|
+
/** Editor hierarchy tree (groups + emitters) rooted at this effect's scene root. */
|
|
17
|
+
getTree() {
|
|
18
|
+
return buildEffectTree(this.root);
|
|
19
|
+
}
|
|
20
|
+
subscribe(listener) {
|
|
21
|
+
this.listeners.add(listener);
|
|
22
|
+
return () => this.listeners.delete(listener);
|
|
23
|
+
}
|
|
24
|
+
/** Monotonic change counter, usable as a React external-store snapshot. */
|
|
25
|
+
getRevision() {
|
|
26
|
+
return this.revision;
|
|
27
|
+
}
|
|
28
|
+
apply(mutate) {
|
|
29
|
+
this.onBeforeChange?.();
|
|
30
|
+
mutate(this.system);
|
|
31
|
+
this.notify();
|
|
32
|
+
}
|
|
33
|
+
addSubSystem(sub) {
|
|
34
|
+
this.subSystems.push(sub);
|
|
35
|
+
this.notify();
|
|
36
|
+
}
|
|
37
|
+
removeSubSystem(sub) {
|
|
38
|
+
const index = this.subSystems.indexOf(sub);
|
|
39
|
+
if (index >= 0) {
|
|
40
|
+
this.subSystems.splice(index, 1);
|
|
41
|
+
}
|
|
42
|
+
this.notify();
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Swaps which system is "main" — used when the current `system` is about to be removed
|
|
46
|
+
* but other top-level systems remain. Being "main" vs. a sub-system is otherwise
|
|
47
|
+
* meaningless to the effect itself (both play/restart/export the same way); it only
|
|
48
|
+
* exists so there's always a designated fallback system to select.
|
|
49
|
+
*/
|
|
50
|
+
promoteToMain(next) {
|
|
51
|
+
const index = this.subSystems.indexOf(next);
|
|
52
|
+
if (index < 0) {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
this.subSystems.splice(index, 1, this.system);
|
|
56
|
+
this.system = next;
|
|
57
|
+
this.notify();
|
|
58
|
+
}
|
|
59
|
+
notify() {
|
|
60
|
+
this.revision++;
|
|
61
|
+
for (const listener of this.listeners) {
|
|
62
|
+
listener();
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
restart() {
|
|
66
|
+
// Replay every independent system; sub-emitter targets (onlyUsedByOther) are only
|
|
67
|
+
// reset — their parents re-drive them, so playing them directly would double-emit.
|
|
68
|
+
for (const system of [this.system, ...this.subSystems]) {
|
|
69
|
+
system.restart();
|
|
70
|
+
if (!system.onlyUsedByOther) {
|
|
71
|
+
system.play();
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Serializes the effect into the Quarks JSON envelope understood by QuarksLoader,
|
|
77
|
+
* preserving the loaded group/emitter hierarchy.
|
|
78
|
+
*/
|
|
79
|
+
exportJSON(name = 'effect') {
|
|
80
|
+
return serializeEffectTree(this.root, name);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Vector3 } from 'babylon.quarks';
|
|
2
|
+
/**
|
|
3
|
+
* A horizontal ground plane collider. quarks.core's ApplyCollision behavior delegates to a
|
|
4
|
+
* host-provided `PhysicsResolver` (a global singleton) and ships no collider primitive of its
|
|
5
|
+
* own, so the editor supplies this — the common "particles bounce off the floor" case.
|
|
6
|
+
*
|
|
7
|
+
* `resolve` returns true and fills the surface normal when the point is at or below the plane,
|
|
8
|
+
* which is exactly what ApplyCollision.update reflects the velocity against.
|
|
9
|
+
*/
|
|
10
|
+
export declare class GroundPlaneResolver {
|
|
11
|
+
y: number;
|
|
12
|
+
constructor(y?: number);
|
|
13
|
+
resolve(position: Vector3, normal: Vector3): boolean;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Registers (once) a shared ground-plane resolver so ApplyCollision behaviors — whether added
|
|
17
|
+
* in the inspector or deserialized from imported JSON — have something to collide against.
|
|
18
|
+
* Idempotent: an already-registered resolver is left untouched and returned.
|
|
19
|
+
*/
|
|
20
|
+
export declare function ensureGroundResolver(): GroundPlaneResolver;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { getPhysicsResolver, setPhysicsResolver } from 'babylon.quarks';
|
|
2
|
+
/**
|
|
3
|
+
* A horizontal ground plane collider. quarks.core's ApplyCollision behavior delegates to a
|
|
4
|
+
* host-provided `PhysicsResolver` (a global singleton) and ships no collider primitive of its
|
|
5
|
+
* own, so the editor supplies this — the common "particles bounce off the floor" case.
|
|
6
|
+
*
|
|
7
|
+
* `resolve` returns true and fills the surface normal when the point is at or below the plane,
|
|
8
|
+
* which is exactly what ApplyCollision.update reflects the velocity against.
|
|
9
|
+
*/
|
|
10
|
+
export class GroundPlaneResolver {
|
|
11
|
+
constructor(y = 0) {
|
|
12
|
+
this.y = y;
|
|
13
|
+
}
|
|
14
|
+
resolve(position, normal) {
|
|
15
|
+
if (position.y <= this.y) {
|
|
16
|
+
normal.set(0, 1, 0);
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Registers (once) a shared ground-plane resolver so ApplyCollision behaviors — whether added
|
|
24
|
+
* in the inspector or deserialized from imported JSON — have something to collide against.
|
|
25
|
+
* Idempotent: an already-registered resolver is left untouched and returned.
|
|
26
|
+
*/
|
|
27
|
+
export function ensureGroundResolver() {
|
|
28
|
+
const existing = getPhysicsResolver();
|
|
29
|
+
if (existing instanceof GroundPlaneResolver) {
|
|
30
|
+
return existing;
|
|
31
|
+
}
|
|
32
|
+
if (existing) {
|
|
33
|
+
// A host registered its own resolver; don't clobber it, but we can't tweak its floor.
|
|
34
|
+
return existing;
|
|
35
|
+
}
|
|
36
|
+
const resolver = new GroundPlaneResolver(0);
|
|
37
|
+
setPhysicsResolver(resolver);
|
|
38
|
+
return resolver;
|
|
39
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { ColorOverLife, Gradient, Vector4 } from 'babylon.quarks';
|
|
2
|
+
import type { Behavior } from 'babylon.quarks';
|
|
3
|
+
/** One RGBA stop of the editable gradient (all channels 0..1, pos 0..1). */
|
|
4
|
+
export interface GradientStop {
|
|
5
|
+
pos: number;
|
|
6
|
+
r: number;
|
|
7
|
+
g: number;
|
|
8
|
+
b: number;
|
|
9
|
+
a: number;
|
|
10
|
+
}
|
|
11
|
+
export declare const DEFAULT_GRADIENT_STOPS: GradientStop[];
|
|
12
|
+
export declare function buildGradient(stops: GradientStop[]): Gradient;
|
|
13
|
+
export declare function buildColorOverLife(stops: GradientStop[]): ColorOverLife;
|
|
14
|
+
export declare function rgbaToVector4(stop: {
|
|
15
|
+
r: number;
|
|
16
|
+
g: number;
|
|
17
|
+
b: number;
|
|
18
|
+
a: number;
|
|
19
|
+
}): Vector4;
|
|
20
|
+
export declare function stopToCss(stop: GradientStop): string;
|
|
21
|
+
export declare function stopsToCssGradient(stops: GradientStop[]): string;
|
|
22
|
+
export declare function hexToRgb(hex: string): {
|
|
23
|
+
r: number;
|
|
24
|
+
g: number;
|
|
25
|
+
b: number;
|
|
26
|
+
};
|
|
27
|
+
export declare function rgbToHex(r: number, g: number, b: number): string;
|
|
28
|
+
/** Reads RGBA stops back from a Gradient (union of color/alpha key positions). */
|
|
29
|
+
export declare function readGradientStops(gradient: Gradient): GradientStop[];
|
|
30
|
+
/** Finds a behavior of the given quarks type string (e.g. 'ColorOverLife'). */
|
|
31
|
+
export declare function findBehavior<T extends Behavior>(behaviors: Behavior[], type: string): T | undefined;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { ColorOverLife, Gradient, Vector3, Vector4 } from 'babylon.quarks';
|
|
2
|
+
export const DEFAULT_GRADIENT_STOPS = [
|
|
3
|
+
{ pos: 0, r: 1, g: 0.85, b: 0.4, a: 1 },
|
|
4
|
+
{ pos: 1, r: 1, g: 0.25, b: 0.05, a: 0 },
|
|
5
|
+
];
|
|
6
|
+
export function buildGradient(stops) {
|
|
7
|
+
const sorted = [...stops].sort((a, b) => a.pos - b.pos);
|
|
8
|
+
return new Gradient(sorted.map((s) => [new Vector3(s.r, s.g, s.b), s.pos]), sorted.map((s) => [s.a, s.pos]));
|
|
9
|
+
}
|
|
10
|
+
export function buildColorOverLife(stops) {
|
|
11
|
+
return new ColorOverLife(buildGradient(stops));
|
|
12
|
+
}
|
|
13
|
+
export function rgbaToVector4(stop) {
|
|
14
|
+
return new Vector4(stop.r, stop.g, stop.b, stop.a);
|
|
15
|
+
}
|
|
16
|
+
export function stopToCss(stop) {
|
|
17
|
+
const c = (v) => Math.round(v * 255);
|
|
18
|
+
return `rgba(${c(stop.r)}, ${c(stop.g)}, ${c(stop.b)}, ${stop.a.toFixed(2)})`;
|
|
19
|
+
}
|
|
20
|
+
export function stopsToCssGradient(stops) {
|
|
21
|
+
const sorted = [...stops].sort((a, b) => a.pos - b.pos);
|
|
22
|
+
const parts = sorted.map((s) => `${stopToCss(s)} ${(s.pos * 100).toFixed(1)}%`);
|
|
23
|
+
return `linear-gradient(90deg, ${parts.join(', ')})`;
|
|
24
|
+
}
|
|
25
|
+
export function hexToRgb(hex) {
|
|
26
|
+
const value = hex.replace('#', '');
|
|
27
|
+
return {
|
|
28
|
+
r: parseInt(value.slice(0, 2), 16) / 255,
|
|
29
|
+
g: parseInt(value.slice(2, 4), 16) / 255,
|
|
30
|
+
b: parseInt(value.slice(4, 6), 16) / 255,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
export function rgbToHex(r, g, b) {
|
|
34
|
+
const c = (v) => Math.round(Math.min(1, Math.max(0, v)) * 255)
|
|
35
|
+
.toString(16)
|
|
36
|
+
.padStart(2, '0');
|
|
37
|
+
return `#${c(r)}${c(g)}${c(b)}`;
|
|
38
|
+
}
|
|
39
|
+
/** Reads RGBA stops back from a Gradient (union of color/alpha key positions). */
|
|
40
|
+
export function readGradientStops(gradient) {
|
|
41
|
+
const positions = new Set();
|
|
42
|
+
for (const [, pos] of gradient.color.keys) {
|
|
43
|
+
positions.add(Math.round(pos * 1000) / 1000);
|
|
44
|
+
}
|
|
45
|
+
for (const [, pos] of gradient.alpha.keys) {
|
|
46
|
+
positions.add(Math.round(pos * 1000) / 1000);
|
|
47
|
+
}
|
|
48
|
+
const color = new Vector3();
|
|
49
|
+
return [...positions]
|
|
50
|
+
.sort((a, b) => a - b)
|
|
51
|
+
.map((pos) => {
|
|
52
|
+
gradient.color.genValue(color, pos);
|
|
53
|
+
const alpha = gradient.alpha.genValue(0, pos);
|
|
54
|
+
return { pos, r: color.x, g: color.y, b: color.z, a: alpha };
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
/** Finds a behavior of the given quarks type string (e.g. 'ColorOverLife'). */
|
|
58
|
+
export function findBehavior(behaviors, type) {
|
|
59
|
+
return behaviors.find((b) => b.type === type);
|
|
60
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { ParticleSystem } from 'babylon.quarks';
|
|
2
|
+
import { TransformNode } from '@babylonjs/core/Meshes/transformNode';
|
|
3
|
+
/** A node in the editor's effect hierarchy: a Group (organizational) or a ParticleEmitter. */
|
|
4
|
+
export interface EffectTreeNode {
|
|
5
|
+
node: TransformNode;
|
|
6
|
+
name: string;
|
|
7
|
+
/** Present when the node is a ParticleEmitter. */
|
|
8
|
+
system?: ParticleSystem;
|
|
9
|
+
depth: number;
|
|
10
|
+
children: EffectTreeNode[];
|
|
11
|
+
}
|
|
12
|
+
/** Walks a loaded root TransformNode into the editor tree (groups + emitters). */
|
|
13
|
+
export declare function buildEffectTree(root: TransformNode, depth?: number): EffectTreeNode;
|
|
14
|
+
/** Flattens the tree into the systems it contains, in display order. */
|
|
15
|
+
export declare function collectSystems(tree: EffectTreeNode): ParticleSystem[];
|
|
16
|
+
/**
|
|
17
|
+
* Serializes an effect tree into the Quarks JSON envelope understood by QuarksLoader,
|
|
18
|
+
* preserving the group/emitter hierarchy (not flattened).
|
|
19
|
+
*/
|
|
20
|
+
export declare function serializeEffectTree(root: TransformNode, name?: string): string;
|
|
21
|
+
/**
|
|
22
|
+
* Serializes several effect roots as children of a synthetic group — used when an effect
|
|
23
|
+
* spans multiple top-level trees (e.g. a demo running several loaded effects at once).
|
|
24
|
+
*/
|
|
25
|
+
export declare function serializeEffectForest(roots: TransformNode[], name?: string): string;
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { ParticleEmitter } from 'babylon.quarks';
|
|
2
|
+
import { TransformNode } from '@babylonjs/core/Meshes/transformNode';
|
|
3
|
+
import { Matrix, Quaternion } from '@babylonjs/core/Maths/math.vector';
|
|
4
|
+
/** Walks a loaded root TransformNode into the editor tree (groups + emitters). */
|
|
5
|
+
export function buildEffectTree(root, depth = 0) {
|
|
6
|
+
const system = root instanceof ParticleEmitter ? root.system : undefined;
|
|
7
|
+
const treeNode = {
|
|
8
|
+
node: root,
|
|
9
|
+
name: root.name || (system ? 'emitter' : 'group'),
|
|
10
|
+
system,
|
|
11
|
+
depth,
|
|
12
|
+
children: [],
|
|
13
|
+
};
|
|
14
|
+
for (const child of root.getChildren()) {
|
|
15
|
+
if (child instanceof TransformNode) {
|
|
16
|
+
treeNode.children.push(buildEffectTree(child, depth + 1));
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return treeNode;
|
|
20
|
+
}
|
|
21
|
+
/** Flattens the tree into the systems it contains, in display order. */
|
|
22
|
+
export function collectSystems(tree) {
|
|
23
|
+
const out = [];
|
|
24
|
+
const walk = (node) => {
|
|
25
|
+
if (node.system) {
|
|
26
|
+
out.push(node.system);
|
|
27
|
+
}
|
|
28
|
+
node.children.forEach(walk);
|
|
29
|
+
};
|
|
30
|
+
walk(tree);
|
|
31
|
+
return out;
|
|
32
|
+
}
|
|
33
|
+
function localMatrix(node) {
|
|
34
|
+
const rotation = node.rotationQuaternion ?? Quaternion.FromEulerVector(node.rotation);
|
|
35
|
+
return Matrix.Compose(node.scaling, rotation, node.position).asArray().slice();
|
|
36
|
+
}
|
|
37
|
+
function serializeNode(tree, meta, fallbackUuid) {
|
|
38
|
+
// Emitter nodes must serialize with the ParticleEmitter's own `.uuid` — that is exactly
|
|
39
|
+
// what EmitSubParticleSystem.toJSON writes as its target reference, so the two match and
|
|
40
|
+
// QuarksLoader.linkReferences can re-resolve sub-emitters on reimport.
|
|
41
|
+
const uuid = tree.system
|
|
42
|
+
? tree.node.uuid
|
|
43
|
+
: (tree.node._quarksUUID ?? fallbackUuid);
|
|
44
|
+
const children = tree.children.map((child, i) => serializeNode(child, meta, `${fallbackUuid}-${i}`));
|
|
45
|
+
const base = {
|
|
46
|
+
uuid,
|
|
47
|
+
name: tree.name,
|
|
48
|
+
layers: 1,
|
|
49
|
+
matrix: localMatrix(tree.node),
|
|
50
|
+
children: children.length > 0 ? children : undefined,
|
|
51
|
+
};
|
|
52
|
+
if (tree.system) {
|
|
53
|
+
return { ...base, type: 'ParticleEmitter', ps: tree.system.toJSON(meta, { useUrlForImage: true }) };
|
|
54
|
+
}
|
|
55
|
+
return { ...base, type: 'Group' };
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Serializes an effect tree into the Quarks JSON envelope understood by QuarksLoader,
|
|
59
|
+
* preserving the group/emitter hierarchy (not flattened).
|
|
60
|
+
*/
|
|
61
|
+
export function serializeEffectTree(root, name = 'effect') {
|
|
62
|
+
const meta = { textures: {}, materials: {}, geometries: {} };
|
|
63
|
+
const object = serializeNode(buildEffectTree(root), meta, `${name}-node`);
|
|
64
|
+
return finishEnvelope(object, meta);
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Serializes several effect roots as children of a synthetic group — used when an effect
|
|
68
|
+
* spans multiple top-level trees (e.g. a demo running several loaded effects at once).
|
|
69
|
+
*/
|
|
70
|
+
export function serializeEffectForest(roots, name = 'effect') {
|
|
71
|
+
if (roots.length === 1) {
|
|
72
|
+
return serializeEffectTree(roots[0], name);
|
|
73
|
+
}
|
|
74
|
+
const meta = { textures: {}, materials: {}, geometries: {} };
|
|
75
|
+
const children = roots.map((root, i) => serializeNode(buildEffectTree(root), meta, `${name}-node-${i}`));
|
|
76
|
+
const object = {
|
|
77
|
+
uuid: `${name}-root`,
|
|
78
|
+
name,
|
|
79
|
+
type: 'Group',
|
|
80
|
+
layers: 1,
|
|
81
|
+
matrix: [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1],
|
|
82
|
+
children,
|
|
83
|
+
};
|
|
84
|
+
return finishEnvelope(object, meta);
|
|
85
|
+
}
|
|
86
|
+
/** Flattens accumulated meta and wraps the serialized object into the Quarks envelope. */
|
|
87
|
+
function finishEnvelope(object, meta) {
|
|
88
|
+
// toJSON stashes live Texture instances and material records in meta; flatten them into
|
|
89
|
+
// the serializable images/textures/materials arrays QuarksLoader expects.
|
|
90
|
+
const images = [];
|
|
91
|
+
const textures = [];
|
|
92
|
+
for (const [uuid, tex] of Object.entries(meta.textures)) {
|
|
93
|
+
const url = tex?.url ?? tex?.name;
|
|
94
|
+
if (url) {
|
|
95
|
+
const imageUuid = `${uuid}-image`;
|
|
96
|
+
images.push({ uuid: imageUuid, url });
|
|
97
|
+
textures.push({ uuid, image: imageUuid });
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
textures.push({ uuid });
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
const materials = Object.values(meta.materials).map((material) => {
|
|
104
|
+
const { sourceMaterial: _sourceMaterial, ...serializable } = material;
|
|
105
|
+
return serializable;
|
|
106
|
+
});
|
|
107
|
+
return JSON.stringify({
|
|
108
|
+
metadata: { version: 4.5, type: 'Object3D', generator: 'babylon.quarks-editor' },
|
|
109
|
+
geometries: Object.values(meta.geometries),
|
|
110
|
+
materials,
|
|
111
|
+
textures,
|
|
112
|
+
images,
|
|
113
|
+
object,
|
|
114
|
+
}, null, 2);
|
|
115
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { TransformNode } from '@babylonjs/core/Meshes/transformNode';
|
|
2
|
+
/**
|
|
3
|
+
* Groups the given nodes under a new organizational TransformNode. Each node's world
|
|
4
|
+
* transform is preserved via `setParent` — grouping never moves anything visually, it only
|
|
5
|
+
* changes the hierarchy so the group can be repositioned/organized as one unit afterwards.
|
|
6
|
+
*/
|
|
7
|
+
export declare function groupNodes(nodes: TransformNode[], name?: string): TransformNode;
|
|
8
|
+
/**
|
|
9
|
+
* Dissolves a group, reparenting its children onto the group's own parent and disposing the
|
|
10
|
+
* (now childless) group node. World transform of each child is preserved.
|
|
11
|
+
*/
|
|
12
|
+
export declare function ungroupNode(group: TransformNode): void;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { TransformNode } from '@babylonjs/core/Meshes/transformNode';
|
|
2
|
+
/**
|
|
3
|
+
* Groups the given nodes under a new organizational TransformNode. Each node's world
|
|
4
|
+
* transform is preserved via `setParent` — grouping never moves anything visually, it only
|
|
5
|
+
* changes the hierarchy so the group can be repositioned/organized as one unit afterwards.
|
|
6
|
+
*/
|
|
7
|
+
export function groupNodes(nodes, name = 'Group') {
|
|
8
|
+
const scene = nodes[0].getScene();
|
|
9
|
+
const group = new TransformNode(name, scene);
|
|
10
|
+
group.setParent(nodes[0].parent);
|
|
11
|
+
for (const node of nodes) {
|
|
12
|
+
node.setParent(group);
|
|
13
|
+
}
|
|
14
|
+
return group;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Dissolves a group, reparenting its children onto the group's own parent and disposing the
|
|
18
|
+
* (now childless) group node. World transform of each child is preserved.
|
|
19
|
+
*/
|
|
20
|
+
export function ungroupNode(group) {
|
|
21
|
+
const parent = group.parent;
|
|
22
|
+
const children = group.getChildren().filter((c) => c instanceof TransformNode);
|
|
23
|
+
for (const child of children) {
|
|
24
|
+
child.setParent(parent);
|
|
25
|
+
}
|
|
26
|
+
group.dispose(true, false);
|
|
27
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { EffectBinding } from './binding';
|
|
2
|
+
/**
|
|
3
|
+
* Snapshot-based undo/redo over serialized effect JSON. Owned by the host (not the
|
|
4
|
+
* binding) so history survives rebinding when a restore rebuilds the systems.
|
|
5
|
+
*
|
|
6
|
+
* Wire-up: call attach() whenever a binding is (re)mounted; on undo()/redo() feed the
|
|
7
|
+
* returned JSON back through the host's import path and attach the new binding.
|
|
8
|
+
*/
|
|
9
|
+
export declare class EffectHistory {
|
|
10
|
+
private undoStack;
|
|
11
|
+
private redoStack;
|
|
12
|
+
private lastCaptureTime;
|
|
13
|
+
private binding;
|
|
14
|
+
/** Points the history at the active binding and hooks snapshot capture. */
|
|
15
|
+
attach(binding: EffectBinding): void;
|
|
16
|
+
get canUndo(): boolean;
|
|
17
|
+
get canRedo(): boolean;
|
|
18
|
+
clear(): void;
|
|
19
|
+
/** Pushes an undo snapshot; rapid consecutive edits are coalesced into one step. */
|
|
20
|
+
capture(): void;
|
|
21
|
+
/** Returns the previous effect state as parsed JSON, or null when nothing to undo. */
|
|
22
|
+
undo(): unknown | null;
|
|
23
|
+
/** Returns the next effect state as parsed JSON, or null when nothing to redo. */
|
|
24
|
+
redo(): unknown | null;
|
|
25
|
+
}
|