babylon.quarks-editor 0.17.7 → 0.17.9
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 +46 -0
- package/README.md +1 -1
- package/dist/core/binding.js +2 -5
- package/dist/core/defaultEffect.d.ts +12 -0
- package/dist/core/defaultEffect.js +43 -0
- package/dist/core/editorScene.d.ts +7 -0
- package/dist/core/editorScene.js +11 -0
- package/dist/core/effectTree.js +2 -1
- package/dist/core/emitterShapeWireframe.d.ts +20 -0
- package/dist/core/emitterShapeWireframe.js +329 -0
- package/dist/core/galleryDragSession.d.ts +4 -0
- package/dist/core/galleryDragSession.js +8 -0
- package/dist/core/galleryDropMarker.d.ts +13 -0
- package/dist/core/galleryDropMarker.js +42 -0
- package/dist/core/galleryLayout.d.ts +20 -0
- package/dist/core/galleryLayout.js +68 -0
- package/dist/core/galleryPlayback.d.ts +9 -0
- package/dist/core/galleryPlayback.js +43 -0
- package/dist/core/galleryScene.d.ts +12 -0
- package/dist/core/galleryScene.js +41 -0
- package/dist/core/gallerySelectionMarker.d.ts +25 -0
- package/dist/core/gallerySelectionMarker.js +69 -0
- package/dist/core/galleryTree.d.ts +49 -0
- package/dist/core/galleryTree.js +138 -0
- package/dist/core/loadEffect.d.ts +13 -0
- package/dist/core/loadEffect.js +31 -17
- package/dist/core/loadEffectGallery.d.ts +36 -0
- package/dist/core/loadEffectGallery.js +97 -0
- package/dist/core/material.d.ts +13 -0
- package/dist/core/material.js +68 -0
- package/dist/core/pickGalleryFiles.d.ts +5 -0
- package/dist/core/pickGalleryFiles.js +42 -0
- package/dist/core/playback.d.ts +23 -0
- package/dist/core/playback.js +32 -0
- package/dist/core/scrub.d.ts +5 -0
- package/dist/core/scrub.js +30 -0
- package/dist/core/timeline.d.ts +3 -1
- package/dist/core/timeline.js +8 -4
- package/dist/core/transformStore.d.ts +3 -0
- package/dist/core/transformStore.js +16 -0
- package/dist/core/viewportPicker.d.ts +25 -0
- package/dist/core/viewportPicker.js +86 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/react/EffectEditor.d.ts +3 -0
- package/dist/react/EffectEditor.js +3 -1
- package/dist/react/EffectEditorHost.d.ts +7 -0
- package/dist/react/EffectEditorHost.js +717 -102
- package/dist/react/GalleryPanel.d.ts +19 -0
- package/dist/react/GalleryPanel.js +215 -0
- package/dist/react/InspectorInfoIcon.d.ts +7 -0
- package/dist/react/InspectorInfoIcon.js +22 -0
- package/dist/react/InspectorLabel.d.ts +7 -0
- package/dist/react/InspectorLabel.js +12 -0
- package/dist/react/MessageDialog.d.ts +9 -0
- package/dist/react/MessageDialog.js +24 -0
- package/dist/react/ModuleSection.d.ts +2 -0
- package/dist/react/ModuleSection.js +8 -1
- package/dist/react/ObjectModule.d.ts +6 -0
- package/dist/react/ObjectModule.js +40 -0
- package/dist/react/TimelinePanel.d.ts +11 -15
- package/dist/react/TimelinePanel.js +129 -65
- package/dist/react/TimelineTrackRow.d.ts +2 -2
- package/dist/react/TimelineTrackRow.js +41 -23
- package/dist/react/Tooltip.d.ts +6 -0
- package/dist/react/Tooltip.js +65 -0
- package/dist/react/ValueField.d.ts +2 -0
- package/dist/react/ValueField.js +1 -1
- package/dist/react/behaviorModules.js +8 -8
- package/dist/react/fields.d.ts +2 -0
- package/dist/react/fields.js +14 -5
- package/dist/react/icons.d.ts +7 -0
- package/dist/react/icons.js +10 -0
- package/dist/react/index.d.ts +4 -0
- package/dist/react/index.js +2 -0
- package/dist/react/inspectorHints.d.ts +2 -0
- package/dist/react/inspectorHints.js +138 -0
- package/dist/react/modules.js +109 -81
- package/dist/react/theme.d.ts +2 -0
- package/dist/react/theme.js +97 -22
- package/package.json +72 -63
- package/src/core/binding.ts +102 -105
- package/src/core/collision.ts +40 -40
- package/src/core/defaultEffect.ts +74 -0
- package/src/core/editorScene.ts +14 -0
- package/src/core/effectTree.ts +2 -1
- package/src/core/emitterShapeWireframe.ts +373 -0
- package/src/core/galleryDragSession.ts +12 -0
- package/src/core/galleryDropMarker.ts +57 -0
- package/src/core/galleryLayout.ts +90 -0
- package/src/core/galleryPlayback.ts +55 -0
- package/src/core/galleryScene.ts +58 -0
- package/src/core/gallerySelectionMarker.ts +84 -0
- package/src/core/galleryTree.ts +187 -0
- package/src/core/groups.ts +29 -29
- package/src/core/loadEffect.ts +139 -102
- package/src/core/loadEffectGallery.ts +146 -0
- package/src/core/material.ts +84 -0
- package/src/core/pickGalleryFiles.ts +58 -0
- package/src/core/playback.ts +49 -0
- package/src/core/scrub.ts +60 -28
- package/src/core/shapes.ts +133 -133
- package/src/core/systems.ts +98 -98
- package/src/core/timeline.ts +98 -94
- package/src/core/transformStore.ts +19 -0
- package/src/core/values.ts +113 -113
- package/src/core/viewportPicker.ts +114 -0
- package/src/index.ts +34 -32
- package/src/react/CurveEditor.tsx +168 -168
- package/src/react/EffectEditor.tsx +123 -111
- package/src/react/EffectEditorHost.tsx +1162 -377
- package/src/react/GalleryPanel.tsx +453 -0
- package/src/react/GradientEditor.tsx +127 -127
- package/src/react/InspectorInfoIcon.tsx +36 -0
- package/src/react/InspectorLabel.tsx +18 -0
- package/src/react/MessageDialog.tsx +57 -0
- package/src/react/ModuleSection.tsx +69 -54
- package/src/react/ObjectModule.tsx +58 -0
- package/src/react/PromptDialog.tsx +85 -85
- package/src/react/TimelinePanel.tsx +448 -354
- package/src/react/TimelineTrackRow.tsx +326 -267
- package/src/react/Tooltip.tsx +93 -0
- package/src/react/ValueField.tsx +3 -1
- package/src/react/behaviorModules.tsx +663 -658
- package/src/react/fields.tsx +85 -76
- package/src/react/icons.ts +19 -0
- package/src/react/index.ts +47 -43
- package/src/react/inspectorHints.ts +150 -0
- package/src/react/modules.tsx +1049 -1034
- package/src/react/theme.ts +161 -85
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { Vector3 } from '@babylonjs/core/Maths/math.vector';
|
|
2
|
+
/** Default gap between neighbouring effects in a folder gallery import. */
|
|
3
|
+
export const DEFAULT_GALLERY_SPACING = 4;
|
|
4
|
+
/** Square-grid column count for `count` items. */
|
|
5
|
+
export function galleryColumnCount(count) {
|
|
6
|
+
return Math.max(1, Math.ceil(Math.sqrt(count)));
|
|
7
|
+
}
|
|
8
|
+
/** World X/Z offset for the `index`th effect in a square grid. */
|
|
9
|
+
export function galleryGridPosition(index, count, spacing = DEFAULT_GALLERY_SPACING) {
|
|
10
|
+
const cols = galleryColumnCount(count);
|
|
11
|
+
const rows = Math.ceil(count / cols);
|
|
12
|
+
const col = index % cols;
|
|
13
|
+
const row = Math.floor(index / cols);
|
|
14
|
+
return {
|
|
15
|
+
x: (col - (cols - 1) / 2) * spacing,
|
|
16
|
+
z: (row - (rows - 1) / 2) * spacing,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
/** Longest side of the grid footprint in world units. */
|
|
20
|
+
export function gallerySpan(count, spacing = DEFAULT_GALLERY_SPACING) {
|
|
21
|
+
const cols = galleryColumnCount(count);
|
|
22
|
+
const rows = Math.ceil(count / cols);
|
|
23
|
+
return Math.max((cols - 1) * spacing, (rows - 1) * spacing, spacing);
|
|
24
|
+
}
|
|
25
|
+
/** Pulls the camera back and scales the ground plane to frame a gallery grid. */
|
|
26
|
+
export function fitGalleryView(camera, ground, count, spacing = DEFAULT_GALLERY_SPACING) {
|
|
27
|
+
const span = gallerySpan(count, spacing);
|
|
28
|
+
camera.setTarget(new Vector3(0, 1.4, 0));
|
|
29
|
+
camera.radius = Math.max(span * 0.85, 12);
|
|
30
|
+
camera.beta = Math.min(camera.beta, 1.1);
|
|
31
|
+
const groundSize = Math.max(span + spacing * 4, 100);
|
|
32
|
+
ground.scaling.x = groundSize / 100;
|
|
33
|
+
ground.scaling.z = groundSize / 100;
|
|
34
|
+
}
|
|
35
|
+
let cameraFocusFrame = 0;
|
|
36
|
+
function easeOutCubic(t) {
|
|
37
|
+
return 1 - (1 - t) ** 3;
|
|
38
|
+
}
|
|
39
|
+
/** Stops an in-flight gallery camera focus animation. */
|
|
40
|
+
export function cancelGalleryCameraFocus() {
|
|
41
|
+
if (cameraFocusFrame) {
|
|
42
|
+
cancelAnimationFrame(cameraFocusFrame);
|
|
43
|
+
cameraFocusFrame = 0;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/** Smoothly frames the camera on an in-scene gallery effect. */
|
|
47
|
+
export function focusGalleryEntry(camera, entryRoot, durationMs = 450) {
|
|
48
|
+
cancelGalleryCameraFocus();
|
|
49
|
+
const pos = entryRoot.getAbsolutePosition();
|
|
50
|
+
const target = new Vector3(pos.x, pos.y + 1.4, pos.z);
|
|
51
|
+
const startTarget = camera.target.clone();
|
|
52
|
+
const startRadius = camera.radius;
|
|
53
|
+
const jumpDist = Vector3.Distance(startTarget, target);
|
|
54
|
+
const desiredRadius = jumpDist > 3 ? 12 : Math.min(startRadius, 16);
|
|
55
|
+
const start = performance.now();
|
|
56
|
+
const tick = () => {
|
|
57
|
+
const t = easeOutCubic(Math.min(1, (performance.now() - start) / durationMs));
|
|
58
|
+
camera.setTarget(Vector3.Lerp(startTarget, target, t));
|
|
59
|
+
camera.radius = startRadius + (desiredRadius - startRadius) * t;
|
|
60
|
+
if (t < 1) {
|
|
61
|
+
cameraFocusFrame = requestAnimationFrame(tick);
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
cameraFocusFrame = 0;
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
cameraFocusFrame = requestAnimationFrame(tick);
|
|
68
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { GalleryEntry } from './loadEffectGallery';
|
|
2
|
+
/** Freezes every gallery entry; unpauses all in-scene entries in the preview set when transport is playing. */
|
|
3
|
+
export declare function applyGalleryPlayback(entries: GalleryEntry[], previewRootIds: ReadonlySet<number>, transportPlaying: boolean): void;
|
|
4
|
+
/** Whether any preview effect on stage should keep simulating. */
|
|
5
|
+
export declare function hasActivePreview(entries: GalleryEntry[], previewRootIds: ReadonlySet<number>): boolean;
|
|
6
|
+
/** In-scene preview effects currently in the preview set. */
|
|
7
|
+
export declare function countActivePreviews(entries: GalleryEntry[], previewRootIds: ReadonlySet<number>): number;
|
|
8
|
+
/** Particle count across all active preview effects. */
|
|
9
|
+
export declare function countGalleryParticles(entries: GalleryEntry[], previewRootIds: ReadonlySet<number>): number;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
function isPreviewEntry(entry, previewRootIds) {
|
|
2
|
+
return entry.inScene && previewRootIds.has(entry.root.uniqueId);
|
|
3
|
+
}
|
|
4
|
+
/** Freezes every gallery entry; unpauses all in-scene entries in the preview set when transport is playing. */
|
|
5
|
+
export function applyGalleryPlayback(entries, previewRootIds, transportPlaying) {
|
|
6
|
+
for (const entry of entries) {
|
|
7
|
+
for (const system of entry.systems) {
|
|
8
|
+
system.pause();
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
if (!transportPlaying) {
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
for (const entry of entries) {
|
|
15
|
+
if (!isPreviewEntry(entry, previewRootIds)) {
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
for (const system of entry.systems) {
|
|
19
|
+
system.play();
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
/** Whether any preview effect on stage should keep simulating. */
|
|
24
|
+
export function hasActivePreview(entries, previewRootIds) {
|
|
25
|
+
return entries.some((entry) => isPreviewEntry(entry, previewRootIds));
|
|
26
|
+
}
|
|
27
|
+
/** In-scene preview effects currently in the preview set. */
|
|
28
|
+
export function countActivePreviews(entries, previewRootIds) {
|
|
29
|
+
return entries.filter((entry) => isPreviewEntry(entry, previewRootIds)).length;
|
|
30
|
+
}
|
|
31
|
+
/** Particle count across all active preview effects. */
|
|
32
|
+
export function countGalleryParticles(entries, previewRootIds) {
|
|
33
|
+
let count = 0;
|
|
34
|
+
for (const entry of entries) {
|
|
35
|
+
if (!isPreviewEntry(entry, previewRootIds)) {
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
for (const system of entry.systems) {
|
|
39
|
+
count += system.particleNum;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return count;
|
|
43
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Vector3 } from '@babylonjs/core/Maths/math.vector';
|
|
2
|
+
import type { Scene } from '@babylonjs/core/scene';
|
|
3
|
+
import type { BatchedRenderer } from 'babylon.quarks';
|
|
4
|
+
import type { GalleryEntry } from './loadEffectGallery';
|
|
5
|
+
/** Parents a catalog entry into the live scene and registers its systems with the renderer. */
|
|
6
|
+
export declare function placeGalleryEntryInScene(entry: GalleryEntry, renderer: BatchedRenderer, worldPosition: Vector3): void;
|
|
7
|
+
/** Spawns a batch of catalog effects in a square grid centered on `center`. */
|
|
8
|
+
export declare function placeGalleryEntriesAt(entries: GalleryEntry[], renderer: BatchedRenderer, center: Vector3, spacing?: number): void;
|
|
9
|
+
/** Raycasts the editor ground plane from canvas/client coordinates. */
|
|
10
|
+
export declare function pickGroundPosition(scene: Scene, clientX: number, clientY: number): Vector3;
|
|
11
|
+
/** Finds a gallery entry by Babylon node id from a drag payload. */
|
|
12
|
+
export declare function findGalleryEntryById(entries: GalleryEntry[], id: string): GalleryEntry | undefined;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { Vector3 } from '@babylonjs/core/Maths/math.vector';
|
|
2
|
+
import { DEFAULT_GALLERY_SPACING, galleryGridPosition } from './galleryLayout';
|
|
3
|
+
/** Parents a catalog entry into the live scene and registers its systems with the renderer. */
|
|
4
|
+
export function placeGalleryEntryInScene(entry, renderer, worldPosition) {
|
|
5
|
+
entry.root.setEnabled(true);
|
|
6
|
+
entry.root.parent = null;
|
|
7
|
+
entry.root.position.copyFrom(worldPosition);
|
|
8
|
+
if (!entry.inScene) {
|
|
9
|
+
for (const system of entry.systems) {
|
|
10
|
+
renderer.addSystem(system);
|
|
11
|
+
system.pause();
|
|
12
|
+
}
|
|
13
|
+
entry.inScene = true;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
/** Spawns a batch of catalog effects in a square grid centered on `center`. */
|
|
17
|
+
export function placeGalleryEntriesAt(entries, renderer, center, spacing = DEFAULT_GALLERY_SPACING) {
|
|
18
|
+
entries.forEach((entry, index) => {
|
|
19
|
+
const offset = galleryGridPosition(index, entries.length, spacing);
|
|
20
|
+
const position = center.add(new Vector3(offset.x, 0, offset.z));
|
|
21
|
+
placeGalleryEntryInScene(entry, renderer, position);
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
/** Raycasts the editor ground plane from canvas/client coordinates. */
|
|
25
|
+
export function pickGroundPosition(scene, clientX, clientY) {
|
|
26
|
+
const canvas = scene.getEngine().getRenderingCanvas();
|
|
27
|
+
if (!canvas) {
|
|
28
|
+
return Vector3.Zero();
|
|
29
|
+
}
|
|
30
|
+
const rect = canvas.getBoundingClientRect();
|
|
31
|
+
const pick = scene.pick(clientX - rect.left, clientY - rect.top, (mesh) => mesh.name === 'ground');
|
|
32
|
+
return pick?.pickedPoint?.clone() ?? Vector3.Zero();
|
|
33
|
+
}
|
|
34
|
+
/** Finds a gallery entry by Babylon node id from a drag payload. */
|
|
35
|
+
export function findGalleryEntryById(entries, id) {
|
|
36
|
+
const numericId = Number(id);
|
|
37
|
+
if (!Number.isFinite(numericId)) {
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
return entries.find((entry) => entry.root.uniqueId === numericId);
|
|
41
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Color3 } from '@babylonjs/core/Maths/math.color';
|
|
2
|
+
import { TransformNode } from '@babylonjs/core/Meshes/transformNode';
|
|
3
|
+
import type { Scene } from '@babylonjs/core/scene';
|
|
4
|
+
export declare const GROUND_RING_RADIUS = 1.2;
|
|
5
|
+
/** Flat ground ring that tracks a transform on the XZ plane. */
|
|
6
|
+
declare class GroundRingMarker {
|
|
7
|
+
private readonly anchor;
|
|
8
|
+
private readonly ring;
|
|
9
|
+
private trackedRoot;
|
|
10
|
+
constructor(scene: Scene, name: string, color: Color3);
|
|
11
|
+
show(entryRoot: TransformNode): void;
|
|
12
|
+
hide(): void;
|
|
13
|
+
follow(): void;
|
|
14
|
+
dispose(): void;
|
|
15
|
+
private syncPosition;
|
|
16
|
+
}
|
|
17
|
+
/** Flat blue selection ring on the ground under the focused in-scene effect. */
|
|
18
|
+
export declare class GallerySelectionMarker extends GroundRingMarker {
|
|
19
|
+
constructor(scene: Scene);
|
|
20
|
+
}
|
|
21
|
+
/** Same ring as selection, yellow — shown while hovering an effect root in the viewport. */
|
|
22
|
+
export declare class GalleryHoverMarker extends GroundRingMarker {
|
|
23
|
+
constructor(scene: Scene);
|
|
24
|
+
}
|
|
25
|
+
export {};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { Color3 } from '@babylonjs/core/Maths/math.color';
|
|
2
|
+
import { Vector3 } from '@babylonjs/core/Maths/math.vector';
|
|
3
|
+
import { MeshBuilder } from '@babylonjs/core/Meshes/meshBuilder';
|
|
4
|
+
import { TransformNode } from '@babylonjs/core/Meshes/transformNode';
|
|
5
|
+
export const GROUND_RING_RADIUS = 1.2;
|
|
6
|
+
const RING_SEGMENTS = 64;
|
|
7
|
+
const RING_Y = 0.03;
|
|
8
|
+
const SELECTION_RING_COLOR = new Color3(0.55, 0.78, 1);
|
|
9
|
+
const HOVER_RING_COLOR = new Color3(1, 0.86, 0.35);
|
|
10
|
+
function buildGroundRingPoints(radius) {
|
|
11
|
+
const points = [];
|
|
12
|
+
for (let i = 0; i <= RING_SEGMENTS; i++) {
|
|
13
|
+
const t = (i / RING_SEGMENTS) * Math.PI * 2;
|
|
14
|
+
points.push(new Vector3(Math.cos(t) * radius, 0, Math.sin(t) * radius));
|
|
15
|
+
}
|
|
16
|
+
return points;
|
|
17
|
+
}
|
|
18
|
+
/** Flat ground ring that tracks a transform on the XZ plane. */
|
|
19
|
+
class GroundRingMarker {
|
|
20
|
+
constructor(scene, name, color) {
|
|
21
|
+
this.trackedRoot = null;
|
|
22
|
+
this.anchor = new TransformNode(`${name}-root`, scene);
|
|
23
|
+
this.ring = MeshBuilder.CreateLines(`${name}-ring`, { points: buildGroundRingPoints(GROUND_RING_RADIUS) }, scene);
|
|
24
|
+
this.ring.color = color;
|
|
25
|
+
this.ring.parent = this.anchor;
|
|
26
|
+
this.ring.isPickable = false;
|
|
27
|
+
this.ring.renderingGroupId = 1;
|
|
28
|
+
this.hide();
|
|
29
|
+
}
|
|
30
|
+
show(entryRoot) {
|
|
31
|
+
this.trackedRoot = entryRoot;
|
|
32
|
+
this.syncPosition();
|
|
33
|
+
this.anchor.setEnabled(true);
|
|
34
|
+
}
|
|
35
|
+
hide() {
|
|
36
|
+
this.trackedRoot = null;
|
|
37
|
+
this.anchor.setEnabled(false);
|
|
38
|
+
}
|
|
39
|
+
follow() {
|
|
40
|
+
if (!this.trackedRoot || this.trackedRoot.isDisposed()) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
this.syncPosition();
|
|
44
|
+
}
|
|
45
|
+
dispose() {
|
|
46
|
+
this.hide();
|
|
47
|
+
this.ring.dispose();
|
|
48
|
+
this.anchor.dispose();
|
|
49
|
+
}
|
|
50
|
+
syncPosition() {
|
|
51
|
+
if (!this.trackedRoot) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
const pos = this.trackedRoot.getAbsolutePosition();
|
|
55
|
+
this.anchor.position.set(pos.x, RING_Y, pos.z);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/** Flat blue selection ring on the ground under the focused in-scene effect. */
|
|
59
|
+
export class GallerySelectionMarker extends GroundRingMarker {
|
|
60
|
+
constructor(scene) {
|
|
61
|
+
super(scene, 'gallery-selection', SELECTION_RING_COLOR);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/** Same ring as selection, yellow — shown while hovering an effect root in the viewport. */
|
|
65
|
+
export class GalleryHoverMarker extends GroundRingMarker {
|
|
66
|
+
constructor(scene) {
|
|
67
|
+
super(scene, 'gallery-hover', HOVER_RING_COLOR);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { GalleryEntry } from './loadEffectGallery';
|
|
2
|
+
export interface GalleryTreeFolder {
|
|
3
|
+
kind: 'folder';
|
|
4
|
+
name: string;
|
|
5
|
+
path: string;
|
|
6
|
+
children: GalleryTreeNode[];
|
|
7
|
+
}
|
|
8
|
+
export interface GalleryTreeEffect {
|
|
9
|
+
kind: 'effect';
|
|
10
|
+
name: string;
|
|
11
|
+
path: string;
|
|
12
|
+
entry: GalleryEntry;
|
|
13
|
+
}
|
|
14
|
+
export type GalleryTreeNode = GalleryTreeFolder | GalleryTreeEffect;
|
|
15
|
+
/** Builds a folder tree from gallery entry relative paths (e.g. `VFX/Fire/Explosion.json`). */
|
|
16
|
+
export declare function buildGalleryTree(entries: GalleryEntry[]): GalleryTreeNode[];
|
|
17
|
+
/** Filters a folder tree while preserving ancestors of matching effects. */
|
|
18
|
+
export declare function filterGalleryTree(nodes: GalleryTreeNode[], query: string): GalleryTreeNode[];
|
|
19
|
+
export declare const GALLERY_DRAG_MIME = "application/x-quarks-gallery-drop";
|
|
20
|
+
export type GalleryDragPayload = {
|
|
21
|
+
kind: 'effect';
|
|
22
|
+
id: number;
|
|
23
|
+
} | {
|
|
24
|
+
kind: 'folder';
|
|
25
|
+
path: string;
|
|
26
|
+
};
|
|
27
|
+
export declare function writeGalleryDrag(dataTransfer: DataTransfer, payload: GalleryDragPayload): void;
|
|
28
|
+
export declare function readGalleryDrag(dataTransfer: DataTransfer): GalleryDragPayload | null;
|
|
29
|
+
/** Returns every effect whose JSON path sits inside `folderPath` (recursive). */
|
|
30
|
+
export declare function collectEntriesInFolder(entries: GalleryEntry[], folderPath: string): GalleryEntry[];
|
|
31
|
+
/** Collects all effects under a folder tree node. */
|
|
32
|
+
export declare function collectFolderEntries(node: GalleryTreeFolder): GalleryEntry[];
|
|
33
|
+
export type GalleryFlatRow = {
|
|
34
|
+
kind: 'folder';
|
|
35
|
+
depth: number;
|
|
36
|
+
node: GalleryTreeFolder;
|
|
37
|
+
effectCount: number;
|
|
38
|
+
} | {
|
|
39
|
+
kind: 'effect';
|
|
40
|
+
depth: number;
|
|
41
|
+
node: GalleryTreeEffect;
|
|
42
|
+
};
|
|
43
|
+
/** Flattens the visible folder tree into rows for virtual scrolling. */
|
|
44
|
+
export declare function flattenVisibleGalleryRows(nodes: GalleryTreeNode[], closedFolders: ReadonlySet<string>, expandAll: boolean, depth?: number): GalleryFlatRow[];
|
|
45
|
+
/** Resolves a drag payload to the entries that would be placed on drop. */
|
|
46
|
+
export declare function describeGalleryDrag(entries: GalleryEntry[], payload: GalleryDragPayload): {
|
|
47
|
+
entries: GalleryEntry[];
|
|
48
|
+
label: string;
|
|
49
|
+
};
|
|
@@ -0,0 +1,138 @@
|
|
|
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 gallery entry relative paths (e.g. `VFX/Fire/Explosion.json`). */
|
|
8
|
+
export function buildGalleryTree(entries) {
|
|
9
|
+
const roots = [];
|
|
10
|
+
for (const entry of entries) {
|
|
11
|
+
const normalized = entry.path.replace(/\\/g, '/');
|
|
12
|
+
const segments = normalized.replace(/\.json$/i, '').split('/').filter(Boolean);
|
|
13
|
+
const fileName = segments.pop() ?? entry.name;
|
|
14
|
+
let level = roots;
|
|
15
|
+
let pathSoFar = '';
|
|
16
|
+
for (const segment of segments) {
|
|
17
|
+
pathSoFar = pathSoFar ? `${pathSoFar}/${segment}` : segment;
|
|
18
|
+
let folder = level.find((node) => node.kind === 'folder' && node.name === segment);
|
|
19
|
+
if (!folder) {
|
|
20
|
+
folder = { kind: 'folder', name: segment, path: pathSoFar, children: [] };
|
|
21
|
+
level.push(folder);
|
|
22
|
+
}
|
|
23
|
+
level = folder.children;
|
|
24
|
+
}
|
|
25
|
+
level.push({
|
|
26
|
+
kind: 'effect',
|
|
27
|
+
name: fileName,
|
|
28
|
+
path: entry.path,
|
|
29
|
+
entry,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
const sortTree = (nodes) => {
|
|
33
|
+
nodes.sort(compareNodes);
|
|
34
|
+
for (const node of nodes) {
|
|
35
|
+
if (node.kind === 'folder') {
|
|
36
|
+
sortTree(node.children);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
sortTree(roots);
|
|
41
|
+
return roots;
|
|
42
|
+
}
|
|
43
|
+
/** Filters a folder tree while preserving ancestors of matching effects. */
|
|
44
|
+
export function filterGalleryTree(nodes, query) {
|
|
45
|
+
const q = query.trim().toLowerCase();
|
|
46
|
+
if (!q) {
|
|
47
|
+
return nodes;
|
|
48
|
+
}
|
|
49
|
+
const walk = (node) => {
|
|
50
|
+
if (node.kind === 'effect') {
|
|
51
|
+
const haystack = `${node.path} ${node.name}`.toLowerCase();
|
|
52
|
+
return haystack.includes(q) ? node : null;
|
|
53
|
+
}
|
|
54
|
+
const children = node.children.map(walk).filter((child) => child !== null);
|
|
55
|
+
if (children.length === 0) {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
return { ...node, children };
|
|
59
|
+
};
|
|
60
|
+
return nodes.map(walk).filter((node) => node !== null);
|
|
61
|
+
}
|
|
62
|
+
export const GALLERY_DRAG_MIME = 'application/x-quarks-gallery-drop';
|
|
63
|
+
export function writeGalleryDrag(dataTransfer, payload) {
|
|
64
|
+
dataTransfer.setData(GALLERY_DRAG_MIME, JSON.stringify(payload));
|
|
65
|
+
dataTransfer.effectAllowed = 'copy';
|
|
66
|
+
}
|
|
67
|
+
export function readGalleryDrag(dataTransfer) {
|
|
68
|
+
const raw = dataTransfer.getData(GALLERY_DRAG_MIME);
|
|
69
|
+
if (!raw) {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
try {
|
|
73
|
+
const parsed = JSON.parse(raw);
|
|
74
|
+
if (parsed?.kind === 'effect' && typeof parsed.id === 'number') {
|
|
75
|
+
return parsed;
|
|
76
|
+
}
|
|
77
|
+
if (parsed?.kind === 'folder' && typeof parsed.path === 'string') {
|
|
78
|
+
return parsed;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
/** Returns every effect whose JSON path sits inside `folderPath` (recursive). */
|
|
87
|
+
export function collectEntriesInFolder(entries, folderPath) {
|
|
88
|
+
const prefix = folderPath.replace(/\\/g, '/');
|
|
89
|
+
return entries.filter((entry) => {
|
|
90
|
+
const path = entry.path.replace(/\\/g, '/');
|
|
91
|
+
const parent = path.replace(/\/[^/]+\.json$/i, '');
|
|
92
|
+
return parent === prefix || parent.startsWith(`${prefix}/`);
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
/** Collects all effects under a folder tree node. */
|
|
96
|
+
export function collectFolderEntries(node) {
|
|
97
|
+
const collected = [];
|
|
98
|
+
const walk = (nodes) => {
|
|
99
|
+
for (const child of nodes) {
|
|
100
|
+
if (child.kind === 'effect') {
|
|
101
|
+
collected.push(child.entry);
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
walk(child.children);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
walk(node.children);
|
|
109
|
+
return collected;
|
|
110
|
+
}
|
|
111
|
+
/** Flattens the visible folder tree into rows for virtual scrolling. */
|
|
112
|
+
export function flattenVisibleGalleryRows(nodes, closedFolders, expandAll, depth = 0) {
|
|
113
|
+
const rows = [];
|
|
114
|
+
for (const node of nodes) {
|
|
115
|
+
if (node.kind === 'folder') {
|
|
116
|
+
const effectCount = collectFolderEntries(node).length;
|
|
117
|
+
rows.push({ kind: 'folder', depth, node, effectCount });
|
|
118
|
+
const open = expandAll || !closedFolders.has(node.path);
|
|
119
|
+
if (open) {
|
|
120
|
+
rows.push(...flattenVisibleGalleryRows(node.children, closedFolders, expandAll, depth + 1));
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
rows.push({ kind: 'effect', depth, node });
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return rows;
|
|
128
|
+
}
|
|
129
|
+
/** Resolves a drag payload to the entries that would be placed on drop. */
|
|
130
|
+
export function describeGalleryDrag(entries, payload) {
|
|
131
|
+
if (payload.kind === 'effect') {
|
|
132
|
+
const entry = entries.find((item) => item.root.uniqueId === payload.id);
|
|
133
|
+
return { entries: entry ? [entry] : [], label: entry?.name ?? 'Effect' };
|
|
134
|
+
}
|
|
135
|
+
const folderEntries = collectEntriesInFolder(entries, payload.path);
|
|
136
|
+
const label = payload.path.split('/').filter(Boolean).pop() ?? payload.path;
|
|
137
|
+
return { entries: folderEntries, label };
|
|
138
|
+
}
|
|
@@ -1,6 +1,19 @@
|
|
|
1
1
|
import type { BatchedRenderer, ParticleSystem } from 'babylon.quarks';
|
|
2
2
|
import type { Scene } from '@babylonjs/core/scene';
|
|
3
3
|
import { TransformNode } from '@babylonjs/core/Meshes/transformNode';
|
|
4
|
+
export interface LoadEffectOptions {
|
|
5
|
+
/** Registers systems with the batched renderer (default true). */
|
|
6
|
+
registerRenderer?: boolean;
|
|
7
|
+
/** Restarts and unpauses after load (default matches registerRenderer). */
|
|
8
|
+
autoplay?: boolean;
|
|
9
|
+
}
|
|
10
|
+
/** Disposes a loaded effect's systems and its remaining group tree. */
|
|
11
|
+
export declare function disposeLoadedEffect(root: TransformNode, systems: ParticleSystem[], renderer: BatchedRenderer, inRenderer?: boolean): void;
|
|
12
|
+
/** Parses Quarks JSON into a live hierarchy without optional renderer registration / playback. */
|
|
13
|
+
export declare function parseEffectFromJson(scene: Scene, renderer: BatchedRenderer | null, json: unknown, options?: LoadEffectOptions, baseUrl?: string): {
|
|
14
|
+
root: TransformNode;
|
|
15
|
+
systems: ParticleSystem[];
|
|
16
|
+
};
|
|
4
17
|
/** Parses a Quarks JSON export, registers all systems with the renderer and starts playback. */
|
|
5
18
|
export declare function loadEffectFromJson(scene: Scene, renderer: BatchedRenderer, json: unknown, baseUrl?: string): {
|
|
6
19
|
root: TransformNode;
|
package/dist/core/loadEffect.js
CHANGED
|
@@ -19,15 +19,6 @@ function collapseInto(wrapper, only, parent) {
|
|
|
19
19
|
only.setParent(parent);
|
|
20
20
|
wrapper.dispose(true, false);
|
|
21
21
|
}
|
|
22
|
-
/**
|
|
23
|
-
* Collapses redundant organizational groups left over by importers — Unity's
|
|
24
|
-
* one-GameObject-per-component convention turns every visual layer into its own Group, so a
|
|
25
|
-
* single particle system becomes "Sparks" > unnamed emitter, and a component quarks doesn't
|
|
26
|
-
* understand (e.g. a Light) becomes an empty Group with nothing under it. Recurses
|
|
27
|
-
* bottom-up so nested wrapper chains collapse all the way down. The tree root itself is
|
|
28
|
-
* handled separately by `promoteIfSingleChildRoot`, since collapsing it means returning a
|
|
29
|
-
* different node as the effect's root.
|
|
30
|
-
*/
|
|
31
22
|
function flattenRedundantGroups(node) {
|
|
32
23
|
for (const child of node.getChildren()) {
|
|
33
24
|
if (child instanceof TransformNode) {
|
|
@@ -51,10 +42,6 @@ function flattenRedundantGroups(node) {
|
|
|
51
42
|
}
|
|
52
43
|
collapseInto(node, only, node.parent);
|
|
53
44
|
}
|
|
54
|
-
/** If the tree root itself only ever wraps a single child (after `flattenRedundantGroups`
|
|
55
|
-
* has settled everything below it), that wrapper adds a tree level with nothing to organize —
|
|
56
|
-
* promote the child to be the new root instead. Loops in case that child is, in turn, also a
|
|
57
|
-
* single-child wrapper. */
|
|
58
45
|
function promoteIfSingleChildRoot(root) {
|
|
59
46
|
let current = root;
|
|
60
47
|
while (!(current instanceof ParticleEmitter)) {
|
|
@@ -69,8 +56,22 @@ function promoteIfSingleChildRoot(root) {
|
|
|
69
56
|
}
|
|
70
57
|
return current;
|
|
71
58
|
}
|
|
72
|
-
/**
|
|
73
|
-
export function
|
|
59
|
+
/** Disposes a loaded effect's systems and its remaining group tree. */
|
|
60
|
+
export function disposeLoadedEffect(root, systems, renderer, inRenderer = true) {
|
|
61
|
+
for (const system of systems) {
|
|
62
|
+
if (inRenderer) {
|
|
63
|
+
renderer.deleteSystem(system);
|
|
64
|
+
}
|
|
65
|
+
system.dispose();
|
|
66
|
+
}
|
|
67
|
+
if (!systems.some((system) => system.emitter === root)) {
|
|
68
|
+
root.dispose(false, true);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/** Parses Quarks JSON into a live hierarchy without optional renderer registration / playback. */
|
|
72
|
+
export function parseEffectFromJson(scene, renderer, json, options = {}, baseUrl = '') {
|
|
73
|
+
const registerRenderer = options.registerRenderer ?? true;
|
|
74
|
+
const autoplay = options.autoplay ?? registerRenderer;
|
|
74
75
|
const loader = new QuarksLoader(scene, { baseUrl });
|
|
75
76
|
let root = loader.parse(json, baseUrl);
|
|
76
77
|
flattenRedundantGroups(root);
|
|
@@ -78,10 +79,23 @@ export function loadEffectFromJson(scene, renderer, json, baseUrl = '') {
|
|
|
78
79
|
const systems = [];
|
|
79
80
|
QuarksUtil.runOnAllParticleEmitters(root, (emitter) => {
|
|
80
81
|
const system = emitter.system;
|
|
81
|
-
renderer
|
|
82
|
+
if (registerRenderer && renderer) {
|
|
83
|
+
renderer.addSystem(system);
|
|
84
|
+
}
|
|
82
85
|
systems.push(system);
|
|
83
86
|
});
|
|
84
87
|
QuarksUtil.restart(root);
|
|
85
|
-
|
|
88
|
+
if (autoplay) {
|
|
89
|
+
QuarksUtil.play(root);
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
for (const system of systems) {
|
|
93
|
+
system.pause();
|
|
94
|
+
}
|
|
95
|
+
}
|
|
86
96
|
return { root, systems };
|
|
87
97
|
}
|
|
98
|
+
/** Parses a Quarks JSON export, registers all systems with the renderer and starts playback. */
|
|
99
|
+
export function loadEffectFromJson(scene, renderer, json, baseUrl = '') {
|
|
100
|
+
return parseEffectFromJson(scene, renderer, json, { registerRenderer: true, autoplay: true }, baseUrl);
|
|
101
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { ParticleSystem } from 'babylon.quarks';
|
|
2
|
+
import type { Scene } from '@babylonjs/core/scene';
|
|
3
|
+
import { TransformNode } from '@babylonjs/core/Meshes/transformNode';
|
|
4
|
+
import { EffectBinding } from './binding';
|
|
5
|
+
export interface GalleryEntry {
|
|
6
|
+
name: string;
|
|
7
|
+
/** Relative path inside the imported folder (preserves subfolders). */
|
|
8
|
+
path: string;
|
|
9
|
+
root: TransformNode;
|
|
10
|
+
systems: ParticleSystem[];
|
|
11
|
+
/** True once the user has dropped this effect into the viewport. */
|
|
12
|
+
inScene: boolean;
|
|
13
|
+
}
|
|
14
|
+
export interface GalleryLoadProgress {
|
|
15
|
+
done: number;
|
|
16
|
+
total: number;
|
|
17
|
+
loaded: number;
|
|
18
|
+
failed: number;
|
|
19
|
+
}
|
|
20
|
+
/** Collects `.json` files from a folder picker result, sorted by relative path. */
|
|
21
|
+
export declare function collectJsonFiles(files: FileList | File[]): File[];
|
|
22
|
+
/**
|
|
23
|
+
* Parses every JSON effect into a hidden catalog — not registered with the renderer until the
|
|
24
|
+
* user drops the effect into the viewport.
|
|
25
|
+
*/
|
|
26
|
+
export declare function loadEffectGallery(scene: Scene, files: FileList | File[], catalogRoot: TransformNode, onProgress: (progress: GalleryLoadProgress) => void): Promise<GalleryEntry[]>;
|
|
27
|
+
/** Builds an edit binding for one catalog entry without unloading the rest. */
|
|
28
|
+
export declare function bindingFromGalleryEntry(entry: GalleryEntry): EffectBinding;
|
|
29
|
+
/** Merges newly loaded catalog entries, skipping duplicate paths. */
|
|
30
|
+
export declare function mergeGalleryEntries(existing: GalleryEntry[], incoming: GalleryEntry[], renderer: import('babylon.quarks').BatchedRenderer | null): {
|
|
31
|
+
merged: GalleryEntry[];
|
|
32
|
+
added: GalleryEntry[];
|
|
33
|
+
skipped: number;
|
|
34
|
+
};
|
|
35
|
+
/** Disposes every catalog entry and the hidden catalog root. */
|
|
36
|
+
export declare function disposeGallery(catalogRoot: TransformNode | null, entries: GalleryEntry[], renderer: import('babylon.quarks').BatchedRenderer): void;
|