babylon.quarks-editor 0.17.7 → 0.17.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +46 -0
- package/README.md +1 -1
- package/dist/core/binding.js +2 -5
- package/dist/core/defaultEffect.d.ts +12 -0
- package/dist/core/defaultEffect.js +43 -0
- package/dist/core/editorScene.d.ts +7 -0
- package/dist/core/editorScene.js +11 -0
- package/dist/core/effectTree.js +2 -1
- package/dist/core/emitterShapeWireframe.d.ts +20 -0
- package/dist/core/emitterShapeWireframe.js +329 -0
- package/dist/core/galleryDragSession.d.ts +4 -0
- package/dist/core/galleryDragSession.js +8 -0
- package/dist/core/galleryDropMarker.d.ts +13 -0
- package/dist/core/galleryDropMarker.js +42 -0
- package/dist/core/galleryLayout.d.ts +20 -0
- package/dist/core/galleryLayout.js +68 -0
- package/dist/core/galleryPlayback.d.ts +9 -0
- package/dist/core/galleryPlayback.js +43 -0
- package/dist/core/galleryScene.d.ts +12 -0
- package/dist/core/galleryScene.js +41 -0
- package/dist/core/gallerySelectionMarker.d.ts +25 -0
- package/dist/core/gallerySelectionMarker.js +69 -0
- package/dist/core/galleryTree.d.ts +49 -0
- package/dist/core/galleryTree.js +138 -0
- package/dist/core/loadEffect.d.ts +13 -0
- package/dist/core/loadEffect.js +31 -17
- package/dist/core/loadEffectGallery.d.ts +36 -0
- package/dist/core/loadEffectGallery.js +97 -0
- package/dist/core/material.d.ts +13 -0
- package/dist/core/material.js +68 -0
- package/dist/core/pickGalleryFiles.d.ts +5 -0
- package/dist/core/pickGalleryFiles.js +42 -0
- package/dist/core/playback.d.ts +23 -0
- package/dist/core/playback.js +32 -0
- package/dist/core/scrub.d.ts +5 -0
- package/dist/core/scrub.js +30 -0
- package/dist/core/timeline.d.ts +3 -1
- package/dist/core/timeline.js +8 -4
- package/dist/core/transformStore.d.ts +3 -0
- package/dist/core/transformStore.js +16 -0
- package/dist/core/viewportPicker.d.ts +25 -0
- package/dist/core/viewportPicker.js +86 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/react/EffectEditor.d.ts +3 -0
- package/dist/react/EffectEditor.js +3 -1
- package/dist/react/EffectEditorHost.d.ts +7 -0
- package/dist/react/EffectEditorHost.js +717 -102
- package/dist/react/GalleryPanel.d.ts +19 -0
- package/dist/react/GalleryPanel.js +215 -0
- package/dist/react/InspectorInfoIcon.d.ts +7 -0
- package/dist/react/InspectorInfoIcon.js +22 -0
- package/dist/react/InspectorLabel.d.ts +7 -0
- package/dist/react/InspectorLabel.js +12 -0
- package/dist/react/MessageDialog.d.ts +9 -0
- package/dist/react/MessageDialog.js +24 -0
- package/dist/react/ModuleSection.d.ts +2 -0
- package/dist/react/ModuleSection.js +8 -1
- package/dist/react/ObjectModule.d.ts +6 -0
- package/dist/react/ObjectModule.js +40 -0
- package/dist/react/TimelinePanel.d.ts +11 -15
- package/dist/react/TimelinePanel.js +129 -65
- package/dist/react/TimelineTrackRow.d.ts +2 -2
- package/dist/react/TimelineTrackRow.js +41 -23
- package/dist/react/Tooltip.d.ts +6 -0
- package/dist/react/Tooltip.js +65 -0
- package/dist/react/ValueField.d.ts +2 -0
- package/dist/react/ValueField.js +1 -1
- package/dist/react/behaviorModules.js +8 -8
- package/dist/react/fields.d.ts +2 -0
- package/dist/react/fields.js +14 -5
- package/dist/react/icons.d.ts +7 -0
- package/dist/react/icons.js +10 -0
- package/dist/react/index.d.ts +4 -0
- package/dist/react/index.js +2 -0
- package/dist/react/inspectorHints.d.ts +2 -0
- package/dist/react/inspectorHints.js +138 -0
- package/dist/react/modules.js +109 -81
- package/dist/react/theme.d.ts +2 -0
- package/dist/react/theme.js +97 -22
- package/package.json +72 -63
- package/src/core/binding.ts +102 -105
- package/src/core/collision.ts +40 -40
- package/src/core/defaultEffect.ts +74 -0
- package/src/core/editorScene.ts +14 -0
- package/src/core/effectTree.ts +2 -1
- package/src/core/emitterShapeWireframe.ts +373 -0
- package/src/core/galleryDragSession.ts +12 -0
- package/src/core/galleryDropMarker.ts +57 -0
- package/src/core/galleryLayout.ts +90 -0
- package/src/core/galleryPlayback.ts +55 -0
- package/src/core/galleryScene.ts +58 -0
- package/src/core/gallerySelectionMarker.ts +84 -0
- package/src/core/galleryTree.ts +187 -0
- package/src/core/groups.ts +29 -29
- package/src/core/loadEffect.ts +139 -102
- package/src/core/loadEffectGallery.ts +146 -0
- package/src/core/material.ts +84 -0
- package/src/core/pickGalleryFiles.ts +58 -0
- package/src/core/playback.ts +49 -0
- package/src/core/scrub.ts +60 -28
- package/src/core/shapes.ts +133 -133
- package/src/core/systems.ts +98 -98
- package/src/core/timeline.ts +98 -94
- package/src/core/transformStore.ts +19 -0
- package/src/core/values.ts +113 -113
- package/src/core/viewportPicker.ts +114 -0
- package/src/index.ts +34 -32
- package/src/react/CurveEditor.tsx +168 -168
- package/src/react/EffectEditor.tsx +123 -111
- package/src/react/EffectEditorHost.tsx +1162 -377
- package/src/react/GalleryPanel.tsx +453 -0
- package/src/react/GradientEditor.tsx +127 -127
- package/src/react/InspectorInfoIcon.tsx +36 -0
- package/src/react/InspectorLabel.tsx +18 -0
- package/src/react/MessageDialog.tsx +57 -0
- package/src/react/ModuleSection.tsx +69 -54
- package/src/react/ObjectModule.tsx +58 -0
- package/src/react/PromptDialog.tsx +85 -85
- package/src/react/TimelinePanel.tsx +448 -354
- package/src/react/TimelineTrackRow.tsx +326 -267
- package/src/react/Tooltip.tsx +93 -0
- package/src/react/ValueField.tsx +3 -1
- package/src/react/behaviorModules.tsx +663 -658
- package/src/react/fields.tsx +85 -76
- package/src/react/icons.ts +19 -0
- package/src/react/index.ts +47 -43
- package/src/react/inspectorHints.ts +150 -0
- package/src/react/modules.tsx +1049 -1034
- package/src/react/theme.ts +161 -85
package/dist/react/theme.js
CHANGED
|
@@ -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.
|
|
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
|
-
"
|
|
36
|
-
"
|
|
37
|
-
"
|
|
38
|
-
"
|
|
39
|
-
"
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
"
|
|
45
|
-
|
|
46
|
-
"
|
|
47
|
-
"
|
|
48
|
-
|
|
49
|
-
"
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
"@
|
|
56
|
-
"@
|
|
57
|
-
"
|
|
58
|
-
"
|
|
59
|
-
"
|
|
60
|
-
|
|
61
|
-
|
|
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
|
+
}
|
package/src/core/binding.ts
CHANGED
|
@@ -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
|
-
//
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
system.
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
+
}
|
package/src/core/collision.ts
CHANGED
|
@@ -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
|
+
}
|
package/src/core/effectTree.ts
CHANGED
|
@@ -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
|
}
|