babylon.quarks-editor 0.19.0 → 0.20.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 +28 -0
- package/dist/core/assetDragSession.d.ts +4 -0
- package/dist/core/assetDragSession.js +8 -0
- package/dist/core/assetTree.d.ts +39 -0
- package/dist/core/assetTree.js +112 -0
- package/dist/core/assets.d.ts +56 -0
- package/dist/core/assets.js +207 -0
- package/dist/core/binding.d.ts +5 -0
- package/dist/core/binding.js +7 -0
- package/dist/core/collision.d.ts +4 -5
- package/dist/core/collision.js +4 -5
- package/dist/core/defaultEffect.d.ts +4 -5
- package/dist/core/defaultEffect.js +5 -14
- package/dist/core/emitterShapeWireframe.d.ts +1 -0
- package/dist/core/emitterShapeWireframe.js +1 -0
- package/dist/core/galleryPlayback.d.ts +7 -7
- package/dist/core/galleryPlayback.js +2 -2
- package/dist/core/galleryScene.d.ts +8 -13
- package/dist/core/galleryScene.js +14 -42
- package/dist/core/gallerySelectionMarker.d.ts +1 -0
- package/dist/core/gallerySelectionMarker.js +1 -0
- package/dist/core/history.d.ts +12 -1
- package/dist/core/history.js +16 -3
- package/dist/core/indexedDbProjectStore.d.ts +35 -0
- package/dist/core/indexedDbProjectStore.js +299 -0
- package/dist/core/lineShaders.d.ts +2 -0
- package/dist/core/lineShaders.js +7 -0
- package/dist/core/pickAssetFiles.d.ts +6 -0
- package/dist/core/{pickGalleryFiles.js → pickAssetFiles.js} +13 -8
- package/dist/core/projectStore.d.ts +61 -0
- package/dist/core/projectStore.js +14 -0
- package/dist/core/sceneEntries.d.ts +35 -0
- package/dist/core/sceneEntries.js +62 -0
- package/dist/core/transport.d.ts +12 -0
- package/dist/core/transport.js +36 -0
- package/dist/core/viewportPicker.d.ts +4 -4
- package/dist/core/viewportPicker.js +3 -3
- package/dist/index.d.ts +4 -0
- package/dist/index.js +2 -0
- package/dist/react/AssetsPanel.d.ts +22 -0
- package/dist/react/AssetsPanel.js +252 -0
- package/dist/react/ChoiceDialog.d.ts +18 -0
- package/dist/react/ChoiceDialog.js +48 -0
- package/dist/react/ContextMenu.d.ts +16 -0
- package/dist/react/ContextMenu.js +70 -0
- package/dist/react/EditorWorkspace.d.ts +21 -0
- package/dist/react/EditorWorkspace.js +63 -0
- package/dist/react/EffectEditor.d.ts +1 -1
- package/dist/react/EffectEditor.js +7 -4
- package/dist/react/EffectEditorHost.d.ts +19 -2
- package/dist/react/EffectEditorHost.js +1385 -607
- package/dist/react/EffectTreeView.d.ts +17 -0
- package/dist/react/EffectTreeView.js +52 -0
- package/dist/react/ObjectModule.d.ts +4 -0
- package/dist/react/ObjectModule.js +7 -10
- package/dist/react/ProjectsPage.d.ts +9 -0
- package/dist/react/ProjectsPage.js +140 -0
- package/dist/react/ScenePanel.d.ts +26 -0
- package/dist/react/ScenePanel.js +134 -0
- package/dist/react/TimelinePanel.d.ts +8 -1
- package/dist/react/TimelinePanel.js +20 -41
- package/dist/react/TransportBar.d.ts +24 -0
- package/dist/react/TransportBar.js +43 -0
- package/dist/react/behaviorModules.js +5 -7
- package/dist/react/index.d.ts +16 -2
- package/dist/react/index.js +8 -1
- package/dist/react/inspectorHints.js +1 -2
- package/package.json +1 -1
- package/src/core/assetDragSession.ts +12 -0
- package/src/core/assetTree.ts +148 -0
- package/src/core/assets.ts +262 -0
- package/src/core/binding.ts +8 -0
- package/src/core/collision.ts +4 -5
- package/src/core/defaultEffect.ts +9 -22
- package/src/core/emitterShapeWireframe.ts +1 -0
- package/src/core/galleryPlayback.ts +9 -9
- package/src/core/galleryScene.ts +15 -59
- package/src/core/gallerySelectionMarker.ts +1 -0
- package/src/core/history.ts +24 -3
- package/src/core/indexedDbProjectStore.ts +357 -0
- package/src/core/lineShaders.ts +7 -0
- package/src/core/{pickGalleryFiles.ts → pickAssetFiles.ts} +13 -8
- package/src/core/projectStore.ts +81 -0
- package/src/core/sceneEntries.ts +96 -0
- package/src/core/transport.ts +53 -0
- package/src/core/viewportPicker.ts +5 -5
- package/src/index.ts +11 -0
- package/src/react/AssetsPanel.tsx +559 -0
- package/src/react/ChoiceDialog.tsx +93 -0
- package/src/react/ContextMenu.tsx +107 -0
- package/src/react/EditorWorkspace.tsx +95 -0
- package/src/react/EffectEditor.tsx +15 -4
- package/src/react/EffectEditorHost.tsx +1922 -813
- package/src/react/EffectTreeView.tsx +117 -0
- package/src/react/ObjectModule.tsx +17 -13
- package/src/react/ProjectsPage.tsx +306 -0
- package/src/react/ScenePanel.tsx +318 -0
- package/src/react/TimelinePanel.tsx +102 -103
- package/src/react/TransportBar.tsx +114 -0
- package/src/react/behaviorModules.tsx +7 -15
- package/src/react/index.ts +16 -2
- package/src/react/inspectorHints.ts +1 -2
- package/dist/core/galleryDragSession.d.ts +0 -4
- package/dist/core/galleryDragSession.js +0 -8
- package/dist/core/galleryTree.d.ts +0 -49
- package/dist/core/galleryTree.js +0 -141
- package/dist/core/loadEffectGallery.d.ts +0 -36
- package/dist/core/loadEffectGallery.js +0 -97
- package/dist/core/pickGalleryFiles.d.ts +0 -5
- package/dist/react/GalleryPanel.d.ts +0 -20
- package/dist/react/GalleryPanel.js +0 -253
- package/src/core/galleryDragSession.ts +0 -12
- package/src/core/galleryTree.ts +0 -190
- package/src/core/loadEffectGallery.ts +0 -150
- package/src/react/GalleryPanel.tsx +0 -561
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,34 @@ All notable changes to the `babylon.quarks-editor` package are documented here.
|
|
|
4
4
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
5
5
|
and the project follows the `quarks.core` 0.x version line.
|
|
6
6
|
|
|
7
|
+
## [0.20.0] — 2026-10-16
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- **Projects**: a start page lists the projects saved in IndexedDB (new, rename, duplicate,
|
|
12
|
+
delete), with storage usage and a "make storage persistent" button. `ProjectStore` with an
|
|
13
|
+
`IndexedDbProjectStore` default keeps each texture once per project by content hash. The scene
|
|
14
|
+
is saved explicitly (Save / Ctrl+S, `*` while unsaved), and assets are written as they change.
|
|
15
|
+
- **Scene and Assets panels**: the Scene panel lists only the effects placed in the viewport
|
|
16
|
+
(show/hide, solo, delete, "Save as new asset…"), and the Assets drawer holds the library.
|
|
17
|
+
- **Prefab mode**: open an asset to edit it on its own, with the scene set aside. Double-clicking
|
|
18
|
+
another asset swaps it in place, asking Save / Don't save / Cancel when there are unsaved edits.
|
|
19
|
+
- **Effect tree** in the Scene and prefab panels: each effect unfolds into its group/emitter tree,
|
|
20
|
+
and clicking a node selects it for the inspector and gizmo, sub-emitters included, without
|
|
21
|
+
opening the timeline.
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
|
|
25
|
+
- Selecting a node no longer stalls the viewport: the inspector renders after the selection
|
|
26
|
+
instead of in the same frame, cutting main-thread blocking per switch from ~50 ms to ~15–19 ms.
|
|
27
|
+
|
|
28
|
+
### Fixed
|
|
29
|
+
|
|
30
|
+
- The camera renders only above the floating Assets drawer, so a focused effect or an opened
|
|
31
|
+
prefab is centred where it can be seen.
|
|
32
|
+
- Selection rings rendered nothing under Vite's dev server: the color shaders they use are now
|
|
33
|
+
registered up front instead of being fetched at runtime.
|
|
34
|
+
|
|
7
35
|
## [0.17.9] — 2026-07-14
|
|
8
36
|
|
|
9
37
|
### Added
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { AssetDragPayload } from './assetTree';
|
|
2
|
+
/** Stores the current asset drag payload for viewport preview (getData is drop-only in most browsers). */
|
|
3
|
+
export declare function setAssetDragSession(payload: AssetDragPayload | null): void;
|
|
4
|
+
export declare function getAssetDragSession(): AssetDragPayload | null;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
let activeDrag = null;
|
|
2
|
+
/** Stores the current asset drag payload for viewport preview (getData is drop-only in most browsers). */
|
|
3
|
+
export function setAssetDragSession(payload) {
|
|
4
|
+
activeDrag = payload;
|
|
5
|
+
}
|
|
6
|
+
export function getAssetDragSession() {
|
|
7
|
+
return activeDrag;
|
|
8
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { EffectAsset } from './assets';
|
|
2
|
+
export interface AssetTreeFolder {
|
|
3
|
+
kind: 'folder';
|
|
4
|
+
name: string;
|
|
5
|
+
path: string;
|
|
6
|
+
children: AssetTreeNode[];
|
|
7
|
+
}
|
|
8
|
+
export interface AssetTreeEffect {
|
|
9
|
+
kind: 'asset';
|
|
10
|
+
name: string;
|
|
11
|
+
path: string;
|
|
12
|
+
asset: EffectAsset;
|
|
13
|
+
}
|
|
14
|
+
export type AssetTreeNode = AssetTreeFolder | AssetTreeEffect;
|
|
15
|
+
/** Builds a folder tree from asset relative paths (e.g. `VFX/Fire/Explosion.json`). */
|
|
16
|
+
export declare function buildAssetTree(assets: EffectAsset[]): AssetTreeNode[];
|
|
17
|
+
/** Children of the folder at `path` (`''` is the top level), or null when it no longer exists. */
|
|
18
|
+
export declare function folderChildren(tree: AssetTreeNode[], path: string): AssetTreeNode[] | null;
|
|
19
|
+
/** Assets whose name or path contains `query`, across every folder, sorted by name. */
|
|
20
|
+
export declare function searchAssets(assets: EffectAsset[], query: string): EffectAsset[];
|
|
21
|
+
export declare const ASSET_DRAG_MIME = "application/x-quarks-asset-drop";
|
|
22
|
+
export type AssetDragPayload = {
|
|
23
|
+
kind: 'asset';
|
|
24
|
+
id: number;
|
|
25
|
+
} | {
|
|
26
|
+
kind: 'folder';
|
|
27
|
+
path: string;
|
|
28
|
+
};
|
|
29
|
+
export declare function writeAssetDrag(dataTransfer: DataTransfer, payload: AssetDragPayload): void;
|
|
30
|
+
export declare function readAssetDrag(dataTransfer: DataTransfer): AssetDragPayload | null;
|
|
31
|
+
/** Returns every asset whose JSON path sits inside `folderPath` (recursive). */
|
|
32
|
+
export declare function collectAssetsInFolder(assets: EffectAsset[], folderPath: string): EffectAsset[];
|
|
33
|
+
/** Collects all assets under a folder tree node. */
|
|
34
|
+
export declare function collectFolderAssets(node: AssetTreeFolder): EffectAsset[];
|
|
35
|
+
/** Resolves a drag payload to the assets that would be instantiated on drop. */
|
|
36
|
+
export declare function describeAssetDrag(assets: EffectAsset[], payload: AssetDragPayload): {
|
|
37
|
+
assets: EffectAsset[];
|
|
38
|
+
label: string;
|
|
39
|
+
};
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
function compareNodes(a, b) {
|
|
2
|
+
if (a.kind !== b.kind) {
|
|
3
|
+
return a.kind === 'folder' ? -1 : 1;
|
|
4
|
+
}
|
|
5
|
+
return a.name.localeCompare(b.name, undefined, { sensitivity: 'base' });
|
|
6
|
+
}
|
|
7
|
+
/** Builds a folder tree from asset relative paths (e.g. `VFX/Fire/Explosion.json`). */
|
|
8
|
+
export function buildAssetTree(assets) {
|
|
9
|
+
const roots = [];
|
|
10
|
+
for (const asset of assets) {
|
|
11
|
+
const segments = asset.path
|
|
12
|
+
.replace(/\.json$/i, '')
|
|
13
|
+
.split('/')
|
|
14
|
+
.filter(Boolean);
|
|
15
|
+
const fileName = segments.pop() ?? asset.name;
|
|
16
|
+
let level = roots;
|
|
17
|
+
let pathSoFar = '';
|
|
18
|
+
for (const segment of segments) {
|
|
19
|
+
pathSoFar = pathSoFar ? `${pathSoFar}/${segment}` : segment;
|
|
20
|
+
let folder = level.find((node) => node.kind === 'folder' && node.name === segment);
|
|
21
|
+
if (!folder) {
|
|
22
|
+
folder = { kind: 'folder', name: segment, path: pathSoFar, children: [] };
|
|
23
|
+
level.push(folder);
|
|
24
|
+
}
|
|
25
|
+
level = folder.children;
|
|
26
|
+
}
|
|
27
|
+
level.push({ kind: 'asset', name: fileName, path: asset.path, asset });
|
|
28
|
+
}
|
|
29
|
+
const sortTree = (nodes) => {
|
|
30
|
+
nodes.sort(compareNodes);
|
|
31
|
+
for (const node of nodes) {
|
|
32
|
+
if (node.kind === 'folder') {
|
|
33
|
+
sortTree(node.children);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
sortTree(roots);
|
|
38
|
+
return roots;
|
|
39
|
+
}
|
|
40
|
+
/** Children of the folder at `path` (`''` is the top level), or null when it no longer exists. */
|
|
41
|
+
export function folderChildren(tree, path) {
|
|
42
|
+
let level = tree;
|
|
43
|
+
for (const segment of path.split('/').filter(Boolean)) {
|
|
44
|
+
const folder = level.find((node) => node.kind === 'folder' && node.name === segment);
|
|
45
|
+
if (!folder) {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
level = folder.children;
|
|
49
|
+
}
|
|
50
|
+
return level;
|
|
51
|
+
}
|
|
52
|
+
/** Assets whose name or path contains `query`, across every folder, sorted by name. */
|
|
53
|
+
export function searchAssets(assets, query) {
|
|
54
|
+
const q = query.trim().toLowerCase();
|
|
55
|
+
return assets
|
|
56
|
+
.filter((asset) => `${asset.path} ${asset.name}`.toLowerCase().includes(q))
|
|
57
|
+
.sort((a, b) => a.name.localeCompare(b.name, undefined, { sensitivity: 'base' }));
|
|
58
|
+
}
|
|
59
|
+
export const ASSET_DRAG_MIME = 'application/x-quarks-asset-drop';
|
|
60
|
+
export function writeAssetDrag(dataTransfer, payload) {
|
|
61
|
+
dataTransfer.setData(ASSET_DRAG_MIME, JSON.stringify(payload));
|
|
62
|
+
dataTransfer.effectAllowed = 'copy';
|
|
63
|
+
}
|
|
64
|
+
export function readAssetDrag(dataTransfer) {
|
|
65
|
+
const raw = dataTransfer.getData(ASSET_DRAG_MIME);
|
|
66
|
+
if (!raw) {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
try {
|
|
70
|
+
const parsed = JSON.parse(raw);
|
|
71
|
+
if (parsed?.kind === 'asset' && typeof parsed.id === 'number') {
|
|
72
|
+
return parsed;
|
|
73
|
+
}
|
|
74
|
+
if (parsed?.kind === 'folder' && typeof parsed.path === 'string') {
|
|
75
|
+
return parsed;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
/** Returns every asset whose JSON path sits inside `folderPath` (recursive). */
|
|
84
|
+
export function collectAssetsInFolder(assets, folderPath) {
|
|
85
|
+
const prefix = `${folderPath}/`;
|
|
86
|
+
return assets.filter((asset) => asset.path.startsWith(prefix));
|
|
87
|
+
}
|
|
88
|
+
/** Collects all assets under a folder tree node. */
|
|
89
|
+
export function collectFolderAssets(node) {
|
|
90
|
+
const collected = [];
|
|
91
|
+
const walk = (nodes) => {
|
|
92
|
+
for (const child of nodes) {
|
|
93
|
+
if (child.kind === 'asset') {
|
|
94
|
+
collected.push(child.asset);
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
walk(child.children);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
walk(node.children);
|
|
102
|
+
return collected;
|
|
103
|
+
}
|
|
104
|
+
/** Resolves a drag payload to the assets that would be instantiated on drop. */
|
|
105
|
+
export function describeAssetDrag(assets, payload) {
|
|
106
|
+
if (payload.kind === 'asset') {
|
|
107
|
+
const asset = assets.find((item) => item.id === payload.id);
|
|
108
|
+
return { assets: asset ? [asset] : [], label: asset?.name ?? 'Effect' };
|
|
109
|
+
}
|
|
110
|
+
const label = payload.path.split('/').filter(Boolean).pop() ?? payload.path;
|
|
111
|
+
return { assets: collectAssetsInFolder(assets, payload.path), label };
|
|
112
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One effect file in the Assets panel. Holds the source JSON only — nothing is live in the
|
|
3
|
+
* Babylon scene until the asset is dropped into the viewport, and every drop instantiates a
|
|
4
|
+
* fresh, independent copy (see `instantiateSceneEntry`).
|
|
5
|
+
*/
|
|
6
|
+
export interface EffectAsset {
|
|
7
|
+
id: number;
|
|
8
|
+
name: string;
|
|
9
|
+
/** Relative path inside the imported folder (preserves subfolders). */
|
|
10
|
+
path: string;
|
|
11
|
+
/** Quarks JSON with every external file reference already inlined. */
|
|
12
|
+
json: unknown;
|
|
13
|
+
/** Texture/geometry files the effect references that were not among the imported files. */
|
|
14
|
+
missingFiles: string[];
|
|
15
|
+
}
|
|
16
|
+
export interface AssetLoadProgress {
|
|
17
|
+
done: number;
|
|
18
|
+
total: number;
|
|
19
|
+
loaded: number;
|
|
20
|
+
failed: number;
|
|
21
|
+
}
|
|
22
|
+
export declare function relativePathOf(file: File): string;
|
|
23
|
+
/** Files an asset import reads: effect `.json` files plus the images/geometries they may reference. */
|
|
24
|
+
export declare function isAssetImportFile(name: string): boolean;
|
|
25
|
+
/** Collects `.json` files from a picker result, sorted by relative path. */
|
|
26
|
+
export declare function collectJsonFiles(files: FileList | File[]): File[];
|
|
27
|
+
/** Resolves `url` against the folder of `fromPath` (`a/b/fx.json` + `../t.png` → `a/t.png`). */
|
|
28
|
+
export declare function resolveRelativePath(fromPath: string, url: string): string;
|
|
29
|
+
/** True for a Quarks effect envelope with at least one particle emitter. */
|
|
30
|
+
export declare function isEffectJson(json: unknown): boolean;
|
|
31
|
+
/**
|
|
32
|
+
* Reads every effect JSON among `files` into an asset. Files that are not effects (geometry
|
|
33
|
+
* files, unrelated JSON, images) are skipped silently; unreadable JSON is skipped with a warning.
|
|
34
|
+
*/
|
|
35
|
+
export declare function loadEffectAssets(files: FileList | File[], onProgress: (progress: AssetLoadProgress) => void): Promise<EffectAsset[]>;
|
|
36
|
+
/** Wraps an in-memory effect JSON (e.g. the host's `effectJson`) as an asset. */
|
|
37
|
+
export declare function createEffectAsset(name: string, path: string, json: unknown): EffectAsset;
|
|
38
|
+
/** Merges newly imported assets, skipping paths already in the list. */
|
|
39
|
+
export declare function mergeAssets(existing: EffectAsset[], incoming: EffectAsset[]): {
|
|
40
|
+
merged: EffectAsset[];
|
|
41
|
+
added: EffectAsset[];
|
|
42
|
+
skipped: number;
|
|
43
|
+
};
|
|
44
|
+
/** Removes one asset, or every asset under a folder path. */
|
|
45
|
+
export declare function removeAssets(assets: EffectAsset[], target: {
|
|
46
|
+
kind: 'asset';
|
|
47
|
+
id: number;
|
|
48
|
+
} | {
|
|
49
|
+
kind: 'folder';
|
|
50
|
+
path: string;
|
|
51
|
+
}): EffectAsset[];
|
|
52
|
+
/**
|
|
53
|
+
* Url of the effect's main texture — the first emitter's material map — used as its icon in
|
|
54
|
+
* the Assets panel. Falls back to the first image; null when the effect has no texture.
|
|
55
|
+
*/
|
|
56
|
+
export declare function assetIconUrl(asset: EffectAsset): string | null;
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { QuarksLoader } from 'babylon.quarks';
|
|
2
|
+
const YIELD_EVERY = 8;
|
|
3
|
+
const IMAGE_FILE = /\.(png|jpe?g|webp|gif|bmp)$/i;
|
|
4
|
+
let nextAssetId = 1;
|
|
5
|
+
function yieldToBrowser() {
|
|
6
|
+
return new Promise((resolve) => requestAnimationFrame(() => resolve()));
|
|
7
|
+
}
|
|
8
|
+
export function relativePathOf(file) {
|
|
9
|
+
return (file.webkitRelativePath || file.name).replace(/\\/g, '/');
|
|
10
|
+
}
|
|
11
|
+
/** Files an asset import reads: effect `.json` files plus the images/geometries they may reference. */
|
|
12
|
+
export function isAssetImportFile(name) {
|
|
13
|
+
return /\.json$/i.test(name) || IMAGE_FILE.test(name);
|
|
14
|
+
}
|
|
15
|
+
/** Collects `.json` files from a picker result, sorted by relative path. */
|
|
16
|
+
export function collectJsonFiles(files) {
|
|
17
|
+
return Array.from(files)
|
|
18
|
+
.filter((file) => file.name.toLowerCase().endsWith('.json'))
|
|
19
|
+
.sort((a, b) => relativePathOf(a).localeCompare(relativePathOf(b)));
|
|
20
|
+
}
|
|
21
|
+
/** Resolves `url` against the folder of `fromPath` (`a/b/fx.json` + `../t.png` → `a/t.png`). */
|
|
22
|
+
export function resolveRelativePath(fromPath, url) {
|
|
23
|
+
const segments = fromPath.split('/').slice(0, -1);
|
|
24
|
+
for (const part of url.split('/')) {
|
|
25
|
+
if (part === '..') {
|
|
26
|
+
segments.pop();
|
|
27
|
+
}
|
|
28
|
+
else if (part !== '.' && part !== '') {
|
|
29
|
+
segments.push(part);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return segments.join('/');
|
|
33
|
+
}
|
|
34
|
+
function isExternalUrl(url) {
|
|
35
|
+
return /^(data|blob):/i.test(url) || /^[a-z][a-z0-9+.-]*:\/\//i.test(url) || url.startsWith('/');
|
|
36
|
+
}
|
|
37
|
+
function readAsDataUrl(file) {
|
|
38
|
+
return new Promise((resolve, reject) => {
|
|
39
|
+
const reader = new FileReader();
|
|
40
|
+
reader.onload = () => resolve(reader.result);
|
|
41
|
+
reader.onerror = () => reject(reader.error);
|
|
42
|
+
reader.readAsDataURL(file);
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
/** True for a Quarks effect envelope with at least one particle emitter. */
|
|
46
|
+
export function isEffectJson(json) {
|
|
47
|
+
const object = json?.object;
|
|
48
|
+
const walk = (node) => {
|
|
49
|
+
if (!node || typeof node !== 'object')
|
|
50
|
+
return false;
|
|
51
|
+
const typed = node;
|
|
52
|
+
if (typed.type === 'ParticleEmitter')
|
|
53
|
+
return true;
|
|
54
|
+
return Array.isArray(typed.children) && typed.children.some(walk);
|
|
55
|
+
};
|
|
56
|
+
return walk(object);
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Looks files up by path relative to the import root, falling back to the bare file name — a
|
|
60
|
+
* multi-file "Import effect" pick has no folder structure, only names.
|
|
61
|
+
*/
|
|
62
|
+
class ImportedFileIndex {
|
|
63
|
+
constructor(files) {
|
|
64
|
+
this.byPath = new Map();
|
|
65
|
+
this.byName = new Map();
|
|
66
|
+
this.cache = new Map();
|
|
67
|
+
for (const file of files) {
|
|
68
|
+
this.byPath.set(relativePathOf(file).toLowerCase(), file);
|
|
69
|
+
if (!this.byName.has(file.name.toLowerCase())) {
|
|
70
|
+
this.byName.set(file.name.toLowerCase(), file);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
find(fromPath, url) {
|
|
75
|
+
const clean = decodeURIComponent(url.split(/[?#]/)[0]);
|
|
76
|
+
return (this.byPath.get(resolveRelativePath(fromPath, clean).toLowerCase()) ??
|
|
77
|
+
this.byName.get((clean.split('/').pop() ?? clean).toLowerCase()));
|
|
78
|
+
}
|
|
79
|
+
/** Reads each file once, however many effects reference it. */
|
|
80
|
+
read(file, read) {
|
|
81
|
+
let pending = this.cache.get(file);
|
|
82
|
+
if (!pending) {
|
|
83
|
+
pending = read(file);
|
|
84
|
+
this.cache.set(file, pending);
|
|
85
|
+
}
|
|
86
|
+
return pending;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Inlines what an "assets as files" export put in separate files: image urls become data URIs
|
|
91
|
+
* and external geometries are read into the JSON, so the effect loads (and later exports) as a
|
|
92
|
+
* self-contained effect. Returns the references that could not be found among the files.
|
|
93
|
+
*/
|
|
94
|
+
async function inlineExternalFiles(json, effectPath, index) {
|
|
95
|
+
const missing = [];
|
|
96
|
+
for (const image of Array.isArray(json.images) ? json.images : []) {
|
|
97
|
+
const url = image?.url;
|
|
98
|
+
if (typeof url !== 'string' || isExternalUrl(url))
|
|
99
|
+
continue;
|
|
100
|
+
const file = index.find(effectPath, url);
|
|
101
|
+
if (file) {
|
|
102
|
+
image.url = await index.read(file, readAsDataUrl);
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
missing.push(url);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
await QuarksLoader.resolveExternalGeometries(json, async (url) => {
|
|
109
|
+
const file = index.find(effectPath, url);
|
|
110
|
+
if (!file) {
|
|
111
|
+
missing.push(url);
|
|
112
|
+
throw new Error('not among the imported files');
|
|
113
|
+
}
|
|
114
|
+
return index.read(file, async (f) => JSON.parse(await f.text()));
|
|
115
|
+
});
|
|
116
|
+
return missing;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Reads every effect JSON among `files` into an asset. Files that are not effects (geometry
|
|
120
|
+
* files, unrelated JSON, images) are skipped silently; unreadable JSON is skipped with a warning.
|
|
121
|
+
*/
|
|
122
|
+
export async function loadEffectAssets(files, onProgress) {
|
|
123
|
+
const all = Array.from(files).filter((file) => isAssetImportFile(file.name));
|
|
124
|
+
const jsonFiles = collectJsonFiles(all);
|
|
125
|
+
const index = new ImportedFileIndex(all);
|
|
126
|
+
const assets = [];
|
|
127
|
+
let failed = 0;
|
|
128
|
+
onProgress({ done: 0, total: jsonFiles.length, loaded: 0, failed: 0 });
|
|
129
|
+
for (let i = 0; i < jsonFiles.length; i++) {
|
|
130
|
+
const file = jsonFiles[i];
|
|
131
|
+
const path = relativePathOf(file);
|
|
132
|
+
try {
|
|
133
|
+
const json = JSON.parse(await file.text());
|
|
134
|
+
if (isEffectJson(json)) {
|
|
135
|
+
const missingFiles = await inlineExternalFiles(json, path, index);
|
|
136
|
+
if (missingFiles.length > 0) {
|
|
137
|
+
console.warn(`[Quarks editor] "${path}" references files that were not imported:`, missingFiles);
|
|
138
|
+
}
|
|
139
|
+
assets.push({ id: nextAssetId++, name: file.name.replace(/\.json$/i, ''), path, json, missingFiles });
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
catch (err) {
|
|
143
|
+
failed++;
|
|
144
|
+
console.warn(`[Quarks editor] Skipped "${path}":`, err);
|
|
145
|
+
}
|
|
146
|
+
onProgress({ done: i + 1, total: jsonFiles.length, loaded: assets.length, failed });
|
|
147
|
+
if ((i + 1) % YIELD_EVERY === 0) {
|
|
148
|
+
await yieldToBrowser();
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return assets;
|
|
152
|
+
}
|
|
153
|
+
/** Wraps an in-memory effect JSON (e.g. the host's `effectJson`) as an asset. */
|
|
154
|
+
export function createEffectAsset(name, path, json) {
|
|
155
|
+
return { id: nextAssetId++, name, path, json, missingFiles: [] };
|
|
156
|
+
}
|
|
157
|
+
/** Merges newly imported assets, skipping paths already in the list. */
|
|
158
|
+
export function mergeAssets(existing, incoming) {
|
|
159
|
+
const paths = new Set(existing.map((asset) => asset.path));
|
|
160
|
+
const added = [];
|
|
161
|
+
for (const asset of incoming) {
|
|
162
|
+
if (paths.has(asset.path))
|
|
163
|
+
continue;
|
|
164
|
+
paths.add(asset.path);
|
|
165
|
+
added.push(asset);
|
|
166
|
+
}
|
|
167
|
+
return { merged: [...existing, ...added], added, skipped: incoming.length - added.length };
|
|
168
|
+
}
|
|
169
|
+
/** Removes one asset, or every asset under a folder path. */
|
|
170
|
+
export function removeAssets(assets, target) {
|
|
171
|
+
if (target.kind === 'asset') {
|
|
172
|
+
return assets.filter((asset) => asset.id !== target.id);
|
|
173
|
+
}
|
|
174
|
+
const prefix = `${target.path}/`;
|
|
175
|
+
return assets.filter((asset) => !asset.path.startsWith(prefix));
|
|
176
|
+
}
|
|
177
|
+
const iconCache = new WeakMap();
|
|
178
|
+
/**
|
|
179
|
+
* Url of the effect's main texture — the first emitter's material map — used as its icon in
|
|
180
|
+
* the Assets panel. Falls back to the first image; null when the effect has no texture.
|
|
181
|
+
*/
|
|
182
|
+
export function assetIconUrl(asset) {
|
|
183
|
+
const cached = iconCache.get(asset);
|
|
184
|
+
if (cached !== undefined) {
|
|
185
|
+
return cached;
|
|
186
|
+
}
|
|
187
|
+
const json = asset.json;
|
|
188
|
+
const findMaterial = (node) => {
|
|
189
|
+
if (!node || typeof node !== 'object')
|
|
190
|
+
return undefined;
|
|
191
|
+
if (node.type === 'ParticleEmitter' && node.ps?.material)
|
|
192
|
+
return node.ps.material;
|
|
193
|
+
for (const child of Array.isArray(node.children) ? node.children : []) {
|
|
194
|
+
const found = findMaterial(child);
|
|
195
|
+
if (found)
|
|
196
|
+
return found;
|
|
197
|
+
}
|
|
198
|
+
return undefined;
|
|
199
|
+
};
|
|
200
|
+
const byUuid = (list, uuid) => (Array.isArray(list) ? list : []).find((item) => item?.uuid === uuid);
|
|
201
|
+
const material = byUuid(json?.materials, findMaterial(json?.object));
|
|
202
|
+
const texture = byUuid(json?.textures, material?.map ?? material?.texture);
|
|
203
|
+
const image = byUuid(json?.images, texture?.image) ?? (Array.isArray(json?.images) ? json.images[0] : undefined);
|
|
204
|
+
const url = typeof image?.url === 'string' && image.url ? image.url : null;
|
|
205
|
+
iconCache.set(asset, url);
|
|
206
|
+
return url;
|
|
207
|
+
}
|
package/dist/core/binding.d.ts
CHANGED
|
@@ -33,6 +33,11 @@ export declare class EffectBinding {
|
|
|
33
33
|
* exists so there's always a designated fallback system to select.
|
|
34
34
|
*/
|
|
35
35
|
promoteToMain(next: ParticleSystem): void;
|
|
36
|
+
/**
|
|
37
|
+
* Notifies subscribers of a change made outside `apply()` — e.g. a transform edited by the
|
|
38
|
+
* gizmo, or a sub-system edited through its own nested binding.
|
|
39
|
+
*/
|
|
40
|
+
markChanged(): void;
|
|
36
41
|
private notify;
|
|
37
42
|
restart(): void;
|
|
38
43
|
/**
|
package/dist/core/binding.js
CHANGED
|
@@ -56,6 +56,13 @@ export class EffectBinding {
|
|
|
56
56
|
this.system = next;
|
|
57
57
|
this.notify();
|
|
58
58
|
}
|
|
59
|
+
/**
|
|
60
|
+
* Notifies subscribers of a change made outside `apply()` — e.g. a transform edited by the
|
|
61
|
+
* gizmo, or a sub-system edited through its own nested binding.
|
|
62
|
+
*/
|
|
63
|
+
markChanged() {
|
|
64
|
+
this.notify();
|
|
65
|
+
}
|
|
59
66
|
notify() {
|
|
60
67
|
this.revision++;
|
|
61
68
|
for (const listener of this.listeners) {
|
package/dist/core/collision.d.ts
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { Vector3 } from 'babylon.quarks';
|
|
2
2
|
/**
|
|
3
|
-
* A horizontal ground plane collider
|
|
4
|
-
*
|
|
5
|
-
* own, so the editor supplies this — the common "particles bounce off the floor" case.
|
|
3
|
+
* A horizontal ground plane collider for hosts/demos that opt in via ensureGroundResolver /
|
|
4
|
+
* setPhysicsResolver. The editor never registers this automatically.
|
|
6
5
|
*
|
|
7
6
|
* `resolve` returns true and fills the surface normal when the point is at or below the plane,
|
|
8
7
|
* which is exactly what ApplyCollision.update reflects the velocity against.
|
|
@@ -13,8 +12,8 @@ export declare class GroundPlaneResolver {
|
|
|
13
12
|
resolve(position: Vector3, normal: Vector3): boolean;
|
|
14
13
|
}
|
|
15
14
|
/**
|
|
16
|
-
*
|
|
17
|
-
*
|
|
15
|
+
* Opt-in helper for hosts/demos that want a simple floor collider.
|
|
16
|
+
* The editor does not call this — like Unity, collision needs an explicit host-provided resolver.
|
|
18
17
|
* Idempotent: an already-registered resolver is left untouched and returned.
|
|
19
18
|
*/
|
|
20
19
|
export declare function ensureGroundResolver(): GroundPlaneResolver;
|
package/dist/core/collision.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { getPhysicsResolver, setPhysicsResolver } from 'babylon.quarks';
|
|
2
2
|
/**
|
|
3
|
-
* A horizontal ground plane collider
|
|
4
|
-
*
|
|
5
|
-
* own, so the editor supplies this — the common "particles bounce off the floor" case.
|
|
3
|
+
* A horizontal ground plane collider for hosts/demos that opt in via ensureGroundResolver /
|
|
4
|
+
* setPhysicsResolver. The editor never registers this automatically.
|
|
6
5
|
*
|
|
7
6
|
* `resolve` returns true and fills the surface normal when the point is at or below the plane,
|
|
8
7
|
* which is exactly what ApplyCollision.update reflects the velocity against.
|
|
@@ -20,8 +19,8 @@ export class GroundPlaneResolver {
|
|
|
20
19
|
}
|
|
21
20
|
}
|
|
22
21
|
/**
|
|
23
|
-
*
|
|
24
|
-
*
|
|
22
|
+
* Opt-in helper for hosts/demos that want a simple floor collider.
|
|
23
|
+
* The editor does not call this — like Unity, collision needs an explicit host-provided resolver.
|
|
25
24
|
* Idempotent: an already-registered resolver is left untouched and returned.
|
|
26
25
|
*/
|
|
27
26
|
export function ensureGroundResolver() {
|
|
@@ -1,12 +1,11 @@
|
|
|
1
|
-
import { TransformNode } from '@babylonjs/core/Meshes/transformNode';
|
|
2
1
|
import type { Scene } from '@babylonjs/core/scene';
|
|
3
2
|
import { ParticleSystem } from 'babylon.quarks';
|
|
4
|
-
import type
|
|
5
|
-
export interface
|
|
3
|
+
import { type SceneEntry } from './sceneEntries';
|
|
4
|
+
export interface CreateSceneDefaultOptions {
|
|
6
5
|
resolveTexture?: (url: string, scene: Scene) => unknown;
|
|
7
6
|
textureUrl?: string;
|
|
8
7
|
}
|
|
9
8
|
/** Creates a simple cone burst preset for new editor effects. */
|
|
10
9
|
export declare function createDefaultEffect(scene: Scene, resolveTexture?: (url: string, scene: Scene) => unknown, textureUrl?: string): ParticleSystem;
|
|
11
|
-
/**
|
|
12
|
-
export declare function
|
|
10
|
+
/** Creates a preset effect as a new scene entry (systems not registered with the renderer yet). */
|
|
11
|
+
export declare function createSceneDefaultEntry(scene: Scene, existing: SceneEntry[], options?: CreateSceneDefaultOptions): SceneEntry;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { TransformNode } from '@babylonjs/core/Meshes/transformNode';
|
|
2
2
|
import { ColorOverLife, ConeEmitter, ConstantColor, ConstantValue, IntervalValue, ParticleSystem, RenderMode, Vector4, } from 'babylon.quarks';
|
|
3
3
|
import { DEFAULT_GRADIENT_STOPS, buildGradient } from './colors';
|
|
4
|
+
import { uniqueEntryName } from './sceneEntries';
|
|
4
5
|
/** Creates a simple cone burst preset for new editor effects. */
|
|
5
6
|
export function createDefaultEffect(scene, resolveTexture, textureUrl) {
|
|
6
7
|
const system = new ParticleSystem({
|
|
@@ -22,22 +23,12 @@ export function createDefaultEffect(scene, resolveTexture, textureUrl) {
|
|
|
22
23
|
system.emitter.rotation.x = -Math.PI / 2;
|
|
23
24
|
return system;
|
|
24
25
|
}
|
|
25
|
-
/**
|
|
26
|
-
export function
|
|
27
|
-
const
|
|
28
|
-
const takenPaths = new Set(existing.map((entry) => entry.path));
|
|
29
|
-
let name = existing.length === 0 ? 'Effect' : 'New Effect';
|
|
30
|
-
let suffix = 2;
|
|
31
|
-
while (takenNames.has(name) || takenPaths.has(`_created/${name}.json`)) {
|
|
32
|
-
name = `New Effect ${suffix}`;
|
|
33
|
-
suffix++;
|
|
34
|
-
}
|
|
35
|
-
const path = `_created/${name}.json`;
|
|
26
|
+
/** Creates a preset effect as a new scene entry (systems not registered with the renderer yet). */
|
|
27
|
+
export function createSceneDefaultEntry(scene, existing, options = {}) {
|
|
28
|
+
const name = uniqueEntryName(existing.length === 0 ? 'Effect' : 'New Effect', existing);
|
|
36
29
|
const system = createDefaultEffect(scene, options.resolveTexture, options.textureUrl);
|
|
37
30
|
system.pause();
|
|
38
31
|
const root = new TransformNode(name, scene);
|
|
39
32
|
system.emitter.parent = root;
|
|
40
|
-
|
|
41
|
-
root.setEnabled(false);
|
|
42
|
-
return { name, path, root, systems: [system], inScene: false };
|
|
33
|
+
return { name, path: `_created/${name}.json`, assetId: null, root, systems: [system] };
|
|
43
34
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Scene } from '@babylonjs/core/scene';
|
|
2
2
|
import type { ParticleSystem } from 'babylon.quarks';
|
|
3
|
+
import './lineShaders';
|
|
3
4
|
/**
|
|
4
5
|
* Optional wireframe overlays for every emitter shape in the current effect.
|
|
5
6
|
* Geometry follows quarks.core spawn math (local XY spawn plane, +Z emit axis).
|
|
@@ -3,6 +3,7 @@ import { Vector3 } from '@babylonjs/core/Maths/math.vector';
|
|
|
3
3
|
import { MeshBuilder } from '@babylonjs/core/Meshes/meshBuilder';
|
|
4
4
|
import { TransformNode } from '@babylonjs/core/Meshes/transformNode';
|
|
5
5
|
import { EDITOR_SCENE_NODE_METADATA } from './editorScene';
|
|
6
|
+
import './lineShaders';
|
|
6
7
|
import { getShapeType, readShapeParams } from './shapes';
|
|
7
8
|
const LINE_COLOR = new Color3(0.45, 0.82, 1);
|
|
8
9
|
const DIRECTION_COLOR = new Color3(0.75, 0.92, 1);
|
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
/** Freezes every
|
|
3
|
-
export declare function applyGalleryPlayback(entries:
|
|
1
|
+
import type { SceneEntry } from './sceneEntries';
|
|
2
|
+
/** Freezes every scene entry; unpauses the visible ones (the preview set) when transport is playing. */
|
|
3
|
+
export declare function applyGalleryPlayback(entries: SceneEntry[], previewRootIds: ReadonlySet<number>, transportPlaying: boolean, focusFinishedRootIds?: ReadonlySet<number>): void;
|
|
4
4
|
/** Whether any preview effect on stage should keep simulating. */
|
|
5
|
-
export declare function hasActivePreview(entries:
|
|
5
|
+
export declare function hasActivePreview(entries: SceneEntry[], previewRootIds: ReadonlySet<number>): boolean;
|
|
6
6
|
/** In-scene preview effects currently in the preview set. */
|
|
7
|
-
export declare function countActivePreviews(entries:
|
|
7
|
+
export declare function countActivePreviews(entries: SceneEntry[], previewRootIds: ReadonlySet<number>): number;
|
|
8
8
|
/** Particle count across all active preview effects. */
|
|
9
|
-
export declare function countGalleryParticles(entries:
|
|
9
|
+
export declare function countGalleryParticles(entries: SceneEntry[], previewRootIds: ReadonlySet<number>): number;
|
|
10
10
|
/**
|
|
11
11
|
* Pauses one-shot preview effects whose particle systems report `isFinished()`.
|
|
12
12
|
* Returns true when any entry was paused (caller should refresh renderer batches).
|
|
13
13
|
*/
|
|
14
|
-
export declare function pauseFinishedPreviews(entries:
|
|
14
|
+
export declare function pauseFinishedPreviews(entries: SceneEntry[], previewRootIds: ReadonlySet<number>, focusFinishedRootIds?: ReadonlySet<number>): boolean;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { QuarksUtil } from 'babylon.quarks';
|
|
2
2
|
function isPreviewEntry(entry, previewRootIds) {
|
|
3
|
-
return
|
|
3
|
+
return previewRootIds.has(entry.root.uniqueId);
|
|
4
4
|
}
|
|
5
|
-
/** Freezes every
|
|
5
|
+
/** Freezes every scene entry; unpauses the visible ones (the preview set) when transport is playing. */
|
|
6
6
|
export function applyGalleryPlayback(entries, previewRootIds, transportPlaying, focusFinishedRootIds = new Set()) {
|
|
7
7
|
for (const entry of entries) {
|
|
8
8
|
for (const system of entry.systems) {
|