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.
Files changed (119) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/README.md +1 -1
  3. package/dist/core/binding.js +2 -5
  4. package/dist/core/defaultEffect.d.ts +12 -0
  5. package/dist/core/defaultEffect.js +43 -0
  6. package/dist/core/editorScene.d.ts +7 -0
  7. package/dist/core/editorScene.js +11 -0
  8. package/dist/core/effectTree.js +2 -1
  9. package/dist/core/emitterShapeWireframe.d.ts +20 -0
  10. package/dist/core/emitterShapeWireframe.js +329 -0
  11. package/dist/core/galleryDragSession.d.ts +4 -0
  12. package/dist/core/galleryDragSession.js +8 -0
  13. package/dist/core/galleryDropMarker.d.ts +13 -0
  14. package/dist/core/galleryDropMarker.js +42 -0
  15. package/dist/core/galleryLayout.d.ts +20 -0
  16. package/dist/core/galleryLayout.js +68 -0
  17. package/dist/core/galleryPlayback.d.ts +9 -0
  18. package/dist/core/galleryPlayback.js +43 -0
  19. package/dist/core/galleryScene.d.ts +12 -0
  20. package/dist/core/galleryScene.js +41 -0
  21. package/dist/core/gallerySelectionMarker.d.ts +25 -0
  22. package/dist/core/gallerySelectionMarker.js +69 -0
  23. package/dist/core/galleryTree.d.ts +49 -0
  24. package/dist/core/galleryTree.js +138 -0
  25. package/dist/core/loadEffect.d.ts +13 -0
  26. package/dist/core/loadEffect.js +31 -17
  27. package/dist/core/loadEffectGallery.d.ts +36 -0
  28. package/dist/core/loadEffectGallery.js +97 -0
  29. package/dist/core/material.d.ts +13 -0
  30. package/dist/core/material.js +68 -0
  31. package/dist/core/pickGalleryFiles.d.ts +5 -0
  32. package/dist/core/pickGalleryFiles.js +42 -0
  33. package/dist/core/playback.d.ts +23 -0
  34. package/dist/core/playback.js +32 -0
  35. package/dist/core/scrub.d.ts +5 -0
  36. package/dist/core/scrub.js +30 -0
  37. package/dist/core/timeline.d.ts +3 -1
  38. package/dist/core/timeline.js +6 -3
  39. package/dist/core/transformStore.d.ts +3 -0
  40. package/dist/core/transformStore.js +16 -0
  41. package/dist/core/viewportPicker.d.ts +25 -0
  42. package/dist/core/viewportPicker.js +86 -0
  43. package/dist/index.d.ts +2 -0
  44. package/dist/index.js +1 -0
  45. package/dist/react/EffectEditor.js +1 -4
  46. package/dist/react/EffectEditorHost.d.ts +7 -0
  47. package/dist/react/EffectEditorHost.js +669 -115
  48. package/dist/react/GalleryPanel.d.ts +19 -0
  49. package/dist/react/GalleryPanel.js +215 -0
  50. package/dist/react/InspectorInfoIcon.d.ts +7 -0
  51. package/dist/react/InspectorInfoIcon.js +22 -0
  52. package/dist/react/InspectorLabel.d.ts +7 -0
  53. package/dist/react/InspectorLabel.js +12 -0
  54. package/dist/react/MessageDialog.d.ts +9 -0
  55. package/dist/react/MessageDialog.js +24 -0
  56. package/dist/react/ModuleSection.d.ts +2 -0
  57. package/dist/react/ModuleSection.js +8 -1
  58. package/dist/react/ObjectModule.js +14 -2
  59. package/dist/react/TimelinePanel.d.ts +8 -13
  60. package/dist/react/TimelinePanel.js +113 -99
  61. package/dist/react/TimelineTrackRow.js +29 -15
  62. package/dist/react/Tooltip.d.ts +6 -0
  63. package/dist/react/Tooltip.js +65 -0
  64. package/dist/react/ValueField.d.ts +2 -0
  65. package/dist/react/ValueField.js +1 -1
  66. package/dist/react/behaviorModules.js +8 -8
  67. package/dist/react/fields.d.ts +2 -0
  68. package/dist/react/fields.js +14 -5
  69. package/dist/react/icons.d.ts +7 -0
  70. package/dist/react/icons.js +10 -0
  71. package/dist/react/index.d.ts +4 -0
  72. package/dist/react/index.js +2 -0
  73. package/dist/react/inspectorHints.d.ts +2 -0
  74. package/dist/react/inspectorHints.js +138 -0
  75. package/dist/react/modules.js +109 -81
  76. package/dist/react/theme.d.ts +2 -0
  77. package/dist/react/theme.js +53 -1
  78. package/package.json +8 -4
  79. package/src/core/binding.ts +2 -5
  80. package/src/core/defaultEffect.ts +74 -0
  81. package/src/core/editorScene.ts +14 -0
  82. package/src/core/effectTree.ts +2 -1
  83. package/src/core/emitterShapeWireframe.ts +373 -0
  84. package/src/core/galleryDragSession.ts +12 -0
  85. package/src/core/galleryDropMarker.ts +57 -0
  86. package/src/core/galleryLayout.ts +90 -0
  87. package/src/core/galleryPlayback.ts +55 -0
  88. package/src/core/galleryScene.ts +58 -0
  89. package/src/core/gallerySelectionMarker.ts +84 -0
  90. package/src/core/galleryTree.ts +187 -0
  91. package/src/core/loadEffect.ts +55 -18
  92. package/src/core/loadEffectGallery.ts +146 -0
  93. package/src/core/material.ts +84 -0
  94. package/src/core/pickGalleryFiles.ts +58 -0
  95. package/src/core/playback.ts +49 -0
  96. package/src/core/scrub.ts +33 -1
  97. package/src/core/timeline.ts +6 -3
  98. package/src/core/transformStore.ts +19 -0
  99. package/src/core/viewportPicker.ts +114 -0
  100. package/src/index.ts +2 -0
  101. package/src/react/EffectEditor.tsx +5 -7
  102. package/src/react/EffectEditorHost.tsx +839 -143
  103. package/src/react/GalleryPanel.tsx +453 -0
  104. package/src/react/InspectorInfoIcon.tsx +36 -0
  105. package/src/react/InspectorLabel.tsx +18 -0
  106. package/src/react/MessageDialog.tsx +57 -0
  107. package/src/react/ModuleSection.tsx +17 -2
  108. package/src/react/ObjectModule.tsx +17 -6
  109. package/src/react/TimelinePanel.tsx +213 -177
  110. package/src/react/TimelineTrackRow.tsx +58 -23
  111. package/src/react/Tooltip.tsx +93 -0
  112. package/src/react/ValueField.tsx +3 -1
  113. package/src/react/behaviorModules.tsx +13 -8
  114. package/src/react/fields.tsx +15 -6
  115. package/src/react/icons.ts +19 -0
  116. package/src/react/index.ts +4 -0
  117. package/src/react/inspectorHints.ts +150 -0
  118. package/src/react/modules.tsx +198 -183
  119. package/src/react/theme.ts +54 -1
@@ -0,0 +1,97 @@
1
+ import { EffectBinding } from './binding';
2
+ import { disposeLoadedEffect, parseEffectFromJson } from './loadEffect';
3
+ const YIELD_EVERY = 8;
4
+ function yieldToBrowser() {
5
+ return new Promise((resolve) => requestAnimationFrame(() => resolve()));
6
+ }
7
+ /** Collects `.json` files from a folder picker result, sorted by relative path. */
8
+ export function collectJsonFiles(files) {
9
+ const list = Array.from(files);
10
+ return list
11
+ .filter((file) => file.name.toLowerCase().endsWith('.json'))
12
+ .sort((a, b) => {
13
+ const pathA = a.webkitRelativePath || a.name;
14
+ const pathB = b.webkitRelativePath || b.name;
15
+ return pathA.localeCompare(pathB);
16
+ });
17
+ }
18
+ /**
19
+ * Parses every JSON effect into a hidden catalog — not registered with the renderer until the
20
+ * user drops the effect into the viewport.
21
+ */
22
+ export async function loadEffectGallery(scene, files, catalogRoot, onProgress) {
23
+ const jsonFiles = collectJsonFiles(files);
24
+ const entries = [];
25
+ onProgress({ done: 0, total: jsonFiles.length, loaded: 0, failed: 0 });
26
+ for (let i = 0; i < jsonFiles.length; i++) {
27
+ const file = jsonFiles[i];
28
+ try {
29
+ const json = JSON.parse(await file.text());
30
+ const { root, systems } = parseEffectFromJson(scene, null, json, {
31
+ registerRenderer: false,
32
+ autoplay: false,
33
+ });
34
+ if (systems.length === 0) {
35
+ disposeLoadedEffect(root, systems, null, false);
36
+ continue;
37
+ }
38
+ const relPath = file.webkitRelativePath || file.name;
39
+ const label = file.name.replace(/\.json$/i, '');
40
+ if (!root.name || root.name === 'Effect' || root.name === 'Object3D') {
41
+ root.name = label;
42
+ }
43
+ root.parent = catalogRoot;
44
+ root.setEnabled(false);
45
+ entries.push({
46
+ name: label,
47
+ path: relPath.replace(/\\/g, '/'),
48
+ root,
49
+ systems,
50
+ inScene: false,
51
+ });
52
+ }
53
+ catch (err) {
54
+ console.warn(`[Quarks editor] Skipped "${file.name}":`, err);
55
+ }
56
+ onProgress({
57
+ done: i + 1,
58
+ total: jsonFiles.length,
59
+ loaded: entries.length,
60
+ failed: i + 1 - entries.length,
61
+ });
62
+ if ((i + 1) % YIELD_EVERY === 0) {
63
+ await yieldToBrowser();
64
+ }
65
+ }
66
+ return entries;
67
+ }
68
+ /** Builds an edit binding for one catalog entry without unloading the rest. */
69
+ export function bindingFromGalleryEntry(entry) {
70
+ const main = entry.systems.find((s) => !s.onlyUsedByOther) ?? entry.systems[0];
71
+ return new EffectBinding(main, entry.systems.filter((s) => s !== main), entry.root);
72
+ }
73
+ /** Merges newly loaded catalog entries, skipping duplicate paths. */
74
+ export function mergeGalleryEntries(existing, incoming, renderer) {
75
+ const paths = new Set(existing.map((entry) => entry.path));
76
+ const added = [];
77
+ let skipped = 0;
78
+ for (const entry of incoming) {
79
+ if (paths.has(entry.path)) {
80
+ disposeLoadedEffect(entry.root, entry.systems, renderer, false);
81
+ skipped++;
82
+ continue;
83
+ }
84
+ paths.add(entry.path);
85
+ added.push(entry);
86
+ }
87
+ return { merged: [...existing, ...added], added, skipped };
88
+ }
89
+ /** Disposes every catalog entry and the hidden catalog root. */
90
+ export function disposeGallery(catalogRoot, entries, renderer) {
91
+ for (const entry of entries) {
92
+ disposeLoadedEffect(entry.root, entry.systems, renderer, entry.inScene);
93
+ }
94
+ if (catalogRoot && !catalogRoot.isDisposed()) {
95
+ catalogRoot.dispose(false, true);
96
+ }
97
+ }
@@ -0,0 +1,13 @@
1
+ import type { ParticleSystem } from 'babylon.quarks';
2
+ export interface RendererMaterialPatch {
3
+ blendMode?: number;
4
+ transparent?: boolean;
5
+ depthTest?: boolean;
6
+ depthWrite?: boolean;
7
+ alphaTest?: number;
8
+ texture?: unknown;
9
+ }
10
+ /** Applies renderer material settings and keeps imported QuarksMaterial JSON in sync for export. */
11
+ export declare function applyRendererMaterial(system: ParticleSystem, patch: RendererMaterialPatch): void;
12
+ /** Human-readable label for an imported or live material reference. */
13
+ export declare function getMaterialLabel(material: unknown): string | null;
@@ -0,0 +1,68 @@
1
+ function isEditableMaterialRecord(material) {
2
+ return !!material && typeof material === 'object' && !('getClassName' in material);
3
+ }
4
+ /** Applies renderer material settings and keeps imported QuarksMaterial JSON in sync for export. */
5
+ export function applyRendererMaterial(system, patch) {
6
+ const settings = system.getRendererSettings();
7
+ if (patch.blendMode !== undefined) {
8
+ system.blending = patch.blendMode;
9
+ }
10
+ if (patch.transparent !== undefined) {
11
+ settings.materialTransparent = patch.transparent;
12
+ }
13
+ if (patch.depthTest !== undefined) {
14
+ settings.materialDepthTest = patch.depthTest;
15
+ }
16
+ if (patch.depthWrite !== undefined) {
17
+ settings.materialDepthWrite = patch.depthWrite;
18
+ }
19
+ if (patch.alphaTest !== undefined) {
20
+ settings.materialAlphaTest = patch.alphaTest;
21
+ }
22
+ if (patch.texture !== undefined) {
23
+ system.texture = patch.texture;
24
+ }
25
+ const material = system.material;
26
+ if (isEditableMaterialRecord(material)) {
27
+ if (patch.blendMode !== undefined) {
28
+ material.alphaMode = patch.blendMode;
29
+ material.blending = patch.blendMode;
30
+ }
31
+ if (patch.transparent !== undefined) {
32
+ material.transparent = patch.transparent;
33
+ }
34
+ if (patch.depthTest !== undefined) {
35
+ material.depthTest = patch.depthTest;
36
+ }
37
+ if (patch.depthWrite !== undefined) {
38
+ material.depthWrite = patch.depthWrite;
39
+ }
40
+ if (patch.alphaTest !== undefined) {
41
+ material.alphaTest = patch.alphaTest;
42
+ }
43
+ if (patch.texture !== undefined) {
44
+ material.texture = patch.texture;
45
+ material.map = patch.texture;
46
+ }
47
+ }
48
+ system.neededToUpdateRender = true;
49
+ }
50
+ /** Human-readable label for an imported or live material reference. */
51
+ export function getMaterialLabel(material) {
52
+ if (!material) {
53
+ return null;
54
+ }
55
+ if (isEditableMaterialRecord(material)) {
56
+ const name = typeof material.name === 'string' ? material.name : null;
57
+ const type = typeof material.type === 'string' ? material.type : 'QuarksMaterial';
58
+ return name ? `${name} (${type})` : type;
59
+ }
60
+ const named = material;
61
+ if (named.name) {
62
+ return named.name;
63
+ }
64
+ if (typeof named.getClassName === 'function') {
65
+ return named.getClassName();
66
+ }
67
+ return 'Material';
68
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Opens a native folder picker when the host supports it. Returns `null` when the user
3
+ * cancels, or when the API is unavailable (caller should fall back to `<input webkitdirectory>`).
4
+ */
5
+ export declare function pickGalleryJsonFiles(): Promise<File[] | null | undefined>;
@@ -0,0 +1,42 @@
1
+ import { collectJsonFiles } from './loadEffectGallery';
2
+ /** Recursively collects `.json` files from a directory handle (File System Access API). */
3
+ async function readJsonFilesFromDirectory(dir, prefix = '') {
4
+ const files = [];
5
+ for await (const [name, entry] of dir.entries()) {
6
+ if (entry.kind === 'file') {
7
+ if (!name.toLowerCase().endsWith('.json'))
8
+ continue;
9
+ const file = await entry.getFile();
10
+ Object.defineProperty(file, 'webkitRelativePath', {
11
+ value: prefix + name,
12
+ configurable: true,
13
+ });
14
+ files.push(file);
15
+ continue;
16
+ }
17
+ if (entry.kind === 'directory') {
18
+ files.push(...(await readJsonFilesFromDirectory(entry, `${prefix}${name}/`)));
19
+ }
20
+ }
21
+ return files;
22
+ }
23
+ /**
24
+ * Opens a native folder picker when the host supports it. Returns `null` when the user
25
+ * cancels, or when the API is unavailable (caller should fall back to `<input webkitdirectory>`).
26
+ */
27
+ export async function pickGalleryJsonFiles() {
28
+ const picker = window.showDirectoryPicker;
29
+ if (typeof picker !== 'function') {
30
+ return undefined;
31
+ }
32
+ try {
33
+ const dir = (await picker.call(window, { mode: 'read' }));
34
+ return collectJsonFiles(await readJsonFilesFromDirectory(dir));
35
+ }
36
+ catch (err) {
37
+ if (err instanceof DOMException && err.name === 'AbortError') {
38
+ return null;
39
+ }
40
+ throw err;
41
+ }
42
+ }
@@ -0,0 +1,23 @@
1
+ /** Stage transport + per-focus timeline state (decoupled from gallery preview set). */
2
+ export interface PlaybackState {
3
+ /** When true, all in-scene preview effects simulate. */
4
+ stagePlaying: boolean;
5
+ focusRootId: number | null;
6
+ focusElapsedByRoot: Map<number, number>;
7
+ focusFinishedByRoot: Set<number>;
8
+ speed: number;
9
+ stepQueued: boolean;
10
+ scrubbing: boolean;
11
+ }
12
+ /** Default playback state for a new editor session. */
13
+ export declare function createPlaybackState(): PlaybackState;
14
+ /** Elapsed timeline time for one focused effect root. */
15
+ export declare function getFocusElapsed(state: PlaybackState, rootId: number): number;
16
+ /** Writes focus elapsed time (playhead position). */
17
+ export declare function setFocusElapsed(state: PlaybackState, rootId: number, elapsed: number): void;
18
+ /** Clears the parked-at-end flag after restart or scrub before the end. */
19
+ export declare function clearFocusFinished(state: PlaybackState, rootId: number): void;
20
+ /** Marks a one-shot focus effect as finished (playhead parked at duration). */
21
+ export declare function markFocusFinished(state: PlaybackState, rootId: number): void;
22
+ /** Whether a one-shot focus effect has reached the end of its timeline. */
23
+ export declare function isFocusFinished(state: PlaybackState, rootId: number): boolean;
@@ -0,0 +1,32 @@
1
+ /** Default playback state for a new editor session. */
2
+ export function createPlaybackState() {
3
+ return {
4
+ stagePlaying: true,
5
+ focusRootId: null,
6
+ focusElapsedByRoot: new Map(),
7
+ focusFinishedByRoot: new Set(),
8
+ speed: 1,
9
+ stepQueued: false,
10
+ scrubbing: false,
11
+ };
12
+ }
13
+ /** Elapsed timeline time for one focused effect root. */
14
+ export function getFocusElapsed(state, rootId) {
15
+ return state.focusElapsedByRoot.get(rootId) ?? 0;
16
+ }
17
+ /** Writes focus elapsed time (playhead position). */
18
+ export function setFocusElapsed(state, rootId, elapsed) {
19
+ state.focusElapsedByRoot.set(rootId, elapsed);
20
+ }
21
+ /** Clears the parked-at-end flag after restart or scrub before the end. */
22
+ export function clearFocusFinished(state, rootId) {
23
+ state.focusFinishedByRoot.delete(rootId);
24
+ }
25
+ /** Marks a one-shot focus effect as finished (playhead parked at duration). */
26
+ export function markFocusFinished(state, rootId) {
27
+ state.focusFinishedByRoot.add(rootId);
28
+ }
29
+ /** Whether a one-shot focus effect has reached the end of its timeline. */
30
+ export function isFocusFinished(state, rootId) {
31
+ return state.focusFinishedByRoot.has(rootId);
32
+ }
@@ -1,5 +1,10 @@
1
1
  import type { BatchedRenderer } from 'babylon.quarks';
2
2
  import type { EffectBinding } from './binding';
3
+ /**
4
+ * Scrub only the focused effect's systems — does not advance other preview effects
5
+ * via the shared BatchedRenderer.update loop.
6
+ */
7
+ export declare function scrubFocusTo(binding: EffectBinding, time: number): void;
3
8
  /**
4
9
  * Approximate timeline scrub: replays every system from t=0 up to `time` using the existing
5
10
  * simulation loop. Not frame-exact (spawn/burst randomization is unseeded), but reuses
@@ -1,4 +1,34 @@
1
1
  const WIDE_STEP_THRESHOLD = 8;
2
+ function bindingSystems(binding) {
3
+ return [binding.system, ...binding.subSystems];
4
+ }
5
+ /**
6
+ * Scrub only the focused effect's systems — does not advance other preview effects
7
+ * via the shared BatchedRenderer.update loop.
8
+ */
9
+ export function scrubFocusTo(binding, time) {
10
+ const systems = bindingSystems(binding);
11
+ for (const system of systems) {
12
+ system.restart();
13
+ system.play();
14
+ }
15
+ if (time <= 0) {
16
+ return;
17
+ }
18
+ const fixedStep = time > WIDE_STEP_THRESHOLD ? 1 / 30 : 1 / 60;
19
+ const steps = Math.floor(time / fixedStep);
20
+ for (let i = 0; i < steps; i++) {
21
+ for (const system of systems) {
22
+ system.update(fixedStep);
23
+ }
24
+ }
25
+ const remainder = time - steps * fixedStep;
26
+ if (remainder > 0) {
27
+ for (const system of systems) {
28
+ system.update(remainder);
29
+ }
30
+ }
31
+ }
2
32
  /**
3
33
  * Approximate timeline scrub: replays every system from t=0 up to `time` using the existing
4
34
  * simulation loop. Not frame-exact (spawn/burst randomization is unseeded), but reuses
@@ -31,5 +31,7 @@ export declare function computeStartOffset(gen: ValueGenerator | FunctionValueGe
31
31
  };
32
32
  /** Flattens the effect's group/emitter tree into display rows, preserving tree-walk order. */
33
33
  export declare function computeTimelineRows(binding: EffectBinding): TimelineRow[];
34
- /** Overall timeline span: the furthest track extent (start offset + duration). */
34
+ /**
35
+ * Overall timeline ruler span — emission window per track (`startOffset + duration`).
36
+ */
35
37
  export declare function computeTimelineSpan(rows: TimelineRow[]): number;
@@ -58,13 +58,16 @@ export function computeTimelineRows(binding) {
58
58
  walk(binding.getTree());
59
59
  return rows;
60
60
  }
61
- /** Overall timeline span: the furthest track extent (start offset + duration). */
61
+ /**
62
+ * Overall timeline ruler span — emission window per track (`startOffset + duration`).
63
+ */
62
64
  export function computeTimelineSpan(rows) {
63
65
  let span = 0;
64
66
  for (const row of rows) {
65
- if (row.kind === 'track') {
66
- span = Math.max(span, row.startOffset + row.duration);
67
+ if (row.kind !== 'track') {
68
+ continue;
67
69
  }
70
+ span = Math.max(span, row.startOffset + row.duration);
68
71
  }
69
72
  return Math.max(span, MIN_TIMELINE_SPAN);
70
73
  }
@@ -0,0 +1,3 @@
1
+ export declare function subscribeTransform(listener: () => void): () => void;
2
+ export declare function getTransformRevision(): number;
3
+ export declare function notifyTransformChanged(): void;
@@ -0,0 +1,16 @@
1
+ /** Lightweight external store so gizmo drags refresh ObjectModule without re-rendering the whole host. */
2
+ let revision = 0;
3
+ const listeners = new Set();
4
+ export function subscribeTransform(listener) {
5
+ listeners.add(listener);
6
+ return () => listeners.delete(listener);
7
+ }
8
+ export function getTransformRevision() {
9
+ return revision;
10
+ }
11
+ export function notifyTransformChanged() {
12
+ revision++;
13
+ for (const listener of listeners) {
14
+ listener();
15
+ }
16
+ }
@@ -0,0 +1,25 @@
1
+ import { TransformNode } from '@babylonjs/core/Meshes/transformNode';
2
+ import type { Scene } from '@babylonjs/core/scene';
3
+ import type { GalleryEntry } from './loadEffectGallery';
4
+ /**
5
+ * Viewport pick/hover for in-scene gallery effect roots only (not child emitters).
6
+ * Hover uses the same ground ring as focus selection, in yellow.
7
+ */
8
+ export declare class GalleryViewportPicker {
9
+ private readonly scene;
10
+ private readonly hoverMarker;
11
+ private readonly handles;
12
+ private hoveredRoot;
13
+ private syncKey;
14
+ constructor(scene: Scene);
15
+ /** Rebuilds root pick colliders when the gallery changes. */
16
+ sync(entries: GalleryEntry[], soloRoot: TransformNode | null, syncKey: string): void;
17
+ /** Yellow focus-style ring under the hovered effect root. */
18
+ setHover(effectRoot: TransformNode | null): void;
19
+ follow(): void;
20
+ /** Ray-picks an in-scene effect root from canvas coordinates. */
21
+ pick(clientX: number, clientY: number): TransformNode | null;
22
+ dispose(): void;
23
+ private createHandle;
24
+ private clearHandles;
25
+ }
@@ -0,0 +1,86 @@
1
+ import { MeshBuilder } from '@babylonjs/core/Meshes/meshBuilder';
2
+ import { EDITOR_SCENE_NODE_METADATA } from './editorScene';
3
+ import { GROUND_RING_RADIUS, GalleryHoverMarker } from './gallerySelectionMarker';
4
+ /** Invisible pick proxy size — matches the ground ring footprint. */
5
+ const PICK_DIAMETER = GROUND_RING_RADIUS * 2;
6
+ /**
7
+ * Viewport pick/hover for in-scene gallery effect roots only (not child emitters).
8
+ * Hover uses the same ground ring as focus selection, in yellow.
9
+ */
10
+ export class GalleryViewportPicker {
11
+ constructor(scene) {
12
+ this.handles = new Map();
13
+ this.hoveredRoot = null;
14
+ this.syncKey = '';
15
+ this.scene = scene;
16
+ this.hoverMarker = new GalleryHoverMarker(scene);
17
+ }
18
+ /** Rebuilds root pick colliders when the gallery changes. */
19
+ sync(entries, soloRoot, syncKey) {
20
+ if (syncKey === this.syncKey) {
21
+ return;
22
+ }
23
+ this.syncKey = syncKey;
24
+ this.clearHandles();
25
+ if (entries.length > 0) {
26
+ for (const entry of entries) {
27
+ if (!entry.inScene || entry.root.isDisposed()) {
28
+ continue;
29
+ }
30
+ this.createHandle(entry.root);
31
+ }
32
+ }
33
+ else if (soloRoot && !soloRoot.isDisposed()) {
34
+ this.createHandle(soloRoot);
35
+ }
36
+ const hovered = this.hoveredRoot;
37
+ this.hoveredRoot = null;
38
+ this.setHover(hovered && !hovered.isDisposed() ? hovered : null);
39
+ }
40
+ /** Yellow focus-style ring under the hovered effect root. */
41
+ setHover(effectRoot) {
42
+ if (effectRoot === this.hoveredRoot) {
43
+ return;
44
+ }
45
+ this.hoveredRoot = effectRoot && !effectRoot.isDisposed() ? effectRoot : null;
46
+ if (!this.hoveredRoot) {
47
+ this.hoverMarker.hide();
48
+ return;
49
+ }
50
+ this.hoverMarker.show(this.hoveredRoot);
51
+ }
52
+ follow() {
53
+ this.hoverMarker.follow();
54
+ }
55
+ /** Ray-picks an in-scene effect root from canvas coordinates. */
56
+ pick(clientX, clientY) {
57
+ const canvas = this.scene.getEngine().getRenderingCanvas();
58
+ if (!canvas) {
59
+ return null;
60
+ }
61
+ const rect = canvas.getBoundingClientRect();
62
+ const pickInfo = this.scene.pick(clientX - rect.left, clientY - rect.top, (mesh) => !!mesh.metadata?.quarksEditorPick);
63
+ const meta = pickInfo?.pickedMesh?.metadata;
64
+ return meta?.quarksEditorPick ? meta.effectRoot : null;
65
+ }
66
+ dispose() {
67
+ this.clearHandles();
68
+ this.hoverMarker.dispose();
69
+ }
70
+ createHandle(effectRoot) {
71
+ const mesh = MeshBuilder.CreateSphere(`viewport-pick-root-${effectRoot.uniqueId}`, { diameter: PICK_DIAMETER, segments: 10 }, this.scene);
72
+ mesh.parent = effectRoot;
73
+ mesh.position.set(0, 0, 0);
74
+ mesh.isPickable = true;
75
+ mesh.visibility = 0;
76
+ const metadata = { quarksEditorPick: true, effectRoot };
77
+ mesh.metadata = { ...EDITOR_SCENE_NODE_METADATA, ...metadata };
78
+ this.handles.set(effectRoot.uniqueId, mesh);
79
+ }
80
+ clearHandles() {
81
+ for (const mesh of this.handles.values()) {
82
+ mesh.dispose();
83
+ }
84
+ this.handles.clear();
85
+ }
86
+ }
package/dist/index.d.ts CHANGED
@@ -12,4 +12,6 @@ export type { GradientStop } from './core/colors';
12
12
  export { SHAPE_TYPES, SHAPE_PARAM_KEYS, DEFAULT_SHAPE_PARAMS, getShapeType, readShapeParams, createShape, } from './core/shapes';
13
13
  export type { ShapeType, ShapeParams } from './core/shapes';
14
14
  export { GroundPlaneResolver, ensureGroundResolver } from './core/collision';
15
+ export { applyRendererMaterial, getMaterialLabel } from './core/material';
16
+ export type { RendererMaterialPatch } from './core/material';
15
17
  export { ensurePortableTextureUrl } from './core/textureExport';
package/dist/index.js CHANGED
@@ -6,4 +6,5 @@ export { readScalar, buildScalar, sampleCurve, readPieces, buildCurve, splitPiec
6
6
  export { DEFAULT_GRADIENT_STOPS, buildGradient, buildColorOverLife, rgbaToVector4, stopToCss, stopsToCssGradient, hexToRgb, rgbToHex, findBehavior, } from './core/colors';
7
7
  export { SHAPE_TYPES, SHAPE_PARAM_KEYS, DEFAULT_SHAPE_PARAMS, getShapeType, readShapeParams, createShape, } from './core/shapes';
8
8
  export { GroundPlaneResolver, ensureGroundResolver } from './core/collision';
9
+ export { applyRendererMaterial, getMaterialLabel } from './core/material';
9
10
  export { ensurePortableTextureUrl } from './core/textureExport';
@@ -28,8 +28,5 @@ export function EffectEditor(props) {
28
28
  }, [props.binding, activeSystem]);
29
29
  useSyncExternalStore((onStoreChange) => binding.subscribe(onStoreChange), () => binding.getRevision());
30
30
  const gizmoTargetIsEmitter = allSystems.some((s) => s.emitter === props.gizmoTargetNode);
31
- if (!gizmoTargetIsEmitter) {
32
- return (_jsx("div", { style: { fontFamily: theme.font, color: theme.text }, children: _jsx(ObjectModule, { node: props.gizmoTargetNode, label: props.gizmoTargetLabel }) }));
33
- }
34
- return (_jsxs("div", { style: { fontFamily: theme.font, color: theme.text }, children: [_jsx(ObjectModule, { node: props.gizmoTargetNode, label: props.gizmoTargetLabel }), _jsx(MainModule, { binding: binding }), _jsx(EmissionModule, { binding: binding }), _jsx(ShapeModule, { binding: binding }), _jsx(EmitDirectionModule, { binding: binding }), _jsx(VelocityOverLifeModule, { binding: binding }), _jsx(SpeedOverLifeModule, { binding: binding }), _jsx(LimitSpeedOverLifeModule, { binding: binding }), _jsx(InheritVelocityModule, { binding: binding }), _jsx(ForceOverLifeModule, { binding: binding }), _jsx(GravityModule, { binding: binding }), _jsx(OrbitOverLifeModule, { binding: binding }), _jsx(ColorOverLifeModule, { binding: binding }), _jsx(ColorBySpeedModule, { binding: binding }), _jsx(SizeOverLifeModule, { binding: binding }), _jsx(SizeBySpeedModule, { binding: binding }), _jsx(RotationOverLifeModule, { binding: binding }), _jsx(RotationBySpeedModule, { binding: binding }), _jsx(NoiseModule, { binding: binding }), _jsx(TurbulenceModule, { binding: binding }), _jsx(CollisionModule, { binding: binding }), _jsx(WidthOverTrailModule, { binding: binding }), _jsx(SubEmittersModule, { binding: binding, rootBinding: props.binding }), _jsx(TextureSheetModule, { binding: binding }), _jsx(RendererModule, { binding: binding, textureOptions: props.textureOptions, geometryOptions: props.geometryOptions, resolveTexture: props.resolveTexture, resolveGeometry: props.resolveGeometry })] }));
31
+ return (_jsxs("div", { style: { fontFamily: theme.font, color: theme.text }, children: [_jsx(ObjectModule, { node: props.gizmoTargetNode, label: props.gizmoTargetLabel }), !gizmoTargetIsEmitter && (_jsxs("div", { style: { fontSize: 11, color: theme.textDim, margin: '0 0 10px', lineHeight: 1.45 }, children: ["Modules below edit ", _jsx("strong", { children: activeSystem.emitter.name || 'particle system' }), ". Select its emitter in the timeline to move it with the gizmo."] })), _jsx(MainModule, { binding: binding }), _jsx(EmissionModule, { binding: binding }), _jsx(ShapeModule, { binding: binding }), _jsx(EmitDirectionModule, { binding: binding }), _jsx(VelocityOverLifeModule, { binding: binding }), _jsx(SpeedOverLifeModule, { binding: binding }), _jsx(LimitSpeedOverLifeModule, { binding: binding }), _jsx(InheritVelocityModule, { binding: binding }), _jsx(ForceOverLifeModule, { binding: binding }), _jsx(GravityModule, { binding: binding }), _jsx(OrbitOverLifeModule, { binding: binding }), _jsx(ColorOverLifeModule, { binding: binding }), _jsx(ColorBySpeedModule, { binding: binding }), _jsx(SizeOverLifeModule, { binding: binding }), _jsx(SizeBySpeedModule, { binding: binding }), _jsx(RotationOverLifeModule, { binding: binding }), _jsx(RotationBySpeedModule, { binding: binding }), _jsx(NoiseModule, { binding: binding }), _jsx(TurbulenceModule, { binding: binding }), _jsx(CollisionModule, { binding: binding }), _jsx(WidthOverTrailModule, { binding: binding }), _jsx(SubEmittersModule, { binding: binding, rootBinding: props.binding }), _jsx(TextureSheetModule, { binding: binding }), _jsx(RendererModule, { binding: binding, textureOptions: props.textureOptions, geometryOptions: props.geometryOptions, resolveTexture: props.resolveTexture, resolveGeometry: props.resolveGeometry })] }));
35
32
  }
@@ -10,6 +10,8 @@ export interface EffectEditorHostHandle {
10
10
  scene: Scene;
11
11
  renderer: BatchedRenderer;
12
12
  importJson: (json: unknown) => void;
13
+ /** Loads many JSON effects at once and appends them to the gallery catalog. */
14
+ importGallery: (files: FileList | File[]) => Promise<void>;
13
15
  exportJson: () => string;
14
16
  }
15
17
  export interface EffectEditorHostProps {
@@ -26,6 +28,11 @@ export interface EffectEditorHostProps {
26
28
  resolveTexture?: (url: string, scene: Scene) => unknown;
27
29
  /** Host loader for the Mesh "Load from file…" option (e.g. GLB via @babylonjs/loaders). */
28
30
  resolveGeometry?: (file: File, scene: Scene) => Promise<GeometryData>;
31
+ /** Optional link rendered in the viewport's top-left corner (e.g. back to a host gallery page). */
32
+ backLink?: {
33
+ href: string;
34
+ label: string;
35
+ };
29
36
  }
30
37
  /**
31
38
  * Self-contained effect editor: live Babylon preview + hierarchy/inspector sidebar.