babylon.quarks-editor 0.17.8 → 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 +6 -3
- 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.js +1 -4
- package/dist/react/EffectEditorHost.d.ts +7 -0
- package/dist/react/EffectEditorHost.js +669 -115
- 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.js +14 -2
- package/dist/react/TimelinePanel.d.ts +8 -13
- package/dist/react/TimelinePanel.js +113 -99
- package/dist/react/TimelineTrackRow.js +29 -15
- 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 +53 -1
- package/package.json +8 -4
- package/src/core/binding.ts +2 -5
- 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/loadEffect.ts +55 -18
- 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 +33 -1
- package/src/core/timeline.ts +6 -3
- package/src/core/transformStore.ts +19 -0
- package/src/core/viewportPicker.ts +114 -0
- package/src/index.ts +2 -0
- package/src/react/EffectEditor.tsx +5 -7
- package/src/react/EffectEditorHost.tsx +839 -143
- package/src/react/GalleryPanel.tsx +453 -0
- 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 +17 -2
- package/src/react/ObjectModule.tsx +17 -6
- package/src/react/TimelinePanel.tsx +213 -177
- package/src/react/TimelineTrackRow.tsx +58 -23
- package/src/react/Tooltip.tsx +93 -0
- package/src/react/ValueField.tsx +3 -1
- package/src/react/behaviorModules.tsx +13 -8
- package/src/react/fields.tsx +15 -6
- package/src/react/icons.ts +19 -0
- package/src/react/index.ts +4 -0
- package/src/react/inspectorHints.ts +150 -0
- package/src/react/modules.tsx +198 -183
- package/src/react/theme.ts +54 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,52 @@ 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.17.9] — 2026-07-14
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- **Effect gallery** — catalog panel with folder import, virtualized tree, drag-and-drop into
|
|
12
|
+
the viewport, and multi-effect scene layout.
|
|
13
|
+
- **Preview set vs focus** — checkbox / Ctrl+click adds effects to stage preview; click selects
|
|
14
|
+
one effect for the inspector and timeline (Unity-style).
|
|
15
|
+
- **Stage vs focus playback** — stage Play/Pause drives all preview effects; timeline
|
|
16
|
+
playhead/scrub/restart apply only to the focused effect. One-shot focus parks at the end
|
|
17
|
+
without stopping looping previews.
|
|
18
|
+
- `core/playback.ts` — decoupled `stagePlaying`, per-root `focusElapsed`, and `focusFinished`
|
|
19
|
+
state.
|
|
20
|
+
- `scrubFocusTo()` — scrubs only the focused binding via `system.update()` without advancing
|
|
21
|
+
other previews through the shared renderer.
|
|
22
|
+
- Gallery scene helpers — drop marker, selection marker, emitter-shape wireframes, camera
|
|
23
|
+
focus on select.
|
|
24
|
+
- **Inspector tooltips** — info icons on module titles and field labels with English
|
|
25
|
+
descriptions (`inspectorHints.ts`); themed portal tooltips (`Tooltip` / `InspectorInfoIcon`).
|
|
26
|
+
- `pickGalleryJsonFiles()` — native folder picker via File System Access API when available.
|
|
27
|
+
- `MessageDialog`, Heroicons-based toolbar/gallery icons, `@tanstack/react-virtual` for the
|
|
28
|
+
gallery list.
|
|
29
|
+
|
|
30
|
+
### Changed
|
|
31
|
+
|
|
32
|
+
- `EffectEditorHost` — gallery state, preview set, transport counter, render loop wired to
|
|
33
|
+
the new playback model.
|
|
34
|
+
- `TimelinePanel` — stage transport controls, focus-only restart/scrub, catalog-only disabled
|
|
35
|
+
state with drop hint.
|
|
36
|
+
- `galleryPlayback` — `play()` on all systems in preview entries (including sub-emitters).
|
|
37
|
+
- `binding.restart()` — also calls `play()` on sub-emitter systems.
|
|
38
|
+
- `RendererModule` — material metadata display and patch helpers; timeline span uses duration
|
|
39
|
+
only (no particle-life tail).
|
|
40
|
+
- Burst editor rows in Emission module use shared `Row` layout with tooltips.
|
|
41
|
+
|
|
42
|
+
### Fixed
|
|
43
|
+
|
|
44
|
+
- Finishing a one-shot **focused** effect no longer pauses looping effects in the preview set.
|
|
45
|
+
- `pickGalleryFiles.ts` — TypeScript typing for directory `entries()` without invalid
|
|
46
|
+
`extends FileSystemDirectoryHandle` override.
|
|
47
|
+
|
|
48
|
+
### Related: Unity exporter 0.2.0
|
|
49
|
+
|
|
50
|
+
- Batch **Export Folder of Effects to JSON** pairs with the editor gallery folder import
|
|
51
|
+
(export a tree of prefabs in Unity → import the folder in the effect editor catalog).
|
|
52
|
+
|
|
7
53
|
## [0.17.7] — 2026-07-09
|
|
8
54
|
|
|
9
55
|
### Added
|
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@ Shuriken-style effect editor for [babylon.quarks](https://www.npmjs.com/package/
|
|
|
7
7
|
## Install
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
|
-
npm install babylon.quarks-editor babylon.quarks @babylonjs/core react
|
|
10
|
+
npm install babylon.quarks-editor babylon.quarks @babylonjs/core react @heroicons/react @tanstack/react-virtual
|
|
11
11
|
```
|
|
12
12
|
|
|
13
13
|
## Usage
|
package/dist/core/binding.js
CHANGED
|
@@ -63,13 +63,10 @@ export class EffectBinding {
|
|
|
63
63
|
}
|
|
64
64
|
}
|
|
65
65
|
restart() {
|
|
66
|
-
//
|
|
67
|
-
// reset — their parents re-drive them, so playing them directly would double-emit.
|
|
66
|
+
// onlyUsedByOther systems skip self-emission in update(); play() only unpauses sim.
|
|
68
67
|
for (const system of [this.system, ...this.subSystems]) {
|
|
69
68
|
system.restart();
|
|
70
|
-
|
|
71
|
-
system.play();
|
|
72
|
-
}
|
|
69
|
+
system.play();
|
|
73
70
|
}
|
|
74
71
|
}
|
|
75
72
|
/**
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Scene } from '@babylonjs/core/scene';
|
|
2
|
+
import { TransformNode } from '@babylonjs/core/Meshes/transformNode';
|
|
3
|
+
import { ParticleSystem } from 'babylon.quarks';
|
|
4
|
+
import type { GalleryEntry } from './loadEffectGallery';
|
|
5
|
+
export interface CreateGalleryDefaultOptions {
|
|
6
|
+
resolveTexture?: (url: string, scene: Scene) => unknown;
|
|
7
|
+
textureUrl?: string;
|
|
8
|
+
}
|
|
9
|
+
/** Creates a simple cone burst preset for new editor effects. */
|
|
10
|
+
export declare function createDefaultEffect(scene: Scene, resolveTexture?: (url: string, scene: Scene) => unknown, textureUrl?: string): ParticleSystem;
|
|
11
|
+
/** Adds a blank preset effect to the hidden gallery catalog (not in the renderer yet). */
|
|
12
|
+
export declare function createGalleryDefaultEntry(scene: Scene, catalogRoot: TransformNode, existing: GalleryEntry[], options?: CreateGalleryDefaultOptions): GalleryEntry;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { TransformNode } from '@babylonjs/core/Meshes/transformNode';
|
|
2
|
+
import { ColorOverLife, ConeEmitter, ConstantColor, ConstantValue, IntervalValue, ParticleSystem, RenderMode, Vector4, } from 'babylon.quarks';
|
|
3
|
+
import { DEFAULT_GRADIENT_STOPS, buildGradient } from './colors';
|
|
4
|
+
/** Creates a simple cone burst preset for new editor effects. */
|
|
5
|
+
export function createDefaultEffect(scene, resolveTexture, textureUrl) {
|
|
6
|
+
const system = new ParticleSystem({
|
|
7
|
+
scene,
|
|
8
|
+
duration: 4,
|
|
9
|
+
looping: true,
|
|
10
|
+
startLife: new IntervalValue(1.2, 1.8),
|
|
11
|
+
startSpeed: new IntervalValue(3, 5),
|
|
12
|
+
startSize: new IntervalValue(0.25, 0.5),
|
|
13
|
+
startColor: new ConstantColor(new Vector4(1, 1, 1, 1)),
|
|
14
|
+
emissionOverTime: new ConstantValue(80),
|
|
15
|
+
shape: new ConeEmitter({ radius: 0.3, angle: 0.25 }),
|
|
16
|
+
renderMode: RenderMode.BillBoard,
|
|
17
|
+
texture: (textureUrl && resolveTexture ? resolveTexture(textureUrl, scene) : undefined),
|
|
18
|
+
transparent: true,
|
|
19
|
+
blendMode: 1,
|
|
20
|
+
});
|
|
21
|
+
system.addBehavior(new ColorOverLife(buildGradient(DEFAULT_GRADIENT_STOPS)));
|
|
22
|
+
system.emitter.rotation.x = -Math.PI / 2;
|
|
23
|
+
return system;
|
|
24
|
+
}
|
|
25
|
+
/** Adds a blank preset effect to the hidden gallery catalog (not in the renderer yet). */
|
|
26
|
+
export function createGalleryDefaultEntry(scene, catalogRoot, existing, options = {}) {
|
|
27
|
+
const takenNames = new Set(existing.map((entry) => entry.name));
|
|
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`;
|
|
36
|
+
const system = createDefaultEffect(scene, options.resolveTexture, options.textureUrl);
|
|
37
|
+
system.pause();
|
|
38
|
+
const root = new TransformNode(name, scene);
|
|
39
|
+
system.emitter.parent = root;
|
|
40
|
+
root.parent = catalogRoot;
|
|
41
|
+
root.setEnabled(false);
|
|
42
|
+
return { name, path, root, systems: [system], inScene: false };
|
|
43
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { TransformNode } from '@babylonjs/core/Meshes/transformNode';
|
|
2
|
+
/** Marks meshes/nodes created only for editor interaction or overlays (not part of the effect). */
|
|
3
|
+
export declare const EDITOR_SCENE_NODE_METADATA: {
|
|
4
|
+
quarksEditorSceneNode: true;
|
|
5
|
+
};
|
|
6
|
+
/** True for viewport pick proxies, shape wireframes, and other non-exportable scene helpers. */
|
|
7
|
+
export declare function isEditorSceneNode(node: TransformNode): boolean;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** Marks meshes/nodes created only for editor interaction or overlays (not part of the effect). */
|
|
2
|
+
export const EDITOR_SCENE_NODE_METADATA = { quarksEditorSceneNode: true };
|
|
3
|
+
/** True for viewport pick proxies, shape wireframes, and other non-exportable scene helpers. */
|
|
4
|
+
export function isEditorSceneNode(node) {
|
|
5
|
+
const meta = node.metadata;
|
|
6
|
+
if (meta?.quarksEditorPick || meta?.quarksEditorSceneNode) {
|
|
7
|
+
return true;
|
|
8
|
+
}
|
|
9
|
+
const name = node.name;
|
|
10
|
+
return name.startsWith('viewport-pick-root-') || name.startsWith('wf-');
|
|
11
|
+
}
|
package/dist/core/effectTree.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { ParticleEmitter } from 'babylon.quarks';
|
|
2
2
|
import { TransformNode } from '@babylonjs/core/Meshes/transformNode';
|
|
3
3
|
import { Matrix, Quaternion } from '@babylonjs/core/Maths/math.vector';
|
|
4
|
+
import { isEditorSceneNode } from './editorScene';
|
|
4
5
|
/** Walks a loaded root TransformNode into the editor tree (groups + emitters). */
|
|
5
6
|
export function buildEffectTree(root, depth = 0) {
|
|
6
7
|
const system = root instanceof ParticleEmitter ? root.system : undefined;
|
|
@@ -12,7 +13,7 @@ export function buildEffectTree(root, depth = 0) {
|
|
|
12
13
|
children: [],
|
|
13
14
|
};
|
|
14
15
|
for (const child of root.getChildren()) {
|
|
15
|
-
if (child instanceof TransformNode) {
|
|
16
|
+
if (child instanceof TransformNode && !isEditorSceneNode(child)) {
|
|
16
17
|
treeNode.children.push(buildEffectTree(child, depth + 1));
|
|
17
18
|
}
|
|
18
19
|
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { ParticleSystem } from 'babylon.quarks';
|
|
2
|
+
import type { Scene } from '@babylonjs/core/scene';
|
|
3
|
+
/**
|
|
4
|
+
* Optional wireframe overlays for every emitter shape in the current effect.
|
|
5
|
+
* Geometry follows quarks.core spawn math (local XY spawn plane, +Z emit axis).
|
|
6
|
+
*/
|
|
7
|
+
export declare class EmitterShapeWireframes {
|
|
8
|
+
private readonly scene;
|
|
9
|
+
private readonly container;
|
|
10
|
+
private meshes;
|
|
11
|
+
private visible;
|
|
12
|
+
private syncKey;
|
|
13
|
+
constructor(scene: Scene);
|
|
14
|
+
setVisible(on: boolean): void;
|
|
15
|
+
/** Rebuilds overlays when the binding revision changes. */
|
|
16
|
+
sync(systems: ParticleSystem[], syncKey: string): void;
|
|
17
|
+
dispose(): void;
|
|
18
|
+
private rebuild;
|
|
19
|
+
private clearMeshes;
|
|
20
|
+
}
|
|
@@ -0,0 +1,329 @@
|
|
|
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
|
+
import { getShapeType, readShapeParams } from './shapes';
|
|
6
|
+
import { EDITOR_SCENE_NODE_METADATA } from './editorScene';
|
|
7
|
+
const LINE_COLOR = new Color3(0.45, 0.82, 1);
|
|
8
|
+
const DIRECTION_COLOR = new Color3(0.75, 0.92, 1);
|
|
9
|
+
/**
|
|
10
|
+
* Optional wireframe overlays for every emitter shape in the current effect.
|
|
11
|
+
* Geometry follows quarks.core spawn math (local XY spawn plane, +Z emit axis).
|
|
12
|
+
*/
|
|
13
|
+
export class EmitterShapeWireframes {
|
|
14
|
+
constructor(scene) {
|
|
15
|
+
this.meshes = [];
|
|
16
|
+
this.visible = false;
|
|
17
|
+
this.syncKey = '';
|
|
18
|
+
this.scene = scene;
|
|
19
|
+
this.container = new TransformNode('emitter-wireframes', scene);
|
|
20
|
+
this.container.setEnabled(false);
|
|
21
|
+
}
|
|
22
|
+
setVisible(on) {
|
|
23
|
+
this.visible = on;
|
|
24
|
+
if (!on) {
|
|
25
|
+
this.clearMeshes();
|
|
26
|
+
this.container.setEnabled(false);
|
|
27
|
+
this.syncKey = '';
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/** Rebuilds overlays when the binding revision changes. */
|
|
31
|
+
sync(systems, syncKey) {
|
|
32
|
+
if (!this.visible) {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
if (syncKey === this.syncKey) {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
this.syncKey = syncKey;
|
|
39
|
+
this.rebuild(systems);
|
|
40
|
+
}
|
|
41
|
+
dispose() {
|
|
42
|
+
this.setVisible(false);
|
|
43
|
+
this.container.dispose();
|
|
44
|
+
}
|
|
45
|
+
rebuild(systems) {
|
|
46
|
+
this.clearMeshes();
|
|
47
|
+
this.container.setEnabled(true);
|
|
48
|
+
for (const system of systems) {
|
|
49
|
+
if (system.emitter.isDisposed()) {
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
for (const mesh of buildEmitterWireframe(system, this.scene)) {
|
|
53
|
+
mesh.parent = system.emitter;
|
|
54
|
+
mesh.metadata = { ...(mesh.metadata ?? {}), ...EDITOR_SCENE_NODE_METADATA };
|
|
55
|
+
mesh.isPickable = false;
|
|
56
|
+
mesh.renderingGroupId = 1;
|
|
57
|
+
this.meshes.push(mesh);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
clearMeshes() {
|
|
62
|
+
for (const mesh of this.meshes) {
|
|
63
|
+
mesh.dispose();
|
|
64
|
+
}
|
|
65
|
+
this.meshes = [];
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
function buildEmitterWireframe(system, scene) {
|
|
69
|
+
const type = getShapeType(system.emitterShape);
|
|
70
|
+
const params = readShapeParams(system.emitterShape);
|
|
71
|
+
const meshes = [];
|
|
72
|
+
const lines = (name, points, color = LINE_COLOR) => {
|
|
73
|
+
if (points.length < 2) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
const mesh = MeshBuilder.CreateLines(name, { points }, scene);
|
|
77
|
+
mesh.color = color.clone();
|
|
78
|
+
mesh.isPickable = false;
|
|
79
|
+
meshes.push(mesh);
|
|
80
|
+
};
|
|
81
|
+
switch (type) {
|
|
82
|
+
case 'point': {
|
|
83
|
+
// Spawn at origin; velocity is random in a unit ball.
|
|
84
|
+
const s = 0.12;
|
|
85
|
+
lines('wf-point-x', [new Vector3(-s, 0, 0), new Vector3(s, 0, 0)]);
|
|
86
|
+
lines('wf-point-y', [new Vector3(0, -s, 0), new Vector3(0, s, 0)]);
|
|
87
|
+
lines('wf-point-z', [new Vector3(0, 0, -s), new Vector3(0, 0, s)], DIRECTION_COLOR);
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
case 'cone': {
|
|
91
|
+
// Spawn: disk/ring on XY at z=0 (radius, thickness, arc).
|
|
92
|
+
// Velocity: +Z with spread angle — rim dir = (sinα·û, cosα).
|
|
93
|
+
const radius = Math.max(params.radius, 0.001);
|
|
94
|
+
const angle = Math.max(0, params.angle);
|
|
95
|
+
const arc = clampArc(params.arc);
|
|
96
|
+
const thickness = clamp01(params.thickness);
|
|
97
|
+
lines('wf-cone-outer', xyArc(radius, arc));
|
|
98
|
+
if (thickness < 1) {
|
|
99
|
+
lines('wf-cone-inner', xyArc(radius * (1 - thickness), arc));
|
|
100
|
+
}
|
|
101
|
+
if (arc < Math.PI * 2 - 1e-3) {
|
|
102
|
+
lines('wf-cone-spoke-a', [Vector3.Zero(), arcPoint(radius, 0)]);
|
|
103
|
+
lines('wf-cone-spoke-b', [Vector3.Zero(), arcPoint(radius, arc)]);
|
|
104
|
+
}
|
|
105
|
+
const preview = Math.max(radius * 2, 1.25);
|
|
106
|
+
const farRadius = radius + preview * Math.tan(angle);
|
|
107
|
+
const spokeCount = Math.max(4, Math.ceil((arc / (Math.PI * 2)) * 12));
|
|
108
|
+
for (let i = 0; i <= spokeCount; i++) {
|
|
109
|
+
const theta = (i / spokeCount) * arc;
|
|
110
|
+
const rim = arcPoint(radius, theta);
|
|
111
|
+
const dir = new Vector3(Math.sin(angle) * Math.cos(theta), Math.sin(angle) * Math.sin(theta), Math.cos(angle));
|
|
112
|
+
const travel = preview / Math.max(Math.cos(angle), 0.05);
|
|
113
|
+
lines(`wf-cone-ray-${i}`, [rim, rim.add(dir.scale(travel))], DIRECTION_COLOR);
|
|
114
|
+
}
|
|
115
|
+
lines('wf-cone-far', xyArc(farRadius, arc, 32, preview), DIRECTION_COLOR);
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
case 'sphere': {
|
|
119
|
+
// Spawn on spherical shell/volume; velocity radial from center.
|
|
120
|
+
const radius = Math.max(params.radius, 0.001);
|
|
121
|
+
const arc = clampArc(params.arc);
|
|
122
|
+
const thickness = clamp01(params.thickness);
|
|
123
|
+
drawSphereShell(lines, 'wf-sphere', radius, arc);
|
|
124
|
+
if (thickness < 1) {
|
|
125
|
+
drawSphereShell(lines, 'wf-sphere-inner', radius * (1 - thickness), arc);
|
|
126
|
+
}
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
case 'hemisphere': {
|
|
130
|
+
// Spawn on +Z hemisphere (phi 0..π/2): flat on XY through origin, dome toward +Z.
|
|
131
|
+
const radius = Math.max(params.radius, 0.001);
|
|
132
|
+
const arc = clampArc(params.arc);
|
|
133
|
+
const thickness = clamp01(params.thickness);
|
|
134
|
+
drawHemisphereShell(lines, 'wf-hemi', radius, arc);
|
|
135
|
+
lines('wf-hemi-base', xyArc(radius, arc));
|
|
136
|
+
if (thickness < 1) {
|
|
137
|
+
drawHemisphereShell(lines, 'wf-hemi-inner', radius * (1 - thickness), arc);
|
|
138
|
+
lines('wf-hemi-base-inner', xyArc(radius * (1 - thickness), arc));
|
|
139
|
+
}
|
|
140
|
+
break;
|
|
141
|
+
}
|
|
142
|
+
case 'circle': {
|
|
143
|
+
// Spawn on circle/ring in XY; velocity radial in XY.
|
|
144
|
+
const radius = Math.max(params.radius, 0.001);
|
|
145
|
+
const arc = clampArc(params.arc);
|
|
146
|
+
const thickness = clamp01(params.thickness);
|
|
147
|
+
lines('wf-circle', xyArc(radius, arc));
|
|
148
|
+
if (thickness < 1) {
|
|
149
|
+
lines('wf-circle-inner', xyArc(radius * (1 - thickness), arc));
|
|
150
|
+
}
|
|
151
|
+
if (arc < Math.PI * 2 - 1e-3) {
|
|
152
|
+
lines('wf-circle-spoke-a', [Vector3.Zero(), arcPoint(radius, 0)]);
|
|
153
|
+
lines('wf-circle-spoke-b', [Vector3.Zero(), arcPoint(radius, arc)]);
|
|
154
|
+
}
|
|
155
|
+
break;
|
|
156
|
+
}
|
|
157
|
+
case 'donut': {
|
|
158
|
+
// Spawn on torus: major ring of `radius` in XY, tube radius `donutRadius`.
|
|
159
|
+
const radius = Math.max(params.radius, 0.001);
|
|
160
|
+
const tube = Math.max(params.donutRadius, 0.001);
|
|
161
|
+
const arc = clampArc(params.arc);
|
|
162
|
+
const thickness = clamp01(params.thickness);
|
|
163
|
+
lines('wf-donut-major', xyArc(radius, arc));
|
|
164
|
+
lines('wf-donut-outer', xyArc(radius + tube, arc));
|
|
165
|
+
lines('wf-donut-inner', xyArc(Math.max(radius - tube, 0), arc));
|
|
166
|
+
if (thickness < 1) {
|
|
167
|
+
lines('wf-donut-tube-inner', xyArc(radius + tube * (1 - thickness), arc));
|
|
168
|
+
}
|
|
169
|
+
const tubeSamples = Math.max(4, Math.ceil((arc / (Math.PI * 2)) * 8));
|
|
170
|
+
for (let i = 0; i <= tubeSamples; i++) {
|
|
171
|
+
const theta = (i / tubeSamples) * arc;
|
|
172
|
+
lines(`wf-donut-tube-${i}`, torusTubeRing(radius, tube, theta));
|
|
173
|
+
}
|
|
174
|
+
break;
|
|
175
|
+
}
|
|
176
|
+
case 'rectangle': {
|
|
177
|
+
// Spawn on rectangle perimeter (thickness fills toward center); velocity outward in XY.
|
|
178
|
+
const thickness = clamp01(params.thickness);
|
|
179
|
+
lines('wf-rect', xyRect(params.width, params.height));
|
|
180
|
+
if (thickness > 0 && thickness < 1) {
|
|
181
|
+
lines('wf-rect-inner', xyRect(params.width * (1 - thickness), params.height * (1 - thickness)));
|
|
182
|
+
}
|
|
183
|
+
break;
|
|
184
|
+
}
|
|
185
|
+
case 'grid': {
|
|
186
|
+
// Discrete spawn cells in XY; velocity straight +Z.
|
|
187
|
+
const cols = Math.max(1, Math.floor(params.column));
|
|
188
|
+
const rows = Math.max(1, Math.floor(params.row));
|
|
189
|
+
const halfW = params.width / 2;
|
|
190
|
+
const halfH = params.height / 2;
|
|
191
|
+
lines('wf-grid-border', xyRect(params.width, params.height));
|
|
192
|
+
const mark = Math.min(params.width / cols, params.height / rows, 0.12) * 0.35;
|
|
193
|
+
const ray = Math.max(mark * 4, 0.25);
|
|
194
|
+
for (let c = 0; c < cols; c++) {
|
|
195
|
+
for (let r = 0; r < rows; r++) {
|
|
196
|
+
const x = (c * params.width) / cols - halfW;
|
|
197
|
+
const y = (r * params.height) / rows - halfH;
|
|
198
|
+
const p = new Vector3(x, y, 0);
|
|
199
|
+
lines(`wf-grid-x-${c}-${r}`, [new Vector3(x - mark, y, 0), new Vector3(x + mark, y, 0)]);
|
|
200
|
+
lines(`wf-grid-y-${c}-${r}`, [new Vector3(x, y - mark, 0), new Vector3(x, y + mark, 0)]);
|
|
201
|
+
lines(`wf-grid-ray-${c}-${r}`, [p, new Vector3(x, y, ray)], DIRECTION_COLOR);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
break;
|
|
205
|
+
}
|
|
206
|
+
case 'mesh_surface': {
|
|
207
|
+
const source = params.mesh;
|
|
208
|
+
if (source && !source.isDisposed()) {
|
|
209
|
+
const positions = source.getVerticesData('position');
|
|
210
|
+
const indices = source.getIndices();
|
|
211
|
+
if (positions && indices && indices.length >= 3) {
|
|
212
|
+
const edgeSet = new Set();
|
|
213
|
+
const pushEdge = (a, b) => {
|
|
214
|
+
const key = a < b ? `${a}_${b}` : `${b}_${a}`;
|
|
215
|
+
if (edgeSet.has(key)) {
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
edgeSet.add(key);
|
|
219
|
+
lines(`wf-mesh-e-${key}`, [
|
|
220
|
+
new Vector3(positions[a * 3], positions[a * 3 + 1], positions[a * 3 + 2]),
|
|
221
|
+
new Vector3(positions[b * 3], positions[b * 3 + 1], positions[b * 3 + 2]),
|
|
222
|
+
]);
|
|
223
|
+
};
|
|
224
|
+
const maxTris = Math.min(indices.length / 3, 400);
|
|
225
|
+
for (let t = 0; t < maxTris; t++) {
|
|
226
|
+
const i = t * 3;
|
|
227
|
+
pushEdge(indices[i], indices[i + 1]);
|
|
228
|
+
pushEdge(indices[i + 1], indices[i + 2]);
|
|
229
|
+
pushEdge(indices[i + 2], indices[i]);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
else {
|
|
233
|
+
lines('wf-mesh-missing', xyRect(0.6, 0.6));
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
else {
|
|
237
|
+
lines('wf-mesh-missing', xyRect(0.6, 0.6));
|
|
238
|
+
}
|
|
239
|
+
break;
|
|
240
|
+
}
|
|
241
|
+
default:
|
|
242
|
+
break;
|
|
243
|
+
}
|
|
244
|
+
return meshes;
|
|
245
|
+
}
|
|
246
|
+
function drawSphereShell(lines, prefix, radius, arc) {
|
|
247
|
+
// Latitude rings + longitude arcs matching spherical spawn (theta around Z via arc).
|
|
248
|
+
const latCount = 4;
|
|
249
|
+
for (let i = 1; i < latCount; i++) {
|
|
250
|
+
const phi = (i / latCount) * Math.PI;
|
|
251
|
+
const ringR = radius * Math.sin(phi);
|
|
252
|
+
const z = radius * Math.cos(phi);
|
|
253
|
+
lines(`${prefix}-lat-${i}`, xyArc(ringR, arc, 48, z));
|
|
254
|
+
}
|
|
255
|
+
const lonCount = Math.max(4, Math.ceil((arc / (Math.PI * 2)) * 8));
|
|
256
|
+
for (let i = 0; i <= lonCount; i++) {
|
|
257
|
+
const theta = (i / lonCount) * arc;
|
|
258
|
+
const points = [];
|
|
259
|
+
for (let j = 0; j <= 16; j++) {
|
|
260
|
+
const phi = (j / 16) * Math.PI;
|
|
261
|
+
points.push(new Vector3(radius * Math.sin(phi) * Math.cos(theta), radius * Math.sin(phi) * Math.sin(theta), radius * Math.cos(phi)));
|
|
262
|
+
}
|
|
263
|
+
lines(`${prefix}-lon-${i}`, points);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
function drawHemisphereShell(lines, prefix, radius, arc) {
|
|
267
|
+
// Quarks: phi = acos(v) ∈ [0, π/2] → z = cos(phi) ∈ [0, 1].
|
|
268
|
+
const latCount = 3;
|
|
269
|
+
for (let i = 1; i <= latCount; i++) {
|
|
270
|
+
const phi = (i / (latCount + 1)) * (Math.PI / 2);
|
|
271
|
+
const ringR = radius * Math.sin(phi);
|
|
272
|
+
const z = radius * Math.cos(phi);
|
|
273
|
+
lines(`${prefix}-lat-${i}`, xyArc(ringR, arc, 32, z));
|
|
274
|
+
}
|
|
275
|
+
const lonCount = Math.max(4, Math.ceil((arc / (Math.PI * 2)) * 8));
|
|
276
|
+
for (let i = 0; i <= lonCount; i++) {
|
|
277
|
+
const theta = (i / lonCount) * arc;
|
|
278
|
+
const points = [new Vector3(0, 0, radius)];
|
|
279
|
+
for (let j = 1; j <= 12; j++) {
|
|
280
|
+
const phi = (j / 12) * (Math.PI / 2);
|
|
281
|
+
points.push(new Vector3(radius * Math.sin(phi) * Math.cos(theta), radius * Math.sin(phi) * Math.sin(theta), radius * Math.cos(phi)));
|
|
282
|
+
}
|
|
283
|
+
lines(`${prefix}-lon-${i}`, points);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
function torusTubeRing(major, tube, theta) {
|
|
287
|
+
const cos = Math.cos(theta);
|
|
288
|
+
const sin = Math.sin(theta);
|
|
289
|
+
const points = [];
|
|
290
|
+
for (let i = 0; i <= 16; i++) {
|
|
291
|
+
const phi = (i / 16) * Math.PI * 2;
|
|
292
|
+
points.push(new Vector3((major + tube * Math.cos(phi)) * cos, (major + tube * Math.cos(phi)) * sin, tube * Math.sin(phi)));
|
|
293
|
+
}
|
|
294
|
+
return points;
|
|
295
|
+
}
|
|
296
|
+
function xyArc(radius, arc, segments = 48, z = 0) {
|
|
297
|
+
const points = [];
|
|
298
|
+
const steps = Math.max(8, Math.ceil(segments * (arc / (Math.PI * 2))));
|
|
299
|
+
for (let i = 0; i <= steps; i++) {
|
|
300
|
+
points.push(arcPoint(radius, (i / steps) * arc, z));
|
|
301
|
+
}
|
|
302
|
+
return points;
|
|
303
|
+
}
|
|
304
|
+
function arcPoint(radius, theta, z = 0) {
|
|
305
|
+
return new Vector3(Math.cos(theta) * radius, Math.sin(theta) * radius, z);
|
|
306
|
+
}
|
|
307
|
+
function xyRect(width, height) {
|
|
308
|
+
const halfW = width / 2;
|
|
309
|
+
const halfH = height / 2;
|
|
310
|
+
return [
|
|
311
|
+
new Vector3(-halfW, -halfH, 0),
|
|
312
|
+
new Vector3(halfW, -halfH, 0),
|
|
313
|
+
new Vector3(halfW, halfH, 0),
|
|
314
|
+
new Vector3(-halfW, halfH, 0),
|
|
315
|
+
new Vector3(-halfW, -halfH, 0),
|
|
316
|
+
];
|
|
317
|
+
}
|
|
318
|
+
function clampArc(arc) {
|
|
319
|
+
if (!Number.isFinite(arc) || arc <= 0) {
|
|
320
|
+
return Math.PI * 2;
|
|
321
|
+
}
|
|
322
|
+
return Math.min(Math.PI * 2, arc);
|
|
323
|
+
}
|
|
324
|
+
function clamp01(value) {
|
|
325
|
+
if (!Number.isFinite(value)) {
|
|
326
|
+
return 1;
|
|
327
|
+
}
|
|
328
|
+
return Math.min(1, Math.max(0, value));
|
|
329
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { GalleryDragPayload } from './galleryTree';
|
|
2
|
+
/** Stores the current gallery drag payload for viewport preview (getData is drop-only in most browsers). */
|
|
3
|
+
export declare function setGalleryDragSession(payload: GalleryDragPayload | null): void;
|
|
4
|
+
export declare function getGalleryDragSession(): GalleryDragPayload | null;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
let activeDrag = null;
|
|
2
|
+
/** Stores the current gallery drag payload for viewport preview (getData is drop-only in most browsers). */
|
|
3
|
+
export function setGalleryDragSession(payload) {
|
|
4
|
+
activeDrag = payload;
|
|
5
|
+
}
|
|
6
|
+
export function getGalleryDragSession() {
|
|
7
|
+
return activeDrag;
|
|
8
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Vector3 } from '@babylonjs/core/Maths/math.vector';
|
|
2
|
+
import type { Scene } from '@babylonjs/core/scene';
|
|
3
|
+
/** Ground decal shown while dragging gallery items over the viewport. */
|
|
4
|
+
export declare class GalleryDropMarker {
|
|
5
|
+
private readonly root;
|
|
6
|
+
private readonly disc;
|
|
7
|
+
private readonly material;
|
|
8
|
+
constructor(scene: Scene);
|
|
9
|
+
/** Positions the marker for a single effect or a folder grid footprint. */
|
|
10
|
+
show(position: Vector3, effectCount: number, spacing?: number): void;
|
|
11
|
+
hide(): void;
|
|
12
|
+
dispose(): void;
|
|
13
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { Color3 } from '@babylonjs/core/Maths/math.color';
|
|
2
|
+
import { StandardMaterial } from '@babylonjs/core/Materials/standardMaterial';
|
|
3
|
+
import { Mesh } from '@babylonjs/core/Meshes/mesh';
|
|
4
|
+
import { MeshBuilder } from '@babylonjs/core/Meshes/meshBuilder';
|
|
5
|
+
import { TransformNode } from '@babylonjs/core/Meshes/transformNode';
|
|
6
|
+
import { DEFAULT_GALLERY_SPACING, gallerySpan } from './galleryLayout';
|
|
7
|
+
/** Ground decal shown while dragging gallery items over the viewport. */
|
|
8
|
+
export class GalleryDropMarker {
|
|
9
|
+
constructor(scene) {
|
|
10
|
+
this.root = new TransformNode('gallery-drop-root', scene);
|
|
11
|
+
this.disc = MeshBuilder.CreateDisc('gallery-drop-fill', { radius: 1, tessellation: 48, sideOrientation: Mesh.DOUBLESIDE }, scene);
|
|
12
|
+
this.disc.parent = this.root;
|
|
13
|
+
this.disc.rotation.x = Math.PI / 2;
|
|
14
|
+
this.material = new StandardMaterial('gallery-drop-fill-mat', scene);
|
|
15
|
+
this.material.emissiveColor = new Color3(0.35, 0.55, 1);
|
|
16
|
+
this.material.alpha = 0.24;
|
|
17
|
+
this.material.disableLighting = true;
|
|
18
|
+
this.material.backFaceCulling = false;
|
|
19
|
+
this.disc.material = this.material;
|
|
20
|
+
this.disc.isPickable = false;
|
|
21
|
+
this.disc.renderingGroupId = 1;
|
|
22
|
+
this.hide();
|
|
23
|
+
}
|
|
24
|
+
/** Positions the marker for a single effect or a folder grid footprint. */
|
|
25
|
+
show(position, effectCount, spacing = DEFAULT_GALLERY_SPACING) {
|
|
26
|
+
const span = Math.max(gallerySpan(effectCount, spacing), spacing);
|
|
27
|
+
const radius = Math.max(0.375, span * 0.25 + spacing * 0.175);
|
|
28
|
+
this.root.position.set(position.x, 0.03, position.z);
|
|
29
|
+
this.root.scaling.set(radius, radius, radius);
|
|
30
|
+
this.material.alpha = effectCount > 1 ? 0.16 : 0.24;
|
|
31
|
+
this.material.emissiveColor = effectCount > 1 ? new Color3(0.45, 0.9, 0.75) : new Color3(0.55, 0.78, 1);
|
|
32
|
+
this.root.setEnabled(true);
|
|
33
|
+
}
|
|
34
|
+
hide() {
|
|
35
|
+
this.root.setEnabled(false);
|
|
36
|
+
}
|
|
37
|
+
dispose() {
|
|
38
|
+
this.material.dispose();
|
|
39
|
+
this.disc.dispose();
|
|
40
|
+
this.root.dispose();
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { ArcRotateCamera } from '@babylonjs/core/Cameras/arcRotateCamera';
|
|
2
|
+
import type { Mesh } from '@babylonjs/core/Meshes/mesh';
|
|
3
|
+
import type { TransformNode } from '@babylonjs/core/Meshes/transformNode';
|
|
4
|
+
/** Default gap between neighbouring effects in a folder gallery import. */
|
|
5
|
+
export declare const DEFAULT_GALLERY_SPACING = 4;
|
|
6
|
+
/** Square-grid column count for `count` items. */
|
|
7
|
+
export declare function galleryColumnCount(count: number): number;
|
|
8
|
+
/** World X/Z offset for the `index`th effect in a square grid. */
|
|
9
|
+
export declare function galleryGridPosition(index: number, count: number, spacing?: number): {
|
|
10
|
+
x: number;
|
|
11
|
+
z: number;
|
|
12
|
+
};
|
|
13
|
+
/** Longest side of the grid footprint in world units. */
|
|
14
|
+
export declare function gallerySpan(count: number, spacing?: number): number;
|
|
15
|
+
/** Pulls the camera back and scales the ground plane to frame a gallery grid. */
|
|
16
|
+
export declare function fitGalleryView(camera: ArcRotateCamera, ground: Mesh, count: number, spacing?: number): void;
|
|
17
|
+
/** Stops an in-flight gallery camera focus animation. */
|
|
18
|
+
export declare function cancelGalleryCameraFocus(): void;
|
|
19
|
+
/** Smoothly frames the camera on an in-scene gallery effect. */
|
|
20
|
+
export declare function focusGalleryEntry(camera: ArcRotateCamera, entryRoot: TransformNode, durationMs?: number): void;
|