babylon.quarks-editor 0.20.0 → 0.23.0
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 +29 -0
- package/dist/core/assets.js +43 -21
- package/dist/react/EditorWorkspace.js +1 -1
- package/dist/react/ProjectsPage.d.ts +5 -0
- package/dist/react/ProjectsPage.js +16 -3
- package/dist/react/behaviorModules.js +2 -2
- package/dist/react/modules.js +18 -0
- package/package.json +1 -1
- package/src/core/assets.ts +45 -22
- package/src/react/EditorWorkspace.tsx +1 -0
- package/src/react/ProjectsPage.tsx +27 -2
- package/src/react/behaviorModules.tsx +17 -8
- package/src/react/modules.tsx +76 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,35 @@ 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.23.0] — 2026-10-18
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- The projects page shows the host's `backLink` (the same one the editor viewport shows), so a
|
|
12
|
+
workspace embedded in a site can be left for the hosting page without the browser's back button.
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
|
|
16
|
+
- Version aligned with babylon.quarks 0.23.0, whose simulation now runs on store columns; the
|
|
17
|
+
editor's preview and gallery get the same speed-up with no editor changes.
|
|
18
|
+
|
|
19
|
+
## [0.22.0] — 2026-10-17
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
|
|
23
|
+
- Renderer module: a "Lighting" selector for mesh particles (`fixed` / `unlit` / `scene`) and
|
|
24
|
+
rim (Fresnel) controls — colour, power, strength, add or mix.
|
|
25
|
+
- Width over Trail module: an "Absolute width" checkbox for `WidthOverLength`'s `absolute`
|
|
26
|
+
flag (the curve as world-unit widths, the pre-0.22 reading).
|
|
27
|
+
|
|
28
|
+
## [0.21.0] — 2026-10-16
|
|
29
|
+
|
|
30
|
+
### Changed
|
|
31
|
+
|
|
32
|
+
- Asset folder import inlines external textures and geometries through
|
|
33
|
+
`QuarksLoader.resolveLinkedResources` and a File-backed `QuarksResourceResolver`, matching the
|
|
34
|
+
runtime loader path instead of a private `resolveExternalGeometries` helper.
|
|
35
|
+
|
|
7
36
|
## [0.20.0] — 2026-10-16
|
|
8
37
|
|
|
9
38
|
### Added
|
package/dist/core/assets.js
CHANGED
|
@@ -87,31 +87,53 @@ class ImportedFileIndex {
|
|
|
87
87
|
}
|
|
88
88
|
}
|
|
89
89
|
/**
|
|
90
|
-
*
|
|
91
|
-
*
|
|
90
|
+
* File-backed {@link QuarksResourceResolver}: relative image urls become data URIs and geometry
|
|
91
|
+
* urls load from the imported file set. Collects missing references into `missing`.
|
|
92
|
+
*/
|
|
93
|
+
function createFileResolver(effectPath, index, missing) {
|
|
94
|
+
return {
|
|
95
|
+
async resolve(url, kind, baseUrl) {
|
|
96
|
+
if (kind === 'image') {
|
|
97
|
+
if (isExternalUrl(url)) {
|
|
98
|
+
return url;
|
|
99
|
+
}
|
|
100
|
+
const file = index.find(baseUrl || effectPath, url);
|
|
101
|
+
if (!file) {
|
|
102
|
+
missing.push(url);
|
|
103
|
+
return url;
|
|
104
|
+
}
|
|
105
|
+
return index.read(file, readAsDataUrl);
|
|
106
|
+
}
|
|
107
|
+
// Geometry (and other kinds): stable path key for fetchJson.
|
|
108
|
+
const file = index.find(baseUrl || effectPath, url);
|
|
109
|
+
if (!file) {
|
|
110
|
+
missing.push(url);
|
|
111
|
+
throw new Error('not among the imported files');
|
|
112
|
+
}
|
|
113
|
+
return relativePathOf(file);
|
|
114
|
+
},
|
|
115
|
+
fetchJson(resolvedUrl) {
|
|
116
|
+
// resolvedUrl is relativePathOf(file) from resolve(); look up by path (empty base).
|
|
117
|
+
const file = index.find('', resolvedUrl);
|
|
118
|
+
if (!file) {
|
|
119
|
+
throw new Error(`missing file "${resolvedUrl}"`);
|
|
120
|
+
}
|
|
121
|
+
return index.read(file, async (f) => JSON.parse(await f.text()));
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Inlines what an "assets as files" export put in separate files via
|
|
127
|
+
* {@link QuarksLoader.resolveLinkedResources}, so the effect loads (and later exports) as a
|
|
92
128
|
* self-contained effect. Returns the references that could not be found among the files.
|
|
93
129
|
*/
|
|
94
130
|
async function inlineExternalFiles(json, effectPath, index) {
|
|
95
131
|
const missing = [];
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
if (file) {
|
|
102
|
-
image.url = await index.read(file, readAsDataUrl);
|
|
103
|
-
}
|
|
104
|
-
else {
|
|
105
|
-
missing.push(url);
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
await QuarksLoader.resolveExternalGeometries(json, async (url) => {
|
|
109
|
-
const file = index.find(effectPath, url);
|
|
110
|
-
if (!file) {
|
|
111
|
-
missing.push(url);
|
|
112
|
-
throw new Error('not among the imported files');
|
|
113
|
-
}
|
|
114
|
-
return index.read(file, async (f) => JSON.parse(await f.text()));
|
|
132
|
+
// baseUrl is the effect file path so resolveRelativePath can walk from its folder; the
|
|
133
|
+
// File resolver ignores HTTP join and looks up files by path/name instead.
|
|
134
|
+
await QuarksLoader.resolveLinkedResources(json, {
|
|
135
|
+
baseUrl: effectPath,
|
|
136
|
+
resolver: createFileResolver(effectPath, index, missing),
|
|
115
137
|
});
|
|
116
138
|
return missing;
|
|
117
139
|
}
|
|
@@ -42,7 +42,7 @@ export function EditorWorkspace(props) {
|
|
|
42
42
|
}, children: "Opening project\u2026" }));
|
|
43
43
|
}
|
|
44
44
|
if (!project) {
|
|
45
|
-
return (_jsx(ProjectsPage, { store: store, title: hostProps.title, onOpen: (meta) => {
|
|
45
|
+
return (_jsx(ProjectsPage, { store: store, title: hostProps.title, backLink: hostProps.backLink, onOpen: (meta) => {
|
|
46
46
|
setProject(meta);
|
|
47
47
|
onProjectChange?.(meta.id);
|
|
48
48
|
} }));
|
|
@@ -4,6 +4,11 @@ export interface ProjectsPageProps {
|
|
|
4
4
|
store: ProjectStore;
|
|
5
5
|
onOpen: (project: ProjectMeta) => void;
|
|
6
6
|
title?: string;
|
|
7
|
+
/** Optional link back to the page that hosts the editor (e.g. a gallery), shown above the title. */
|
|
8
|
+
backLink?: {
|
|
9
|
+
href: string;
|
|
10
|
+
label: string;
|
|
11
|
+
};
|
|
7
12
|
}
|
|
8
13
|
/** Start page: the projects saved in the store, plus storage usage. */
|
|
9
14
|
export declare function ProjectsPage(props: ProjectsPageProps): React.ReactElement;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { FolderIcon, PlusIcon } from '@heroicons/react/24/solid';
|
|
2
|
+
import { ArrowLeftIcon, FolderIcon, PlusIcon } from '@heroicons/react/24/solid';
|
|
3
3
|
import { useCallback, useEffect, useState } from 'react';
|
|
4
4
|
import { formatBytes } from '../core/projectStore';
|
|
5
5
|
import { ChoiceDialog } from './ChoiceDialog';
|
|
@@ -31,7 +31,7 @@ function StorageBar(props) {
|
|
|
31
31
|
}
|
|
32
32
|
/** Start page: the projects saved in the store, plus storage usage. */
|
|
33
33
|
export function ProjectsPage(props) {
|
|
34
|
-
const { store, onOpen, title = 'Effect editor' } = props;
|
|
34
|
+
const { store, onOpen, title = 'Effect editor', backLink } = props;
|
|
35
35
|
const [projects, setProjects] = useState(null);
|
|
36
36
|
const [estimate, setEstimate] = useState(null);
|
|
37
37
|
const [dialog, setDialog] = useState(null);
|
|
@@ -84,7 +84,20 @@ export function ProjectsPage(props) {
|
|
|
84
84
|
display: 'flex',
|
|
85
85
|
flexDirection: 'column',
|
|
86
86
|
gap: 18,
|
|
87
|
-
}, children: [
|
|
87
|
+
}, children: [backLink && (_jsxs("a", { className: "qe-hover", href: backLink.href, style: {
|
|
88
|
+
alignSelf: 'flex-start',
|
|
89
|
+
display: 'inline-flex',
|
|
90
|
+
alignItems: 'center',
|
|
91
|
+
gap: 6,
|
|
92
|
+
background: theme.panelBg,
|
|
93
|
+
border: `1px solid ${theme.border}`,
|
|
94
|
+
borderRadius: 8,
|
|
95
|
+
padding: '6px 12px',
|
|
96
|
+
fontSize: 13,
|
|
97
|
+
fontWeight: 500,
|
|
98
|
+
color: theme.text,
|
|
99
|
+
textDecoration: 'none',
|
|
100
|
+
}, children: [_jsx(ArrowLeftIcon, { style: iconStyle(14) }), backLink.label.replace(/^←\s*/, '')] })), _jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: 12 }, children: [_jsxs("div", { children: [_jsx("div", { style: { fontSize: 22, fontWeight: 700 }, children: title }), _jsx("div", { style: { fontSize: 13, color: theme.textDim, marginTop: 4 }, children: "Projects keep their assets and scene between sessions." })] }), _jsx("span", { style: { flex: 1 } }), _jsxs("button", { className: "qe-hover", style: {
|
|
88
101
|
...buttonStyle,
|
|
89
102
|
display: 'inline-flex',
|
|
90
103
|
alignItems: 'center',
|
|
@@ -3,7 +3,7 @@ import { ApplyCollision, ApplyForce, ChangeEmitDirection, ColorBySpeed, Constant
|
|
|
3
3
|
import { buildGradient, DEFAULT_GRADIENT_STOPS, findBehavior, readGradientStops } from '../core/colors';
|
|
4
4
|
import { buildCurve, buildScalar, readPieces, readScalar } from '../core/values';
|
|
5
5
|
import { CurveEditor } from './CurveEditor';
|
|
6
|
-
import { NumberField, Row, SelectField } from './fields';
|
|
6
|
+
import { CheckboxField, NumberField, Row, SelectField } from './fields';
|
|
7
7
|
import { GradientEditor } from './GradientEditor';
|
|
8
8
|
import { ModuleSection } from './ModuleSection';
|
|
9
9
|
import { ValueField } from './ValueField';
|
|
@@ -93,7 +93,7 @@ export function OrbitOverLifeModule({ binding }) {
|
|
|
93
93
|
return (_jsx(BehaviorModule, { binding: binding, title: "Orbit over Lifetime", type: "OrbitOverLife", create: () => new OrbitOverLife(new ConstantValue(Math.PI), new Vector3(0, 1, 0)), children: (behavior) => (_jsx(ValueField, { label: "Orbit speed", generator: behavior.orbitSpeed, curveMax: Math.PI * 4, onChange: (g) => binding.apply(() => (behavior.orbitSpeed = g)) })) }));
|
|
94
94
|
}
|
|
95
95
|
export function WidthOverTrailModule({ binding }) {
|
|
96
|
-
return (_jsx(BehaviorModule, { binding: binding, title: "Width over Trail", type: "WidthOverLength", create: () => new WidthOverLength(buildScalar({ mode: 'curve', value: 1, min: 0, max: 1, curve: [1, 0.75, 0.4, 0] })), children: (behavior) => (_jsx("div", { style: { marginTop: 6 }, children: _jsx(CurveEditor, { pieces: readPieces(behavior.width), maxValue: 2, onChange: (pieces) => binding.apply(() => (behavior.width = buildCurve(pieces))) }) })) }));
|
|
96
|
+
return (_jsx(BehaviorModule, { binding: binding, title: "Width over Trail", type: "WidthOverLength", create: () => new WidthOverLength(buildScalar({ mode: 'curve', value: 1, min: 0, max: 1, curve: [1, 0.75, 0.4, 0] })), children: (behavior) => (_jsxs(_Fragment, { children: [_jsx("div", { style: { marginTop: 6 }, children: _jsx(CurveEditor, { pieces: readPieces(behavior.width), maxValue: 2, onChange: (pieces) => binding.apply(() => (behavior.width = buildCurve(pieces))) }) }), _jsx(Row, { label: "Absolute width", children: _jsx(CheckboxField, { value: behavior.absolute, onChange: (v) => binding.apply(() => (behavior.absolute = v)) }) })] })) }));
|
|
97
97
|
}
|
|
98
98
|
export function TextureSheetModule({ binding }) {
|
|
99
99
|
const system = binding.system;
|
package/dist/react/modules.js
CHANGED
|
@@ -485,6 +485,24 @@ export function RendererModule({ binding, textureOptions, geometryOptions, resol
|
|
|
485
485
|
}
|
|
486
486
|
});
|
|
487
487
|
} }), _jsx(MeshPreview, { positions: system.instancingGeometry, indices: system.getRendererSettings().instancingIndices }), preset === 'custom' && (_jsxs("span", { style: { fontSize: 10.5, color: '#9eb9ff' }, children: [vertexCount, " verts / ", triangleCount, " tris \u2014 picking a preset replaces this mesh"] }))] }) }));
|
|
488
|
+
})(), system.renderMode === RenderMode.Mesh && (_jsx(Row, { label: "Lighting", children: _jsx(SelectField, { value: system.lighting, options: [
|
|
489
|
+
{ value: 'fixed', label: 'Fixed light' },
|
|
490
|
+
{ value: 'scene', label: 'Scene light' },
|
|
491
|
+
{ value: 'unlit', label: 'Unlit' },
|
|
492
|
+
], onChange: (v) => binding.apply((s) => (s.lighting = v)) }) })), system.renderMode === RenderMode.Mesh && (_jsx(Row, { label: "Rim", children: _jsx(CheckboxField, { value: !!system.rim, onChange: (v) => binding.apply((s) => (s.rim = v ? {} : undefined)) }) })), system.renderMode === RenderMode.Mesh &&
|
|
493
|
+
system.rim &&
|
|
494
|
+
(() => {
|
|
495
|
+
const rim = system.rim;
|
|
496
|
+
const patchRim = (patch) => binding.apply((s) => (s.rim = { ...s.rim, ...patch }));
|
|
497
|
+
const setChannel = (channel, value) => {
|
|
498
|
+
const color = [...rim.color];
|
|
499
|
+
color[channel] = value;
|
|
500
|
+
patchRim({ color });
|
|
501
|
+
};
|
|
502
|
+
return (_jsxs(_Fragment, { children: [_jsx(Row, { label: "Rim mode", children: _jsx(SelectField, { value: rim.mode, options: [
|
|
503
|
+
{ value: 'add', label: 'Add (glow)' },
|
|
504
|
+
{ value: 'mix', label: 'Mix (outline)' },
|
|
505
|
+
], onChange: (mode) => patchRim({ mode }) }) }), _jsx(Row, { label: "Rim color", children: _jsx("div", { style: { display: 'flex', gap: 6 }, children: [0, 1, 2].map((channel) => (_jsx(NumberField, { value: rim.color[channel], min: 0, step: 0.05, onChange: (v) => setChannel(channel, v) }, channel))) }) }), _jsx(Row, { label: "Rim power", children: _jsx(NumberField, { value: rim.power, min: 0.01, step: 0.25, onChange: (power) => patchRim({ power }) }) }), _jsx(Row, { label: "Rim strength", children: _jsx(NumberField, { value: rim.strength, min: 0, step: 0.05, onChange: (strength) => patchRim({ strength }) }) })] }));
|
|
488
506
|
})(), _jsx(Row, { label: "Render order", children: _jsx(NumberField, { value: system.renderOrder, step: 1, onChange: (v) => binding.apply((s) => (s.renderOrder = Math.round(v))) }) }), _jsx(Row, { label: "Soft particles", children: _jsx(CheckboxField, { value: system.softParticles, onChange: (v) => binding.apply((s) => (s.softParticles = v)) }) }), system.softParticles && (_jsx(Row, { label: "Fade near/far", children: _jsxs("div", { style: { display: 'flex', gap: 6 }, children: [_jsx(NumberField, { value: system.softNearFade, min: 0, onChange: (v) => binding.apply((s) => (s.softNearFade = v)) }), _jsx(NumberField, { value: system.softFarFade, min: 0, onChange: (v) => binding.apply((s) => (s.softFarFade = v)) })] }) }))] }));
|
|
489
507
|
}
|
|
490
508
|
function removeBehavior(system, type) {
|
package/package.json
CHANGED
package/src/core/assets.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type {QuarksResourceKind, QuarksResourceResolver} from 'babylon.quarks';
|
|
1
2
|
import {QuarksLoader} from 'babylon.quarks';
|
|
2
3
|
|
|
3
4
|
/**
|
|
@@ -124,33 +125,55 @@ class ImportedFileIndex {
|
|
|
124
125
|
}
|
|
125
126
|
|
|
126
127
|
/**
|
|
127
|
-
*
|
|
128
|
-
*
|
|
128
|
+
* File-backed {@link QuarksResourceResolver}: relative image urls become data URIs and geometry
|
|
129
|
+
* urls load from the imported file set. Collects missing references into `missing`.
|
|
130
|
+
*/
|
|
131
|
+
function createFileResolver(effectPath: string, index: ImportedFileIndex, missing: string[]): QuarksResourceResolver {
|
|
132
|
+
return {
|
|
133
|
+
async resolve(url: string, kind: QuarksResourceKind, baseUrl: string): Promise<string> {
|
|
134
|
+
if (kind === 'image') {
|
|
135
|
+
if (isExternalUrl(url)) {
|
|
136
|
+
return url;
|
|
137
|
+
}
|
|
138
|
+
const file = index.find(baseUrl || effectPath, url);
|
|
139
|
+
if (!file) {
|
|
140
|
+
missing.push(url);
|
|
141
|
+
return url;
|
|
142
|
+
}
|
|
143
|
+
return index.read(file, readAsDataUrl);
|
|
144
|
+
}
|
|
145
|
+
// Geometry (and other kinds): stable path key for fetchJson.
|
|
146
|
+
const file = index.find(baseUrl || effectPath, url);
|
|
147
|
+
if (!file) {
|
|
148
|
+
missing.push(url);
|
|
149
|
+
throw new Error('not among the imported files');
|
|
150
|
+
}
|
|
151
|
+
return relativePathOf(file);
|
|
152
|
+
},
|
|
153
|
+
fetchJson(resolvedUrl: string): Promise<unknown> {
|
|
154
|
+
// resolvedUrl is relativePathOf(file) from resolve(); look up by path (empty base).
|
|
155
|
+
const file = index.find('', resolvedUrl);
|
|
156
|
+
if (!file) {
|
|
157
|
+
throw new Error(`missing file "${resolvedUrl}"`);
|
|
158
|
+
}
|
|
159
|
+
return index.read(file, async (f) => JSON.parse(await f.text()));
|
|
160
|
+
},
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Inlines what an "assets as files" export put in separate files via
|
|
166
|
+
* {@link QuarksLoader.resolveLinkedResources}, so the effect loads (and later exports) as a
|
|
129
167
|
* self-contained effect. Returns the references that could not be found among the files.
|
|
130
168
|
*/
|
|
131
169
|
async function inlineExternalFiles(json: any, effectPath: string, index: ImportedFileIndex): Promise<string[]> {
|
|
132
170
|
const missing: string[] = [];
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
if (file) {
|
|
139
|
-
image.url = await index.read(file, readAsDataUrl);
|
|
140
|
-
} else {
|
|
141
|
-
missing.push(url);
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
await QuarksLoader.resolveExternalGeometries(json, async (url) => {
|
|
146
|
-
const file = index.find(effectPath, url);
|
|
147
|
-
if (!file) {
|
|
148
|
-
missing.push(url);
|
|
149
|
-
throw new Error('not among the imported files');
|
|
150
|
-
}
|
|
151
|
-
return index.read(file, async (f) => JSON.parse(await f.text()));
|
|
171
|
+
// baseUrl is the effect file path so resolveRelativePath can walk from its folder; the
|
|
172
|
+
// File resolver ignores HTTP join and looks up files by path/name instead.
|
|
173
|
+
await QuarksLoader.resolveLinkedResources(json, {
|
|
174
|
+
baseUrl: effectPath,
|
|
175
|
+
resolver: createFileResolver(effectPath, index, missing),
|
|
152
176
|
});
|
|
153
|
-
|
|
154
177
|
return missing;
|
|
155
178
|
}
|
|
156
179
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {FolderIcon, PlusIcon} from '@heroicons/react/24/solid';
|
|
1
|
+
import {ArrowLeftIcon, FolderIcon, PlusIcon} from '@heroicons/react/24/solid';
|
|
2
2
|
import React, {useCallback, useEffect, useState} from 'react';
|
|
3
3
|
import {formatBytes, type ProjectMeta, type ProjectStore, type StorageEstimate} from '../core/projectStore';
|
|
4
4
|
import {ChoiceDialog} from './ChoiceDialog';
|
|
@@ -11,6 +11,8 @@ export interface ProjectsPageProps {
|
|
|
11
11
|
store: ProjectStore;
|
|
12
12
|
onOpen: (project: ProjectMeta) => void;
|
|
13
13
|
title?: string;
|
|
14
|
+
/** Optional link back to the page that hosts the editor (e.g. a gallery), shown above the title. */
|
|
15
|
+
backLink?: {href: string; label: string};
|
|
14
16
|
}
|
|
15
17
|
|
|
16
18
|
type Dialog =
|
|
@@ -79,7 +81,7 @@ function StorageBar(props: {estimate: StorageEstimate | null; onPersist: () => v
|
|
|
79
81
|
|
|
80
82
|
/** Start page: the projects saved in the store, plus storage usage. */
|
|
81
83
|
export function ProjectsPage(props: ProjectsPageProps): React.ReactElement {
|
|
82
|
-
const {store, onOpen, title = 'Effect editor'} = props;
|
|
84
|
+
const {store, onOpen, title = 'Effect editor', backLink} = props;
|
|
83
85
|
const [projects, setProjects] = useState<ProjectMeta[] | null>(null);
|
|
84
86
|
const [estimate, setEstimate] = useState<StorageEstimate | null>(null);
|
|
85
87
|
const [dialog, setDialog] = useState<Dialog>(null);
|
|
@@ -145,6 +147,29 @@ export function ProjectsPage(props: ProjectsPageProps): React.ReactElement {
|
|
|
145
147
|
gap: 18,
|
|
146
148
|
}}
|
|
147
149
|
>
|
|
150
|
+
{backLink && (
|
|
151
|
+
<a
|
|
152
|
+
className="qe-hover"
|
|
153
|
+
href={backLink.href}
|
|
154
|
+
style={{
|
|
155
|
+
alignSelf: 'flex-start',
|
|
156
|
+
display: 'inline-flex',
|
|
157
|
+
alignItems: 'center',
|
|
158
|
+
gap: 6,
|
|
159
|
+
background: theme.panelBg,
|
|
160
|
+
border: `1px solid ${theme.border}`,
|
|
161
|
+
borderRadius: 8,
|
|
162
|
+
padding: '6px 12px',
|
|
163
|
+
fontSize: 13,
|
|
164
|
+
fontWeight: 500,
|
|
165
|
+
color: theme.text,
|
|
166
|
+
textDecoration: 'none',
|
|
167
|
+
}}
|
|
168
|
+
>
|
|
169
|
+
<ArrowLeftIcon style={iconStyle(14)} />
|
|
170
|
+
{backLink.label.replace(/^←\s*/, '')}
|
|
171
|
+
</a>
|
|
172
|
+
)}
|
|
148
173
|
<div style={{display: 'flex', alignItems: 'center', gap: 12}}>
|
|
149
174
|
<div>
|
|
150
175
|
<div style={{fontSize: 22, fontWeight: 700}}>{title}</div>
|
|
@@ -28,7 +28,7 @@ import {EffectBinding} from '../core/binding';
|
|
|
28
28
|
import {buildGradient, DEFAULT_GRADIENT_STOPS, findBehavior, readGradientStops} from '../core/colors';
|
|
29
29
|
import {buildCurve, buildScalar, readPieces, readScalar} from '../core/values';
|
|
30
30
|
import {CurveEditor} from './CurveEditor';
|
|
31
|
-
import {NumberField, Row, SelectField} from './fields';
|
|
31
|
+
import {CheckboxField, NumberField, Row, SelectField} from './fields';
|
|
32
32
|
import {GradientEditor} from './GradientEditor';
|
|
33
33
|
import {ModuleSection} from './ModuleSection';
|
|
34
34
|
import {ValueField} from './ValueField';
|
|
@@ -614,13 +614,22 @@ export function WidthOverTrailModule({binding}: ModuleProps) {
|
|
|
614
614
|
}
|
|
615
615
|
>
|
|
616
616
|
{(behavior) => (
|
|
617
|
-
|
|
618
|
-
<
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
617
|
+
<>
|
|
618
|
+
<div style={{marginTop: 6}}>
|
|
619
|
+
<CurveEditor
|
|
620
|
+
pieces={readPieces(behavior.width as never)}
|
|
621
|
+
maxValue={2}
|
|
622
|
+
onChange={(pieces) => binding.apply(() => (behavior.width = buildCurve(pieces) as never))}
|
|
623
|
+
/>
|
|
624
|
+
</div>
|
|
625
|
+
{/* Off: the curve multiplies the particle size. On: it is the width in world units (files from 0.21 and earlier). */}
|
|
626
|
+
<Row label="Absolute width">
|
|
627
|
+
<CheckboxField
|
|
628
|
+
value={behavior.absolute}
|
|
629
|
+
onChange={(v) => binding.apply(() => (behavior.absolute = v))}
|
|
630
|
+
/>
|
|
631
|
+
</Row>
|
|
632
|
+
</>
|
|
624
633
|
)}
|
|
625
634
|
</BehaviorModule>
|
|
626
635
|
);
|
package/src/react/modules.tsx
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {Texture} from '@babylonjs/core/Materials/Textures/texture';
|
|
2
2
|
import {Mesh} from '@babylonjs/core/Meshes/mesh';
|
|
3
3
|
import {PlusIcon, XMarkIcon} from '@heroicons/react/24/solid';
|
|
4
|
-
import type {Behavior, ParticleSystem} from 'babylon.quarks';
|
|
4
|
+
import type {Behavior, MeshLighting, MeshRim, ParticleSystem} from 'babylon.quarks';
|
|
5
5
|
import {
|
|
6
6
|
ColorOverLife,
|
|
7
7
|
ConstantColor,
|
|
@@ -1122,6 +1122,81 @@ export function RendererModule({
|
|
|
1122
1122
|
</Row>
|
|
1123
1123
|
);
|
|
1124
1124
|
})()}
|
|
1125
|
+
{system.renderMode === RenderMode.Mesh && (
|
|
1126
|
+
<Row label="Lighting">
|
|
1127
|
+
<SelectField<MeshLighting>
|
|
1128
|
+
value={system.lighting}
|
|
1129
|
+
options={[
|
|
1130
|
+
{value: 'fixed', label: 'Fixed light'},
|
|
1131
|
+
{value: 'scene', label: 'Scene light'},
|
|
1132
|
+
{value: 'unlit', label: 'Unlit'},
|
|
1133
|
+
]}
|
|
1134
|
+
onChange={(v) => binding.apply((s) => (s.lighting = v))}
|
|
1135
|
+
/>
|
|
1136
|
+
</Row>
|
|
1137
|
+
)}
|
|
1138
|
+
{system.renderMode === RenderMode.Mesh && (
|
|
1139
|
+
<Row label="Rim">
|
|
1140
|
+
<CheckboxField
|
|
1141
|
+
value={!!system.rim}
|
|
1142
|
+
onChange={(v) => binding.apply((s) => (s.rim = v ? {} : undefined))}
|
|
1143
|
+
/>
|
|
1144
|
+
</Row>
|
|
1145
|
+
)}
|
|
1146
|
+
{system.renderMode === RenderMode.Mesh &&
|
|
1147
|
+
system.rim &&
|
|
1148
|
+
(() => {
|
|
1149
|
+
const rim = system.rim;
|
|
1150
|
+
const patchRim = (patch: Partial<MeshRim>) => binding.apply((s) => (s.rim = {...s.rim, ...patch}));
|
|
1151
|
+
const setChannel = (channel: number, value: number) => {
|
|
1152
|
+
const color: [number, number, number] = [...rim.color];
|
|
1153
|
+
color[channel] = value;
|
|
1154
|
+
patchRim({color});
|
|
1155
|
+
};
|
|
1156
|
+
return (
|
|
1157
|
+
<>
|
|
1158
|
+
<Row label="Rim mode">
|
|
1159
|
+
<SelectField<MeshRim['mode']>
|
|
1160
|
+
value={rim.mode}
|
|
1161
|
+
options={[
|
|
1162
|
+
{value: 'add', label: 'Add (glow)'},
|
|
1163
|
+
{value: 'mix', label: 'Mix (outline)'},
|
|
1164
|
+
]}
|
|
1165
|
+
onChange={(mode) => patchRim({mode})}
|
|
1166
|
+
/>
|
|
1167
|
+
</Row>
|
|
1168
|
+
<Row label="Rim color">
|
|
1169
|
+
<div style={{display: 'flex', gap: 6}}>
|
|
1170
|
+
{[0, 1, 2].map((channel) => (
|
|
1171
|
+
<NumberField
|
|
1172
|
+
key={channel}
|
|
1173
|
+
value={rim.color[channel]}
|
|
1174
|
+
min={0}
|
|
1175
|
+
step={0.05}
|
|
1176
|
+
onChange={(v) => setChannel(channel, v)}
|
|
1177
|
+
/>
|
|
1178
|
+
))}
|
|
1179
|
+
</div>
|
|
1180
|
+
</Row>
|
|
1181
|
+
<Row label="Rim power">
|
|
1182
|
+
<NumberField
|
|
1183
|
+
value={rim.power}
|
|
1184
|
+
min={0.01}
|
|
1185
|
+
step={0.25}
|
|
1186
|
+
onChange={(power) => patchRim({power})}
|
|
1187
|
+
/>
|
|
1188
|
+
</Row>
|
|
1189
|
+
<Row label="Rim strength">
|
|
1190
|
+
<NumberField
|
|
1191
|
+
value={rim.strength}
|
|
1192
|
+
min={0}
|
|
1193
|
+
step={0.05}
|
|
1194
|
+
onChange={(strength) => patchRim({strength})}
|
|
1195
|
+
/>
|
|
1196
|
+
</Row>
|
|
1197
|
+
</>
|
|
1198
|
+
);
|
|
1199
|
+
})()}
|
|
1125
1200
|
<Row label="Render order">
|
|
1126
1201
|
<NumberField
|
|
1127
1202
|
value={system.renderOrder}
|