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.
Files changed (130) 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 +8 -4
  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.d.ts +3 -0
  46. package/dist/react/EffectEditor.js +3 -1
  47. package/dist/react/EffectEditorHost.d.ts +7 -0
  48. package/dist/react/EffectEditorHost.js +717 -102
  49. package/dist/react/GalleryPanel.d.ts +19 -0
  50. package/dist/react/GalleryPanel.js +215 -0
  51. package/dist/react/InspectorInfoIcon.d.ts +7 -0
  52. package/dist/react/InspectorInfoIcon.js +22 -0
  53. package/dist/react/InspectorLabel.d.ts +7 -0
  54. package/dist/react/InspectorLabel.js +12 -0
  55. package/dist/react/MessageDialog.d.ts +9 -0
  56. package/dist/react/MessageDialog.js +24 -0
  57. package/dist/react/ModuleSection.d.ts +2 -0
  58. package/dist/react/ModuleSection.js +8 -1
  59. package/dist/react/ObjectModule.d.ts +6 -0
  60. package/dist/react/ObjectModule.js +40 -0
  61. package/dist/react/TimelinePanel.d.ts +11 -15
  62. package/dist/react/TimelinePanel.js +129 -65
  63. package/dist/react/TimelineTrackRow.d.ts +2 -2
  64. package/dist/react/TimelineTrackRow.js +41 -23
  65. package/dist/react/Tooltip.d.ts +6 -0
  66. package/dist/react/Tooltip.js +65 -0
  67. package/dist/react/ValueField.d.ts +2 -0
  68. package/dist/react/ValueField.js +1 -1
  69. package/dist/react/behaviorModules.js +8 -8
  70. package/dist/react/fields.d.ts +2 -0
  71. package/dist/react/fields.js +14 -5
  72. package/dist/react/icons.d.ts +7 -0
  73. package/dist/react/icons.js +10 -0
  74. package/dist/react/index.d.ts +4 -0
  75. package/dist/react/index.js +2 -0
  76. package/dist/react/inspectorHints.d.ts +2 -0
  77. package/dist/react/inspectorHints.js +138 -0
  78. package/dist/react/modules.js +109 -81
  79. package/dist/react/theme.d.ts +2 -0
  80. package/dist/react/theme.js +97 -22
  81. package/package.json +72 -63
  82. package/src/core/binding.ts +102 -105
  83. package/src/core/collision.ts +40 -40
  84. package/src/core/defaultEffect.ts +74 -0
  85. package/src/core/editorScene.ts +14 -0
  86. package/src/core/effectTree.ts +2 -1
  87. package/src/core/emitterShapeWireframe.ts +373 -0
  88. package/src/core/galleryDragSession.ts +12 -0
  89. package/src/core/galleryDropMarker.ts +57 -0
  90. package/src/core/galleryLayout.ts +90 -0
  91. package/src/core/galleryPlayback.ts +55 -0
  92. package/src/core/galleryScene.ts +58 -0
  93. package/src/core/gallerySelectionMarker.ts +84 -0
  94. package/src/core/galleryTree.ts +187 -0
  95. package/src/core/groups.ts +29 -29
  96. package/src/core/loadEffect.ts +139 -102
  97. package/src/core/loadEffectGallery.ts +146 -0
  98. package/src/core/material.ts +84 -0
  99. package/src/core/pickGalleryFiles.ts +58 -0
  100. package/src/core/playback.ts +49 -0
  101. package/src/core/scrub.ts +60 -28
  102. package/src/core/shapes.ts +133 -133
  103. package/src/core/systems.ts +98 -98
  104. package/src/core/timeline.ts +98 -94
  105. package/src/core/transformStore.ts +19 -0
  106. package/src/core/values.ts +113 -113
  107. package/src/core/viewportPicker.ts +114 -0
  108. package/src/index.ts +34 -32
  109. package/src/react/CurveEditor.tsx +168 -168
  110. package/src/react/EffectEditor.tsx +123 -111
  111. package/src/react/EffectEditorHost.tsx +1162 -377
  112. package/src/react/GalleryPanel.tsx +453 -0
  113. package/src/react/GradientEditor.tsx +127 -127
  114. package/src/react/InspectorInfoIcon.tsx +36 -0
  115. package/src/react/InspectorLabel.tsx +18 -0
  116. package/src/react/MessageDialog.tsx +57 -0
  117. package/src/react/ModuleSection.tsx +69 -54
  118. package/src/react/ObjectModule.tsx +58 -0
  119. package/src/react/PromptDialog.tsx +85 -85
  120. package/src/react/TimelinePanel.tsx +448 -354
  121. package/src/react/TimelineTrackRow.tsx +326 -267
  122. package/src/react/Tooltip.tsx +93 -0
  123. package/src/react/ValueField.tsx +3 -1
  124. package/src/react/behaviorModules.tsx +663 -658
  125. package/src/react/fields.tsx +85 -76
  126. package/src/react/icons.ts +19 -0
  127. package/src/react/index.ts +47 -43
  128. package/src/react/inspectorHints.ts +150 -0
  129. package/src/react/modules.tsx +1049 -1034
  130. package/src/react/theme.ts +161 -85
@@ -42,10 +42,22 @@ export const buttonStyle = {
42
42
  color: '#d8e3ff',
43
43
  padding: '6px 10px',
44
44
  borderRadius: 8,
45
- fontSize: 12.5,
46
45
  cursor: 'pointer',
46
+ fontSize: 12.5,
47
47
  fontFamily: theme.font,
48
48
  };
49
+ /** Visually hides a file input while keeping it activatable via an associated <label>. */
50
+ export const hiddenFileInputStyle = {
51
+ position: 'absolute',
52
+ width: 1,
53
+ height: 1,
54
+ padding: 0,
55
+ margin: -1,
56
+ overflow: 'hidden',
57
+ clip: 'rect(0, 0, 0, 0)',
58
+ whiteSpace: 'nowrap',
59
+ border: 0,
60
+ };
49
61
  /**
50
62
  * Two hover conventions, applied via className (inline `style` can't express `:hover`):
51
63
  * - `qe-hover` — brightens elements that already have a solid inline background/border
@@ -54,25 +66,88 @@ export const buttonStyle = {
54
66
  * rows), since brightening transparent does nothing. Must not be combined with an inline
55
67
  * `background` on the same element or the tint would need `!important` to show through.
56
68
  */
57
- export const globalEditorStyles = `
58
- .qe-hover, .qe-hover-bg {
59
- transition: filter 0.12s ease, background 0.12s ease;
60
- }
61
- .qe-hover:hover:not(:disabled) {
62
- filter: brightness(1.3);
63
- }
64
- .qe-hover:active:not(:disabled) {
65
- filter: brightness(0.85);
66
- }
67
- .qe-hover:disabled {
68
- opacity: 0.45;
69
- cursor: default;
70
- }
71
- .qe-hover-bg:hover {
72
- background: rgba(255, 255, 255, 0.07);
73
- }
74
- select.qe-hover:hover, input.qe-hover:hover, select.qe-hover:focus, input.qe-hover:focus {
75
- border-color: ${theme.borderActive};
76
- filter: none;
77
- }
69
+ export const globalEditorStyles = `
70
+ .qe-hover, .qe-hover-bg {
71
+ transition: filter 0.12s ease, background 0.12s ease;
72
+ }
73
+ .qe-hover:hover:not(:disabled) {
74
+ filter: brightness(1.3);
75
+ }
76
+ .qe-hover:active:not(:disabled) {
77
+ filter: brightness(0.85);
78
+ }
79
+ .qe-hover:disabled {
80
+ opacity: 0.45;
81
+ cursor: default;
82
+ }
83
+ .qe-hover-bg:hover {
84
+ background: rgba(255, 255, 255, 0.07);
85
+ }
86
+ select.qe-hover:hover, input.qe-hover:hover, select.qe-hover:focus, input.qe-hover:focus {
87
+ border-color: ${theme.borderActive};
88
+ filter: none;
89
+ }
90
+ .qe-root, .qe-root * {
91
+ scrollbar-width: thin;
92
+ scrollbar-color: ${theme.border} transparent;
93
+ }
94
+ .qe-root ::-webkit-scrollbar {
95
+ width: 10px;
96
+ height: 10px;
97
+ }
98
+ .qe-root ::-webkit-scrollbar-track {
99
+ background: transparent;
100
+ }
101
+ .qe-root ::-webkit-scrollbar-thumb {
102
+ background-color: ${theme.border};
103
+ background-clip: padding-box;
104
+ border: 2px solid transparent;
105
+ border-radius: 8px;
106
+ }
107
+ .qe-root ::-webkit-scrollbar-thumb:hover {
108
+ background-color: ${theme.borderActive};
109
+ }
110
+ .qe-root ::-webkit-scrollbar-corner {
111
+ background: transparent;
112
+ }
113
+ .qe-tooltip {
114
+ z-index: 100000;
115
+ padding: 8px 10px;
116
+ font-size: 11.5px;
117
+ line-height: 1.45;
118
+ color: ${theme.text};
119
+ background: linear-gradient(180deg, rgba(18, 26, 52, 0.98), rgba(8, 12, 26, 0.98));
120
+ border: 1px solid ${theme.borderActive};
121
+ border-radius: 8px;
122
+ box-shadow: 0 10px 28px rgba(0, 0, 0, 0.45);
123
+ pointer-events: none;
124
+ font-family: ${theme.font};
125
+ animation: qe-tooltip-in 0.12s ease;
126
+ }
127
+ .qe-tooltip--right::before,
128
+ .qe-tooltip--left::before {
129
+ content: '';
130
+ position: absolute;
131
+ top: 50%;
132
+ width: 0;
133
+ height: 0;
134
+ border: 5px solid transparent;
135
+ transform: translateY(-50%);
136
+ }
137
+ .qe-tooltip--right::before {
138
+ left: -10px;
139
+ border-right-color: ${theme.borderActive};
140
+ }
141
+ .qe-tooltip--left::before {
142
+ right: -10px;
143
+ border-left-color: ${theme.borderActive};
144
+ }
145
+ @keyframes qe-tooltip-in {
146
+ from {
147
+ opacity: 0;
148
+ }
149
+ to {
150
+ opacity: 1;
151
+ }
152
+ }
78
153
  `;
package/package.json CHANGED
@@ -1,63 +1,72 @@
1
- {
2
- "name": "babylon.quarks-editor",
3
- "version": "0.17.7",
4
- "description": "Shuriken-style effect editor for babylon.quarks: headless edit-model core + React module-stack UI",
5
- "type": "module",
6
- "main": "./dist/index.js",
7
- "types": "./dist/index.d.ts",
8
- "exports": {
9
- ".": {
10
- "import": "./dist/index.js",
11
- "types": "./dist/index.d.ts"
12
- },
13
- "./react": {
14
- "import": "./dist/react/index.js",
15
- "types": "./dist/react/index.d.ts"
16
- }
17
- },
18
- "files": [
19
- "dist",
20
- "src",
21
- "LICENSE",
22
- "README.md",
23
- "CHANGELOG.md"
24
- ],
25
- "scripts": {
26
- "clean": "rimraf dist",
27
- "build": "npm run clean && tsc -p tsconfig.json"
28
- },
29
- "repository": {
30
- "type": "git",
31
- "url": "git+https://github.com/Soullnik/babylon.quarks-standalone.git"
32
- },
33
- "keywords": [
34
- "babylonjs",
35
- "particles",
36
- "vfx",
37
- "editor",
38
- "react",
39
- "quarks"
40
- ],
41
- "author": "soullnik <soullnik96@gmail.com>",
42
- "license": "MIT",
43
- "bugs": {
44
- "url": "https://github.com/Soullnik/babylon.quarks-standalone/issues"
45
- },
46
- "homepage": "https://github.com/Soullnik/babylon.quarks-standalone#readme",
47
- "peerDependencies": {
48
- "@babylonjs/core": ">=9.0.0",
49
- "@babylonjs/materials": ">=9.0.0",
50
- "babylon.quarks": ">=0.17.0",
51
- "react": ">=18",
52
- "react-dom": ">=18"
53
- },
54
- "devDependencies": {
55
- "@babylonjs/materials": "9.6.2",
56
- "@types/react": "^19.0.0",
57
- "react": "^19.0.0",
58
- "rimraf": "^6.0.0",
59
- "typescript": "^5.9.3",
60
- "react-dom": "^19.0.0",
61
- "@types/react-dom": "^19.0.0"
62
- }
63
- }
1
+ {
2
+ "name": "babylon.quarks-editor",
3
+ "version": "0.17.9",
4
+ "description": "Shuriken-style effect editor for babylon.quarks: headless edit-model core + React module-stack UI",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "import": "./dist/index.js",
11
+ "types": "./dist/index.d.ts"
12
+ },
13
+ "./react": {
14
+ "import": "./dist/react/index.js",
15
+ "types": "./dist/react/index.d.ts"
16
+ }
17
+ },
18
+ "files": [
19
+ "dist",
20
+ "src",
21
+ "LICENSE",
22
+ "README.md",
23
+ "CHANGELOG.md"
24
+ ],
25
+ "scripts": {
26
+ "clean": "rimraf dist",
27
+ "build": "npm run clean && tsc -p tsconfig.json"
28
+ },
29
+ "repository": {
30
+ "type": "git",
31
+ "url": "git+https://github.com/Soullnik/babylon.quarks-standalone.git"
32
+ },
33
+ "keywords": [
34
+ "babylonjs",
35
+ "babylon.js",
36
+ "particles",
37
+ "particle-system",
38
+ "particle-editor",
39
+ "vfx",
40
+ "vfx-editor",
41
+ "shuriken",
42
+ "editor",
43
+ "react",
44
+ "quarks"
45
+ ],
46
+ "author": "soullnik <soullnik96@gmail.com>",
47
+ "license": "MIT",
48
+ "bugs": {
49
+ "url": "https://github.com/Soullnik/babylon.quarks-standalone/issues"
50
+ },
51
+ "homepage": "https://github.com/Soullnik/babylon.quarks-standalone#readme",
52
+ "peerDependencies": {
53
+ "@babylonjs/core": ">=9.0.0",
54
+ "@babylonjs/materials": ">=9.0.0",
55
+ "@heroicons/react": ">=2.0.0",
56
+ "@tanstack/react-virtual": ">=3.0.0",
57
+ "babylon.quarks": ">=0.17.0",
58
+ "react": ">=18",
59
+ "react-dom": ">=18"
60
+ },
61
+ "devDependencies": {
62
+ "@babylonjs/materials": "9.6.2",
63
+ "@heroicons/react": "^2.2.0",
64
+ "@tanstack/react-virtual": "^3.14.6",
65
+ "@types/react": "^19.0.0",
66
+ "@types/react-dom": "^19.0.0",
67
+ "react": "^19.0.0",
68
+ "react-dom": "^19.0.0",
69
+ "rimraf": "^6.0.0",
70
+ "typescript": "^5.9.3"
71
+ }
72
+ }
@@ -1,105 +1,102 @@
1
- import type {ParticleSystem} from 'babylon.quarks';
2
- import type {TransformNode} from '@babylonjs/core/Meshes/transformNode';
3
- import {buildEffectTree, serializeEffectTree} from './effectTree';
4
- import type {EffectTreeNode} from './effectTree';
5
-
6
- export type EditorListener = () => void;
7
-
8
- /**
9
- * Headless edit-model over a live ParticleSystem: mutate through apply() so every
10
- * change notifies subscribers (React UI, save indicators, hosts like BabylonJS Editor).
11
- */
12
- export class EffectBinding {
13
- /** Not `readonly` — `promoteToMain` reassigns this when the current main system is removed. */
14
- system: ParticleSystem;
15
- /** Systems spawned by sub-emitter behaviors; serialized as children of the root emitter. */
16
- readonly subSystems: ParticleSystem[] = [];
17
- /** Loaded scene root of the whole effect (Group tree); defaults to the system's emitter. */
18
- readonly root: TransformNode;
19
- /** Invoked before any mutation — used by EffectHistory to capture undo snapshots. */
20
- onBeforeChange?: () => void;
21
- private listeners = new Set<EditorListener>();
22
- private revision = 0;
23
-
24
- constructor(system: ParticleSystem, subSystems: ParticleSystem[] = [], root?: TransformNode) {
25
- this.system = system;
26
- this.subSystems.push(...subSystems);
27
- this.root = root ?? system.emitter;
28
- }
29
-
30
- /** Editor hierarchy tree (groups + emitters) rooted at this effect's scene root. */
31
- getTree(): EffectTreeNode {
32
- return buildEffectTree(this.root);
33
- }
34
-
35
- subscribe(listener: EditorListener): () => void {
36
- this.listeners.add(listener);
37
- return () => this.listeners.delete(listener);
38
- }
39
-
40
- /** Monotonic change counter, usable as a React external-store snapshot. */
41
- getRevision(): number {
42
- return this.revision;
43
- }
44
-
45
- apply(mutate: (system: ParticleSystem) => void): void {
46
- this.onBeforeChange?.();
47
- mutate(this.system);
48
- this.notify();
49
- }
50
-
51
- addSubSystem(sub: ParticleSystem): void {
52
- this.subSystems.push(sub);
53
- this.notify();
54
- }
55
-
56
- removeSubSystem(sub: ParticleSystem): void {
57
- const index = this.subSystems.indexOf(sub);
58
- if (index >= 0) {
59
- this.subSystems.splice(index, 1);
60
- }
61
- this.notify();
62
- }
63
-
64
- /**
65
- * Swaps which system is "main" — used when the current `system` is about to be removed
66
- * but other top-level systems remain. Being "main" vs. a sub-system is otherwise
67
- * meaningless to the effect itself (both play/restart/export the same way); it only
68
- * exists so there's always a designated fallback system to select.
69
- */
70
- promoteToMain(next: ParticleSystem): void {
71
- const index = this.subSystems.indexOf(next);
72
- if (index < 0) {
73
- return;
74
- }
75
- this.subSystems.splice(index, 1, this.system);
76
- this.system = next;
77
- this.notify();
78
- }
79
-
80
- private notify(): void {
81
- this.revision++;
82
- for (const listener of this.listeners) {
83
- listener();
84
- }
85
- }
86
-
87
- restart(): void {
88
- // Replay every independent system; sub-emitter targets (onlyUsedByOther) are only
89
- // reset — their parents re-drive them, so playing them directly would double-emit.
90
- for (const system of [this.system, ...this.subSystems]) {
91
- system.restart();
92
- if (!system.onlyUsedByOther) {
93
- system.play();
94
- }
95
- }
96
- }
97
-
98
- /**
99
- * Serializes the effect into the Quarks JSON envelope understood by QuarksLoader,
100
- * preserving the loaded group/emitter hierarchy.
101
- */
102
- exportJSON(name = 'effect'): string {
103
- return serializeEffectTree(this.root, name);
104
- }
105
- }
1
+ import type {ParticleSystem} from 'babylon.quarks';
2
+ import type {TransformNode} from '@babylonjs/core/Meshes/transformNode';
3
+ import {buildEffectTree, serializeEffectTree} from './effectTree';
4
+ import type {EffectTreeNode} from './effectTree';
5
+
6
+ export type EditorListener = () => void;
7
+
8
+ /**
9
+ * Headless edit-model over a live ParticleSystem: mutate through apply() so every
10
+ * change notifies subscribers (React UI, save indicators, hosts like BabylonJS Editor).
11
+ */
12
+ export class EffectBinding {
13
+ /** Not `readonly` — `promoteToMain` reassigns this when the current main system is removed. */
14
+ system: ParticleSystem;
15
+ /** Systems spawned by sub-emitter behaviors; serialized as children of the root emitter. */
16
+ readonly subSystems: ParticleSystem[] = [];
17
+ /** Loaded scene root of the whole effect (Group tree); defaults to the system's emitter. */
18
+ readonly root: TransformNode;
19
+ /** Invoked before any mutation — used by EffectHistory to capture undo snapshots. */
20
+ onBeforeChange?: () => void;
21
+ private listeners = new Set<EditorListener>();
22
+ private revision = 0;
23
+
24
+ constructor(system: ParticleSystem, subSystems: ParticleSystem[] = [], root?: TransformNode) {
25
+ this.system = system;
26
+ this.subSystems.push(...subSystems);
27
+ this.root = root ?? system.emitter;
28
+ }
29
+
30
+ /** Editor hierarchy tree (groups + emitters) rooted at this effect's scene root. */
31
+ getTree(): EffectTreeNode {
32
+ return buildEffectTree(this.root);
33
+ }
34
+
35
+ subscribe(listener: EditorListener): () => void {
36
+ this.listeners.add(listener);
37
+ return () => this.listeners.delete(listener);
38
+ }
39
+
40
+ /** Monotonic change counter, usable as a React external-store snapshot. */
41
+ getRevision(): number {
42
+ return this.revision;
43
+ }
44
+
45
+ apply(mutate: (system: ParticleSystem) => void): void {
46
+ this.onBeforeChange?.();
47
+ mutate(this.system);
48
+ this.notify();
49
+ }
50
+
51
+ addSubSystem(sub: ParticleSystem): void {
52
+ this.subSystems.push(sub);
53
+ this.notify();
54
+ }
55
+
56
+ removeSubSystem(sub: ParticleSystem): void {
57
+ const index = this.subSystems.indexOf(sub);
58
+ if (index >= 0) {
59
+ this.subSystems.splice(index, 1);
60
+ }
61
+ this.notify();
62
+ }
63
+
64
+ /**
65
+ * Swaps which system is "main" — used when the current `system` is about to be removed
66
+ * but other top-level systems remain. Being "main" vs. a sub-system is otherwise
67
+ * meaningless to the effect itself (both play/restart/export the same way); it only
68
+ * exists so there's always a designated fallback system to select.
69
+ */
70
+ promoteToMain(next: ParticleSystem): void {
71
+ const index = this.subSystems.indexOf(next);
72
+ if (index < 0) {
73
+ return;
74
+ }
75
+ this.subSystems.splice(index, 1, this.system);
76
+ this.system = next;
77
+ this.notify();
78
+ }
79
+
80
+ private notify(): void {
81
+ this.revision++;
82
+ for (const listener of this.listeners) {
83
+ listener();
84
+ }
85
+ }
86
+
87
+ restart(): void {
88
+ // onlyUsedByOther systems skip self-emission in update(); play() only unpauses sim.
89
+ for (const system of [this.system, ...this.subSystems]) {
90
+ system.restart();
91
+ system.play();
92
+ }
93
+ }
94
+
95
+ /**
96
+ * Serializes the effect into the Quarks JSON envelope understood by QuarksLoader,
97
+ * preserving the loaded group/emitter hierarchy.
98
+ */
99
+ exportJSON(name = 'effect'): string {
100
+ return serializeEffectTree(this.root, name);
101
+ }
102
+ }
@@ -1,40 +1,40 @@
1
- import {Vector3, getPhysicsResolver, setPhysicsResolver} from 'babylon.quarks';
2
-
3
- /**
4
- * A horizontal ground plane collider. quarks.core's ApplyCollision behavior delegates to a
5
- * host-provided `PhysicsResolver` (a global singleton) and ships no collider primitive of its
6
- * own, so the editor supplies this — the common "particles bounce off the floor" case.
7
- *
8
- * `resolve` returns true and fills the surface normal when the point is at or below the plane,
9
- * which is exactly what ApplyCollision.update reflects the velocity against.
10
- */
11
- export class GroundPlaneResolver {
12
- constructor(public y = 0) {}
13
-
14
- resolve(position: Vector3, normal: Vector3): boolean {
15
- if (position.y <= this.y) {
16
- normal.set(0, 1, 0);
17
- return true;
18
- }
19
- return false;
20
- }
21
- }
22
-
23
- /**
24
- * Registers (once) a shared ground-plane resolver so ApplyCollision behaviors — whether added
25
- * in the inspector or deserialized from imported JSON — have something to collide against.
26
- * Idempotent: an already-registered resolver is left untouched and returned.
27
- */
28
- export function ensureGroundResolver(): GroundPlaneResolver {
29
- const existing = getPhysicsResolver();
30
- if (existing instanceof GroundPlaneResolver) {
31
- return existing;
32
- }
33
- if (existing) {
34
- // A host registered its own resolver; don't clobber it, but we can't tweak its floor.
35
- return existing as GroundPlaneResolver;
36
- }
37
- const resolver = new GroundPlaneResolver(0);
38
- setPhysicsResolver(resolver);
39
- return resolver;
40
- }
1
+ import {Vector3, getPhysicsResolver, setPhysicsResolver} from 'babylon.quarks';
2
+
3
+ /**
4
+ * A horizontal ground plane collider. quarks.core's ApplyCollision behavior delegates to a
5
+ * host-provided `PhysicsResolver` (a global singleton) and ships no collider primitive of its
6
+ * own, so the editor supplies this — the common "particles bounce off the floor" case.
7
+ *
8
+ * `resolve` returns true and fills the surface normal when the point is at or below the plane,
9
+ * which is exactly what ApplyCollision.update reflects the velocity against.
10
+ */
11
+ export class GroundPlaneResolver {
12
+ constructor(public y = 0) {}
13
+
14
+ resolve(position: Vector3, normal: Vector3): boolean {
15
+ if (position.y <= this.y) {
16
+ normal.set(0, 1, 0);
17
+ return true;
18
+ }
19
+ return false;
20
+ }
21
+ }
22
+
23
+ /**
24
+ * Registers (once) a shared ground-plane resolver so ApplyCollision behaviors — whether added
25
+ * in the inspector or deserialized from imported JSON — have something to collide against.
26
+ * Idempotent: an already-registered resolver is left untouched and returned.
27
+ */
28
+ export function ensureGroundResolver(): GroundPlaneResolver {
29
+ const existing = getPhysicsResolver();
30
+ if (existing instanceof GroundPlaneResolver) {
31
+ return existing;
32
+ }
33
+ if (existing) {
34
+ // A host registered its own resolver; don't clobber it, but we can't tweak its floor.
35
+ return existing as GroundPlaneResolver;
36
+ }
37
+ const resolver = new GroundPlaneResolver(0);
38
+ setPhysicsResolver(resolver);
39
+ return resolver;
40
+ }
@@ -0,0 +1,74 @@
1
+ import type {Scene} from '@babylonjs/core/scene';
2
+ import {TransformNode} from '@babylonjs/core/Meshes/transformNode';
3
+ import {
4
+ ColorOverLife,
5
+ ConeEmitter,
6
+ ConstantColor,
7
+ ConstantValue,
8
+ IntervalValue,
9
+ ParticleSystem,
10
+ RenderMode,
11
+ Vector4,
12
+ } from 'babylon.quarks';
13
+ import {DEFAULT_GRADIENT_STOPS, buildGradient} from './colors';
14
+ import type {GalleryEntry} from './loadEffectGallery';
15
+
16
+ export interface CreateGalleryDefaultOptions {
17
+ resolveTexture?: (url: string, scene: Scene) => unknown;
18
+ textureUrl?: string;
19
+ }
20
+
21
+ /** Creates a simple cone burst preset for new editor effects. */
22
+ export function createDefaultEffect(
23
+ scene: Scene,
24
+ resolveTexture?: (url: string, scene: Scene) => unknown,
25
+ textureUrl?: string
26
+ ): ParticleSystem {
27
+ const system = new ParticleSystem({
28
+ scene,
29
+ duration: 4,
30
+ looping: true,
31
+ startLife: new IntervalValue(1.2, 1.8),
32
+ startSpeed: new IntervalValue(3, 5),
33
+ startSize: new IntervalValue(0.25, 0.5),
34
+ startColor: new ConstantColor(new Vector4(1, 1, 1, 1)),
35
+ emissionOverTime: new ConstantValue(80),
36
+ shape: new ConeEmitter({radius: 0.3, angle: 0.25}),
37
+ renderMode: RenderMode.BillBoard,
38
+ texture: (textureUrl && resolveTexture ? resolveTexture(textureUrl, scene) : undefined) as never,
39
+ transparent: true,
40
+ blendMode: 1,
41
+ });
42
+ system.addBehavior(new ColorOverLife(buildGradient(DEFAULT_GRADIENT_STOPS)));
43
+ system.emitter.rotation.x = -Math.PI / 2;
44
+ return system;
45
+ }
46
+
47
+ /** Adds a blank preset effect to the hidden gallery catalog (not in the renderer yet). */
48
+ export function createGalleryDefaultEntry(
49
+ scene: Scene,
50
+ catalogRoot: TransformNode,
51
+ existing: GalleryEntry[],
52
+ options: CreateGalleryDefaultOptions = {}
53
+ ): GalleryEntry {
54
+ const takenNames = new Set(existing.map((entry) => entry.name));
55
+ const takenPaths = new Set(existing.map((entry) => entry.path));
56
+
57
+ let name = existing.length === 0 ? 'Effect' : 'New Effect';
58
+ let suffix = 2;
59
+ while (takenNames.has(name) || takenPaths.has(`_created/${name}.json`)) {
60
+ name = `New Effect ${suffix}`;
61
+ suffix++;
62
+ }
63
+
64
+ const path = `_created/${name}.json`;
65
+ const system = createDefaultEffect(scene, options.resolveTexture, options.textureUrl);
66
+ system.pause();
67
+
68
+ const root = new TransformNode(name, scene);
69
+ system.emitter.parent = root;
70
+ root.parent = catalogRoot;
71
+ root.setEnabled(false);
72
+
73
+ return {name, path, root, systems: [system], inScene: false};
74
+ }
@@ -0,0 +1,14 @@
1
+ import type {TransformNode} from '@babylonjs/core/Meshes/transformNode';
2
+
3
+ /** Marks meshes/nodes created only for editor interaction or overlays (not part of the effect). */
4
+ export const EDITOR_SCENE_NODE_METADATA = {quarksEditorSceneNode: true as const};
5
+
6
+ /** True for viewport pick proxies, shape wireframes, and other non-exportable scene helpers. */
7
+ export function isEditorSceneNode(node: TransformNode): boolean {
8
+ const meta = node.metadata as {quarksEditorPick?: boolean; quarksEditorSceneNode?: boolean} | undefined;
9
+ if (meta?.quarksEditorPick || meta?.quarksEditorSceneNode) {
10
+ return true;
11
+ }
12
+ const name = node.name;
13
+ return name.startsWith('viewport-pick-root-') || name.startsWith('wf-');
14
+ }
@@ -2,6 +2,7 @@ import {ParticleEmitter} from 'babylon.quarks';
2
2
  import type {ParticleSystem} from 'babylon.quarks';
3
3
  import {TransformNode} from '@babylonjs/core/Meshes/transformNode';
4
4
  import {Matrix, Quaternion} from '@babylonjs/core/Maths/math.vector';
5
+ import {isEditorSceneNode} from './editorScene';
5
6
 
6
7
  /** A node in the editor's effect hierarchy: a Group (organizational) or a ParticleEmitter. */
7
8
  export interface EffectTreeNode {
@@ -24,7 +25,7 @@ export function buildEffectTree(root: TransformNode, depth = 0): EffectTreeNode
24
25
  children: [],
25
26
  };
26
27
  for (const child of root.getChildren()) {
27
- if (child instanceof TransformNode) {
28
+ if (child instanceof TransformNode && !isEditorSceneNode(child)) {
28
29
  treeNode.children.push(buildEffectTree(child, depth + 1));
29
30
  }
30
31
  }