From f102d4818a3891b05bff67a077c16f8163733e2e Mon Sep 17 00:00:00 2001 From: Mikalai Lazitski Date: Fri, 5 Dec 2025 15:58:41 +0300 Subject: [PATCH 01/62] feat: implement FX Editor with toolbar, layout, and preview components --- editor/src/editor/layout/toolbar.tsx | 10 + .../editor/windows/fx-editor/animation.tsx | 17 ++ editor/src/editor/windows/fx-editor/graph.tsx | 17 ++ editor/src/editor/windows/fx-editor/index.tsx | 85 ++++++++ .../src/editor/windows/fx-editor/layout.tsx | 148 ++++++++++++++ .../src/editor/windows/fx-editor/preview.tsx | 188 ++++++++++++++++++ .../editor/windows/fx-editor/properties.tsx | 17 ++ .../src/editor/windows/fx-editor/toolbar.tsx | 109 ++++++++++ 8 files changed, 591 insertions(+) create mode 100644 editor/src/editor/windows/fx-editor/animation.tsx create mode 100644 editor/src/editor/windows/fx-editor/graph.tsx create mode 100644 editor/src/editor/windows/fx-editor/index.tsx create mode 100644 editor/src/editor/windows/fx-editor/layout.tsx create mode 100644 editor/src/editor/windows/fx-editor/preview.tsx create mode 100644 editor/src/editor/windows/fx-editor/properties.tsx create mode 100644 editor/src/editor/windows/fx-editor/toolbar.tsx diff --git a/editor/src/editor/layout/toolbar.tsx b/editor/src/editor/layout/toolbar.tsx index 7e23c7a7a..aa07469d4 100644 --- a/editor/src/editor/layout/toolbar.tsx +++ b/editor/src/editor/layout/toolbar.tsx @@ -205,6 +205,12 @@ export class EditorToolbar extends Component { Window + this._handleOpenFXEditor()}> + FX Editor... + + + + ipcRenderer.send("window:minimize")}> Minimize CTRL+M @@ -258,4 +264,8 @@ export class EditorToolbar extends Component { const p = await execNodePty(`code "${join(dirname(this.props.editor.state.projectPath), "/")}"`); await p.wait(); } + + private _handleOpenFXEditor(): void { + ipcRenderer.send("window:open", "build/src/editor/windows/fx-editor", {}); + } } diff --git a/editor/src/editor/windows/fx-editor/animation.tsx b/editor/src/editor/windows/fx-editor/animation.tsx new file mode 100644 index 000000000..bd1dd5fc5 --- /dev/null +++ b/editor/src/editor/windows/fx-editor/animation.tsx @@ -0,0 +1,17 @@ +import { Component, ReactNode } from "react"; + +export interface IFXEditorAnimationProps { + filePath: string | null; +} + +export class FXEditorAnimation extends Component { + public render(): ReactNode { + return ( +
+
Animation Panel
+
Animation timeline will be displayed here
+
+ ); + } +} + diff --git a/editor/src/editor/windows/fx-editor/graph.tsx b/editor/src/editor/windows/fx-editor/graph.tsx new file mode 100644 index 000000000..ba9241912 --- /dev/null +++ b/editor/src/editor/windows/fx-editor/graph.tsx @@ -0,0 +1,17 @@ +import { Component, ReactNode } from "react"; + +export interface IFXEditorGraphProps { + filePath: string | null; +} + +export class FXEditorGraph extends Component { + public render(): ReactNode { + return ( +
+
Particles Graph
+
Particle systems will be displayed here
+
+ ); + } +} + diff --git a/editor/src/editor/windows/fx-editor/index.tsx b/editor/src/editor/windows/fx-editor/index.tsx new file mode 100644 index 000000000..32d7bfa54 --- /dev/null +++ b/editor/src/editor/windows/fx-editor/index.tsx @@ -0,0 +1,85 @@ +import { ipcRenderer } from "electron"; +import { readJSON, writeJSON } from "fs-extra"; + +import { toast } from "sonner"; + +import { Component, ReactNode } from "react"; + +import { Toaster } from "../../../ui/shadcn/ui/sonner"; + +import { FXEditorLayout } from "./layout"; +import { FXEditorToolbar } from "./toolbar"; + +export interface IFXEditorWindowProps { + filePath?: string; +} + +export interface IFXEditorWindowState { + filePath: string | null; +} + +export default class FXEditorWindow extends Component { + public constructor(props: IFXEditorWindowProps) { + super(props); + + this.state = { + filePath: props.filePath || null, + }; + } + + public render(): ReactNode { + return ( + <> +
+ + +
+ +
+
+ + + + ); + } + + public async componentDidMount(): Promise { + ipcRenderer.on("save", () => this.save()); + ipcRenderer.on("editor:close-window", () => this.close()); + } + + public close(): void { + ipcRenderer.send("window:close"); + } + + public async loadFile(filePath: string): Promise { + this.setState({ filePath }); + // TODO: Load file data into editor + } + + public async save(): Promise { + if (!this.state.filePath) { + return; + } + + try { + const data = await readJSON(this.state.filePath); + await writeJSON(this.state.filePath, data, { spaces: 4 }); + toast.success("FX saved"); + ipcRenderer.send("editor:asset-updated", "fx", data); + } catch (error) { + toast.error("Failed to save FX"); + } + } + + public async saveAs(filePath: string): Promise { + this.setState({ filePath }); + await this.save(); + } + + public async importFile(filePath: string): Promise { + // TODO: Import file data into current editor + toast.success("FX imported"); + } +} + diff --git a/editor/src/editor/windows/fx-editor/layout.tsx b/editor/src/editor/windows/fx-editor/layout.tsx new file mode 100644 index 000000000..1f2febcaa --- /dev/null +++ b/editor/src/editor/windows/fx-editor/layout.tsx @@ -0,0 +1,148 @@ +import { Component, ReactNode } from "react"; +import { IJsonModel, Layout, Model, TabNode } from "flexlayout-react"; + +import { waitNextAnimationFrame } from "../../../tools/tools"; + +import { FXEditorPreview } from "./preview"; +import { FXEditorGraph } from "./graph"; +import { FXEditorAnimation } from "./animation"; +import { FXEditorProperties } from "./properties"; + +const layoutModel: IJsonModel = { + global: { + tabSetEnableMaximize: true, + tabEnableRename: false, + tabSetMinHeight: 50, + tabSetMinWidth: 240, + enableEdgeDock: false, + }, + layout: { + type: "row", + width: 100, + height: 100, + children: [ + { + type: "row", + weight: 75, + children: [ + { + type: "tabset", + weight: 75, + children: [ + { + type: "tab", + id: "preview", + name: "Preview", + component: "preview", + enableClose: false, + enableRenderOnDemand: false, + }, + ], + }, + { + type: "tabset", + weight: 25, + children: [ + { + type: "tab", + id: "animation", + name: "Animation", + component: "animation", + enableClose: false, + enableRenderOnDemand: false, + }, + ], + }, + ], + }, + { + type: "row", + weight: 25, + children: [ + { + type: "tabset", + weight: 40, + children: [ + { + type: "tab", + id: "graph", + name: "Particles", + component: "graph", + enableClose: false, + enableRenderOnDemand: false, + }, + ], + }, + { + type: "tabset", + weight: 60, + children: [ + { + type: "tab", + id: "properties", + name: "Properties", + component: "properties", + enableClose: false, + enableRenderOnDemand: false, + }, + ], + }, + ], + }, + ], + }, +}; + +export interface IFXEditorLayoutProps { + filePath: string | null; +} + +export class FXEditorLayout extends Component { + public preview: FXEditorPreview; + public graph: FXEditorGraph; + public animation: FXEditorAnimation; + public properties: FXEditorProperties; + + private _layoutRef: Layout | null = null; + private _model: Model = Model.fromJson(layoutModel as any); + + private _components: Record = {}; + + public constructor(props: IFXEditorLayoutProps) { + super(props); + + this._components = { + preview: (this.preview = r!)} filePath={this.props.filePath} />, + graph: (this.graph = r!)} filePath={this.props.filePath} />, + animation: (this.animation = r!)} filePath={this.props.filePath} />, + properties: (this.properties = r!)} filePath={this.props.filePath} />, + }; + } + + public render(): ReactNode { + return ( +
+ (this._layoutRef = r)} factory={(n) => this._layoutFactory(n)} /> +
+ ); + } + + private _layoutFactory(node: TabNode): ReactNode { + const componentName = node.getComponent(); + if (!componentName) { + return
Error, see console...
; + } + + const component = this._components[componentName]; + if (!component) { + return
Error, see console...
; + } + + node.setEventListener("resize", () => { + waitNextAnimationFrame().then(() => this.preview?.resize()); + }); + + return component; + } +} + diff --git a/editor/src/editor/windows/fx-editor/preview.tsx b/editor/src/editor/windows/fx-editor/preview.tsx new file mode 100644 index 000000000..5c03ed24a --- /dev/null +++ b/editor/src/editor/windows/fx-editor/preview.tsx @@ -0,0 +1,188 @@ +import { Component, ReactNode } from "react"; + +import { Engine, Scene, ArcRotateCamera, DirectionalLight, Vector3, Color3, Color4, MeshBuilder } from "babylonjs"; +import { GridMaterial } from "babylonjs-materials"; + +import { Button } from "../../../ui/shadcn/ui/button"; +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "../../../ui/shadcn/ui/tooltip"; + +import { IoPlay, IoStop, IoRefresh } from "react-icons/io5"; + +export interface IFXEditorPreviewProps { + filePath: string | null; +} + +export interface IFXEditorPreviewState { + playing: boolean; +} + +export class FXEditorPreview extends Component { + public engine: Engine | null = null; + public scene: Scene | null = null; + public camera: ArcRotateCamera | null = null; + + private _canvasRef: HTMLCanvasElement | null = null; + private _renderLoopId: number = -1; + + public constructor(props: IFXEditorPreviewProps) { + super(props); + + this.state = { + playing: false, + }; + } + + public render(): ReactNode { + return ( +
+ this._onGotCanvasRef(r!)} + className="w-full h-full outline-none" + /> + + {/* Play/Stop/Restart buttons */} +
+ + + + + + {this.state.playing ? "Stop" : "Play"} + + + {this.state.playing && ( + + + + + Restart + + )} + +
+
+ ); + } + + public componentDidMount(): void { + // Canvas ref will be set in render, _onGotCanvasRef will be called automatically + } + + public componentWillUnmount(): void { + if (this._renderLoopId !== -1) { + cancelAnimationFrame(this._renderLoopId); + } + + this.scene?.dispose(); + this.engine?.dispose(); + } + + /** + * Resizes the engine. + */ + public resize(): void { + this.engine?.resize(); + } + + private async _onGotCanvasRef(canvas: HTMLCanvasElement | null): Promise { + if (!canvas || this.engine) { + return; + } + + this._canvasRef = canvas; + + this.engine = new Engine(canvas, true, { + antialias: true, + adaptToDeviceRatio: true, + }); + + this.scene = new Scene(this.engine); + + // Scene settings + this.scene.clearColor = new Color4(0.1, 0.1, 0.1, 1.0); + this.scene.ambientColor = new Color3(1, 1, 1); + + // Camera + this.camera = new ArcRotateCamera("Camera", 0, 0.8, 4, Vector3.Zero(), this.scene); + this.camera.doNotSerialize = true; + this.camera.lowerRadiusLimit = 3; + this.camera.upperRadiusLimit = 10; + this.camera.wheelPrecision = 20; + this.camera.minZ = 0.001; + this.camera.attachControl(canvas, true); + this.camera.useFramingBehavior = true; + this.camera.wheelDeltaPercentage = 0.01; + this.camera.pinchDeltaPercentage = 0.01; + + // Directional light (sun) + const sunLight = new DirectionalLight("sun", new Vector3(-1, -1, -1), this.scene); + sunLight.intensity = 1.0; + sunLight.diffuse = new Color3(1, 1, 1); + sunLight.specular = new Color3(1, 1, 1); + + // Ground with grid material + const groundMaterial = new GridMaterial("groundMaterial", this.scene); + groundMaterial.majorUnitFrequency = 2; + groundMaterial.minorUnitVisibility = 0.1; + groundMaterial.gridRatio = 0.5; + groundMaterial.backFaceCulling = false; + groundMaterial.mainColor = new Color3(1, 1, 1); + groundMaterial.lineColor = new Color3(1.0, 1.0, 1.0); + groundMaterial.opacity = 0.5; + + const ground = MeshBuilder.CreateGround("ground", { width: 100, height: 100 }, this.scene); + ground.material = groundMaterial; + + // Render loop + this.engine.runRenderLoop(() => { + if (this.scene) { + this.scene.render(); + } + }); + + // Handle resize + window.addEventListener("resize", () => { + this.engine?.resize(); + }); + + this.forceUpdate(); + } + + private _handlePlayStop(): void { + this.setState({ playing: !this.state.playing }); + } + + private _handleRestart(): void { + if (!this.scene) { + return; + } + + // Restart all particle systems + this.scene.particleSystems.forEach((ps) => { + ps.reset(); + }); + + this.setState({ playing: false }, () => { + this.setState({ playing: true }); + }); + } +} + diff --git a/editor/src/editor/windows/fx-editor/properties.tsx b/editor/src/editor/windows/fx-editor/properties.tsx new file mode 100644 index 000000000..6f98b9362 --- /dev/null +++ b/editor/src/editor/windows/fx-editor/properties.tsx @@ -0,0 +1,17 @@ +import { Component, ReactNode } from "react"; + +export interface IFXEditorPropertiesProps { + filePath: string | null; +} + +export class FXEditorProperties extends Component { + public render(): ReactNode { + return ( +
+
Properties
+
Properties will be displayed here
+
+ ); + } +} + diff --git a/editor/src/editor/windows/fx-editor/toolbar.tsx b/editor/src/editor/windows/fx-editor/toolbar.tsx new file mode 100644 index 000000000..ef68675a9 --- /dev/null +++ b/editor/src/editor/windows/fx-editor/toolbar.tsx @@ -0,0 +1,109 @@ +import { Component, ReactNode } from "react"; + +import { Menubar, MenubarContent, MenubarItem, MenubarMenu, MenubarSeparator, MenubarShortcut, MenubarTrigger } from "../../../ui/shadcn/ui/menubar"; + +import { openSingleFileDialog, saveSingleFileDialog } from "../../../tools/dialog"; +import { ToolbarComponent } from "../../../ui/toolbar"; + +import FXEditorWindow from "./index"; + +export interface IFXEditorToolbarProps { + fxEditor: FXEditorWindow; +} + +export class FXEditorToolbar extends Component { + public render(): ReactNode { + return ( + + + + + {/* File */} + + File + + this._handleOpen()}> + Open... CTRL+O + + + + + this._handleSave()}> + Save CTRL+S + + + this._handleSaveAs()}> + Save As... CTRL+SHIFT+S + + + + + this._handleImport()}>Import... + + + + +
+
+ FX Editor + {this.props.fxEditor.state.filePath && ( +
+ (...{this.props.fxEditor.state.filePath.substring(this.props.fxEditor.state.filePath.length - 30)}) +
+ )} +
+
+
+ ); + } + + private _handleOpen(): void { + const file = openSingleFileDialog({ + title: "Open FX File", + filters: [{ name: "FX Files", extensions: ["fx", "json"] }], + }); + + if (!file) { + return; + } + + this.props.fxEditor.loadFile(file); + } + + private _handleSave(): void { + if (!this.props.fxEditor.state.filePath) { + this._handleSaveAs(); + return; + } + + this.props.fxEditor.save(); + } + + private _handleSaveAs(): void { + const file = saveSingleFileDialog({ + title: "Save FX File", + filters: [{ name: "FX Files", extensions: ["fx", "json"] }], + defaultPath: this.props.fxEditor.state.filePath || "untitled.fx", + }); + + if (!file) { + return; + } + + this.props.fxEditor.saveAs(file); + } + + private _handleImport(): void { + const file = openSingleFileDialog({ + title: "Import FX File", + filters: [{ name: "FX Files", extensions: ["fx", "json"] }], + }); + + if (!file) { + return; + } + + this.props.fxEditor.importFile(file); + } +} + From b3e0158bcd43b4c6284bd2521e3fce5bef20f99b Mon Sep 17 00:00:00 2001 From: Mikalai Lazitski Date: Fri, 5 Dec 2025 16:38:51 +0300 Subject: [PATCH 02/62] feat: enhance FX Editor Graph with particle management features including add, delete, and context menu interactions --- editor/src/editor/windows/fx-editor/graph.tsx | 204 +++++++++++++++++- 1 file changed, 199 insertions(+), 5 deletions(-) diff --git a/editor/src/editor/windows/fx-editor/graph.tsx b/editor/src/editor/windows/fx-editor/graph.tsx index ba9241912..1780f92fd 100644 --- a/editor/src/editor/windows/fx-editor/graph.tsx +++ b/editor/src/editor/windows/fx-editor/graph.tsx @@ -1,17 +1,211 @@ import { Component, ReactNode } from "react"; +import { Tree, TreeNodeInfo } from "@blueprintjs/core"; + +import { AiOutlinePlus, AiOutlineClose } from "react-icons/ai"; +import { IoSparklesSharp } from "react-icons/io5"; + +import { + ContextMenu, + ContextMenuContent, + ContextMenuItem, + ContextMenuTrigger, + ContextMenuSeparator, + ContextMenuSub, + ContextMenuSubTrigger, + ContextMenuSubContent, +} from "../../../ui/shadcn/ui/context-menu"; export interface IFXEditorGraphProps { filePath: string | null; } -export class FXEditorGraph extends Component { +export interface IFXParticleNode { + id: string; + name: string; +} + +export interface IFXEditorGraphState { + nodes: TreeNodeInfo[]; +} + +export class FXEditorGraph extends Component { + public constructor(props: IFXEditorGraphProps) { + super(props); + + this.state = { + nodes: [], + }; + } + public render(): ReactNode { return ( -
-
Particles Graph
-
Particle systems will be displayed here
+
+ {this.state.nodes.length > 0 && ( +
+ this._handleNodeExpanded(n)} + onNodeCollapse={(n) => this._handleNodeCollapsed(n)} + onNodeClick={(n) => this._handleNodeClicked(n)} + /> +
+ )} + +
ev.preventDefault()} + > + + +
+ {this.state.nodes.length === 0 && ( +
No particles. Right-click to add.
+ )} +
+
+ + + + Add + + + this._handleAddParticles()}> + Particle + + + + +
+
); } -} + private _handleNodeExpanded(node: TreeNodeInfo): void { + const nodeId = node.id; + const nodes = this._updateNodeExpanded(this.state.nodes, nodeId as string | number, true); + this.setState({ nodes }); + } + + private _handleNodeCollapsed(node: TreeNodeInfo): void { + const nodeId = node.id; + const nodes = this._updateNodeExpanded(this.state.nodes, nodeId as string | number, false); + this.setState({ nodes }); + } + + private _updateNodeExpanded(nodes: TreeNodeInfo[], nodeId: string | number, isExpanded: boolean): TreeNodeInfo[] { + return nodes.map((n) => { + const nodeName = this._getNodeName(n); + if (n.id === nodeId) { + return { + ...n, + label: this._getNodeLabelComponent(n, nodeName), + isExpanded, + childNodes: n.childNodes ? this._updateNodeExpanded(n.childNodes, nodeId, isExpanded) : undefined, + }; + } + const childNodes = n.childNodes ? this._updateNodeExpanded(n.childNodes, nodeId, isExpanded) : undefined; + return { + ...n, + label: this._getNodeLabelComponent(n, nodeName), + childNodes, + }; + }); + } + + private _getNodeName(node: TreeNodeInfo): string { + // Extract name from label - if it's a React element, try to get text content + if (typeof node.label === "string") { + return node.label; + } + // Default name for particles + return "Particle"; + } + + private _handleNodeClicked(node: TreeNodeInfo): void { + const nodes = this._updateNodeSelection(this.state.nodes, node.id as string | number); + this.setState({ nodes }); + } + + + private _updateNodeSelection(nodes: TreeNodeInfo[], selectedId: string | number): TreeNodeInfo[] { + return nodes.map((n) => { + const nodeName = this._getNodeName(n); + const isSelected = n.id === selectedId; + const childNodes = n.childNodes ? this._updateNodeSelection(n.childNodes, selectedId) : undefined; + return { + ...n, + label: this._getNodeLabelComponent(n, nodeName), + isSelected, + childNodes, + }; + }); + } + + private _getNodeLabelComponent(node: TreeNodeInfo, name: string): JSX.Element { + const label =
{name}
; + + return ( + + {label} + + + + Add + + + this._handleAddParticles()}> + Particle + + + + + this._handleDeleteNode(node)} + > + Delete + + + + ); + } + + private _handleAddParticles(): void { + const nodeId = `particle-${Date.now()}`; + const newNode: TreeNodeInfo = { + id: nodeId, + label: this._getNodeLabelComponent({ id: nodeId } as TreeNodeInfo, "Particle"), + icon: , + isExpanded: false, + childNodes: undefined, + isSelected: false, + hasCaret: false, + }; + + this.setState({ + nodes: [...this.state.nodes, newNode], + }); + } + + private _handleDeleteNode(node: TreeNodeInfo): void { + const deleteNodeById = (nodes: TreeNodeInfo[], id: string | number): TreeNodeInfo[] => { + return nodes + .filter((n) => n.id !== id) + .map((n) => { + if (n.childNodes) { + return { + ...n, + childNodes: deleteNodeById(n.childNodes, id), + }; + } + return n; + }); + }; + + this.setState({ + nodes: deleteNodeById(this.state.nodes, node.id!), + }); + } +} From 4d9f043acbaabd5da00ad6fb4ac41c6130a10130 Mon Sep 17 00:00:00 2001 From: Mikalai Lazitski Date: Sat, 6 Dec 2025 21:44:44 +0300 Subject: [PATCH 03/62] feat: enhance FX Editor with particle properties management, including initialization, emission, and behaviors --- editor/src/editor/layout/toolbar.tsx | 5 +- editor/src/editor/windows/fx-editor/graph.tsx | 20 +- editor/src/editor/windows/fx-editor/index.tsx | 10 + .../src/editor/windows/fx-editor/layout.tsx | 36 ++- .../editor/windows/fx-editor/properties.tsx | 68 +++++- .../fx-editor/properties/behaviors.tsx | 208 +++++++++++++++++ .../windows/fx-editor/properties/data.ts | 79 +++++++ .../windows/fx-editor/properties/emission.tsx | 84 +++++++ .../fx-editor/properties/emitter-shape.tsx | 212 +++++++++++++++++ .../windows/fx-editor/properties/object.tsx | 26 +++ .../properties/particle-initialization.tsx | 50 ++++ .../properties/particle-renderer.tsx | 219 ++++++++++++++++++ .../windows/fx-editor/properties/types.ts | 62 +++++ 13 files changed, 1065 insertions(+), 14 deletions(-) create mode 100644 editor/src/editor/windows/fx-editor/properties/behaviors.tsx create mode 100644 editor/src/editor/windows/fx-editor/properties/data.ts create mode 100644 editor/src/editor/windows/fx-editor/properties/emission.tsx create mode 100644 editor/src/editor/windows/fx-editor/properties/emitter-shape.tsx create mode 100644 editor/src/editor/windows/fx-editor/properties/object.tsx create mode 100644 editor/src/editor/windows/fx-editor/properties/particle-initialization.tsx create mode 100644 editor/src/editor/windows/fx-editor/properties/particle-renderer.tsx create mode 100644 editor/src/editor/windows/fx-editor/properties/types.ts diff --git a/editor/src/editor/layout/toolbar.tsx b/editor/src/editor/layout/toolbar.tsx index aa07469d4..c0e7ed99f 100644 --- a/editor/src/editor/layout/toolbar.tsx +++ b/editor/src/editor/layout/toolbar.tsx @@ -15,6 +15,7 @@ import { ToolbarComponent } from "../../ui/toolbar"; import { saveProject } from "../../project/save/save"; import { startProjectDevProcess } from "../../project/run"; import { exportProject } from "../../project/export/export"; +import { projectConfiguration } from "../../project/configuration"; import { Editor } from "../main"; import { getNodeCommands } from "../dialogs/command-palette/node"; @@ -266,6 +267,8 @@ export class EditorToolbar extends Component { } private _handleOpenFXEditor(): void { - ipcRenderer.send("window:open", "build/src/editor/windows/fx-editor", {}); + ipcRenderer.send("window:open", "build/src/editor/windows/fx-editor", { + projectConfiguration: { ...projectConfiguration }, + }); } } diff --git a/editor/src/editor/windows/fx-editor/graph.tsx b/editor/src/editor/windows/fx-editor/graph.tsx index 1780f92fd..5a8cf1a19 100644 --- a/editor/src/editor/windows/fx-editor/graph.tsx +++ b/editor/src/editor/windows/fx-editor/graph.tsx @@ -17,6 +17,7 @@ import { export interface IFXEditorGraphProps { filePath: string | null; + onNodeSelected?: (nodeId: string | number | null) => void; } export interface IFXParticleNode { @@ -26,6 +27,7 @@ export interface IFXParticleNode { export interface IFXEditorGraphState { nodes: TreeNodeInfo[]; + selectedNodeId: string | number | null; } export class FXEditorGraph extends Component { @@ -34,6 +36,7 @@ export class FXEditorGraph extends Component { ipcRenderer.on("save", () => this.save()); ipcRenderer.on("editor:close-window", () => this.close()); + + // Set project configuration if provided + if (this.props.projectConfiguration) { + projectConfiguration.path = this.props.projectConfiguration.path; + projectConfiguration.compressedTexturesEnabled = this.props.projectConfiguration.compressedTexturesEnabled; + onProjectConfigurationChangedObservable.notifyObservers(projectConfiguration); + } } public close(): void { diff --git a/editor/src/editor/windows/fx-editor/layout.tsx b/editor/src/editor/windows/fx-editor/layout.tsx index 1f2febcaa..96944bc2d 100644 --- a/editor/src/editor/windows/fx-editor/layout.tsx +++ b/editor/src/editor/windows/fx-editor/layout.tsx @@ -97,13 +97,16 @@ export interface IFXEditorLayoutProps { filePath: string | null; } -export class FXEditorLayout extends Component { +export interface IFXEditorLayoutState { + selectedNodeId: string | number | null; +} + +export class FXEditorLayout extends Component { public preview: FXEditorPreview; public graph: FXEditorGraph; public animation: FXEditorAnimation; public properties: FXEditorProperties; - private _layoutRef: Layout | null = null; private _model: Model = Model.fromJson(layoutModel as any); private _components: Record = {}; @@ -111,18 +114,41 @@ export class FXEditorLayout extends Component { public constructor(props: IFXEditorLayoutProps) { super(props); + this.state = { + selectedNodeId: null, + }; + } + + public componentDidMount(): void { + this._updateComponents(); + } + + public componentDidUpdate(): void { + this._updateComponents(); + } + + private _handleNodeSelected = (nodeId: string | number | null): void => { + this.setState({ selectedNodeId: nodeId }, () => { + // Force update properties component after state change + if (this.properties) { + this.properties.forceUpdate(); + } + }); + }; + + private _updateComponents(): void { this._components = { preview: (this.preview = r!)} filePath={this.props.filePath} />, - graph: (this.graph = r!)} filePath={this.props.filePath} />, + graph: (this.graph = r!)} filePath={this.props.filePath} onNodeSelected={this._handleNodeSelected} />, animation: (this.animation = r!)} filePath={this.props.filePath} />, - properties: (this.properties = r!)} filePath={this.props.filePath} />, + properties: (this.properties = r!)} filePath={this.props.filePath} selectedNodeId={this.state.selectedNodeId} scene={this.preview?.scene || undefined} />, }; } public render(): ReactNode { return (
- (this._layoutRef = r)} factory={(n) => this._layoutFactory(n)} /> + this._layoutFactory(n)} />
); } diff --git a/editor/src/editor/windows/fx-editor/properties.tsx b/editor/src/editor/windows/fx-editor/properties.tsx index 6f98b9362..7b4958726 100644 --- a/editor/src/editor/windows/fx-editor/properties.tsx +++ b/editor/src/editor/windows/fx-editor/properties.tsx @@ -1,17 +1,75 @@ import { Component, ReactNode } from "react"; +import { Scene } from "babylonjs"; + +import { EditorInspectorSectionField } from "../../layout/inspector/fields/section"; + +import { FXEditorObjectProperties } from "./properties/object"; +import { FXEditorEmitterShapeProperties } from "./properties/emitter-shape"; +import { FXEditorParticleRendererProperties } from "./properties/particle-renderer"; +import { FXEditorEmissionProperties } from "./properties/emission"; +import { FXEditorParticleInitializationProperties } from "./properties/particle-initialization"; +import { FXEditorBehaviorsProperties, FXEditorBehaviorsDropdown } from "./properties/behaviors"; +import { getOrCreateParticleData } from "./properties/data"; export interface IFXEditorPropertiesProps { filePath: string | null; + selectedNodeId: string | number | null; + scene?: Scene; } -export class FXEditorProperties extends Component { +export interface IFXEditorPropertiesState {} + +export class FXEditorProperties extends Component { + public constructor(props: IFXEditorPropertiesProps) { + super(props); + this.state = {}; + } + public render(): ReactNode { + if (!this.props.selectedNodeId) { + return ( +
+

No particle selected

+
+ ); + } + + const particleData = getOrCreateParticleData(this.props.selectedNodeId); + return ( -
-
Properties
-
Properties will be displayed here
+
+ + + + + + this.forceUpdate()} /> + + + + this.forceUpdate()} /> + + + + this.forceUpdate()} /> + + + + + + + + Behaviors + this.forceUpdate()} /> +
+ } + > + this.forceUpdate()} /> +
); } -} +} diff --git a/editor/src/editor/windows/fx-editor/properties/behaviors.tsx b/editor/src/editor/windows/fx-editor/properties/behaviors.tsx new file mode 100644 index 000000000..1c2507faf --- /dev/null +++ b/editor/src/editor/windows/fx-editor/properties/behaviors.tsx @@ -0,0 +1,208 @@ +import { ReactNode } from "react"; + +import { EditorInspectorSectionField } from "../../../layout/inspector/fields/section"; + +import { Button } from "../../../../ui/shadcn/ui/button"; +import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "../../../../ui/shadcn/ui/dropdown-menu"; +import { AiOutlinePlus, AiOutlineClose } from "react-icons/ai"; + +import { IFXParticleData } from "./types"; + +export interface IFXEditorBehaviorsPropertiesProps { + particleData: IFXParticleData; + onChange: () => void; +} + +export function FXEditorBehaviorsProperties(props: IFXEditorBehaviorsPropertiesProps): ReactNode { + const { particleData, onChange } = props; + + return ( + <> + {particleData.behaviors.map((behavior, index) => ( + + {/* TODO: Add behavior-specific properties */} + + + ))} + + ); +} + +export function FXEditorBehaviorsDropdown(props: IFXEditorBehaviorsPropertiesProps): ReactNode { + const { particleData, onChange } = props; + + return ( + + + + + + { + particleData.behaviors.push({ type: "ApplyForce" }); + onChange(); + }} + > + ApplyForce + + { + particleData.behaviors.push({ type: "Noise" }); + onChange(); + }} + > + Noise + + { + particleData.behaviors.push({ type: "TurbulenceField" }); + onChange(); + }} + > + TurbulenceField + + { + particleData.behaviors.push({ type: "GravityForce" }); + onChange(); + }} + > + GravityForce + + { + particleData.behaviors.push({ type: "ColorOverLife" }); + onChange(); + }} + > + ColorOverLife + + { + particleData.behaviors.push({ type: "RotationOverLife" }); + onChange(); + }} + > + RotationOverLife + + { + particleData.behaviors.push({ type: "Rotation3DOverLife" }); + onChange(); + }} + > + Rotation3DOverLife + + { + particleData.behaviors.push({ type: "SizeOverLife" }); + onChange(); + }} + > + SizeOverLife + + { + particleData.behaviors.push({ type: "ColorBySpeed" }); + onChange(); + }} + > + ColorBySpeed + + { + particleData.behaviors.push({ type: "RotationBySpeed" }); + onChange(); + }} + > + RotationBySpeed + + { + particleData.behaviors.push({ type: "SizeBySpeed" }); + onChange(); + }} + > + SizeBySpeed + + { + particleData.behaviors.push({ type: "SpeedOverLife" }); + onChange(); + }} + > + SpeedOverLife + + { + particleData.behaviors.push({ type: "FrameOverLife" }); + onChange(); + }} + > + FrameOverLife + + { + particleData.behaviors.push({ type: "ForceOverLife" }); + onChange(); + }} + > + ForceOverLife + + { + particleData.behaviors.push({ type: "OrbitOverLife" }); + onChange(); + }} + > + OrbitOverLife + + { + particleData.behaviors.push({ type: "WidthOverLength" }); + onChange(); + }} + > + WidthOverLength + + { + particleData.behaviors.push({ type: "ChangeEmitDirection" }); + onChange(); + }} + > + ChangeEmitDirection + + { + particleData.behaviors.push({ type: "EmitSubParticleSystem" }); + onChange(); + }} + > + EmitSubParticleSystem + + { + particleData.behaviors.push({ type: "LimitSpeedOverLife" }); + onChange(); + }} + > + LimitSpeedOverLife + + + + ); +} + diff --git a/editor/src/editor/windows/fx-editor/properties/data.ts b/editor/src/editor/windows/fx-editor/properties/data.ts new file mode 100644 index 000000000..d163de0f4 --- /dev/null +++ b/editor/src/editor/windows/fx-editor/properties/data.ts @@ -0,0 +1,79 @@ +import { Vector3, Color4 } from "babylonjs"; +import { IFXParticleData } from "./types"; + +// Mock data storage - in real implementation this would be managed by the editor +const particleDataMap: Map = new Map(); + +export function getOrCreateParticleData(nodeId: string | number): IFXParticleData { + if (!particleDataMap.has(nodeId)) { + particleDataMap.set(nodeId, { + id: String(nodeId), + name: "Particle", + visibility: true, + position: new Vector3(0, 0, 0), + rotation: new Vector3(0, 0, 0), + scale: new Vector3(1, 1, 1), + emitterShape: { + shape: "Box", + // Box properties + direction1: new Vector3(0, 1, 0), + direction2: new Vector3(0, 1, 0), + minEmitBox: new Vector3(-0.5, -0.5, -0.5), + maxEmitBox: new Vector3(0.5, 0.5, 0.5), + // Cone properties + radius: 1.0, + angle: 0.785398, // 45 degrees in radians + radiusRange: 0.0, + heightRange: 0.0, + emitFromSpawnPointOnly: false, + // Cylinder properties + height: 1.0, + directionRandomizer: 0.0, + // Sphere properties + // Hemispheric properties + // Mesh properties + meshPath: null, + }, + particleRenderer: { + renderMode: "Billboard", + worldSpace: false, + material: null, + type: "Standard", + transparent: true, + opacity: 1.0, + side: "Double", + blending: "Add", + color: new Color4(1, 1, 1, 1), + renderOrder: 0, + uvTile: { + column: 1, + row: 1, + startTileIndex: 0, + blendTiles: false, + }, + texture: null, + meshPath: null, + softParticles: false, + }, + emission: { + looping: true, + duration: 5.0, + prewarm: false, + onlyUsedByOtherSystem: false, + emitOverTime: 10, + emitOverDistance: 0, + }, + bursts: [], + particleInitialization: { + startLife: { min: 1.0, max: 2.0 }, + startSize: { min: 0.1, max: 0.2 }, + startSpeed: { min: 1.0, max: 2.0 }, + startColor: new Color4(1, 1, 1, 1), + startRotation: { min: 0, max: 360 }, + }, + behaviors: [], + }); + } + return particleDataMap.get(nodeId)!; +} + diff --git a/editor/src/editor/windows/fx-editor/properties/emission.tsx b/editor/src/editor/windows/fx-editor/properties/emission.tsx new file mode 100644 index 000000000..dbc0acb02 --- /dev/null +++ b/editor/src/editor/windows/fx-editor/properties/emission.tsx @@ -0,0 +1,84 @@ +import { ReactNode } from "react"; + +import { EditorInspectorSectionField } from "../../../layout/inspector/fields/section"; +import { EditorInspectorNumberField } from "../../../layout/inspector/fields/number"; +import { EditorInspectorSwitchField } from "../../../layout/inspector/fields/switch"; + +import { Button } from "../../../../ui/shadcn/ui/button"; +import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "../../../../ui/shadcn/ui/dropdown-menu"; +import { AiOutlinePlus, AiOutlineClose } from "react-icons/ai"; + +import { IFXParticleData } from "./types"; + +export interface IFXEditorEmissionPropertiesProps { + particleData: IFXParticleData; + onChange: () => void; +} + +export function FXEditorEmissionProperties(props: IFXEditorEmissionPropertiesProps): ReactNode { + const { particleData, onChange } = props; + + return ( + <> + + + + + + + + + Bursts + + + + + + { + particleData.bursts.push({ + time: 0, + count: 10, + cycle: 1, + interval: 1, + probability: 1.0, + }); + onChange(); + }} + > + Add Burst + + + +
+ } + > + {particleData.bursts.map((burst, index) => ( + + + + + + + + + ))} + + + ); +} + diff --git a/editor/src/editor/windows/fx-editor/properties/emitter-shape.tsx b/editor/src/editor/windows/fx-editor/properties/emitter-shape.tsx new file mode 100644 index 000000000..f660076a1 --- /dev/null +++ b/editor/src/editor/windows/fx-editor/properties/emitter-shape.tsx @@ -0,0 +1,212 @@ +import { Component, ReactNode, DragEvent } from "react"; +import { Vector3 } from "babylonjs"; +import { extname } from "path/posix"; + +import { EditorInspectorNumberField } from "../../../layout/inspector/fields/number"; +import { EditorInspectorVectorField } from "../../../layout/inspector/fields/vector"; +import { EditorInspectorSwitchField } from "../../../layout/inspector/fields/switch"; +import { EditorInspectorListField } from "../../../layout/inspector/fields/list"; +import { EditorInspectorBlockField } from "../../../layout/inspector/fields/block"; + +import { Button } from "../../../../ui/shadcn/ui/button"; +import { AiOutlineClose } from "react-icons/ai"; +import { getProjectAssetsRootUrl } from "../../../../project/configuration"; + +import { IFXParticleData } from "./types"; + +export interface IFXEditorEmitterShapePropertiesProps { + particleData: IFXParticleData; + onChange: () => void; +} + +export interface IFXEditorEmitterShapePropertiesState { + meshDragOver: boolean; +} + +export class FXEditorEmitterShapeProperties extends Component { + public constructor(props: IFXEditorEmitterShapePropertiesProps) { + super(props); + this.state = { + meshDragOver: false, + }; + } + + public render(): ReactNode { + const { particleData } = this.props; + const shape = particleData.emitterShape.shape; + + return ( + <> + this.props.onChange()} + /> + {this._getShapeProperties(shape)} + + ); + } + + private _getShapeProperties(shape: string): ReactNode { + const { particleData } = this.props; + + if (shape === "Box") { + return ( + <> + +
Direction
+ + +
+ +
Emit Box
+ + +
+ + ); + } + + if (shape === "Cone") { + return ( + <> + + + + + + + ); + } + + if (shape === "Sphere") { + return ( + <> + + + + + ); + } + + if (shape === "Cylinder") { + return ( + <> + + + + + + ); + } + + if (shape === "Hemispheric") { + return ( + <> + + + + + ); + } + + if (shape === "Mesh") { + return this._getMeshEmitterField(); + } + + // Point - no properties + return null; + } + + private _getMeshEmitterField(): ReactNode { + const { particleData } = this.props; + + return ( +
+
Mesh
+
this._handleMeshEmitterDrop(ev)} + onDragOver={this._handleMeshDragOver} + onDragLeave={this._handleMeshDragLeave} + className={`flex items-center px-5 py-2 rounded-lg w-2/3 ${ + this.state.meshDragOver ? "bg-muted-foreground/75 dark:bg-muted-foreground/20" : "bg-muted-foreground/10 dark:bg-muted-foreground/5" + } transition-all duration-300 ease-in-out`} + > +
+ {particleData.emitterShape.meshPath || "Drop mesh file here"} +
+ {particleData.emitterShape.meshPath && ( + + )} +
+
+ ); + } + + private _handleMeshEmitterDrop = (ev: DragEvent): void => { + ev.preventDefault(); + ev.stopPropagation(); + this.setState({ meshDragOver: false }); + + try { + const data = JSON.parse(ev.dataTransfer.getData("assets")) as string[]; + if (!data || !data.length) { + return; + } + + const absolutePath = data[0]; + const extension = extname(absolutePath).toLowerCase(); + + const meshExtensions = [".x", ".b3d", ".dae", ".glb", ".gltf", ".fbx", ".stl", ".lwo", ".dxf", ".obj", ".3ds", ".ms3d", ".blend", ".babylon"]; + if (!meshExtensions.includes(extension)) { + return; + } + + const rootUrl = getProjectAssetsRootUrl(); + if (!rootUrl) { + return; + } + + const relativePath = absolutePath.replace(rootUrl, ""); + this.props.particleData.emitterShape.meshPath = relativePath; + this.props.onChange(); + } catch (e) { + console.error("Failed to handle mesh emitter drop", e); + } + }; + + private _handleMeshDragOver = (ev: DragEvent): void => { + ev.preventDefault(); + ev.stopPropagation(); + if (ev.dataTransfer.types.includes("assets")) { + this.setState({ meshDragOver: true }); + } + }; + + private _handleMeshDragLeave = (ev: DragEvent): void => { + ev.preventDefault(); + ev.stopPropagation(); + this.setState({ meshDragOver: false }); + }; +} + diff --git a/editor/src/editor/windows/fx-editor/properties/object.tsx b/editor/src/editor/windows/fx-editor/properties/object.tsx new file mode 100644 index 000000000..d0ba820b1 --- /dev/null +++ b/editor/src/editor/windows/fx-editor/properties/object.tsx @@ -0,0 +1,26 @@ +import { ReactNode } from "react"; + +import { EditorInspectorStringField } from "../../../layout/inspector/fields/string"; +import { EditorInspectorVectorField } from "../../../layout/inspector/fields/vector"; +import { EditorInspectorSwitchField } from "../../../layout/inspector/fields/switch"; + +import { IFXParticleData } from "./types"; + +export interface IFXEditorObjectPropertiesProps { + particleData: IFXParticleData; +} + +export function FXEditorObjectProperties(props: IFXEditorObjectPropertiesProps): ReactNode { + const { particleData } = props; + + return ( + <> + + + + + + + ); +} + diff --git a/editor/src/editor/windows/fx-editor/properties/particle-initialization.tsx b/editor/src/editor/windows/fx-editor/properties/particle-initialization.tsx new file mode 100644 index 000000000..dcb9db507 --- /dev/null +++ b/editor/src/editor/windows/fx-editor/properties/particle-initialization.tsx @@ -0,0 +1,50 @@ +import { ReactNode } from "react"; + +import { EditorInspectorNumberField } from "../../../layout/inspector/fields/number"; +import { EditorInspectorColorField } from "../../../layout/inspector/fields/color"; +import { EditorInspectorBlockField } from "../../../layout/inspector/fields/block"; + +import { IFXParticleData } from "./types"; + +export interface IFXEditorParticleInitializationPropertiesProps { + particleData: IFXParticleData; +} + +export function FXEditorParticleInitializationProperties(props: IFXEditorParticleInitializationPropertiesProps): ReactNode { + const { particleData } = props; + + return ( + <> + +
Start Life
+
+ + +
+
+ +
Start Size
+
+ + +
+
+ +
Start Speed
+
+ + +
+
+ + +
Start Rotation
+
+ + +
+
+ + ); +} + diff --git a/editor/src/editor/windows/fx-editor/properties/particle-renderer.tsx b/editor/src/editor/windows/fx-editor/properties/particle-renderer.tsx new file mode 100644 index 000000000..dc9b86565 --- /dev/null +++ b/editor/src/editor/windows/fx-editor/properties/particle-renderer.tsx @@ -0,0 +1,219 @@ +import { Component, ReactNode, DragEvent } from "react"; +import { Color4, Scene } from "babylonjs"; +import { extname } from "path/posix"; + +import { EditorInspectorSectionField } from "../../../layout/inspector/fields/section"; +import { EditorInspectorNumberField } from "../../../layout/inspector/fields/number"; +import { EditorInspectorColorField } from "../../../layout/inspector/fields/color"; +import { EditorInspectorSwitchField } from "../../../layout/inspector/fields/switch"; +import { EditorInspectorListField } from "../../../layout/inspector/fields/list"; +import { EditorInspectorTextureField } from "../../../layout/inspector/fields/texture"; + +import { Button } from "../../../../ui/shadcn/ui/button"; +import { AiOutlineClose } from "react-icons/ai"; +import { getProjectAssetsRootUrl } from "../../../../project/configuration"; + +import { IFXParticleData } from "./types"; + +export interface IFXEditorParticleRendererPropertiesProps { + particleData: IFXParticleData; + scene?: Scene; + onChange: () => void; +} + +export interface IFXEditorParticleRendererPropertiesState { + meshDragOver: boolean; +} + +export class FXEditorParticleRendererProperties extends Component { + public constructor(props: IFXEditorParticleRendererPropertiesProps) { + super(props); + this.state = { + meshDragOver: false, + }; + } + + public render(): ReactNode { + const { particleData } = this.props; + const renderMode = particleData.particleRenderer.renderMode; + + return ( + <> + this.props.onChange()} + /> + + {/* TODO: Material field */} + + + + + + + + {this._getUVTileSection()} + {this._getTextureField()} + {this._getRenderModeSpecificProperties(renderMode)} + + + ); + } + + private _getUVTileSection(): ReactNode { + const { particleData } = this.props; + + return ( + + + + + + + ); + } + + private _getTextureField(): ReactNode { + const { particleData, scene } = this.props; + + if (!scene) { + return null; + } + + return ( + this.props.onChange()} + /> + ); + } + + private _getRenderModeSpecificProperties(renderMode: string): ReactNode { + if (renderMode === "Mesh") { + return this._getMeshField(); + } + + // TODO: Add properties for other render modes (StretchedBillboard, Trail, etc.) + return null; + } + + private _getMeshField(): ReactNode { + const { particleData } = this.props; + + return ( +
+
Mesh
+
this._handleMeshDrop(ev)} + onDragOver={this._handleMeshDragOver} + onDragLeave={this._handleMeshDragLeave} + className={`flex items-center px-5 py-2 rounded-lg w-2/3 ${ + this.state.meshDragOver ? "bg-muted-foreground/75 dark:bg-muted-foreground/20" : "bg-muted-foreground/10 dark:bg-muted-foreground/5" + } transition-all duration-300 ease-in-out`} + > +
+ {particleData.particleRenderer.meshPath || "Drop mesh file here"} +
+ {particleData.particleRenderer.meshPath && ( + + )} +
+
+ ); + } + + private _handleMeshDrop = (ev: DragEvent): void => { + ev.preventDefault(); + ev.stopPropagation(); + this.setState({ meshDragOver: false }); + + try { + const data = JSON.parse(ev.dataTransfer.getData("assets")) as string[]; + if (!data || !data.length) { + return; + } + + const absolutePath = data[0]; + const extension = extname(absolutePath).toLowerCase(); + + const meshExtensions = [".x", ".b3d", ".dae", ".glb", ".gltf", ".fbx", ".stl", ".lwo", ".dxf", ".obj", ".3ds", ".ms3d", ".blend", ".babylon"]; + if (!meshExtensions.includes(extension)) { + return; + } + + const rootUrl = getProjectAssetsRootUrl(); + if (!rootUrl) { + return; + } + + const relativePath = absolutePath.replace(rootUrl, ""); + this.props.particleData.particleRenderer.meshPath = relativePath; + this.props.onChange(); + } catch (e) { + console.error("Failed to handle mesh drop", e); + } + }; + + private _handleMeshDragOver = (ev: DragEvent): void => { + ev.preventDefault(); + ev.stopPropagation(); + if (ev.dataTransfer.types.includes("assets")) { + this.setState({ meshDragOver: true }); + } + }; + + private _handleMeshDragLeave = (ev: DragEvent): void => { + ev.preventDefault(); + ev.stopPropagation(); + this.setState({ meshDragOver: false }); + }; +} + diff --git a/editor/src/editor/windows/fx-editor/properties/types.ts b/editor/src/editor/windows/fx-editor/properties/types.ts new file mode 100644 index 000000000..97e3be0fa --- /dev/null +++ b/editor/src/editor/windows/fx-editor/properties/types.ts @@ -0,0 +1,62 @@ +import { Vector3, Color4 } from "babylonjs"; + +export interface IFXParticleData { + id: string; + name: string; + visibility: boolean; + position: Vector3; + rotation: Vector3; + scale: Vector3; + emitterShape: { + shape: string; + [key: string]: any; + }; + particleRenderer: { + renderMode: string; + worldSpace: boolean; + material: any; + type: string; + transparent: boolean; + opacity: number; + side: string; + blending: string; + color: Color4; + renderOrder: number; + uvTile: { + column: number; + row: number; + startTileIndex: number; + blendTiles: boolean; + }; + texture: any; + meshPath: string | null; + softParticles: boolean; + }; + emission: { + looping: boolean; + duration: number; + prewarm: boolean; + onlyUsedByOtherSystem: boolean; + emitOverTime: number; + emitOverDistance: number; + }; + bursts: Array<{ + time: number; + count: number; + cycle: number; + interval: number; + probability: number; + }>; + particleInitialization: { + startLife: { min: number; max: number }; + startSize: { min: number; max: number }; + startSpeed: { min: number; max: number }; + startColor: Color4; + startRotation: { min: number; max: number }; + }; + behaviors: Array<{ + type: string; + [key: string]: any; + }>; +} + From 0b506185d6e28dad4f654c19218967881d497d18 Mon Sep 17 00:00:00 2001 From: Mikalai Lazitski Date: Sun, 7 Dec 2025 13:33:12 +0300 Subject: [PATCH 04/62] feat: enhance FX Editor with new behavior properties and function editors for particle management --- editor/src/editor/windows/fx-editor/index.tsx | 2 +- .../src/editor/windows/fx-editor/preview.tsx | 2 +- .../editor/windows/fx-editor/properties.tsx | 10 +- .../fx-editor/properties/behaviors.tsx | 203 ++----- .../behaviors/behavior-properties.tsx | 149 +++++ .../properties/behaviors/bezier-editor.tsx | 552 ++++++++++++++++++ .../behaviors/color-function-editor.tsx | 279 +++++++++ .../properties/behaviors/function-editor.tsx | 151 +++++ .../properties/behaviors/registry.ts | 349 +++++++++++ .../windows/fx-editor/properties/emission.tsx | 10 +- .../fx-editor/properties/emitter-shape.tsx | 1 - .../properties/particle-renderer.tsx | 2 +- 12 files changed, 1522 insertions(+), 188 deletions(-) create mode 100644 editor/src/editor/windows/fx-editor/properties/behaviors/behavior-properties.tsx create mode 100644 editor/src/editor/windows/fx-editor/properties/behaviors/bezier-editor.tsx create mode 100644 editor/src/editor/windows/fx-editor/properties/behaviors/color-function-editor.tsx create mode 100644 editor/src/editor/windows/fx-editor/properties/behaviors/function-editor.tsx create mode 100644 editor/src/editor/windows/fx-editor/properties/behaviors/registry.ts diff --git a/editor/src/editor/windows/fx-editor/index.tsx b/editor/src/editor/windows/fx-editor/index.tsx index 3e54073d6..5344c1357 100644 --- a/editor/src/editor/windows/fx-editor/index.tsx +++ b/editor/src/editor/windows/fx-editor/index.tsx @@ -87,7 +87,7 @@ export default class FXEditorWindow extends Component { + public async importFile(_filePath: string): Promise { // TODO: Import file data into current editor toast.success("FX imported"); } diff --git a/editor/src/editor/windows/fx-editor/preview.tsx b/editor/src/editor/windows/fx-editor/preview.tsx index 5c03ed24a..22e2789bd 100644 --- a/editor/src/editor/windows/fx-editor/preview.tsx +++ b/editor/src/editor/windows/fx-editor/preview.tsx @@ -109,7 +109,7 @@ export class FXEditorPreview extends Component - - Behaviors - this.forceUpdate()} /> - - } - > + + this.forceUpdate()} /> this.forceUpdate()} /> diff --git a/editor/src/editor/windows/fx-editor/properties/behaviors.tsx b/editor/src/editor/windows/fx-editor/properties/behaviors.tsx index 1c2507faf..f95d8a3e9 100644 --- a/editor/src/editor/windows/fx-editor/properties/behaviors.tsx +++ b/editor/src/editor/windows/fx-editor/properties/behaviors.tsx @@ -7,6 +7,8 @@ import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigge import { AiOutlinePlus, AiOutlineClose } from "react-icons/ai"; import { IFXParticleData } from "./types"; +import { BehaviorRegistry, createDefaultBehaviorData, getBehaviorDefinition } from "./behaviors/registry"; +import { BehaviorProperties } from "./behaviors/behavior-properties"; export interface IFXEditorBehaviorsPropertiesProps { particleData: IFXParticleData; @@ -18,22 +20,27 @@ export function FXEditorBehaviorsProperties(props: IFXEditorBehaviorsPropertiesP return ( <> - {particleData.behaviors.map((behavior, index) => ( - - {/* TODO: Add behavior-specific properties */} - - - ))} + {particleData.behaviors.map((behavior, index) => { + const definition = getBehaviorDefinition(behavior.type); + const title = definition?.label || behavior.type; + + return ( + + + + + ); + })} ); } @@ -49,158 +56,18 @@ export function FXEditorBehaviorsDropdown(props: IFXEditorBehaviorsPropertiesPro - { - particleData.behaviors.push({ type: "ApplyForce" }); - onChange(); - }} - > - ApplyForce - - { - particleData.behaviors.push({ type: "Noise" }); - onChange(); - }} - > - Noise - - { - particleData.behaviors.push({ type: "TurbulenceField" }); - onChange(); - }} - > - TurbulenceField - - { - particleData.behaviors.push({ type: "GravityForce" }); - onChange(); - }} - > - GravityForce - - { - particleData.behaviors.push({ type: "ColorOverLife" }); - onChange(); - }} - > - ColorOverLife - - { - particleData.behaviors.push({ type: "RotationOverLife" }); - onChange(); - }} - > - RotationOverLife - - { - particleData.behaviors.push({ type: "Rotation3DOverLife" }); - onChange(); - }} - > - Rotation3DOverLife - - { - particleData.behaviors.push({ type: "SizeOverLife" }); - onChange(); - }} - > - SizeOverLife - - { - particleData.behaviors.push({ type: "ColorBySpeed" }); - onChange(); - }} - > - ColorBySpeed - - { - particleData.behaviors.push({ type: "RotationBySpeed" }); - onChange(); - }} - > - RotationBySpeed - - { - particleData.behaviors.push({ type: "SizeBySpeed" }); - onChange(); - }} - > - SizeBySpeed - - { - particleData.behaviors.push({ type: "SpeedOverLife" }); - onChange(); - }} - > - SpeedOverLife - - { - particleData.behaviors.push({ type: "FrameOverLife" }); - onChange(); - }} - > - FrameOverLife - - { - particleData.behaviors.push({ type: "ForceOverLife" }); - onChange(); - }} - > - ForceOverLife - - { - particleData.behaviors.push({ type: "OrbitOverLife" }); - onChange(); - }} - > - OrbitOverLife - - { - particleData.behaviors.push({ type: "WidthOverLength" }); - onChange(); - }} - > - WidthOverLength - - { - particleData.behaviors.push({ type: "ChangeEmitDirection" }); - onChange(); - }} - > - ChangeEmitDirection - - { - particleData.behaviors.push({ type: "EmitSubParticleSystem" }); - onChange(); - }} - > - EmitSubParticleSystem - - { - particleData.behaviors.push({ type: "LimitSpeedOverLife" }); - onChange(); - }} - > - LimitSpeedOverLife - + {Object.values(BehaviorRegistry).map((definition) => ( + { + const behaviorData = createDefaultBehaviorData(definition.type); + particleData.behaviors.push(behaviorData); + onChange(); + }} + > + {definition.label} + + ))} ); diff --git a/editor/src/editor/windows/fx-editor/properties/behaviors/behavior-properties.tsx b/editor/src/editor/windows/fx-editor/properties/behaviors/behavior-properties.tsx new file mode 100644 index 000000000..4aa510772 --- /dev/null +++ b/editor/src/editor/windows/fx-editor/properties/behaviors/behavior-properties.tsx @@ -0,0 +1,149 @@ +import { ReactNode } from "react"; +import { Vector3, Color4 } from "babylonjs"; + +import { EditorInspectorNumberField } from "../../../../layout/inspector/fields/number"; +import { EditorInspectorVectorField } from "../../../../layout/inspector/fields/vector"; +import { EditorInspectorColorField } from "../../../../layout/inspector/fields/color"; +import { EditorInspectorSwitchField } from "../../../../layout/inspector/fields/switch"; +import { EditorInspectorStringField } from "../../../../layout/inspector/fields/string"; +import { EditorInspectorListField } from "../../../../layout/inspector/fields/list"; +import { EditorInspectorBlockField } from "../../../../layout/inspector/fields/block"; + +import { getBehaviorDefinition } from "./registry"; +import { FunctionEditor } from "./function-editor"; +import { ColorFunctionEditor } from "./color-function-editor"; + +export interface IBehaviorPropertiesProps { + behavior: any; + onChange: () => void; +} + +export function BehaviorProperties(props: IBehaviorPropertiesProps): ReactNode { + const { behavior, onChange } = props; + const definition = getBehaviorDefinition(behavior.type); + + if (!definition) { + return null; + } + + return ( + <> + {definition.properties.map((prop) => { + if (prop.type === "vector3") { + // Ensure vector3 property exists and is a Vector3 or object + if (!behavior[prop.name]) { + const defaultVal = prop.default || { x: 0, y: 0, z: 0 }; + behavior[prop.name] = new Vector3(defaultVal.x, defaultVal.y, defaultVal.z); + } else if (!(behavior[prop.name] instanceof Vector3)) { + // Convert to Vector3 if it's an object + const obj = behavior[prop.name]; + behavior[prop.name] = new Vector3(obj.x || 0, obj.y || 0, obj.z || 0); + } + return ; + } + + if (prop.type === "number") { + if (behavior[prop.name] === undefined) { + behavior[prop.name] = prop.default !== undefined ? prop.default : 0; + } + return ; + } + + if (prop.type === "color") { + if (!behavior[prop.name]) { + behavior[prop.name] = prop.default ? new Color4(prop.default.r, prop.default.g, prop.default.b, prop.default.a) : new Color4(1, 1, 1, 1); + } + return ; + } + + if (prop.type === "range") { + if (!behavior[prop.name]) { + behavior[prop.name] = prop.default ? { ...prop.default } : { min: 0, max: 1 }; + } + return ( + +
{prop.label}
+
+ + +
+
+ ); + } + + if (prop.type === "boolean") { + if (behavior[prop.name] === undefined) { + behavior[prop.name] = prop.default !== undefined ? prop.default : false; + } + return ; + } + + if (prop.type === "string") { + if (behavior[prop.name] === undefined) { + behavior[prop.name] = prop.default !== undefined ? prop.default : ""; + } + return ; + } + + if (prop.type === "enum") { + if (behavior[prop.name] === undefined) { + behavior[prop.name] = prop.default !== undefined ? prop.default : prop.enumItems?.[0]?.value ?? 0; + } + if (!prop.enumItems || prop.enumItems.length === 0) { + return null; + } + return ( + + ); + } + + if (prop.type === "colorFunction") { + // Initialize color function value if not set + if (!behavior[prop.name]) { + behavior[prop.name] = { + colorFunctionType: prop.colorFunctionTypes?.[0] || "ConstantColor", + data: {}, + }; + } + return ( + + ); + } + + if (prop.type === "function") { + // Initialize function value if not set + if (!behavior[prop.name]) { + behavior[prop.name] = { + functionType: prop.functionTypes?.[0] || "ConstantValue", + data: {}, + }; + } + return ( + + ); + } + + return null; + })} + + ); +} + diff --git a/editor/src/editor/windows/fx-editor/properties/behaviors/bezier-editor.tsx b/editor/src/editor/windows/fx-editor/properties/behaviors/bezier-editor.tsx new file mode 100644 index 000000000..94d2075c0 --- /dev/null +++ b/editor/src/editor/windows/fx-editor/properties/behaviors/bezier-editor.tsx @@ -0,0 +1,552 @@ +import { Component, ReactNode, MouseEvent } from "react"; +import { Button } from "../../../../../ui/shadcn/ui/button"; +import { EditorInspectorNumberField } from "../../../../layout/inspector/fields/number"; +import { EditorInspectorBlockField } from "../../../../layout/inspector/fields/block"; +import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "../../../../../ui/shadcn/ui/dropdown-menu"; +import { HiOutlineArrowPath } from "react-icons/hi2"; + +export interface IBezierCurve { + p0: number; + p1: number; + p2: number; + p3: number; + start: number; +} + +export interface IBezierEditorProps { + value: any; + onChange: () => void; +} + +export interface IBezierEditorState { + curve: IBezierCurve; + dragging: boolean; + dragPoint: "p0" | "p1" | "p2" | "p3" | null; + hoveredPoint: "p0" | "p1" | "p2" | "p3" | null; + width: number; + height: number; + showValues: boolean; +} + +type CurvePreset = "linear" | "easeIn" | "easeOut" | "easeInOut" | "easeInBack" | "easeOutBack"; + +const CURVE_PRESETS: Record = { + linear: { p0: 0, p1: 0, p2: 1, p3: 1, start: 0 }, + easeIn: { p0: 0, p1: 0.42, p2: 1, p3: 1, start: 0 }, + easeOut: { p0: 0, p1: 0, p2: 0.58, p3: 1, start: 0 }, + easeInOut: { p0: 0, p1: 0.42, p2: 0.58, p3: 1, start: 0 }, + easeInBack: { p0: 0, p1: -0.36, p2: 0.36, p3: 1, start: 0 }, + easeOutBack: { p0: 0, p1: 0.64, p2: 1.36, p3: 1, start: 0 }, +}; + +export class BezierEditor extends Component { + private _svgRef: SVGSVGElement | null = null; + private _containerRef: HTMLDivElement | null = null; + + public constructor(props: IBezierEditorProps) { + super(props); + this.state = { + curve: this._getCurveFromValue(), + dragging: false, + dragPoint: null, + hoveredPoint: null, + width: 400, + height: 250, + showValues: false, + }; + } + + public componentDidMount(): void { + this._updateDimensions(); + window.addEventListener("resize", this._updateDimensions); + } + + public componentWillUnmount(): void { + window.removeEventListener("resize", this._updateDimensions); + } + + private _updateDimensions = (): void => { + if (this._containerRef) { + const rect = this._containerRef.getBoundingClientRect(); + this.setState({ + width: Math.max(300, rect.width - 20), + height: 250, + }); + } + }; + + private _getCurveFromValue(): IBezierCurve { + if (!this.props.value || !this.props.value.data) { + return CURVE_PRESETS.linear; + } + + // Support both old format (array) and new format (direct object) + if (this.props.value.data.functions && Array.isArray(this.props.value.data.functions)) { + const firstFunction = this.props.value.data.functions[0]; + if (firstFunction && firstFunction.function) { + return { + p0: firstFunction.function.p0 ?? 0, + p1: firstFunction.function.p1 ?? 1.0 / 3, + p2: firstFunction.function.p2 ?? (1.0 / 3) * 2, + p3: firstFunction.function.p3 ?? 1, + start: 0, + }; + } + } + + // New format: direct function object + if (this.props.value.data.function) { + return { + p0: this.props.value.data.function.p0 ?? 0, + p1: this.props.value.data.function.p1 ?? 1.0 / 3, + p2: this.props.value.data.function.p2 ?? (1.0 / 3) * 2, + p3: this.props.value.data.function.p3 ?? 1, + start: 0, + }; + } + + return CURVE_PRESETS.linear; + } + + private _saveCurveToValue(): void { + if (!this.props.value) { + return; + } + + if (!this.props.value.data) { + this.props.value.data = {}; + } + + // Save as direct function object (not array) + this.props.value.data.function = { + p0: Math.max(0, Math.min(1, this.state.curve.p0)), + p1: Math.max(0, Math.min(1, this.state.curve.p1)), + p2: Math.max(0, Math.min(1, this.state.curve.p2)), + p3: Math.max(0, Math.min(1, this.state.curve.p3)), + }; + } + + private _applyPreset(preset: CurvePreset): void { + const presetCurve = CURVE_PRESETS[preset]; + this.setState({ curve: { ...presetCurve } }, () => { + this._saveCurveToValue(); + this.props.onChange(); + }); + } + + private _screenToSvg(clientX: number, clientY: number): { x: number; y: number } { + if (!this._svgRef) { + return { x: 0, y: 0 }; + } + + const rect = this._svgRef.getBoundingClientRect(); + const vb = this._svgRef.viewBox?.baseVal; + if (!vb) { + return { + x: clientX - rect.left, + y: clientY - rect.top, + }; + } + + const scaleX = rect.width / vb.width; + const scaleY = rect.height / vb.height; + const useScale = Math.min(scaleX, scaleY); + + const offsetX = (rect.width - vb.width * useScale) / 2; + const offsetY = (rect.height - vb.height * useScale) / 2; + + return { + x: (clientX - rect.left - offsetX) / useScale, + y: (clientY - rect.top - offsetY) / useScale, + }; + } + + private _valueToSvgY(value: number): number { + // Map value from [0, 1] to SVG Y coordinate + // Center is at height/2, full range is height * 0.8 (40% above and below center) + const padding = this.state.height * 0.1; + const range = this.state.height * 0.8; + return padding + (1 - value) * range; + } + + private _svgYToValue(svgY: number): number { + const padding = this.state.height * 0.1; + const range = this.state.height * 0.8; + return Math.max(0, Math.min(1, (this.state.height - svgY - padding) / range)); + } + + private _handleMouseDown = (ev: MouseEvent, point: "p0" | "p1" | "p2" | "p3"): void => { + ev.stopPropagation(); + if (ev.button !== 0) { + return; + } + + this.setState({ + dragging: true, + dragPoint: point, + }); + + let mouseMoveListener: (event: globalThis.MouseEvent) => void; + let mouseUpListener: (event: globalThis.MouseEvent) => void; + + mouseMoveListener = (ev: globalThis.MouseEvent) => { + if (!this.state.dragging || !this.state.dragPoint) { + return; + } + + const svgPos = this._screenToSvg(ev.clientX, ev.clientY); + const value = this._svgYToValue(svgPos.y); + + const curve = { ...this.state.curve }; + + if (this.state.dragPoint === "p0") { + curve.p0 = value; + } else if (this.state.dragPoint === "p1") { + curve.p1 = value; + } else if (this.state.dragPoint === "p2") { + curve.p2 = value; + } else if (this.state.dragPoint === "p3") { + curve.p3 = value; + } + + this.setState({ curve }); + }; + + mouseUpListener = () => { + document.body.removeEventListener("mousemove", mouseMoveListener); + document.body.removeEventListener("mouseup", mouseUpListener); + document.body.style.cursor = ""; + + this._saveCurveToValue(); + this.props.onChange(); + + this.setState({ + dragging: false, + dragPoint: null, + }); + }; + + document.body.style.cursor = "move"; + document.body.addEventListener("mousemove", mouseMoveListener); + document.body.addEventListener("mouseup", mouseUpListener); + }; + + private _bezierValue(t: number, p0: number, p1: number, p2: number, p3: number): number { + const t2 = t * t; + const t3 = t2 * t; + const mt = 1 - t; + const mt2 = mt * mt; + const mt3 = mt2 * mt; + return p0 * mt3 + p1 * mt2 * t * 3 + p2 * mt * t2 * 3 + p3 * t3; + } + + private _renderCurve(curve: IBezierCurve): ReactNode { + const segments = 100; + const pathData: string[] = []; + const gradientId = `gradient-${Math.random().toString(36).substr(2, 9)}`; + + // Calculate actual Bezier curve points + // For cubic Bezier: B(t) = (1-t)³P₀ + 3(1-t)²tP₁ + 3(1-t)t²P₂ + t³P₃ + // But we're using p0, p1, p2, p3 as control values, not positions + // We need to map them to actual control points + const p0X = 0; + const p0Y = this._valueToSvgY(curve.p0); + const p1X = this.state.width / 3; + const p1Y = this._valueToSvgY(curve.p1); + const p2X = (this.state.width * 2) / 3; + const p2Y = this._valueToSvgY(curve.p2); + const p3X = this.state.width; + const p3Y = this._valueToSvgY(curve.p3); + + // Generate curve path + for (let i = 0; i <= segments; i++) { + const t = i / segments; + const x = t * this.state.width; + const y = this._bezierValue(t, p0Y, p1Y, p2Y, p3Y); + + if (i === 0) { + pathData.push(`M ${x} ${y}`); + } else { + pathData.push(`L ${x} ${y}`); + } + } + + const isHovered = (point: "p0" | "p1" | "p2" | "p3") => this.state.hoveredPoint === point || this.state.dragPoint === point; + const getPointRadius = (point: "p0" | "p1" | "p2" | "p3") => { + if (point === "p0" || point === "p3") return isHovered(point) ? 7 : 5; + return isHovered(point) ? 6 : 4; + }; + const getPointColor = (point: "p0" | "p1" | "p2" | "p3") => { + if (point === "p0" || point === "p3") return isHovered(point) ? "#3b82f6" : "#2563eb"; + return isHovered(point) ? "#8b5cf6" : "#7c3aed"; + }; + + return ( + + + + + + + + + {/* Filled area under curve */} + + + {/* Curve line */} + + + {/* Control lines */} + + + + {/* Control points */} + {(["p0", "p1", "p2", "p3"] as const).map((point) => { + const x = point === "p0" ? p0X : point === "p1" ? p1X : point === "p2" ? p2X : p3X; + const y = point === "p0" ? p0Y : point === "p1" ? p1Y : point === "p2" ? p2Y : p3Y; + const value = curve[point]; + const radius = getPointRadius(point); + const color = getPointColor(point); + + return ( + + {/* Outer glow when hovered */} + {isHovered(point) && ( + + )} + {/* Point circle */} + this._handleMouseDown(ev, point)} + onMouseEnter={() => this.setState({ hoveredPoint: point, showValues: true })} + onMouseLeave={() => this.setState({ hoveredPoint: null, showValues: false })} + /> + {/* Value label */} + {isHovered(point) && ( + + {value.toFixed(2)} + + )} + + ); + })} + + ); + } + + private _renderGrid(): ReactNode { + const gridLines: ReactNode[] = []; + + // Horizontal grid lines (value markers) + for (let i = 0; i <= 10; i++) { + const value = i / 10; + const y = this._valueToSvgY(value); + const isMainLine = i % 5 === 0; + + gridLines.push( + + + {isMainLine && ( + + {value.toFixed(1)} + + )} + + ); + } + + // Vertical grid lines (time markers) + for (let i = 0; i <= 10; i++) { + const t = i / 10; + const x = t * this.state.width; + const isMainLine = i % 5 === 0; + + gridLines.push( + + + {isMainLine && ( + + {t.toFixed(1)} + + )} + + ); + } + + // Center line (value = 0.5) + gridLines.push( + + ); + + return {gridLines}; + } + + public render(): ReactNode { + return ( +
(this._containerRef = ref)} className="flex flex-col gap-3 w-full"> + {/* Toolbar */} +
+
+ Curve Editor +
+
+ + + + + + this._applyPreset("linear")}>Linear + this._applyPreset("easeIn")}>Ease In + this._applyPreset("easeOut")}>Ease Out + this._applyPreset("easeInOut")}>Ease In-Out + this._applyPreset("easeInBack")}>Ease In Back + this._applyPreset("easeOutBack")}>Ease Out Back + + + +
+
+ + {/* SVG Canvas */} +
+ (this._svgRef = ref)} + width={this.state.width} + height={this.state.height} + viewBox={`0 0 ${this.state.width} ${this.state.height}`} + className="w-full h-full" + style={{ background: "var(--background)" }} + > + {/* Grid */} + {this._renderGrid()} + + {/* Curve */} + {this._renderCurve(this.state.curve)} + + + {/* Axis labels */} +
Time
+
Value
+
+ + {/* Value inputs */} + +
+
+ + { + this.setState({ curve: { ...this.state.curve } }); + this._saveCurveToValue(); + this.props.onChange(); + }} + /> +
+
+ + { + this.setState({ curve: { ...this.state.curve } }); + this._saveCurveToValue(); + this.props.onChange(); + }} + /> +
+
+ + { + this.setState({ curve: { ...this.state.curve } }); + this._saveCurveToValue(); + this.props.onChange(); + }} + /> +
+
+ + { + this.setState({ curve: { ...this.state.curve } }); + this._saveCurveToValue(); + this.props.onChange(); + }} + /> +
+
+
+
+ ); + } +} diff --git a/editor/src/editor/windows/fx-editor/properties/behaviors/color-function-editor.tsx b/editor/src/editor/windows/fx-editor/properties/behaviors/color-function-editor.tsx new file mode 100644 index 000000000..21b8d214a --- /dev/null +++ b/editor/src/editor/windows/fx-editor/properties/behaviors/color-function-editor.tsx @@ -0,0 +1,279 @@ +import { ReactNode } from "react"; +import { Color4, Vector3, Color3 } from "babylonjs"; + +import { EditorInspectorColorField } from "../../../../layout/inspector/fields/color"; +import { EditorInspectorListField } from "../../../../layout/inspector/fields/list"; +import { EditorInspectorBlockField } from "../../../../layout/inspector/fields/block"; +import { EditorInspectorNumberField } from "../../../../layout/inspector/fields/number"; + +import { Button } from "../../../../../ui/shadcn/ui/button"; +import { AiOutlinePlus, AiOutlineClose } from "react-icons/ai"; +import { Slider } from "../../../../../ui/shadcn/ui/slider"; + +export type ColorFunctionType = "ConstantColor" | "ColorRange" | "Gradient" | "RandomColorBetweenGradient"; + +export interface IColorFunctionEditorProps { + value: any; + onChange: () => void; + label: string; +} + +export function ColorFunctionEditor(props: IColorFunctionEditorProps): ReactNode { + const { value, onChange, label } = props; + + // Initialize color function type if not set + if (!value || !value.colorFunctionType) { + value.colorFunctionType = "ConstantColor"; + value.data = {}; + } + + const functionType = value.colorFunctionType as ColorFunctionType; + + // Ensure data object exists + if (!value.data) { + value.data = {}; + } + + const typeItems = [ + { text: "Color", value: "ConstantColor" }, + { text: "Color Range", value: "ColorRange" }, + { text: "Gradient", value: "Gradient" }, + { text: "Random Between Gradient", value: "RandomColorBetweenGradient" }, + ]; + + return ( + <> + { + // Reset data when type changes and initialize defaults + const newType = value.colorFunctionType; + value.data = {}; + if (newType === "ConstantColor") { + value.data.color = new Color4(1, 1, 1, 1); + } else if (newType === "ColorRange") { + value.data.colorA = new Color4(0, 0, 0, 1); + value.data.colorB = new Color4(1, 1, 1, 1); + } else if (newType === "Gradient") { + value.data.colorKeys = [ + { color: new Vector3(0, 0, 0), position: 0 }, + { color: new Vector3(1, 1, 1), position: 1 }, + ]; + value.data.alphaKeys = [ + { value: 1, position: 0 }, + { value: 1, position: 1 }, + ]; + } else if (newType === "RandomColorBetweenGradient") { + value.data.gradient1 = { + colorKeys: [ + { color: new Vector3(0, 0, 0), position: 0 }, + { color: new Vector3(1, 1, 1), position: 1 }, + ], + alphaKeys: [ + { value: 1, position: 0 }, + { value: 1, position: 1 }, + ], + }; + value.data.gradient2 = { + colorKeys: [ + { color: new Vector3(1, 0, 0), position: 0 }, + { color: new Vector3(0, 1, 0), position: 1 }, + ], + alphaKeys: [ + { value: 1, position: 0 }, + { value: 1, position: 1 }, + ], + }; + } + onChange(); + }} + /> + + {functionType === "ConstantColor" && ( + <> + {!value.data.color && (value.data.color = new Color4(1, 1, 1, 1))} + + + )} + + {functionType === "ColorRange" && ( + <> + {!value.data.colorA && (value.data.colorA = new Color4(0, 0, 0, 1))} + {!value.data.colorB && (value.data.colorB = new Color4(1, 1, 1, 1))} + + + + )} + + {functionType === "Gradient" && } + + {functionType === "RandomColorBetweenGradient" && ( + <> + {!value.data.gradient1 && (value.data.gradient1 = {})} + {!value.data.gradient2 && (value.data.gradient2 = {})} + +
Gradient 1
+ +
+ +
Gradient 2
+ +
+ + )} + + ); +} + +interface IGradientEditorProps { + value: any; + onChange: () => void; +} + +function GradientEditor(props: IGradientEditorProps): ReactNode { + const { value, onChange } = props; + + // Initialize gradient data + if (!value.colorKeys || value.colorKeys.length === 0) { + value.colorKeys = [ + { color: new Vector3(0, 0, 0), position: 0 }, + { color: new Vector3(1, 1, 1), position: 1 }, + ]; + } + if (!value.alphaKeys || value.alphaKeys.length === 0) { + value.alphaKeys = [ + { value: 1, position: 0 }, + { value: 1, position: 1 }, + ]; + } + + return ( +
+
Color Keys
+ {value.colorKeys.map((key: any, index: number) => { + // Ensure color is Vector3 and convert to Color3 for color picker + if (!key.color) { + key.color = new Vector3(0, 0, 0); + } + if (!key._color3) { + key._color3 = new Color3(key.color.x, key.color.y, key.color.z); + } + // Sync Vector3 with Color3 before render + key._color3.r = key.color.x; + key._color3.g = key.color.y; + key._color3.b = key.color.z; + + return ( + +
+
+ { + key.color.x = color.r; + key.color.y = color.g; + key.color.z = color.b; + key._color3.r = color.r; + key._color3.g = color.g; + key._color3.b = color.b; + onChange(); + }} + /> +
+
+ {key.position === undefined && (key.position = index / Math.max(1, value.colorKeys.length - 1))} + { + key.position = vals[0]; + onChange(); + }} + /> +
+ +
+
+ ); + })} + + +
Alpha Keys
+ {value.alphaKeys.map((key: any, index: number) => ( + +
+
+ {key.value === undefined && (key.value = 1)} + +
+
+ {key.position === undefined && (key.position = index / Math.max(1, value.alphaKeys.length - 1))} + { + key.position = vals[0]; + onChange(); + }} + /> +
+ +
+
+ ))} + +
+ ); +} + diff --git a/editor/src/editor/windows/fx-editor/properties/behaviors/function-editor.tsx b/editor/src/editor/windows/fx-editor/properties/behaviors/function-editor.tsx new file mode 100644 index 000000000..92a3bb6cf --- /dev/null +++ b/editor/src/editor/windows/fx-editor/properties/behaviors/function-editor.tsx @@ -0,0 +1,151 @@ +import { ReactNode } from "react"; + +import { EditorInspectorNumberField } from "../../../../layout/inspector/fields/number"; +import { EditorInspectorListField } from "../../../../layout/inspector/fields/list"; +import { EditorInspectorBlockField } from "../../../../layout/inspector/fields/block"; + +import { BezierEditor } from "./bezier-editor"; + +export type FunctionType = "ConstantValue" | "IntervalValue" | "PiecewiseBezier" | "Vector3Function"; + +export interface IFunctionEditorProps { + value: any; + onChange: () => void; + availableTypes?: FunctionType[]; + label: string; +} + +export function FunctionEditor(props: IFunctionEditorProps): ReactNode { + const { value, onChange, availableTypes, label } = props; + + // Default available types if not specified + const types = availableTypes || ["ConstantValue", "IntervalValue", "PiecewiseBezier", "Vector3Function"]; + + // Initialize function type if not set + if (!value || !value.functionType) { + value.functionType = types[0]; + value.data = {}; + } + + const functionType = value.functionType as FunctionType; + + // Ensure data object exists + if (!value.data) { + value.data = {}; + } + + const typeItems = types.map((type) => ({ + text: type, + value: type, + })); + + return ( + <> + { + // Reset data when type changes and initialize defaults + const newType = value.functionType; + value.data = {}; + if (newType === "ConstantValue") { + value.data.value = 1.0; + } else if (newType === "IntervalValue") { + value.data.min = 0; + value.data.max = 1; + } else if (newType === "PiecewiseBezier") { + value.data.function = { p0: 0, p1: 1.0 / 3, p2: (1.0 / 3) * 2, p3: 1 }; + } else if (newType === "Vector3Function") { + value.data.x = { functionType: "ConstantValue", data: { value: 0 } }; + value.data.y = { functionType: "ConstantValue", data: { value: 0 } }; + value.data.z = { functionType: "ConstantValue", data: { value: 0 } }; + } + onChange(); + }} + /> + + {functionType === "ConstantValue" && ( + <> + {value.data.value === undefined && (value.data.value = 1.0)} + + + )} + + {functionType === "IntervalValue" && ( + <> + {value.data.min === undefined && (value.data.min = 0)} + {value.data.max === undefined && (value.data.max = 1)} + +
{label ? "Range" : ""}
+
+ + +
+
+ + )} + + {functionType === "PiecewiseBezier" && ( + <> + {!value.data.function && (value.data.function = { p0: 0, p1: 1.0 / 3, p2: (1.0 / 3) * 2, p3: 1 })} + + + )} + + {functionType === "Vector3Function" && ( + <> + +
X
+ +
+ +
Y
+ +
+ +
Z
+ +
+ + )} + + ); +} + diff --git a/editor/src/editor/windows/fx-editor/properties/behaviors/registry.ts b/editor/src/editor/windows/fx-editor/properties/behaviors/registry.ts new file mode 100644 index 000000000..cc128b318 --- /dev/null +++ b/editor/src/editor/windows/fx-editor/properties/behaviors/registry.ts @@ -0,0 +1,349 @@ +import { ReactNode } from "react"; + +export type FunctionType = "ConstantValue" | "IntervalValue" | "PiecewiseBezier" | "Vector3Function"; +export type ColorFunctionType = "ConstantColor" | "ColorRange" | "Gradient" | "RandomColorBetweenGradient"; + +export interface IBehaviorProperty { + name: string; + type: "vector3" | "number" | "color" | "range" | "boolean" | "string" | "function" | "enum" | "colorFunction"; + label: string; + default?: any; + enumItems?: Array<{ text: string; value: any }>; + functionTypes?: FunctionType[]; + colorFunctionTypes?: ColorFunctionType[]; +} + +export interface IBehaviorDefinition { + type: string; + label: string; + properties: IBehaviorProperty[]; + component?: (props: { behavior: any; onChange: () => void }) => ReactNode; +} + +export const BehaviorRegistry: { [key: string]: IBehaviorDefinition } = { + ApplyForce: { + type: "ApplyForce", + label: "Apply Force", + properties: [ + { name: "direction", type: "vector3", label: "Direction", default: { x: 0, y: 1, z: 0 } }, + { + name: "magnitude", + type: "function", + label: "Magnitude", + default: null, + functionTypes: ["ConstantValue", "IntervalValue"], + }, + ], + }, + Noise: { + type: "Noise", + label: "Noise", + properties: [ + { + name: "frequency", + type: "function", + label: "Frequency", + default: 1.0, + functionTypes: ["ConstantValue", "IntervalValue"], + }, + { + name: "power", + type: "function", + label: "Power", + default: 1.0, + functionTypes: ["ConstantValue", "IntervalValue"], + }, + { + name: "positionAmount", + type: "function", + label: "Position Amount", + default: 1.0, + functionTypes: ["ConstantValue", "IntervalValue"], + }, + { + name: "rotationAmount", + type: "function", + label: "Rotation Amount", + default: 0.0, + functionTypes: ["ConstantValue", "IntervalValue"], + }, + ], + }, + TurbulenceField: { + type: "TurbulenceField", + label: "Turbulence Field", + properties: [ + { name: "scale", type: "vector3", label: "Scale", default: { x: 1, y: 1, z: 1 } }, + { name: "octaves", type: "number", label: "Octaves", default: 1 }, + { name: "velocityMultiplier", type: "vector3", label: "Velocity Multiplier", default: { x: 1, y: 1, z: 1 } }, + { name: "timeScale", type: "vector3", label: "Time Scale", default: { x: 1, y: 1, z: 1 } }, + ], + }, + GravityForce: { + type: "GravityForce", + label: "Gravity Force", + properties: [ + { name: "center", type: "vector3", label: "Center", default: { x: 0, y: 0, z: 0 } }, + { name: "magnitude", type: "number", label: "Magnitude", default: 1.0 }, + ], + }, + ColorOverLife: { + type: "ColorOverLife", + label: "Color Over Life", + properties: [ + { + name: "color", + type: "colorFunction", + label: "Color", + default: null, + colorFunctionTypes: ["ConstantColor", "ColorRange", "Gradient", "RandomColorBetweenGradient"], + }, + ], + }, + RotationOverLife: { + type: "RotationOverLife", + label: "Rotation Over Life", + properties: [ + { + name: "angularVelocity", + type: "function", + label: "Angular Velocity", + default: null, + functionTypes: ["ConstantValue", "IntervalValue", "PiecewiseBezier"], + }, + ], + }, + Rotation3DOverLife: { + type: "Rotation3DOverLife", + label: "Rotation 3D Over Life", + properties: [ + { + name: "angularVelocity", + type: "function", + label: "Angular Velocity", + default: null, + functionTypes: ["ConstantValue", "IntervalValue", "PiecewiseBezier"], + }, + ], + }, + SizeOverLife: { + type: "SizeOverLife", + label: "Size Over Life", + properties: [ + { + name: "size", + type: "function", + label: "Size", + default: null, + functionTypes: ["ConstantValue", "IntervalValue", "PiecewiseBezier", "Vector3Function"], + }, + ], + }, + ColorBySpeed: { + type: "ColorBySpeed", + label: "Color By Speed", + properties: [ + { + name: "color", + type: "colorFunction", + label: "Color", + default: null, + colorFunctionTypes: ["ConstantColor", "ColorRange", "Gradient", "RandomColorBetweenGradient"], + }, + { name: "speedRange", type: "range", label: "Speed Range", default: { min: 0, max: 10 } }, + ], + }, + RotationBySpeed: { + type: "RotationBySpeed", + label: "Rotation By Speed", + properties: [ + { + name: "angularVelocity", + type: "function", + label: "Angular Velocity", + default: null, + functionTypes: ["ConstantValue", "IntervalValue", "PiecewiseBezier"], + }, + { name: "speedRange", type: "range", label: "Speed Range", default: { min: 0, max: 10 } }, + ], + }, + SizeBySpeed: { + type: "SizeBySpeed", + label: "Size By Speed", + properties: [ + { + name: "size", + type: "function", + label: "Size", + default: null, + functionTypes: ["ConstantValue", "IntervalValue", "PiecewiseBezier", "Vector3Function"], + }, + { name: "speedRange", type: "range", label: "Speed Range", default: { min: 0, max: 10 } }, + ], + }, + SpeedOverLife: { + type: "SpeedOverLife", + label: "Speed Over Life", + properties: [ + { + name: "speed", + type: "function", + label: "Speed", + default: null, + functionTypes: ["ConstantValue", "IntervalValue", "PiecewiseBezier"], + }, + ], + }, + FrameOverLife: { + type: "FrameOverLife", + label: "Frame Over Life", + properties: [ + { + name: "frame", + type: "function", + label: "Frame", + default: null, + functionTypes: ["ConstantValue", "IntervalValue", "PiecewiseBezier"], + }, + ], + }, + ForceOverLife: { + type: "ForceOverLife", + label: "Force Over Life", + properties: [ + { + name: "x", + type: "function", + label: "X", + default: null, + functionTypes: ["ConstantValue", "IntervalValue", "PiecewiseBezier"], + }, + { + name: "y", + type: "function", + label: "Y", + default: null, + functionTypes: ["ConstantValue", "IntervalValue", "PiecewiseBezier"], + }, + { + name: "z", + type: "function", + label: "Z", + default: null, + functionTypes: ["ConstantValue", "IntervalValue", "PiecewiseBezier"], + }, + ], + }, + OrbitOverLife: { + type: "OrbitOverLife", + label: "Orbit Over Life", + properties: [ + { + name: "orbitSpeed", + type: "function", + label: "Orbit Speed", + default: null, + functionTypes: ["ConstantValue", "IntervalValue", "PiecewiseBezier"], + }, + { name: "axis", type: "vector3", label: "Axis", default: { x: 0, y: 1, z: 0 } }, + ], + }, + WidthOverLength: { + type: "WidthOverLength", + label: "Width Over Length", + properties: [ + { + name: "width", + type: "function", + label: "Width", + default: null, + functionTypes: ["ConstantValue", "IntervalValue", "PiecewiseBezier"], + }, + ], + }, + ChangeEmitDirection: { + type: "ChangeEmitDirection", + label: "Change Emit Direction", + properties: [{ name: "angle", type: "number", label: "Angle", default: 0.0 }], + }, + EmitSubParticleSystem: { + type: "EmitSubParticleSystem", + label: "Emit Sub Particle System", + properties: [ + { name: "subParticleSystem", type: "string", label: "Sub Particle System", default: "" }, + { name: "useVelocityAsBasis", type: "boolean", label: "Use Velocity As Basis", default: false }, + { + name: "mode", + type: "enum", + label: "Mode", + default: 0, + enumItems: [ + { text: "Death", value: 0 }, + { text: "Birth", value: 1 }, + { text: "Frame", value: 2 }, + ], + }, + { name: "emitProbability", type: "number", label: "Emit Probability", default: 1.0 }, + ], + }, + LimitSpeedOverLife: { + type: "LimitSpeedOverLife", + label: "Limit Speed Over Life", + properties: [ + { + name: "speed", + type: "function", + label: "Speed", + default: null, + functionTypes: ["ConstantValue", "IntervalValue", "PiecewiseBezier"], + }, + { name: "dampen", type: "number", label: "Dampen", default: 0.0 }, + ], + }, +}; + +export function getBehaviorDefinition(type: string): IBehaviorDefinition | undefined { + return BehaviorRegistry[type]; +} + +export function createDefaultBehaviorData(type: string): any { + const definition = BehaviorRegistry[type]; + if (!definition) { + return { type }; + } + + const data: any = { type }; + for (const prop of definition.properties) { + if (prop.type === "function") { + // Initialize function with default type + data[prop.name] = { + functionType: prop.functionTypes?.[0] || "ConstantValue", + data: {}, + }; + // Set default value for ConstantValue + if (data[prop.name].functionType === "ConstantValue") { + data[prop.name].data.value = prop.default !== undefined ? prop.default : 1.0; + } else if (data[prop.name].functionType === "IntervalValue") { + data[prop.name].data.min = 0; + data[prop.name].data.max = 1; + } + } else if (prop.type === "colorFunction") { + // Initialize color function with default type + data[prop.name] = { + colorFunctionType: prop.colorFunctionTypes?.[0] || "ConstantColor", + data: {}, + }; + } else if (prop.default !== undefined) { + if (prop.type === "vector3") { + // Store as object, will be converted to Vector3 in behavior-properties.tsx + data[prop.name] = { x: prop.default.x, y: prop.default.y, z: prop.default.z }; + } else if (prop.type === "range") { + data[prop.name] = { min: prop.default.min, max: prop.default.max }; + } else { + data[prop.name] = prop.default; + } + } + } + return data; +} + diff --git a/editor/src/editor/windows/fx-editor/properties/emission.tsx b/editor/src/editor/windows/fx-editor/properties/emission.tsx index dbc0acb02..5efe47c9f 100644 --- a/editor/src/editor/windows/fx-editor/properties/emission.tsx +++ b/editor/src/editor/windows/fx-editor/properties/emission.tsx @@ -27,11 +27,8 @@ export function FXEditorEmissionProperties(props: IFXEditorEmissionPropertiesPro - - Bursts - + + {this.state.playing ? "Stop" : "Play"} @@ -64,12 +52,7 @@ export class FXEditorPreview extends Component - @@ -185,4 +168,3 @@ export class FXEditorPreview extends Component ); } - } diff --git a/editor/src/editor/windows/fx-editor/properties/behaviors.tsx b/editor/src/editor/windows/fx-editor/properties/behaviors.tsx index f95d8a3e9..cd3074d3c 100644 --- a/editor/src/editor/windows/fx-editor/properties/behaviors.tsx +++ b/editor/src/editor/windows/fx-editor/properties/behaviors.tsx @@ -72,4 +72,3 @@ export function FXEditorBehaviorsDropdown(props: IFXEditorBehaviorsPropertiesPro ); } - diff --git a/editor/src/editor/windows/fx-editor/properties/behaviors/behavior-properties.tsx b/editor/src/editor/windows/fx-editor/properties/behaviors/behavior-properties.tsx index 4aa510772..6a83bb5b5 100644 --- a/editor/src/editor/windows/fx-editor/properties/behaviors/behavior-properties.tsx +++ b/editor/src/editor/windows/fx-editor/properties/behaviors/behavior-properties.tsx @@ -87,21 +87,12 @@ export function BehaviorProperties(props: IBehaviorPropertiesProps): ReactNode { if (prop.type === "enum") { if (behavior[prop.name] === undefined) { - behavior[prop.name] = prop.default !== undefined ? prop.default : prop.enumItems?.[0]?.value ?? 0; + behavior[prop.name] = prop.default !== undefined ? prop.default : (prop.enumItems?.[0]?.value ?? 0); } if (!prop.enumItems || prop.enumItems.length === 0) { return null; } - return ( - - ); + return ; } if (prop.type === "colorFunction") { @@ -112,14 +103,7 @@ export function BehaviorProperties(props: IBehaviorPropertiesProps): ReactNode { data: {}, }; } - return ( - - ); + return ; } if (prop.type === "function") { @@ -130,15 +114,7 @@ export function BehaviorProperties(props: IBehaviorPropertiesProps): ReactNode { data: {}, }; } - return ( - - ); + return ; } return null; @@ -146,4 +122,3 @@ export function BehaviorProperties(props: IBehaviorPropertiesProps): ReactNode { ); } - diff --git a/editor/src/editor/windows/fx-editor/properties/behaviors/bezier-editor.tsx b/editor/src/editor/windows/fx-editor/properties/behaviors/bezier-editor.tsx index 94d2075c0..467b6853d 100644 --- a/editor/src/editor/windows/fx-editor/properties/behaviors/bezier-editor.tsx +++ b/editor/src/editor/windows/fx-editor/properties/behaviors/bezier-editor.tsx @@ -273,11 +273,15 @@ export class BezierEditor extends Component this.state.hoveredPoint === point || this.state.dragPoint === point; const getPointRadius = (point: "p0" | "p1" | "p2" | "p3") => { - if (point === "p0" || point === "p3") return isHovered(point) ? 7 : 5; + if (point === "p0" || point === "p3") { + return isHovered(point) ? 7 : 5; + } return isHovered(point) ? 6 : 4; }; const getPointColor = (point: "p0" | "p1" | "p2" | "p3") => { - if (point === "p0" || point === "p3") return isHovered(point) ? "#3b82f6" : "#2563eb"; + if (point === "p0" || point === "p3") { + return isHovered(point) ? "#3b82f6" : "#2563eb" + }; return isHovered(point) ? "#8b5cf6" : "#7c3aed"; }; @@ -315,9 +319,7 @@ export class BezierEditor extends Component {/* Outer glow when hovered */} - {isHovered(point) && ( - - )} + {isHovered(point) && } {/* Point circle */} {/* Value label */} {isHovered(point) && ( - + {value.toFixed(2)} )} diff --git a/editor/src/editor/windows/fx-editor/properties/behaviors/color-function-editor.tsx b/editor/src/editor/windows/fx-editor/properties/behaviors/color-function-editor.tsx index 21b8d214a..a3816147e 100644 --- a/editor/src/editor/windows/fx-editor/properties/behaviors/color-function-editor.tsx +++ b/editor/src/editor/windows/fx-editor/properties/behaviors/color-function-editor.tsx @@ -276,4 +276,3 @@ function GradientEditor(props: IGradientEditorProps): ReactNode { ); } - diff --git a/editor/src/editor/windows/fx-editor/properties/behaviors/function-editor.tsx b/editor/src/editor/windows/fx-editor/properties/behaviors/function-editor.tsx index 92a3bb6cf..0e97eb15e 100644 --- a/editor/src/editor/windows/fx-editor/properties/behaviors/function-editor.tsx +++ b/editor/src/editor/windows/fx-editor/properties/behaviors/function-editor.tsx @@ -69,13 +69,7 @@ export function FunctionEditor(props: IFunctionEditorProps): ReactNode { {functionType === "ConstantValue" && ( <> {value.data.value === undefined && (value.data.value = 1.0)} - + )} @@ -86,22 +80,8 @@ export function FunctionEditor(props: IFunctionEditorProps): ReactNode {
{label ? "Range" : ""}
- - + +
@@ -148,4 +128,3 @@ export function FunctionEditor(props: IFunctionEditorProps): ReactNode { ); } - diff --git a/editor/src/editor/windows/fx-editor/properties/behaviors/registry.ts b/editor/src/editor/windows/fx-editor/properties/behaviors/registry.ts index cc128b318..7dd65ac6d 100644 --- a/editor/src/editor/windows/fx-editor/properties/behaviors/registry.ts +++ b/editor/src/editor/windows/fx-editor/properties/behaviors/registry.ts @@ -346,4 +346,3 @@ export function createDefaultBehaviorData(type: string): any { } return data; } - diff --git a/editor/src/editor/windows/fx-editor/properties/data.ts b/editor/src/editor/windows/fx-editor/properties/data.ts index d163de0f4..10fda3f21 100644 --- a/editor/src/editor/windows/fx-editor/properties/data.ts +++ b/editor/src/editor/windows/fx-editor/properties/data.ts @@ -76,4 +76,3 @@ export function getOrCreateParticleData(nodeId: string | number): IFXParticleDat } return particleDataMap.get(nodeId)!; } - diff --git a/editor/src/editor/windows/fx-editor/properties/emission.tsx b/editor/src/editor/windows/fx-editor/properties/emission.tsx index 5efe47c9f..4cdfe0235 100644 --- a/editor/src/editor/windows/fx-editor/properties/emission.tsx +++ b/editor/src/editor/windows/fx-editor/properties/emission.tsx @@ -29,28 +29,28 @@ export function FXEditorEmissionProperties(props: IFXEditorEmissionPropertiesPro - - - - - { - particleData.bursts.push({ - time: 0, - count: 10, - cycle: 1, - interval: 1, - probability: 1.0, - }); - onChange(); - }} - > - Add Burst - - - + + + + + { + particleData.bursts.push({ + time: 0, + count: 10, + cycle: 1, + interval: 1, + probability: 1.0, + }); + onChange(); + }} + > + Add Burst + + +
{particleData.bursts.map((burst, index) => ( @@ -75,4 +75,3 @@ export function FXEditorEmissionProperties(props: IFXEditorEmissionPropertiesPro ); } - diff --git a/editor/src/editor/windows/fx-editor/properties/emitter-shape.tsx b/editor/src/editor/windows/fx-editor/properties/emitter-shape.tsx index 3905c3ae6..835dde7bf 100644 --- a/editor/src/editor/windows/fx-editor/properties/emitter-shape.tsx +++ b/editor/src/editor/windows/fx-editor/properties/emitter-shape.tsx @@ -141,9 +141,7 @@ export class FXEditorEmitterShapeProperties extends Component -
- {particleData.emitterShape.meshPath || "Drop mesh file here"} -
+
{particleData.emitterShape.meshPath || "Drop mesh file here"}
{particleData.emitterShape.meshPath && ( + + } + > - + + + {Object.values(BehaviorRegistry).map((definition) => ( + { - particleData.behaviors.splice(index, 1); + const behaviorData = createDefaultBehaviorData(definition.type); + behaviorData.id = `behavior-${Date.now()}-${Math.random()}`; + particleData.behaviors.push(behaviorData); onChange(); }} - className="mt-2" > - Remove - -
- ); - })} + {definition.label} + + ))} + + ); } -export function FXEditorBehaviorsDropdown(props: IFXEditorBehaviorsPropertiesProps): ReactNode { - const { particleData, onChange } = props; - - return ( - - - - - - {Object.values(BehaviorRegistry).map((definition) => ( - { - const behaviorData = createDefaultBehaviorData(definition.type); - particleData.behaviors.push(behaviorData); - onChange(); - }} - > - {definition.label} - - ))} - - - ); -} diff --git a/editor/src/editor/windows/fx-editor/properties/behaviors/color-function-editor.tsx b/editor/src/editor/windows/fx-editor/properties/behaviors/color-function-editor.tsx index a3816147e..e1938953f 100644 --- a/editor/src/editor/windows/fx-editor/properties/behaviors/color-function-editor.tsx +++ b/editor/src/editor/windows/fx-editor/properties/behaviors/color-function-editor.tsx @@ -10,7 +10,7 @@ import { Button } from "../../../../../ui/shadcn/ui/button"; import { AiOutlinePlus, AiOutlineClose } from "react-icons/ai"; import { Slider } from "../../../../../ui/shadcn/ui/slider"; -export type ColorFunctionType = "ConstantColor" | "ColorRange" | "Gradient" | "RandomColorBetweenGradient"; +export type ColorFunctionType = "ConstantColor" | "ColorRange" | "Gradient" | "RandomColor" | "RandomColorBetweenGradient"; export interface IColorFunctionEditorProps { value: any; @@ -38,6 +38,7 @@ export function ColorFunctionEditor(props: IColorFunctionEditorProps): ReactNode { text: "Color", value: "ConstantColor" }, { text: "Color Range", value: "ColorRange" }, { text: "Gradient", value: "Gradient" }, + { text: "Random Color", value: "RandomColor" }, { text: "Random Between Gradient", value: "RandomColorBetweenGradient" }, ]; @@ -66,6 +67,9 @@ export function ColorFunctionEditor(props: IColorFunctionEditorProps): ReactNode { value: 1, position: 0 }, { value: 1, position: 1 }, ]; + } else if (newType === "RandomColor") { + value.data.colorA = new Color4(0, 0, 0, 1); + value.data.colorB = new Color4(1, 1, 1, 1); } else if (newType === "RandomColorBetweenGradient") { value.data.gradient1 = { colorKeys: [ @@ -110,6 +114,15 @@ export function ColorFunctionEditor(props: IColorFunctionEditorProps): ReactNode {functionType === "Gradient" && } + {functionType === "RandomColor" && ( + <> + {!value.data.colorA && (value.data.colorA = new Color4(0, 0, 0, 1))} + {!value.data.colorB && (value.data.colorB = new Color4(1, 1, 1, 1))} + + + + )} + {functionType === "RandomColorBetweenGradient" && ( <> {!value.data.gradient1 && (value.data.gradient1 = {})} diff --git a/editor/src/editor/windows/fx-editor/properties/behaviors/registry.ts b/editor/src/editor/windows/fx-editor/properties/behaviors/registry.ts index 7dd65ac6d..302861c6b 100644 --- a/editor/src/editor/windows/fx-editor/properties/behaviors/registry.ts +++ b/editor/src/editor/windows/fx-editor/properties/behaviors/registry.ts @@ -1,7 +1,7 @@ import { ReactNode } from "react"; export type FunctionType = "ConstantValue" | "IntervalValue" | "PiecewiseBezier" | "Vector3Function"; -export type ColorFunctionType = "ConstantColor" | "ColorRange" | "Gradient" | "RandomColorBetweenGradient"; +export type ColorFunctionType = "ConstantColor" | "ColorRange" | "Gradient" | "RandomColor" | "RandomColorBetweenGradient"; export interface IBehaviorProperty { name: string; @@ -264,7 +264,15 @@ export const BehaviorRegistry: { [key: string]: IBehaviorDefinition } = { ChangeEmitDirection: { type: "ChangeEmitDirection", label: "Change Emit Direction", - properties: [{ name: "angle", type: "number", label: "Angle", default: 0.0 }], + properties: [ + { + name: "angle", + type: "function", + label: "Angle", + default: 0.0, + functionTypes: ["ConstantValue", "IntervalValue"], + }, + ], }, EmitSubParticleSystem: { type: "EmitSubParticleSystem", diff --git a/editor/src/editor/windows/fx-editor/properties/data.ts b/editor/src/editor/windows/fx-editor/properties/data.ts deleted file mode 100644 index 10fda3f21..000000000 --- a/editor/src/editor/windows/fx-editor/properties/data.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { Vector3, Color4 } from "babylonjs"; -import { IFXParticleData } from "./types"; - -// Mock data storage - in real implementation this would be managed by the editor -const particleDataMap: Map = new Map(); - -export function getOrCreateParticleData(nodeId: string | number): IFXParticleData { - if (!particleDataMap.has(nodeId)) { - particleDataMap.set(nodeId, { - id: String(nodeId), - name: "Particle", - visibility: true, - position: new Vector3(0, 0, 0), - rotation: new Vector3(0, 0, 0), - scale: new Vector3(1, 1, 1), - emitterShape: { - shape: "Box", - // Box properties - direction1: new Vector3(0, 1, 0), - direction2: new Vector3(0, 1, 0), - minEmitBox: new Vector3(-0.5, -0.5, -0.5), - maxEmitBox: new Vector3(0.5, 0.5, 0.5), - // Cone properties - radius: 1.0, - angle: 0.785398, // 45 degrees in radians - radiusRange: 0.0, - heightRange: 0.0, - emitFromSpawnPointOnly: false, - // Cylinder properties - height: 1.0, - directionRandomizer: 0.0, - // Sphere properties - // Hemispheric properties - // Mesh properties - meshPath: null, - }, - particleRenderer: { - renderMode: "Billboard", - worldSpace: false, - material: null, - type: "Standard", - transparent: true, - opacity: 1.0, - side: "Double", - blending: "Add", - color: new Color4(1, 1, 1, 1), - renderOrder: 0, - uvTile: { - column: 1, - row: 1, - startTileIndex: 0, - blendTiles: false, - }, - texture: null, - meshPath: null, - softParticles: false, - }, - emission: { - looping: true, - duration: 5.0, - prewarm: false, - onlyUsedByOtherSystem: false, - emitOverTime: 10, - emitOverDistance: 0, - }, - bursts: [], - particleInitialization: { - startLife: { min: 1.0, max: 2.0 }, - startSize: { min: 0.1, max: 0.2 }, - startSpeed: { min: 1.0, max: 2.0 }, - startColor: new Color4(1, 1, 1, 1), - startRotation: { min: 0, max: 360 }, - }, - behaviors: [], - }); - } - return particleDataMap.get(nodeId)!; -} diff --git a/editor/src/editor/windows/fx-editor/properties/emission.tsx b/editor/src/editor/windows/fx-editor/properties/emission.tsx index 4cdfe0235..2c10f6c59 100644 --- a/editor/src/editor/windows/fx-editor/properties/emission.tsx +++ b/editor/src/editor/windows/fx-editor/properties/emission.tsx @@ -5,8 +5,8 @@ import { EditorInspectorNumberField } from "../../../layout/inspector/fields/num import { EditorInspectorSwitchField } from "../../../layout/inspector/fields/switch"; import { Button } from "../../../../ui/shadcn/ui/button"; -import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "../../../../ui/shadcn/ui/dropdown-menu"; -import { AiOutlinePlus, AiOutlineClose } from "react-icons/ai"; +import { HiOutlineTrash } from "react-icons/hi2"; +import { IoAddSharp } from "react-icons/io5"; import { IFXParticleData } from "./types"; @@ -28,49 +28,50 @@ export function FXEditorEmissionProperties(props: IFXEditorEmissionPropertiesPro - - - - - - { - particleData.bursts.push({ - time: 0, - count: 10, - cycle: 1, - interval: 1, - probability: 1.0, - }); - onChange(); - }} - > - Add Burst - - - {particleData.bursts.map((burst, index) => ( - + + Burst {index + 1} + + + } + > - ))} + ); diff --git a/editor/src/editor/windows/fx-editor/properties/object.tsx b/editor/src/editor/windows/fx-editor/properties/object.tsx index 83d4874b9..7478dc341 100644 --- a/editor/src/editor/windows/fx-editor/properties/object.tsx +++ b/editor/src/editor/windows/fx-editor/properties/object.tsx @@ -8,14 +8,20 @@ import { IFXParticleData } from "./types"; export interface IFXEditorObjectPropertiesProps { particleData: IFXParticleData; + onChange?: () => void; } export function FXEditorObjectProperties(props: IFXEditorObjectPropertiesProps): ReactNode { - const { particleData } = props; + const { particleData, onChange } = props; return ( <> - + diff --git a/editor/src/editor/windows/fx-editor/properties/particle-initialization.tsx b/editor/src/editor/windows/fx-editor/properties/particle-initialization.tsx index f026d383f..af602d6a0 100644 --- a/editor/src/editor/windows/fx-editor/properties/particle-initialization.tsx +++ b/editor/src/editor/windows/fx-editor/properties/particle-initialization.tsx @@ -1,49 +1,88 @@ import { ReactNode } from "react"; - -import { EditorInspectorNumberField } from "../../../layout/inspector/fields/number"; -import { EditorInspectorColorField } from "../../../layout/inspector/fields/color"; -import { EditorInspectorBlockField } from "../../../layout/inspector/fields/block"; +import { Color4 } from "babylonjs"; import { IFXParticleData } from "./types"; +import { FunctionEditor } from "./behaviors/function-editor"; +import { ColorFunctionEditor } from "./behaviors/color-function-editor"; export interface IFXEditorParticleInitializationPropertiesProps { particleData: IFXParticleData; + onChange?: () => void; } export function FXEditorParticleInitializationProperties(props: IFXEditorParticleInitializationPropertiesProps): ReactNode { const { particleData } = props; + const onChange = props.onChange || (() => {}); + + // Initialize function values if not set + const init = particleData.particleInitialization; + + if (!init.startLife || !init.startLife.functionType) { + init.startLife = { + functionType: "IntervalValue", + data: { min: 1.0, max: 2.0 }, + }; + } + + if (!init.startSize || !init.startSize.functionType) { + init.startSize = { + functionType: "IntervalValue", + data: { min: 0.1, max: 0.2 }, + }; + } + + if (!init.startSpeed || !init.startSpeed.functionType) { + init.startSpeed = { + functionType: "IntervalValue", + data: { min: 1.0, max: 2.0 }, + }; + } + + if (!init.startColor || !init.startColor.colorFunctionType) { + init.startColor = { + colorFunctionType: "ConstantColor", + data: { color: new Color4(1, 1, 1, 1) }, + }; + } + + if (!init.startRotation || !init.startRotation.functionType) { + init.startRotation = { + functionType: "IntervalValue", + data: { min: 0, max: 360 }, + }; + } return ( <> - -
Start Life
-
- - -
-
- -
Start Size
-
- - -
-
- -
Start Speed
-
- - -
-
- - -
Start Rotation
-
- - -
-
+ + + + + ); } diff --git a/editor/src/editor/windows/fx-editor/properties/particle-renderer.tsx b/editor/src/editor/windows/fx-editor/properties/particle-renderer.tsx index 242cf50ec..76446fb58 100644 --- a/editor/src/editor/windows/fx-editor/properties/particle-renderer.tsx +++ b/editor/src/editor/windows/fx-editor/properties/particle-renderer.tsx @@ -55,13 +55,13 @@ export class FXEditorParticleRendererProperties extends Component this.props.onChange()} /> diff --git a/editor/src/editor/windows/fx-editor/properties/types.ts b/editor/src/editor/windows/fx-editor/properties/types.ts index d38ff6db5..f8f97e892 100644 --- a/editor/src/editor/windows/fx-editor/properties/types.ts +++ b/editor/src/editor/windows/fx-editor/properties/types.ts @@ -1,6 +1,17 @@ import { Vector3, Color4 } from "babylonjs"; +export interface IFXGroupData { + id: string; + name: string; + visibility: boolean; + position: Vector3; + rotation: Vector3; + scale: Vector3; + type: "group"; +} + export interface IFXParticleData { + type: "particle"; id: string; name: string; visibility: boolean; @@ -15,7 +26,7 @@ export interface IFXParticleData { renderMode: string; worldSpace: boolean; material: any; - type: string; + materialType: string; // MeshBasicMaterial or MeshStandardMaterial transparent: boolean; opacity: number; side: string; @@ -41,6 +52,7 @@ export interface IFXParticleData { emitOverDistance: number; }; bursts: Array<{ + id?: string; time: number; count: number; cycle: number; @@ -48,14 +60,31 @@ export interface IFXParticleData { probability: number; }>; particleInitialization: { - startLife: { min: number; max: number }; - startSize: { min: number; max: number }; - startSpeed: { min: number; max: number }; - startColor: Color4; - startRotation: { min: number; max: number }; + startLife: any; // Function: ConstantValue | IntervalValue | PiecewiseBezier + startSize: any; // Function: ConstantValue | IntervalValue | PiecewiseBezier + startSpeed: any; // Function: ConstantValue | IntervalValue | PiecewiseBezier + startColor: any; // ColorFunction: ConstantColor | ColorRange | Gradient | RandomColor | RandomColorBetweenGradient + startRotation: any; // Function: ConstantValue | IntervalValue | PiecewiseBezier }; behaviors: Array<{ + id?: string; type: string; [key: string]: any; }>; } + +export type IFXNodeData = IFXParticleData | IFXGroupData; + +/** + * Type guard to check if node data is a group + */ +export function isGroupData(data: IFXNodeData): data is IFXGroupData { + return data.type === "group"; +} + +/** + * Type guard to check if node data is a particle + */ +export function isParticleData(data: IFXNodeData): data is IFXParticleData { + return data.type === "particle"; +} diff --git a/editor/src/editor/windows/fx-editor/resources.tsx b/editor/src/editor/windows/fx-editor/resources.tsx new file mode 100644 index 000000000..68bd51559 --- /dev/null +++ b/editor/src/editor/windows/fx-editor/resources.tsx @@ -0,0 +1,89 @@ +import { Component, ReactNode } from "react"; +import { Tree, TreeNodeInfo } from "@blueprintjs/core"; +import { IConvertedNode } from "./loader"; + +import { IoImageOutline, IoCubeOutline } from "react-icons/io5"; + +import { + ContextMenu, + ContextMenuContent, + ContextMenuItem, + ContextMenuTrigger, +} from "../../../ui/shadcn/ui/context-menu"; + +export interface IFXEditorResourcesProps { + resources: IConvertedNode[]; +} + +export interface IFXEditorResourcesState { + nodes: TreeNodeInfo[]; +} + +export class FXEditorResources extends Component { + public constructor(props: IFXEditorResourcesProps) { + super(props); + + this.state = { + nodes: this._convertToTreeNodeInfo(props.resources), + }; + } + + public componentDidUpdate(prevProps: IFXEditorResourcesProps): void { + if (prevProps.resources !== this.props.resources) { + this.setState({ + nodes: this._convertToTreeNodeInfo(this.props.resources), + }); + } + } + + private _convertToTreeNodeInfo(resources: IConvertedNode[]): TreeNodeInfo[] { + return resources.map((resource) => { + const icon = resource.type === "texture" ? ( + + ) : ( + + ); + + const label = ( + + +
{resource.name}
+
+ + UUID: {resource.resourceData?.uuid || resource.id} + {resource.resourceData?.path && Path: {resource.resourceData.path}} + +
+ ); + + return { + id: resource.id, + label, + icon, + isExpanded: false, + childNodes: undefined, + isSelected: false, + hasCaret: false, + }; + }); + } + + public render(): ReactNode { + if (this.state.nodes.length === 0) { + return ( +
+

No resources

+
+ ); + } + + return ( +
+
+ +
+
+ ); + } +} + diff --git a/editor/src/editor/windows/fx-editor/t-318 (1).json b/editor/src/editor/windows/fx-editor/t-318 (1).json new file mode 100644 index 000000000..dfcb49719 --- /dev/null +++ b/editor/src/editor/windows/fx-editor/t-318 (1).json @@ -0,0 +1 @@ +{"metadata":{"version":4.6,"type":"Object","generator":"Object3D.toJSON"},"geometries":[{"uuid":"780917d8-bd1b-4d63-8aca-f79e3211f964","type":"PlaneGeometry","name":"PlaneGeometry","width":1,"height":1,"widthSegments":1,"heightSegments":1},{"uuid":"f40b6ee0-aa01-46e0-b05a-d938b54eec83","type":"BufferGeometry","name":"GlowCircleEmitter_geometry","data":{"attributes":{"position":{"itemSize":3,"type":"Float32Array","array":[0.41758671402931213,0.08306316286325455,0.10689251124858856,0.42576777935028076,-1.2535783966427516e-8,0.10689251124858856,0.3199999928474426,0,0,0.3199999928474426,0,0,0.3138512670993805,0.062428902834653854,0,0.41758671402931213,0.08306316286325455,0.10689251124858856,0.39335811138153076,0.16293425858020782,0.10689251124858856,0.41758671402931213,0.08306316286325455,0.10689251124858856,0.3138512670993805,0.062428902834653854,0,0.3138512670993805,0.062428902834653854,0,0.2956414520740509,0.12245870381593704,0,0.39335811138153076,0.16293425858020782,0.10689251124858856,0.35401293635368347,0.23654387891292572,0.10689251124858856,0.39335811138153076,0.16293425858020782,0.10689251124858856,0.2956414520740509,0.12245870381593704,0,0.2956414520740509,0.12245870381593704,0,0.26607027649879456,0.17778247594833374,0,0.35401293635368347,0.23654387891292572,0.10689251124858856,0.3010632395744324,0.30106326937675476,0.10689251124858856,0.35401293635368347,0.23654387891292572,0.10689251124858856,0.26607027649879456,0.17778247594833374,0,0.26607027649879456,0.17778247594833374,0,0.22627416253089905,0.22627416253089905,0,0.3010632395744324,0.30106326937675476,0.10689251124858856,0.23654384911060333,0.35401299595832825,0.10689251124858856,0.3010632395744324,0.30106326937675476,0.10689251124858856,0.22627416253089905,0.22627416253089905,0,0.22627416253089905,0.22627416253089905,0,0.17778246104717255,0.26607027649879456,0,0.23654384911060333,0.35401299595832825,0.10689251124858856,0.16293422877788544,0.39335811138153076,0.10689251124858856,0.23654384911060333,0.35401299595832825,0.10689251124858856,0.17778246104717255,0.26607027649879456,0,0.17778246104717255,0.26607027649879456,0,0.12245869636535645,0.2956414520740509,0,0.16293422877788544,0.39335811138153076,0.10689251124858856,0.08306317031383514,0.41758671402931213,0.10689251124858856,0.16293422877788544,0.39335811138153076,0.10689251124858856,0.12245869636535645,0.2956414520740509,0,0.12245869636535645,0.2956414520740509,0,0.06242891401052475,0.3138512670993805,0,0.08306317031383514,0.41758671402931213,0.10689251124858856,2.0868840877596995e-8,0.42576777935028076,0.10689251124858856,0.08306317031383514,0.41758671402931213,0.10689251124858856,0.06242891401052475,0.3138512670993805,0,0.06242891401052475,0.3138512670993805,0,2.415932875976523e-8,0.3199999928474426,0,2.0868840877596995e-8,0.42576777935028076,0.10689251124858856,-0.08306313306093216,0.4175867438316345,0.10689251124858856,2.0868840877596995e-8,0.42576777935028076,0.10689251124858856,2.415932875976523e-8,0.3199999928474426,0,2.415932875976523e-8,0.3199999928474426,0,-0.06242886558175087,0.3138512969017029,0,-0.08306313306093216,0.4175867438316345,0.10689251124858856,-0.16293422877788544,0.39335814118385315,0.10689251124858856,-0.08306313306093216,0.4175867438316345,0.10689251124858856,-0.06242886558175087,0.3138512969017029,0,-0.06242886558175087,0.3138512969017029,0,-0.12245865166187286,0.2956414520740509,0,-0.16293422877788544,0.39335814118385315,0.10689251124858856,-0.23654387891292572,0.35401299595832825,0.10689251124858856,-0.16293422877788544,0.39335814118385315,0.10689251124858856,-0.12245865166187286,0.2956414520740509,0,-0.12245865166187286,0.2956414520740509,0,-0.17778246104717255,0.26607027649879456,0,-0.23654387891292572,0.35401299595832825,0.10689251124858856,-0.30106329917907715,0.30106326937675476,0.10689251124858856,-0.23654387891292572,0.35401299595832825,0.10689251124858856,-0.17778246104717255,0.26607027649879456,0,-0.17778246104717255,0.26607027649879456,0,-0.22627416253089905,0.22627416253089905,0,-0.30106329917907715,0.30106326937675476,0.10689251124858856,-0.35401299595832825,0.23654386401176453,0.10689251124858856,-0.30106329917907715,0.30106326937675476,0.10689251124858856,-0.22627416253089905,0.22627416253089905,0,-0.22627416253089905,0.22627416253089905,0,-0.26607027649879456,0.17778246104717255,0,-0.35401299595832825,0.23654386401176453,0.10689251124858856,-0.39335814118385315,0.16293418407440186,0.10689251124858856,-0.35401299595832825,0.23654386401176453,0.10689251124858856,-0.26607027649879456,0.17778246104717255,0,-0.26607027649879456,0.17778246104717255,0,-0.2956414818763733,0.12245865166187286,0,-0.39335814118385315,0.16293418407440186,0.10689251124858856,-0.4175867438316345,0.08306305855512619,0.10689251124858856,-0.39335814118385315,0.16293418407440186,0.10689251124858856,-0.2956414818763733,0.12245865166187286,0,-0.2956414818763733,0.12245865166187286,0,-0.3138512969017029,0.062428828328847885,0,-0.4175867438316345,0.08306305855512619,0.10689251124858856,-0.42576777935028076,-1.5126852304092608e-7,0.10689251124858856,-0.4175867438316345,0.08306305855512619,0.10689251124858856,-0.3138512969017029,0.062428828328847885,0,-0.3138512969017029,0.062428828328847885,0,-0.3199999928474426,-1.0426924035300544e-7,0,-0.42576777935028076,-1.5126852304092608e-7,0.10689251124858856,-0.41758671402931213,-0.08306335657835007,0.10689251124858856,-0.42576777935028076,-1.5126852304092608e-7,0.10689251124858856,-0.3199999928474426,-1.0426924035300544e-7,0,-0.3199999928474426,-1.0426924035300544e-7,0,-0.3138512670993805,-0.0624290332198143,0,-0.41758671402931213,-0.08306335657835007,0.10689251124858856,-0.393358051776886,-0.16293445229530334,0.10689251124858856,-0.41758671402931213,-0.08306335657835007,0.10689251124858856,-0.3138512670993805,-0.0624290332198143,0,-0.3138512670993805,-0.0624290332198143,0,-0.29564139246940613,-0.12245883792638779,0,-0.393358051776886,-0.16293445229530334,0.10689251124858856,-0.35401278734207153,-0.23654408752918243,0.10689251124858856,-0.393358051776886,-0.16293445229530334,0.10689251124858856,-0.29564139246940613,-0.12245883792638779,0,-0.29564139246940613,-0.12245883792638779,0,-0.2660701870918274,-0.17778262495994568,0,-0.35401278734207153,-0.23654408752918243,0.10689251124858856,-0.30106309056282043,-0.3010634481906891,0.10689251124858856,-0.35401278734207153,-0.23654408752918243,0.10689251124858856,-0.2660701870918274,-0.17778262495994568,0,-0.2660701870918274,-0.17778262495994568,0,-0.2262740284204483,-0.226274311542511,0,-0.30106309056282043,-0.3010634481906891,0.10689251124858856,-0.2365436553955078,-0.3540131449699402,0.10689251124858856,-0.30106309056282043,-0.3010634481906891,0.10689251124858856,-0.2262740284204483,-0.226274311542511,0,-0.2262740284204483,-0.226274311542511,0,-0.17778228223323822,-0.2660703957080841,0,-0.2365436553955078,-0.3540131449699402,0.10689251124858856,-0.16293397545814514,-0.3933582603931427,0.10689251124858856,-0.2365436553955078,-0.3540131449699402,0.10689251124858856,-0.17778228223323822,-0.2660703957080841,0,-0.17778228223323822,-0.2660703957080841,0,-0.12245845794677734,-0.29564154148101807,0,-0.16293397545814514,-0.3933582603931427,0.10689251124858856,-0.08306281268596649,-0.4175868332386017,0.10689251124858856,-0.16293397545814514,-0.3933582603931427,0.10689251124858856,-0.12245845794677734,-0.29564154148101807,0,-0.12245845794677734,-0.29564154148101807,0,-0.06242862716317177,-0.31385132670402527,0,-0.08306281268596649,-0.4175868332386017,0.10689251124858856,3.9984527688829985e-7,-0.42576777935028076,0.10689251124858856,-0.08306281268596649,-0.4175868332386017,0.10689251124858856,-0.06242862716317177,-0.31385132670402527,0,-0.06242862716317177,-0.31385132670402527,0,3.0899172998033464e-7,-0.3199999928474426,0,3.9984527688829985e-7,-0.42576777935028076,0.10689251124858856,0.08306359499692917,-0.41758668422698975,0.10689251124858856,3.9984527688829985e-7,-0.42576777935028076,0.10689251124858856,3.0899172998033464e-7,-0.3199999928474426,0,3.0899172998033464e-7,-0.3199999928474426,0,0.06242923438549042,-0.3138512372970581,0,0.08306359499692917,-0.41758668422698975,0.10689251124858856,0.16293466091156006,-0.39335793256759644,0.10689251124858856,0.08306359499692917,-0.41758668422698975,0.10689251124858856,0.06242923438549042,-0.3138512372970581,0,0.06242923438549042,-0.3138512372970581,0,0.1224590316414833,-0.29564130306243896,0,0.16293466091156006,-0.39335793256759644,0.10689251124858856,0.23654431104660034,-0.3540126383304596,0.10689251124858856,0.16293466091156006,-0.39335793256759644,0.10689251124858856,0.1224590316414833,-0.29564130306243896,0,0.1224590316414833,-0.29564130306243896,0,0.17778280377388,-0.26607006788253784,0,0.23654431104660034,-0.3540126383304596,0.10689251124858856,0.3010636270046234,-0.3010628819465637,0.10689251124858856,0.23654431104660034,-0.3540126383304596,0.10689251124858856,0.17778280377388,-0.26607006788253784,0,0.17778280377388,-0.26607006788253784,0,0.22627444565296173,-0.22627387940883636,0,0.3010636270046234,-0.3010628819465637,0.10689251124858856,0.3540132939815521,-0.23654340207576752,0.10689251124858856,0.3010636270046234,-0.3010628819465637,0.10689251124858856,0.22627444565296173,-0.22627387940883636,0,0.22627444565296173,-0.22627387940883636,0,0.26607051491737366,-0.1777821183204651,0,0.3540132939815521,-0.23654340207576752,0.10689251124858856,0.39335834980010986,-0.16293370723724365,0.10689251124858856,0.3540132939815521,-0.23654340207576752,0.10689251124858856,0.26607051491737366,-0.1777821183204651,0,0.26607051491737366,-0.1777821183204651,0,0.29564163088798523,-0.12245826423168182,0,0.39335834980010986,-0.16293370723724365,0.10689251124858856,0.4175868630409241,-0.083062544465065,0.10689251124858856,0.39335834980010986,-0.16293370723724365,0.10689251124858856,0.29564163088798523,-0.12245826423168182,0,0.29564163088798523,-0.12245826423168182,0,0.31385138630867004,-0.06242842227220535,0,0.4175868630409241,-0.083062544465065,0.10689251124858856,0.42576777935028076,-1.2535783966427516e-8,0.10689251124858856,0.4175868630409241,-0.083062544465065,0.10689251124858856,0.31385138630867004,-0.06242842227220535,0,0.31385138630867004,-0.06242842227220535,0,0.3199999928474426,0,0,0.42576777935028076,-1.2535783966427516e-8,0.10689251124858856,0.4175865948200226,0.08306316286325455,0.10689251124858856,0.31385117769241333,0.062428902834653854,0,0.3199998736381531,0,0,0.3199998736381531,0,0,0.4257676303386688,-1.2535783966427516e-8,0.10689251124858856,0.4175865948200226,0.08306316286325455,0.10689251124858856,0.3933579623699188,0.16293425858020782,0.10689251124858856,0.29564133286476135,0.12245870381593704,0,0.31385117769241333,0.062428902834653854,0,0.31385117769241333,0.062428902834653854,0,0.4175865948200226,0.08306316286325455,0.10689251124858856,0.3933579623699188,0.16293425858020782,0.10689251124858856,0.35401275753974915,0.23654387891292572,0.10689251124858856,0.2660701274871826,0.17778247594833374,0,0.29564133286476135,0.12245870381593704,0,0.29564133286476135,0.12245870381593704,0,0.3933579623699188,0.16293425858020782,0.10689251124858856,0.35401275753974915,0.23654387891292572,0.10689251124858856,0.30106306076049805,0.30106326937675476,0.10689251124858856,0.2262740284204483,0.22627416253089905,0,0.2660701274871826,0.17778247594833374,0,0.2660701274871826,0.17778247594833374,0,0.35401275753974915,0.23654387891292572,0.10689251124858856,0.30106306076049805,0.30106326937675476,0.10689251124858856,0.2365437150001526,0.35401299595832825,0.10689251124858856,0.177782341837883,0.26607027649879456,0,0.2262740284204483,0.22627416253089905,0,0.2262740284204483,0.22627416253089905,0,0.30106306076049805,0.30106326937675476,0.10689251124858856,0.2365437150001526,0.35401299595832825,0.10689251124858856,0.1629340499639511,0.39335811138153076,0.10689251124858856,0.1224585697054863,0.2956414520740509,0,0.177782341837883,0.26607027649879456,0,0.177782341837883,0.26607027649879456,0,0.2365437150001526,0.35401299595832825,0.10689251124858856,0.1629340499639511,0.39335811138153076,0.10689251124858856,0.08306301385164261,0.41758671402931213,0.10689251124858856,0.0624287948012352,0.3138512670993805,0,0.1224585697054863,0.2956414520740509,0,0.1224585697054863,0.2956414520740509,0,0.1629340499639511,0.39335811138153076,0.10689251124858856,0.08306301385164261,0.41758671402931213,0.10689251124858856,-1.3816443811265344e-7,0.42576777935028076,0.10689251124858856,-9.536743306171047e-8,0.3199999928474426,0,0.0624287948012352,0.3138512670993805,0,0.0624287948012352,0.3138512670993805,0,0.08306301385164261,0.41758671402931213,0.10689251124858856,-1.3816443811265344e-7,0.42576777935028076,0.10689251124858856,-0.0830632895231247,0.4175867438316345,0.10689251124858856,-0.06242898479104042,0.3138512969017029,0,-9.536743306171047e-8,0.3199999928474426,0,-9.536743306171047e-8,0.3199999928474426,0,-1.3816443811265344e-7,0.42576777935028076,0.10689251124858856,-0.0830632895231247,0.4175867438316345,0.10689251124858856,-0.16293437778949738,0.39335814118385315,0.10689251124858856,-0.12245876342058182,0.2956414520740509,0,-0.06242898479104042,0.3138512969017029,0,-0.06242898479104042,0.3138512969017029,0,-0.0830632895231247,0.4175867438316345,0.10689251124858856,-0.16293437778949738,0.39335814118385315,0.10689251124858856,-0.23654407262802124,0.35401299595832825,0.10689251124858856,-0.1777825951576233,0.26607027649879456,0,-0.12245876342058182,0.2956414520740509,0,-0.12245876342058182,0.2956414520740509,0,-0.16293437778949738,0.39335814118385315,0.10689251124858856,-0.23654407262802124,0.35401299595832825,0.10689251124858856,-0.3010634481906891,0.30106326937675476,0.10689251124858856,-0.2262742966413498,0.22627416253089905,0,-0.1777825951576233,0.26607027649879456,0,-0.1777825951576233,0.26607027649879456,0,-0.23654407262802124,0.35401299595832825,0.10689251124858856,-0.3010634481906891,0.30106326937675476,0.10689251124858856,-0.3540131449699402,0.23654386401176453,0.10689251124858856,-0.2660703957080841,0.17778246104717255,0,-0.2262742966413498,0.22627416253089905,0,-0.2262742966413498,0.22627416253089905,0,-0.3010634481906891,0.30106326937675476,0.10689251124858856,-0.3540131449699402,0.23654386401176453,0.10689251124858856,-0.3933583199977875,0.16293418407440186,0.10689251124858856,-0.29564160108566284,0.12245865166187286,0,-0.2660703957080841,0.17778246104717255,0,-0.2660703957080841,0.17778246104717255,0,-0.3540131449699402,0.23654386401176453,0.10689251124858856,-0.3933583199977875,0.16293418407440186,0.10689251124858856,-0.41758692264556885,0.08306305855512619,0.10689251124858856,-0.3138514459133148,0.062428828328847885,0,-0.29564160108566284,0.12245865166187286,0,-0.29564160108566284,0.12245865166187286,0,-0.3933583199977875,0.16293418407440186,0.10689251124858856,-0.41758692264556885,0.08306305855512619,0.10689251124858856,-0.4257678985595703,-1.5126852304092608e-7,0.10689251124858856,-0.3200001120567322,-1.0426924035300544e-7,0,-0.3138514459133148,0.062428828328847885,0,-0.3138514459133148,0.062428828328847885,0,-0.41758692264556885,0.08306305855512619,0.10689251124858856,-0.4257678985595703,-1.5126852304092608e-7,0.10689251124858856,-0.41758689284324646,-0.08306335657835007,0.10689251124858856,-0.31385138630867004,-0.0624290332198143,0,-0.3200001120567322,-1.0426924035300544e-7,0,-0.3200001120567322,-1.0426924035300544e-7,0,-0.4257678985595703,-1.5126852304092608e-7,0.10689251124858856,-0.41758689284324646,-0.08306335657835007,0.10689251124858856,-0.3933582305908203,-0.16293445229530334,0.10689251124858856,-0.2956415116786957,-0.12245883792638779,0,-0.31385138630867004,-0.0624290332198143,0,-0.31385138630867004,-0.0624290332198143,0,-0.41758689284324646,-0.08306335657835007,0.10689251124858856,-0.3933582305908203,-0.16293445229530334,0.10689251124858856,-0.35401299595832825,-0.23654408752918243,0.10689251124858856,-0.26607027649879456,-0.17778262495994568,0,-0.2956415116786957,-0.12245883792638779,0,-0.2956415116786957,-0.12245883792638779,0,-0.3933582305908203,-0.16293445229530334,0.10689251124858856,-0.35401299595832825,-0.23654408752918243,0.10689251124858856,-0.3010632395744324,-0.3010634481906891,0.10689251124858856,-0.22627414762973785,-0.226274311542511,0,-0.26607027649879456,-0.17778262495994568,0,-0.26607027649879456,-0.17778262495994568,0,-0.35401299595832825,-0.23654408752918243,0.10689251124858856,-0.3010632395744324,-0.3010634481906891,0.10689251124858856,-0.23654380440711975,-0.3540131449699402,0.10689251124858856,-0.17778240144252777,-0.2660703957080841,0,-0.22627414762973785,-0.226274311542511,0,-0.22627414762973785,-0.226274311542511,0,-0.3010632395744324,-0.3010634481906891,0.10689251124858856,-0.23654380440711975,-0.3540131449699402,0.10689251124858856,-0.16293412446975708,-0.3933582603931427,0.10689251124858856,-0.1224585697054863,-0.29564154148101807,0,-0.17778240144252777,-0.2660703957080841,0,-0.17778240144252777,-0.2660703957080841,0,-0.23654380440711975,-0.3540131449699402,0.10689251124858856,-0.16293412446975708,-0.3933582603931427,0.10689251124858856,-0.08306297659873962,-0.4175868332386017,0.10689251124858856,-0.06242874637246132,-0.31385132670402527,0,-0.1224585697054863,-0.29564154148101807,0,-0.1224585697054863,-0.29564154148101807,0,-0.16293412446975708,-0.3933582603931427,0.10689251124858856,-0.08306297659873962,-0.4175868332386017,0.10689251124858856,2.4250164187833434e-7,-0.42576777935028076,0.10689251124858856,1.9073486612342094e-7,-0.3199999928474426,0,-0.06242874637246132,-0.31385132670402527,0,-0.06242874637246132,-0.31385132670402527,0,-0.08306297659873962,-0.4175868332386017,0.10689251124858856,2.4250164187833434e-7,-0.42576777935028076,0.10689251124858856,0.08306345343589783,-0.41758668422698975,0.10689251124858856,0.062429118901491165,-0.3138512372970581,0,1.9073486612342094e-7,-0.3199999928474426,0,1.9073486612342094e-7,-0.3199999928474426,0,2.4250164187833434e-7,-0.42576777935028076,0.10689251124858856,0.08306345343589783,-0.41758668422698975,0.10689251124858856,0.16293452680110931,-0.39335793256759644,0.10689251124858856,0.12245891243219376,-0.29564130306243896,0,0.062429118901491165,-0.3138512372970581,0,0.062429118901491165,-0.3138512372970581,0,0.08306345343589783,-0.41758668422698975,0.10689251124858856,0.16293452680110931,-0.39335793256759644,0.10689251124858856,0.2365441471338272,-0.3540126383304596,0.10689251124858856,0.17778268456459045,-0.26607006788253784,0,0.12245891243219376,-0.29564130306243896,0,0.12245891243219376,-0.29564130306243896,0,0.16293452680110931,-0.39335793256759644,0.10689251124858856,0.2365441471338272,-0.3540126383304596,0.10689251124858856,0.3010634779930115,-0.3010628819465637,0.10689251124858856,0.22627434134483337,-0.22627387940883636,0,0.17778268456459045,-0.26607006788253784,0,0.17778268456459045,-0.26607006788253784,0,0.2365441471338272,-0.3540126383304596,0.10689251124858856,0.3010634779930115,-0.3010628819465637,0.10689251124858856,0.3540131449699402,-0.23654340207576752,0.10689251124858856,0.2660703957080841,-0.1777821183204651,0,0.22627434134483337,-0.22627387940883636,0,0.22627434134483337,-0.22627387940883636,0,0.3010634779930115,-0.3010628819465637,0.10689251124858856,0.3540131449699402,-0.23654340207576752,0.10689251124858856,0.3933582305908203,-0.16293370723724365,0.10689251124858856,0.2956415116786957,-0.12245826423168182,0,0.2660703957080841,-0.1777821183204651,0,0.2660703957080841,-0.1777821183204651,0,0.3540131449699402,-0.23654340207576752,0.10689251124858856,0.3933582305908203,-0.16293370723724365,0.10689251124858856,0.41758668422698975,-0.083062544465065,0.10689251124858856,0.3138512372970581,-0.06242842227220535,0,0.2956415116786957,-0.12245826423168182,0,0.2956415116786957,-0.12245826423168182,0,0.3933582305908203,-0.16293370723724365,0.10689251124858856,0.41758668422698975,-0.083062544465065,0.10689251124858856,0.4257676303386688,-1.2535783966427516e-8,0.10689251124858856,0.3199998736381531,0,0,0.3138512372970581,-0.06242842227220535,0,0.3138512372970581,-0.06242842227220535,0,0.41758668422698975,-0.083062544465065,0.10689251124858856,0.4257676303386688,-1.2535783966427516e-8,0.10689251124858856],"normalized":false},"normal":{"itemSize":3,"type":"Float32Array","array":[0.6971781849861145,0.1386774480342865,-0.7033570408821106,0.7108367085456848,3.257474361362256e-7,-0.7033571004867554,0.71083664894104,3.6210147413839877e-7,-0.7033571600914001,0.71083664894104,3.6210147413839877e-7,-0.7033571600914001,0.6971781849861145,0.1386774480342865,-0.7033570408821106,0.6971781849861145,0.1386774480342865,-0.7033570408821106,0.6567274928092957,0.27202534675598145,-0.7033570408821106,0.6971781849861145,0.1386774480342865,-0.7033570408821106,0.6971781849861145,0.1386774480342865,-0.7033570408821106,0.6971781849861145,0.1386774480342865,-0.7033570408821106,0.6567275524139404,0.27202534675598145,-0.7033570408821106,0.6567274928092957,0.27202534675598145,-0.7033570408821106,0.5910391211509705,0.394919753074646,-0.7033570408821106,0.6567274928092957,0.27202534675598145,-0.7033570408821106,0.6567275524139404,0.27202534675598145,-0.7033570408821106,0.6567275524139404,0.27202534675598145,-0.7033570408821106,0.5910391211509705,0.3949197828769684,-0.7033570408821106,0.5910391211509705,0.394919753074646,-0.7033570408821106,0.5026374459266663,0.5026374459266663,-0.7033571004867554,0.5910391211509705,0.394919753074646,-0.7033570408821106,0.5910391211509705,0.3949197828769684,-0.7033570408821106,0.5910391211509705,0.3949197828769684,-0.7033570408821106,0.5026374459266663,0.5026374459266663,-0.7033570408821106,0.5026374459266663,0.5026374459266663,-0.7033571004867554,0.39491966366767883,0.5910391807556152,-0.7033571004867554,0.5026374459266663,0.5026374459266663,-0.7033571004867554,0.5026374459266663,0.5026374459266663,-0.7033570408821106,0.5026374459266663,0.5026374459266663,-0.7033570408821106,0.39491966366767883,0.5910391211509705,-0.7033571004867554,0.39491966366767883,0.5910391807556152,-0.7033571004867554,0.27202531695365906,0.6567276120185852,-0.7033570408821106,0.39491966366767883,0.5910391807556152,-0.7033571004867554,0.39491966366767883,0.5910391211509705,-0.7033571004867554,0.39491966366767883,0.5910391211509705,-0.7033571004867554,0.27202528715133667,0.6567275524139404,-0.7033570408821106,0.27202531695365906,0.6567276120185852,-0.7033570408821106,0.13867750763893127,0.6971781849861145,-0.7033570408821106,0.27202531695365906,0.6567276120185852,-0.7033570408821106,0.27202528715133667,0.6567275524139404,-0.7033570408821106,0.27202528715133667,0.6567275524139404,-0.7033570408821106,0.13867750763893127,0.6971781849861145,-0.7033570408821106,0.13867750763893127,0.6971781849861145,-0.7033570408821106,1.3676421417585516e-7,0.71083664894104,-0.7033571004867554,0.13867750763893127,0.6971781849861145,-0.7033570408821106,0.13867750763893127,0.6971781849861145,-0.7033570408821106,0.13867750763893127,0.6971781849861145,-0.7033570408821106,1.525836097471256e-7,0.7108367085456848,-0.7033571004867554,1.3676421417585516e-7,0.71083664894104,-0.7033571004867554,-0.13867735862731934,0.6971781253814697,-0.7033571004867554,1.3676421417585516e-7,0.71083664894104,-0.7033571004867554,1.525836097471256e-7,0.7108367085456848,-0.7033571004867554,1.525836097471256e-7,0.7108367085456848,-0.7033571004867554,-0.13867734372615814,0.6971781253814697,-0.7033571004867554,-0.13867735862731934,0.6971781253814697,-0.7033571004867554,-0.2720252573490143,0.6567274332046509,-0.7033571600914001,-0.13867735862731934,0.6971781253814697,-0.7033571004867554,-0.13867734372615814,0.6971781253814697,-0.7033571004867554,-0.13867734372615814,0.6971781253814697,-0.7033571004867554,-0.2720252573490143,0.6567274928092957,-0.7033571600914001,-0.2720252573490143,0.6567274332046509,-0.7033571600914001,-0.39491966366767883,0.5910390019416809,-0.7033572196960449,-0.2720252573490143,0.6567274332046509,-0.7033571600914001,-0.2720252573490143,0.6567274928092957,-0.7033571600914001,-0.2720252573490143,0.6567274928092957,-0.7033571600914001,-0.39491963386535645,0.5910390615463257,-0.7033571600914001,-0.39491966366767883,0.5910390019416809,-0.7033572196960449,-0.502637505531311,0.5026373863220215,-0.7033571004867554,-0.39491966366767883,0.5910390019416809,-0.7033572196960449,-0.39491963386535645,0.5910390615463257,-0.7033571600914001,-0.39491963386535645,0.5910390615463257,-0.7033571600914001,-0.5026374459266663,0.5026373863220215,-0.7033571600914001,-0.502637505531311,0.5026373863220215,-0.7033571004867554,-0.5910391211509705,0.39491963386535645,-0.7033571600914001,-0.502637505531311,0.5026373863220215,-0.7033571004867554,-0.5026374459266663,0.5026373863220215,-0.7033571600914001,-0.5026374459266663,0.5026373863220215,-0.7033571600914001,-0.5910391211509705,0.39491963386535645,-0.7033571600914001,-0.5910391211509705,0.39491963386535645,-0.7033571600914001,-0.6567275524139404,0.2720252275466919,-0.7033571004867554,-0.5910391211509705,0.39491963386535645,-0.7033571600914001,-0.5910391211509705,0.39491963386535645,-0.7033571600914001,-0.5910391211509705,0.39491963386535645,-0.7033571600914001,-0.6567275524139404,0.2720251977443695,-0.7033571004867554,-0.6567275524139404,0.2720252275466919,-0.7033571004867554,-0.6971781849861145,0.1386772245168686,-0.7033570408821106,-0.6567275524139404,0.2720252275466919,-0.7033571004867554,-0.6567275524139404,0.2720251977443695,-0.7033571004867554,-0.6567275524139404,0.2720251977443695,-0.7033571004867554,-0.6971781849861145,0.1386772245168686,-0.7033570408821106,-0.6971781849861145,0.1386772245168686,-0.7033570408821106,-0.71083664894104,-1.9644313908884214e-7,-0.7033571004867554,-0.6971781849861145,0.1386772245168686,-0.7033570408821106,-0.6971781849861145,0.1386772245168686,-0.7033570408821106,-0.6971781849861145,0.1386772245168686,-0.7033570408821106,-0.71083664894104,-1.8446674232563964e-7,-0.7033571004867554,-0.71083664894104,-1.9644313908884214e-7,-0.7033571004867554,-0.697178065776825,-0.13867776095867157,-0.7033571004867554,-0.71083664894104,-1.9644313908884214e-7,-0.7033571004867554,-0.71083664894104,-1.8446674232563964e-7,-0.7033571004867554,-0.71083664894104,-1.8446674232563964e-7,-0.7033571004867554,-0.697178065776825,-0.13867774605751038,-0.7033571004867554,-0.697178065776825,-0.13867776095867157,-0.7033571004867554,-0.6567273139953613,-0.27202582359313965,-0.7033571004867554,-0.697178065776825,-0.13867776095867157,-0.7033571004867554,-0.697178065776825,-0.13867774605751038,-0.7033571004867554,-0.697178065776825,-0.13867774605751038,-0.7033571004867554,-0.6567272543907166,-0.27202582359313965,-0.7033571004867554,-0.6567273139953613,-0.27202582359313965,-0.7033571004867554,-0.5910389423370361,-0.3949200212955475,-0.7033571004867554,-0.6567273139953613,-0.27202582359313965,-0.7033571004867554,-0.6567272543907166,-0.27202582359313965,-0.7033571004867554,-0.6567272543907166,-0.27202582359313965,-0.7033571004867554,-0.5910389423370361,-0.3949199914932251,-0.7033570408821106,-0.5910389423370361,-0.3949200212955475,-0.7033571004867554,-0.5026372671127319,-0.5026376843452454,-0.7033571004867554,-0.5910389423370361,-0.3949200212955475,-0.7033571004867554,-0.5910389423370361,-0.3949199914932251,-0.7033570408821106,-0.5910389423370361,-0.3949199914932251,-0.7033570408821106,-0.5026372075080872,-0.5026376843452454,-0.7033571004867554,-0.5026372671127319,-0.5026376843452454,-0.7033571004867554,-0.3949193060398102,-0.5910392999649048,-0.7033571600914001,-0.5026372671127319,-0.5026376843452454,-0.7033571004867554,-0.5026372075080872,-0.5026376843452454,-0.7033571004867554,-0.5026372075080872,-0.5026376843452454,-0.7033571004867554,-0.39491933584213257,-0.5910392999649048,-0.7033571600914001,-0.3949193060398102,-0.5910392999649048,-0.7033571600914001,-0.27202484011650085,-0.6567276120185852,-0.7033571600914001,-0.3949193060398102,-0.5910392999649048,-0.7033571600914001,-0.39491933584213257,-0.5910392999649048,-0.7033571600914001,-0.39491933584213257,-0.5910392999649048,-0.7033571600914001,-0.27202484011650085,-0.65672767162323,-0.7033571600914001,-0.27202484011650085,-0.6567276120185852,-0.7033571600914001,-0.1386767476797104,-0.697178304195404,-0.7033571004867554,-0.27202484011650085,-0.6567276120185852,-0.7033571600914001,-0.27202484011650085,-0.65672767162323,-0.7033571600914001,-0.27202484011650085,-0.65672767162323,-0.7033571600914001,-0.1386767327785492,-0.6971782445907593,-0.7033571004867554,-0.1386767476797104,-0.697178304195404,-0.7033571004867554,6.514948722724512e-7,-0.7108367085456848,-0.7033571004867554,-0.1386767476797104,-0.697178304195404,-0.7033571004867554,-0.1386767327785492,-0.6971782445907593,-0.7033571004867554,-0.1386767327785492,-0.6971782445907593,-0.7033571004867554,6.536043883897946e-7,-0.71083664894104,-0.7033571004867554,6.514948722724512e-7,-0.7108367085456848,-0.7033571004867554,0.1386781930923462,-0.6971779465675354,-0.7033571004867554,6.514948722724512e-7,-0.7108367085456848,-0.7033571004867554,6.536043883897946e-7,-0.71083664894104,-0.7033571004867554,6.536043883897946e-7,-0.71083664894104,-0.7033571004867554,0.1386781930923462,-0.6971779465675354,-0.7033571004867554,0.1386781930923462,-0.6971779465675354,-0.7033571004867554,0.2720262110233307,-0.6567271947860718,-0.7033570408821106,0.1386781930923462,-0.6971779465675354,-0.7033571004867554,0.1386781930923462,-0.6971779465675354,-0.7033571004867554,0.1386781930923462,-0.6971779465675354,-0.7033571004867554,0.2720262408256531,-0.6567271947860718,-0.7033570408821106,0.2720262110233307,-0.6567271947860718,-0.7033570408821106,0.3949204683303833,-0.591038703918457,-0.7033569812774658,0.2720262110233307,-0.6567271947860718,-0.7033570408821106,0.2720262408256531,-0.6567271947860718,-0.7033570408821106,0.2720262408256531,-0.6567271947860718,-0.7033570408821106,0.3949204683303833,-0.591038703918457,-0.7033569812774658,0.3949204683303833,-0.591038703918457,-0.7033569812774658,0.5026381015777588,-0.5026369094848633,-0.7033569812774658,0.3949204683303833,-0.591038703918457,-0.7033569812774658,0.3949204683303833,-0.591038703918457,-0.7033569812774658,0.3949204683303833,-0.591038703918457,-0.7033569812774658,0.502638041973114,-0.5026369094848633,-0.7033570408821106,0.5026381015777588,-0.5026369094848633,-0.7033569812774658,0.5910396575927734,-0.39491894841194153,-0.7033571004867554,0.5026381015777588,-0.5026369094848633,-0.7033569812774658,0.502638041973114,-0.5026369094848633,-0.7033570408821106,0.502638041973114,-0.5026369094848633,-0.7033570408821106,0.5910395979881287,-0.3949189782142639,-0.7033571004867554,0.5910396575927734,-0.39491894841194153,-0.7033571004867554,0.6567279100418091,-0.2720244228839874,-0.7033571004867554,0.5910396575927734,-0.39491894841194153,-0.7033571004867554,0.5910395979881287,-0.3949189782142639,-0.7033571004867554,0.5910395979881287,-0.3949189782142639,-0.7033571004867554,0.6567278504371643,-0.27202436327934265,-0.7033571004867554,0.6567279100418091,-0.2720244228839874,-0.7033571004867554,0.697178304195404,-0.13867662847042084,-0.7033571004867554,0.6567279100418091,-0.2720244228839874,-0.7033571004867554,0.6567278504371643,-0.27202436327934265,-0.7033571004867554,0.6567278504371643,-0.27202436327934265,-0.7033571004867554,0.697178304195404,-0.13867664337158203,-0.7033571004867554,0.697178304195404,-0.13867662847042084,-0.7033571004867554,0.7108367085456848,3.257474361362256e-7,-0.7033571004867554,0.697178304195404,-0.13867662847042084,-0.7033571004867554,0.697178304195404,-0.13867664337158203,-0.7033571004867554,0.697178304195404,-0.13867664337158203,-0.7033571004867554,0.71083664894104,3.6210147413839877e-7,-0.7033571600914001,0.7108367085456848,3.257474361362256e-7,-0.7033571004867554,-0.6971782445907593,-0.1386774629354477,0.7033569812774658,-0.6971781849861145,-0.1386774629354477,0.7033570408821106,-0.7108367085456848,-1.1159099955193597e-7,0.7033570408821106,-0.7108367085456848,-1.1159099955193597e-7,0.7033570408821106,-0.7108367085456848,-9.200498851669181e-8,0.7033570408821106,-0.6971782445907593,-0.1386774629354477,0.7033569812774658,-0.6567275524139404,-0.27202561497688293,0.7033569812774658,-0.6567274928092957,-0.27202558517456055,0.7033569812774658,-0.6971781849861145,-0.1386774629354477,0.7033570408821106,-0.6971781849861145,-0.1386774629354477,0.7033570408821106,-0.6971782445907593,-0.1386774629354477,0.7033569812774658,-0.6567275524139404,-0.27202561497688293,0.7033569812774658,-0.5910391807556152,-0.3949199616909027,0.703356921672821,-0.5910391211509705,-0.3949199616909027,0.703356921672821,-0.6567274928092957,-0.27202558517456055,0.7033569812774658,-0.6567274928092957,-0.27202558517456055,0.7033569812774658,-0.6567275524139404,-0.27202561497688293,0.7033569812774658,-0.5910391807556152,-0.3949199616909027,0.703356921672821,-0.5026376247406006,-0.502637505531311,0.703356921672821,-0.5026376247406006,-0.5026374459266663,0.703356921672821,-0.5910391211509705,-0.3949199616909027,0.703356921672821,-0.5910391211509705,-0.3949199616909027,0.703356921672821,-0.5910391807556152,-0.3949199616909027,0.703356921672821,-0.5026376247406006,-0.502637505531311,0.703356921672821,-0.3949197232723236,-0.5910391807556152,0.7033570408821106,-0.394919753074646,-0.5910391807556152,0.7033569812774658,-0.5026376247406006,-0.5026374459266663,0.703356921672821,-0.5026376247406006,-0.5026374459266663,0.703356921672821,-0.5026376247406006,-0.502637505531311,0.703356921672821,-0.3949197232723236,-0.5910391807556152,0.7033570408821106,-0.27202528715133667,-0.65672767162323,0.7033569812774658,-0.27202528715133667,-0.65672767162323,0.703356921672821,-0.394919753074646,-0.5910391807556152,0.7033569812774658,-0.394919753074646,-0.5910391807556152,0.7033569812774658,-0.3949197232723236,-0.5910391807556152,0.7033570408821106,-0.27202528715133667,-0.65672767162323,0.7033569812774658,-0.13867752254009247,-0.6971781849861145,0.7033569812774658,-0.13867753744125366,-0.6971782445907593,0.7033569812774658,-0.27202528715133667,-0.65672767162323,0.703356921672821,-0.27202528715133667,-0.65672767162323,0.703356921672821,-0.27202528715133667,-0.65672767162323,0.7033569812774658,-0.13867752254009247,-0.6971781849861145,0.7033569812774658,-1.2184446518404002e-7,-0.7108367085456848,0.7033571004867554,-1.594157055251344e-7,-0.71083664894104,0.7033571004867554,-0.13867753744125366,-0.6971782445907593,0.7033569812774658,-0.13867753744125366,-0.6971782445907593,0.7033569812774658,-0.13867752254009247,-0.6971781849861145,0.7033569812774658,-1.2184446518404002e-7,-0.7108367085456848,0.7033571004867554,0.13867734372615814,-0.6971781253814697,0.7033571600914001,0.13867731392383575,-0.6971781253814697,0.7033571004867554,-1.594157055251344e-7,-0.71083664894104,0.7033571004867554,-1.594157055251344e-7,-0.71083664894104,0.7033571004867554,-1.2184446518404002e-7,-0.7108367085456848,0.7033571004867554,0.13867734372615814,-0.6971781253814697,0.7033571600914001,0.2720251977443695,-0.6567274332046509,0.7033571600914001,0.27202513813972473,-0.6567274928092957,0.7033571600914001,0.13867731392383575,-0.6971781253814697,0.7033571004867554,0.13867731392383575,-0.6971781253814697,0.7033571004867554,0.13867734372615814,-0.6971781253814697,0.7033571600914001,0.2720251977443695,-0.6567274332046509,0.7033571600914001,0.3949195146560669,-0.5910390615463257,0.7033572793006897,0.3949195146560669,-0.5910390615463257,0.7033572793006897,0.27202513813972473,-0.6567274928092957,0.7033571600914001,0.27202513813972473,-0.6567274928092957,0.7033571600914001,0.2720251977443695,-0.6567274332046509,0.7033571600914001,0.3949195146560669,-0.5910390615463257,0.7033572793006897,0.5026373863220215,-0.5026372671127319,0.7033572196960449,0.5026374459266663,-0.5026372671127319,0.7033572196960449,0.3949195146560669,-0.5910390615463257,0.7033572793006897,0.3949195146560669,-0.5910390615463257,0.7033572793006897,0.3949195146560669,-0.5910390615463257,0.7033572793006897,0.5026373863220215,-0.5026372671127319,0.7033572196960449,0.5910391211509705,-0.3949195444583893,0.7033572196960449,0.5910390615463257,-0.39491957426071167,0.7033572196960449,0.5026374459266663,-0.5026372671127319,0.7033572196960449,0.5026374459266663,-0.5026372671127319,0.7033572196960449,0.5026373863220215,-0.5026372671127319,0.7033572196960449,0.5910391211509705,-0.3949195444583893,0.7033572196960449,0.6567274332046509,-0.27202528715133667,0.7033571600914001,0.6567274928092957,-0.27202534675598145,0.7033571600914001,0.5910390615463257,-0.39491957426071167,0.7033572196960449,0.5910390615463257,-0.39491957426071167,0.7033572196960449,0.5910391211509705,-0.3949195444583893,0.7033572196960449,0.6567274332046509,-0.27202528715133667,0.7033571600914001,0.6971781253814697,-0.13867710530757904,0.7033572196960449,0.6971781253814697,-0.13867712020874023,0.7033572196960449,0.6567274928092957,-0.27202534675598145,0.7033571600914001,0.6567274928092957,-0.27202534675598145,0.7033571600914001,0.6567274332046509,-0.27202528715133667,0.7033571600914001,0.6971781253814697,-0.13867710530757904,0.7033572196960449,0.7108365893363953,1.9644303961285914e-7,0.7033571600914001,0.7108365893363953,1.8674414548058849e-7,0.7033572196960449,0.6971781253814697,-0.13867712020874023,0.7033572196960449,0.6971781253814697,-0.13867712020874023,0.7033572196960449,0.6971781253814697,-0.13867710530757904,0.7033572196960449,0.7108365893363953,1.9644303961285914e-7,0.7033571600914001,0.6971780061721802,0.1386774778366089,0.7033572793006897,0.6971780061721802,0.13867749273777008,0.7033572793006897,0.7108365893363953,1.8674414548058849e-7,0.7033572196960449,0.7108365893363953,1.8674414548058849e-7,0.7033572196960449,0.7108365893363953,1.9644303961285914e-7,0.7033571600914001,0.6971780061721802,0.1386774778366089,0.7033572793006897,0.656727135181427,0.2720257043838501,0.7033573389053345,0.6567271947860718,0.2720257341861725,0.7033572793006897,0.6971780061721802,0.13867749273777008,0.7033572793006897,0.6971780061721802,0.13867749273777008,0.7033572793006897,0.6971780061721802,0.1386774778366089,0.7033572793006897,0.656727135181427,0.2720257043838501,0.7033573389053345,0.5910387635231018,0.3949199616909027,0.7033572196960449,0.5910387635231018,0.3949199616909027,0.7033572793006897,0.6567271947860718,0.2720257341861725,0.7033572793006897,0.6567271947860718,0.2720257341861725,0.7033572793006897,0.656727135181427,0.2720257043838501,0.7033573389053345,0.5910387635231018,0.3949199616909027,0.7033572196960449,0.5026370286941528,0.5026376247406006,0.7033572793006897,0.5026370882987976,0.5026376843452454,0.7033572196960449,0.5910387635231018,0.3949199616909027,0.7033572793006897,0.5910387635231018,0.3949199616909027,0.7033572793006897,0.5910387635231018,0.3949199616909027,0.7033572196960449,0.5026370286941528,0.5026376247406006,0.7033572793006897,0.39491918683052063,0.59103924036026,0.7033572196960449,0.394919216632843,0.5910392999649048,0.7033572196960449,0.5026370882987976,0.5026376843452454,0.7033572196960449,0.5026370882987976,0.5026376843452454,0.7033572196960449,0.5026370286941528,0.5026376247406006,0.7033572793006897,0.39491918683052063,0.59103924036026,0.7033572196960449,0.27202481031417847,0.6567276120185852,0.7033572196960449,0.2720247805118561,0.6567276120185852,0.7033572196960449,0.394919216632843,0.5910392999649048,0.7033572196960449,0.394919216632843,0.5910392999649048,0.7033572196960449,0.39491918683052063,0.59103924036026,0.7033572196960449,0.27202481031417847,0.6567276120185852,0.7033572196960449,0.1386767327785492,0.6971782445907593,0.7033571600914001,0.138676717877388,0.6971781849861145,0.7033571600914001,0.2720247805118561,0.6567276120185852,0.7033572196960449,0.2720247805118561,0.6567276120185852,0.7033572196960449,0.27202481031417847,0.6567276120185852,0.7033572196960449,0.1386767327785492,0.6971782445907593,0.7033571600914001,-6.365750095937983e-7,0.7108365893363953,0.7033571600914001,-6.627138304793334e-7,0.71083664894104,0.7033571600914001,0.138676717877388,0.6971781849861145,0.7033571600914001,0.138676717877388,0.6971781849861145,0.7033571600914001,0.1386767327785492,0.6971782445907593,0.7033571600914001,-6.365750095937983e-7,0.7108365893363953,0.7033571600914001,-0.138678178191185,0.697178065776825,0.7033570408821106,-0.13867820799350739,0.6971779465675354,0.7033571004867554,-6.627138304793334e-7,0.71083664894104,0.7033571600914001,-6.627138304793334e-7,0.71083664894104,0.7033571600914001,-6.365750095937983e-7,0.7108365893363953,0.7033571600914001,-0.138678178191185,0.697178065776825,0.7033570408821106,-0.27202627062797546,0.6567271947860718,0.7033569812774658,-0.27202630043029785,0.6567271947860718,0.7033570408821106,-0.13867820799350739,0.6971779465675354,0.7033571004867554,-0.13867820799350739,0.6971779465675354,0.7033571004867554,-0.138678178191185,0.697178065776825,0.7033570408821106,-0.27202627062797546,0.6567271947860718,0.7033569812774658,-0.3949204385280609,0.5910387635231018,0.703356921672821,-0.3949204683303833,0.591038703918457,0.7033569812774658,-0.27202630043029785,0.6567271947860718,0.7033570408821106,-0.27202630043029785,0.6567271947860718,0.7033570408821106,-0.27202627062797546,0.6567271947860718,0.7033569812774658,-0.3949204385280609,0.5910387635231018,0.703356921672821,-0.5026381015777588,0.5026369094848633,0.7033569812774658,-0.5026381015777588,0.5026369094848633,0.7033569812774658,-0.3949204683303833,0.591038703918457,0.7033569812774658,-0.3949204683303833,0.591038703918457,0.7033569812774658,-0.3949204385280609,0.5910387635231018,0.703356921672821,-0.5026381015777588,0.5026369094848633,0.7033569812774658,-0.5910396575927734,0.3949190378189087,0.7033569812774658,-0.5910396575927734,0.3949190676212311,0.7033569812774658,-0.5026381015777588,0.5026369094848633,0.7033569812774658,-0.5026381015777588,0.5026369094848633,0.7033569812774658,-0.5026381015777588,0.5026369094848633,0.7033569812774658,-0.5910396575927734,0.3949190378189087,0.7033569812774658,-0.6567279696464539,0.27202433347702026,0.7033570408821106,-0.6567279696464539,0.2720243036746979,0.7033570408821106,-0.5910396575927734,0.3949190676212311,0.7033569812774658,-0.5910396575927734,0.3949190676212311,0.7033569812774658,-0.5910396575927734,0.3949190378189087,0.7033569812774658,-0.6567279696464539,0.27202433347702026,0.7033570408821106,-0.6971784234046936,0.13867659866809845,0.7033569812774658,-0.6971784234046936,0.13867662847042084,0.703356921672821,-0.6567279696464539,0.2720243036746979,0.7033570408821106,-0.6567279696464539,0.2720243036746979,0.7033570408821106,-0.6567279696464539,0.27202433347702026,0.7033570408821106,-0.6971784234046936,0.13867659866809845,0.7033569812774658,-0.7108367085456848,-9.200498851669181e-8,0.7033570408821106,-0.7108367085456848,-1.1159099955193597e-7,0.7033570408821106,-0.6971784234046936,0.13867662847042084,0.703356921672821,-0.6971784234046936,0.13867662847042084,0.703356921672821,-0.6971784234046936,0.13867659866809845,0.7033569812774658,-0.7108367085456848,-9.200498851669181e-8,0.7033570408821106],"normalized":false},"uv":{"itemSize":2,"type":"Float32Array","array":[0.8906737565994263,0.9400254487991333,0.4999995231628418,0.9400254487991333,0.4999995231628418,0.0599745512008667,0.4999995231628418,0.0599745512008667,0.8906737565994263,0.0599745512008667,0.8906737565994263,0.9400254487991333,1.2663346529006958,0.9400254487991333,0.8906737565994263,0.9400254487991333,0.8906737565994263,0.0599745512008667,0.8906737565994263,0.0599745512008667,1.2663346529006958,0.0599745512008667,1.2663346529006958,0.9400254487991333,1.6125456094741821,0.9400254487991333,1.2663346529006958,0.9400254487991333,1.2663346529006958,0.0599745512008667,1.2663346529006958,0.0599745512008667,1.6125456094741821,0.0599745512008667,1.6125456094741821,0.9400254487991333,1.9160020351409912,0.9400254487991333,1.6125456094741821,0.9400254487991333,1.6125456094741821,0.0599745512008667,1.6125456094741821,0.0599745512008667,1.9160020351409912,0.0599745512008667,1.9160020351409912,0.9400254487991333,-0.6125462055206299,0.9400254487991333,-0.9160027503967285,0.9400254487991333,-0.9160027503967285,0.0599745512008667,-0.9160027503967285,0.0599745512008667,-0.6125462055206299,0.0599745512008667,-0.6125462055206299,0.9400254487991333,-0.26633548736572266,0.9400254487991333,-0.6125462055206299,0.9400254487991333,-0.6125462055206299,0.0599745512008667,-0.6125462055206299,0.0599745512008667,-0.26633548736572266,0.0599745512008667,-0.26633548736572266,0.9400254487991333,0.10932528972625732,0.9400254487991333,-0.26633548736572266,0.9400254487991333,-0.26633548736572266,0.0599745512008667,-0.26633548736572266,0.0599745512008667,0.10932528972625732,0.0599745512008667,0.10932528972625732,0.9400254487991333,0.49999934434890747,0.9400254487991333,0.10932528972625732,0.9400254487991333,0.10932528972625732,0.0599745512008667,0.10932528972625732,0.0599745512008667,0.49999934434890747,0.0599745512008667,0.49999934434890747,0.9400254487991333,0.8906735181808472,0.9400254487991333,0.49999934434890747,0.9400254487991333,0.49999934434890747,0.0599745512008667,0.49999934434890747,0.0599745512008667,0.8906735181808472,0.0599745512008667,0.8906735181808472,0.9400254487991333,1.2663342952728271,0.9400254487991333,0.8906735181808472,0.9400254487991333,0.8906735181808472,0.0599745512008667,0.8906735181808472,0.0599745512008667,1.2663342952728271,0.0599745512008667,1.2663342952728271,0.9400254487991333,1.6125454902648926,0.9400254487991333,1.2663342952728271,0.9400254487991333,1.2663342952728271,0.0599745512008667,1.2663342952728271,0.0599745512008667,1.6125454902648926,0.0599745512008667,1.6125454902648926,0.9400254487991333,1.9160020351409912,0.9400254487991333,1.6125454902648926,0.9400254487991333,1.6125454902648926,0.0599745512008667,1.6125454902648926,0.0599745512008667,1.9160020351409912,0.0599745512008667,1.9160020351409912,0.9400254487991333,-0.6125462055206299,0.9400254487991333,-0.9160027503967285,0.9400254487991333,-0.9160027503967285,0.0599745512008667,-0.9160027503967285,0.0599745512008667,-0.6125462055206299,0.0599745512008667,-0.6125462055206299,0.9400254487991333,-0.266335129737854,0.9400254487991333,-0.6125462055206299,0.9400254487991333,-0.6125462055206299,0.0599745512008667,-0.6125462055206299,0.0599745512008667,-0.266335129737854,0.0599745512008667,-0.266335129737854,0.9400254487991333,0.10932576656341553,0.9400254487991333,-0.266335129737854,0.9400254487991333,-0.266335129737854,0.0599745512008667,-0.266335129737854,0.0599745512008667,0.10932576656341553,0.0599745512008667,0.10932576656341553,0.9400254487991333,0.5000001788139343,0.9400254487991333,0.10932576656341553,0.9400254487991333,0.10932576656341553,0.0599745512008667,0.10932576656341553,0.0599745512008667,0.5000001788139343,0.0599745512008667,0.5000001788139343,0.9400254487991333,0.8906745314598083,0.9400254487991333,0.5000001788139343,0.9400254487991333,0.5000001788139343,0.0599745512008667,0.5000001788139343,0.0599745512008667,0.8906745314598083,0.0599745512008667,0.8906745314598083,0.9400254487991333,1.2663354873657227,0.9400254487991333,0.8906745314598083,0.9400254487991333,0.8906745314598083,0.0599745512008667,0.8906745314598083,0.0599745512008667,1.2663354873657227,0.0599745512008667,1.2663354873657227,0.9400254487991333,1.6125465631484985,0.9400254487991333,1.2663354873657227,0.9400254487991333,1.2663354873657227,0.0599745512008667,1.2663354873657227,0.0599745512008667,1.6125465631484985,0.0599745512008667,1.6125465631484985,0.9400254487991333,1.9160029888153076,0.9400254487991333,1.6125465631484985,0.9400254487991333,1.6125465631484985,0.0599745512008667,1.6125465631484985,0.0599745512008667,1.9160029888153076,0.0599745512008667,1.9160029888153076,0.9400254487991333,-0.6125451326370239,0.9400254487991333,-0.9160019159317017,0.9400254487991333,-0.9160019159317017,0.0599745512008667,-0.9160019159317017,0.0599745512008667,-0.6125451326370239,0.0599745512008667,-0.6125451326370239,0.9400254487991333,-0.2663339376449585,0.9400254487991333,-0.6125451326370239,0.9400254487991333,-0.6125451326370239,0.0599745512008667,-0.6125451326370239,0.0599745512008667,-0.2663339376449585,0.0599745512008667,-0.2663339376449585,0.9400254487991333,0.1093270480632782,0.9400254487991333,-0.2663339376449585,0.9400254487991333,-0.2663339376449585,0.0599745512008667,-0.2663339376449585,0.0599745512008667,0.1093270480632782,0.0599745512008667,0.1093270480632782,0.9400254487991333,0.5000014305114746,0.9400254487991333,0.1093270480632782,0.9400254487991333,0.1093270480632782,0.0599745512008667,0.1093270480632782,0.0599745512008667,0.5000014305114746,0.0599745512008667,0.5000014305114746,0.9400254487991333,0.8906757831573486,0.9400254487991333,0.5000014305114746,0.9400254487991333,0.5000014305114746,0.0599745512008667,0.5000014305114746,0.0599745512008667,0.8906757831573486,0.0599745512008667,0.8906757831573486,0.9400254487991333,1.2663366794586182,0.9400254487991333,0.8906757831573486,0.9400254487991333,0.8906757831573486,0.0599745512008667,0.8906757831573486,0.0599745512008667,1.2663366794586182,0.0599745512008667,1.2663366794586182,0.9400254487991333,1.6125476360321045,0.9400254487991333,1.2663366794586182,0.9400254487991333,1.2663366794586182,0.0599745512008667,1.2663366794586182,0.0599745512008667,1.6125476360321045,0.0599745512008667,1.6125476360321045,0.9400254487991333,1.9160038232803345,0.9400254487991333,1.6125476360321045,0.9400254487991333,1.6125476360321045,0.0599745512008667,1.6125476360321045,0.0599745512008667,1.9160038232803345,0.0599745512008667,1.9160038232803345,0.9400254487991333,-0.612544059753418,0.9400254487991333,-0.9160009622573853,0.9400254487991333,-0.9160009622573853,0.0599745512008667,-0.9160009622573853,0.0599745512008667,-0.612544059753418,0.0599745512008667,-0.612544059753418,0.9400254487991333,-0.266332745552063,0.9400254487991333,-0.612544059753418,0.9400254487991333,-0.612544059753418,0.0599745512008667,-0.612544059753418,0.0599745512008667,-0.266332745552063,0.0599745512008667,-0.266332745552063,0.9400254487991333,0.10932832956314087,0.9400254487991333,-0.266332745552063,0.9400254487991333,-0.266332745552063,0.0599745512008667,-0.266332745552063,0.0599745512008667,0.10932832956314087,0.0599745512008667,0.10932832956314087,0.9400254487991333,0.4999995231628418,0.9400254487991333,0.10932832956314087,0.9400254487991333,0.10932832956314087,0.0599745512008667,0.10932832956314087,0.0599745512008667,0.4999995231628418,0.0599745512008667,0.4999995231628418,0.9400254487991333,0.8906737565994263,0.9400254487991333,0.8906737565994263,0.0599745512008667,0.4999995231628418,0.0599745512008667,0.4999995231628418,0.0599745512008667,0.4999995231628418,0.9400254487991333,0.8906737565994263,0.9400254487991333,1.2663346529006958,0.9400254487991333,1.2663346529006958,0.0599745512008667,0.8906737565994263,0.0599745512008667,0.8906737565994263,0.0599745512008667,0.8906737565994263,0.9400254487991333,1.2663346529006958,0.9400254487991333,1.6125456094741821,0.9400254487991333,1.6125456094741821,0.0599745512008667,1.2663346529006958,0.0599745512008667,1.2663346529006958,0.0599745512008667,1.2663346529006958,0.9400254487991333,1.6125456094741821,0.9400254487991333,1.9160020351409912,0.9400254487991333,1.9160020351409912,0.0599745512008667,1.6125456094741821,0.0599745512008667,1.6125456094741821,0.0599745512008667,1.6125456094741821,0.9400254487991333,1.9160020351409912,0.9400254487991333,-0.6125462055206299,0.9400254487991333,-0.6125462055206299,0.0599745512008667,-0.9160027503967285,0.0599745512008667,-0.9160027503967285,0.0599745512008667,-0.9160027503967285,0.9400254487991333,-0.6125462055206299,0.9400254487991333,-0.26633548736572266,0.9400254487991333,-0.26633548736572266,0.0599745512008667,-0.6125462055206299,0.0599745512008667,-0.6125462055206299,0.0599745512008667,-0.6125462055206299,0.9400254487991333,-0.26633548736572266,0.9400254487991333,0.10932528972625732,0.9400254487991333,0.10932528972625732,0.0599745512008667,-0.26633548736572266,0.0599745512008667,-0.26633548736572266,0.0599745512008667,-0.26633548736572266,0.9400254487991333,0.10932528972625732,0.9400254487991333,0.49999934434890747,0.9400254487991333,0.49999934434890747,0.0599745512008667,0.10932528972625732,0.0599745512008667,0.10932528972625732,0.0599745512008667,0.10932528972625732,0.9400254487991333,0.49999934434890747,0.9400254487991333,0.8906735181808472,0.9400254487991333,0.8906735181808472,0.0599745512008667,0.49999934434890747,0.0599745512008667,0.49999934434890747,0.0599745512008667,0.49999934434890747,0.9400254487991333,0.8906735181808472,0.9400254487991333,1.2663342952728271,0.9400254487991333,1.2663342952728271,0.0599745512008667,0.8906735181808472,0.0599745512008667,0.8906735181808472,0.0599745512008667,0.8906735181808472,0.9400254487991333,1.2663342952728271,0.9400254487991333,1.6125454902648926,0.9400254487991333,1.6125454902648926,0.0599745512008667,1.2663342952728271,0.0599745512008667,1.2663342952728271,0.0599745512008667,1.2663342952728271,0.9400254487991333,1.6125454902648926,0.9400254487991333,1.9160020351409912,0.9400254487991333,1.9160020351409912,0.0599745512008667,1.6125454902648926,0.0599745512008667,1.6125454902648926,0.0599745512008667,1.6125454902648926,0.9400254487991333,1.9160020351409912,0.9400254487991333,-0.6125462055206299,0.9400254487991333,-0.6125462055206299,0.0599745512008667,-0.9160027503967285,0.0599745512008667,-0.9160027503967285,0.0599745512008667,-0.9160027503967285,0.9400254487991333,-0.6125462055206299,0.9400254487991333,-0.266335129737854,0.9400254487991333,-0.266335129737854,0.0599745512008667,-0.6125462055206299,0.0599745512008667,-0.6125462055206299,0.0599745512008667,-0.6125462055206299,0.9400254487991333,-0.266335129737854,0.9400254487991333,0.10932576656341553,0.9400254487991333,0.10932576656341553,0.0599745512008667,-0.266335129737854,0.0599745512008667,-0.266335129737854,0.0599745512008667,-0.266335129737854,0.9400254487991333,0.10932576656341553,0.9400254487991333,0.5000001788139343,0.9400254487991333,0.5000001788139343,0.0599745512008667,0.10932576656341553,0.0599745512008667,0.10932576656341553,0.0599745512008667,0.10932576656341553,0.9400254487991333,0.5000001788139343,0.9400254487991333,0.8906745314598083,0.9400254487991333,0.8906745314598083,0.0599745512008667,0.5000001788139343,0.0599745512008667,0.5000001788139343,0.0599745512008667,0.5000001788139343,0.9400254487991333,0.8906745314598083,0.9400254487991333,1.2663354873657227,0.9400254487991333,1.2663354873657227,0.0599745512008667,0.8906745314598083,0.0599745512008667,0.8906745314598083,0.0599745512008667,0.8906745314598083,0.9400254487991333,1.2663354873657227,0.9400254487991333,1.6125465631484985,0.9400254487991333,1.6125465631484985,0.0599745512008667,1.2663354873657227,0.0599745512008667,1.2663354873657227,0.0599745512008667,1.2663354873657227,0.9400254487991333,1.6125465631484985,0.9400254487991333,1.9160029888153076,0.9400254487991333,1.9160029888153076,0.0599745512008667,1.6125465631484985,0.0599745512008667,1.6125465631484985,0.0599745512008667,1.6125465631484985,0.9400254487991333,1.9160029888153076,0.9400254487991333,-0.6125451326370239,0.9400254487991333,-0.6125451326370239,0.0599745512008667,-0.9160019159317017,0.0599745512008667,-0.9160019159317017,0.0599745512008667,-0.9160019159317017,0.9400254487991333,-0.6125451326370239,0.9400254487991333,-0.2663339376449585,0.9400254487991333,-0.2663339376449585,0.0599745512008667,-0.6125451326370239,0.0599745512008667,-0.6125451326370239,0.0599745512008667,-0.6125451326370239,0.9400254487991333,-0.2663339376449585,0.9400254487991333,0.1093270480632782,0.9400254487991333,0.1093270480632782,0.0599745512008667,-0.2663339376449585,0.0599745512008667,-0.2663339376449585,0.0599745512008667,-0.2663339376449585,0.9400254487991333,0.1093270480632782,0.9400254487991333,0.5000014305114746,0.9400254487991333,0.5000014305114746,0.0599745512008667,0.1093270480632782,0.0599745512008667,0.1093270480632782,0.0599745512008667,0.1093270480632782,0.9400254487991333,0.5000014305114746,0.9400254487991333,0.8906757831573486,0.9400254487991333,0.8906757831573486,0.0599745512008667,0.5000014305114746,0.0599745512008667,0.5000014305114746,0.0599745512008667,0.5000014305114746,0.9400254487991333,0.8906757831573486,0.9400254487991333,1.2663366794586182,0.9400254487991333,1.2663366794586182,0.0599745512008667,0.8906757831573486,0.0599745512008667,0.8906757831573486,0.0599745512008667,0.8906757831573486,0.9400254487991333,1.2663366794586182,0.9400254487991333,1.6125476360321045,0.9400254487991333,1.6125476360321045,0.0599745512008667,1.2663366794586182,0.0599745512008667,1.2663366794586182,0.0599745512008667,1.2663366794586182,0.9400254487991333,1.6125476360321045,0.9400254487991333,1.9160038232803345,0.9400254487991333,1.9160038232803345,0.0599745512008667,1.6125476360321045,0.0599745512008667,1.6125476360321045,0.0599745512008667,1.6125476360321045,0.9400254487991333,1.9160038232803345,0.9400254487991333,-0.612544059753418,0.9400254487991333,-0.612544059753418,0.0599745512008667,-0.9160009622573853,0.0599745512008667,-0.9160009622573853,0.0599745512008667,-0.9160009622573853,0.9400254487991333,-0.612544059753418,0.9400254487991333,-0.266332745552063,0.9400254487991333,-0.266332745552063,0.0599745512008667,-0.612544059753418,0.0599745512008667,-0.612544059753418,0.0599745512008667,-0.612544059753418,0.9400254487991333,-0.266332745552063,0.9400254487991333,0.10932832956314087,0.9400254487991333,0.10932832956314087,0.0599745512008667,-0.266332745552063,0.0599745512008667,-0.266332745552063,0.0599745512008667,-0.266332745552063,0.9400254487991333,0.10932832956314087,0.9400254487991333,0.4999995231628418,0.9400254487991333,0.4999995231628418,0.0599745512008667,0.10932832956314087,0.0599745512008667,0.10932832956314087,0.0599745512008667,0.10932832956314087,0.9400254487991333,0.4999995231628418,0.9400254487991333],"normalized":false}}}}],"materials":[{"uuid":"769df3ee-4567-40b7-8da4-473fb149f350","type":"MeshBasicMaterial","color":16777215,"map":"3874a02e-6d61-4cbb-8379-9c1436361bb4","envMapRotation":[0,0,0,"XYZ"],"reflectivity":1,"refractionRatio":0.98,"blending":2,"side":2,"transparent":true,"blendColor":0,"depthWrite":false},{"uuid":"6d9283b7-81c2-4063-84cc-f696054ce6f6","type":"MeshBasicMaterial","color":16777215,"map":"93d77365-4fc6-43a7-b19a-e2fb18ab38d0","envMapRotation":[0,0,0,"XYZ"],"reflectivity":1,"refractionRatio":0.98,"blending":2,"side":2,"transparent":true,"blendColor":0,"depthWrite":false},{"uuid":"7442c205-fb42-4fb9-baec-82a192b81351","type":"MeshBasicMaterial","color":16777215,"map":"65e3d0e9-bf33-48b4-a8a1-5bc966d46e4e","envMapRotation":[0,0,0,"XYZ"],"reflectivity":1,"refractionRatio":0.98,"blending":2,"side":2,"transparent":true,"blendColor":0,"depthWrite":false}],"textures":[{"uuid":"3874a02e-6d61-4cbb-8379-9c1436361bb4","name":"GroundGlowEmitter_texture","image":"396bc86c-4059-45f7-b34f-f6228436b397","mapping":300,"channel":0,"repeat":[1,1],"offset":[0,0],"center":[0,0],"rotation":0,"wrap":[1001,1001],"format":1023,"internalFormat":null,"type":1009,"colorSpace":"","minFilter":1008,"magFilter":1006,"anisotropy":1,"flipY":true,"generateMipmaps":true,"premultiplyAlpha":false,"unpackAlignment":4},{"uuid":"93d77365-4fc6-43a7-b19a-e2fb18ab38d0","name":"GlowCircleEmitter_texture","image":"55a3bc1f-853b-4d4a-bbe1-77abe1ccb390","mapping":300,"channel":0,"repeat":[1,1],"offset":[0,0],"center":[0,0],"rotation":0,"wrap":[1001,1001],"format":1023,"internalFormat":null,"type":1009,"colorSpace":"","minFilter":1008,"magFilter":1006,"anisotropy":1,"flipY":true,"generateMipmaps":true,"premultiplyAlpha":false,"unpackAlignment":4},{"uuid":"65e3d0e9-bf33-48b4-a8a1-5bc966d46e4e","name":"BasicZoneBlueEmitter_texture","image":"a44aaf69-213b-4f68-96fc-304a19e9cdae","mapping":300,"channel":0,"repeat":[1,1],"offset":[0,0],"center":[0,0],"rotation":0,"wrap":[1001,1001],"format":1023,"internalFormat":null,"type":1009,"colorSpace":"","minFilter":1008,"magFilter":1006,"anisotropy":1,"flipY":true,"generateMipmaps":true,"premultiplyAlpha":false,"unpackAlignment":4}],"images":[{"uuid":"396bc86c-4059-45f7-b34f-f6228436b397","url":"data:image/jpeg;base64,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"},{"uuid":"55a3bc1f-853b-4d4a-bbe1-77abe1ccb390","url":"data:image/jpeg;base64,iVBORw0KGgoAAAANSUhEUgAAAQAAAAEACAQAAAD2e2DtAAAACXBIWXMAAAsTAAALEwEAmpwYAAADGGlDQ1BQaG90b3Nob3AgSUNDIHByb2ZpbGUAAHjaY2BgnuDo4uTKJMDAUFBUUuQe5BgZERmlwH6egY2BmYGBgYGBITG5uMAxIMCHgYGBIS8/L5UBFTAyMHy7xsDIwMDAcFnX0cXJlYE0wJpcUFTCwMBwgIGBwSgltTiZgYHhCwMDQ3p5SUEJAwNjDAMDg0hSdkEJAwNjAQMDg0h2SJAzAwNjCwMDE09JakUJAwMDg3N+QWVRZnpGiYKhpaWlgmNKflKqQnBlcUlqbrGCZ15yflFBflFiSWoKAwMD1A4GBgYGXpf8EgX3xMw8BSMDVQYqg4jIKAUICxE+CDEESC4tKoMHJQODAIMCgwGDA0MAQyJDPcMChqMMbxjFGV0YSxlXMN5jEmMKYprAdIFZmDmSeSHzGxZLlg6WW6x6rK2s99gs2aaxfWMPZ9/NocTRxfGFM5HzApcj1xZuTe4FPFI8U3mFeCfxCfNN45fhXyygI7BD0FXwilCq0A/hXhEVkb2i4aJfxCaJG4lfkaiQlJM8JpUvLS19QqZMVl32llyfvIv8H4WtioVKekpvldeqFKiaqP5UO6jepRGqqaT5QeuA9iSdVF0rPUG9V/pHDBYY1hrFGNuayJsym740u2C+02KJ5QSrOutcmzjbQDtXe2sHY0cdJzVnJRcFV3k3BXdlD3VPXS8Tbxsfd99gvwT//ID6wIlBS4N3hVwMfRnOFCEXaRUVEV0RMzN2T9yDBLZE3aSw5IaUNak30zkyLDIzs+ZmX8xlz7PPryjYVPiuWLskq3RV2ZsK/cqSql01jLVedVPrHzbqNdU0n22VaytsP9op3VXUfbpXta+x/+5Em0mzJ/+dGj/t8AyNmf2zvs9JmHt6vvmCpYtEFrcu+bYsc/m9lSGrTq9xWbtvveWGbZtMNm/ZarJt+w6rnft3u+45uy9s/4ODOYd+Hmk/Jn58xUnrU+fOJJ/9dX7SRe1LR68kXv13fc5Nm1t379TfU75/4mHeY7En+59lvhB5efB1/lv5dxc+NH0y/fzq64Lv4T8Ffp360/rP8f9/AA0ADzT6lvFdAAAAIGNIUk0AAHolAACAgwAA+f8AAIDpAAB1MAAA6mAAADqYAAAXb5JfxUYAALVrSURBVHja7P152GXXdd4H/t537XPu/eav5sJYBYAjwFEkNVADCpoHy5JsAfbTdkLQkyLLsaS43VYidaqqHXfbThxLSSRbTtICuz20AdmybNmyZVko2JasiBRNUgQ4giiQGGv8quob771nr/7j3BpRmMjC4Dw6fB7WxfdV3bPPPmuvvda73vVuJa+HS7w+xnHVsSlfv4P7ii/z+9d/kpf+z2AAesUf7/U12a/HUbymBpC/v5C/7NnK/3N7gN+/Xsrc6fc9wCsfAF4cp/TaGbGuOnf5n7oB/CfmgvO1XCqvlKH9vgG8TG/wWnmx/j4/pf+TGMDv7/5f3vXfTe3tv9e1igJ+3wN8hT5Ar4kx5zWLAvz7K/Slv/zXAyL4QwK47pXeAvJVt2j9J2EEz/UBr9ZM3SX4K0LPALBwzZbp62YLyNfpqr/oEe9QvobDfDDhWR6mBR7RLFern+g/ZQPQ69kchK7Xw6m82yDdo1d/rIcFc4y5GUg6/j/Ka7KI/HpZ769XUOi9AjikN3OLHvNAKLn/NRjswZSe4DiPAbtJvvOVjQH+r4K/r1drvb8+A9B+HB/hbtCh/EH25dH8/6YubA13XYgIXo0RP6TM3TSIf6ddzHD6lTWAIdC+TraD1zYqUd4PwDNa1ueF7rCmDvnAq+q97sxv1hnO0PIbwE5OXqPvLVf/8QJw9jUK+V5PGUECh3W7yG/J5Pd8JIeCQzqYh3VQ+17Vnes38vtkir4q4SRf9Up6gP+gEbAf+Kd6Ldb2ax8PaPp/QjqU/1oHJH+/8QGd9XviUf9Xfpjr+X+8qmP6kObZZJPfY0ULeuyVTAMfZshf0BrwB/PVW/mvuysB/gz369v1A/4dvqBFfTDewO/S6L/kv9E38oVX9XHexAbJmKdYx+x+wdnUV2YAlV0ke/n/veK++HwYdY9eh++eW3SA6zmiLQprWN+iNd0j9DZ+27+m23nvqzbuuyT9Ju/nZsw3sY0FEvhz+soXmq8eAYhvZZ41/rngwCv4kOfB1fvz+aptryUE9E3aBVrKFe1kpz6jJ53xTXorj2ifJnT8Lvv1aoxbejCTNU5qUwN9hNNsY4Uj2vVKBYGnWWTMLBt8icd15JVzdJeBWReQ9tcHRVj38ECFu/N3fCIb79cJ3+Shbqw3aKzZHOlb8u3cXV+NCkHmLTrKIc5yloWco+UJ3srteeCVigEWeJIt5gg69rxEC/+ylsE00zr8OoMBpUzy/jzENt+uGxip480YxXvjM7Gl3Szq7SSPCP2QXo3x/HUyH2JCZYuzOgsUXRvP46v/cINnWOWrqQzzpVl4vryVf2H0h/UAB/sVp9dbLHCIW/MXc5630nHWaNFzpdMuhcxED/GM7uLngXv8SnuAu/MBvZdtOsl1nKFqwlrey2Ed+IrN4KpDP8l2vYMRv87cK4PKZO83JXQwz9vDA9NY4HUQAibAzzh5C9s8oOOYTosSMVPChVs5rtSdPDMtyd9fX9loREL3cDdvyV3AMJ3i0/wCB/nKt+crDOD90wR4Nj9Px4gt/tdXDgdQcvg8rHpZTvDaI0ES/GhK79Gfz5NsU7LpIQvRxpomXmOZuXwoZ3RUeoUZIZmQ+QD3s86/ZRO4E7GbG6bT9JVGIFcYwG8lwHaGmmXETrbx9lcMBzgEeSeHQLAgSA4B0i2vlz6xvJ5/wEP8TnY5oWNWJVuve4C0niMe1Vs4ROYrPlrBh7mbx9jgHK2kDQ3ZC/zIKxUDNIw4zvU8yogb+Zt6ZR7qMNLPcZBHlAw4wj26WxLsfx14gH5dPcVR1vQDPpNvTRPKUiI1qw0g9H7dmfflKx0E9pv8f88D7GdR83qCki3Pqgp+lvuuDKCvlQE07OaMzvJGfoUfz2vtXHtXcB9/Un9A/0Fwj+7mIc7odo7o9QQYfit3QT5Dq6fU2mocGjuy1dBt7uCndYSff8UD0Uxpt+7hIdpcyG0M2GRC5lEOcW8ezFfAAFZZ42Oc4xZW2c0DuvZrC0n7dRMneIK7dVx3g76Kh85jDq+LXEB6hPvrfuZYUGgrZRJJcy6Qi+zm2PlQ9hW8DiZk3p6HOEjLhFl+R5t0FODYK7MFrGjELt7MAuY427n7mq7H84le5ipPsUO/qyd1QBgGSA+zogchD78OTKA31YdzkhvMyQ7LYUdkwxZjHaptAvlqUEU/oIP5M0rgnIYsqQWOavc1ML6rGMCQIcfYyVy+i4b9/Nw1fRk96Cvwm3gb380b2KNO8Agwz736Ne6TdPB1AAYd1pMVPaCzGTTZ1QGd7NA8J3KekxxhS7/4KgFB91b0Y7kkWMxZZjM4x1158BoY33Og4B/R+9jGcU4y0aN8cz5AuaYSCffo/sxE5IZGmvAp7s5NjivZzkzel/dPU8LXHguUjujP6NtyXZuEcFAcpdOcR/W4lvKXfCx/sA5Nvhq+6AF+WiMqJ9inFT2eGzqQf8GL1zoGkB5hP59jwGcw4qe4jc9fU7S7h3oE2su/51HNa4biXezSHqq+ztKcDrxOsoDv1SgP5S7abDIptrJKM5p31FUeyVuR/lm+8sYId+ke4Gg+RcMOrq/LjNivzTxyrWOAzA+wwj7dxNsJzvLj+ip+Nq/5AwnI49zAKS0x1lKukwwZcTZh9ho81rW4/oz+fv5CPswTDHUOK1RtNww0ovUC7+c3E34+Xw1/BMlKLrBEl2NajYH7cjcPXmskUHojyzydqzzLOd6ZxxL+6jXGujIFHGZep5gwyNOp3MTsorKo+3nUrzUrTBLS38mP8H6nhBmok5SmkQB1eU7v4k69Ot4o80EAtlS5Rc6g4WPA+/RBvwja+jINQJl/gzPs0po+yipP8kW28RMvgnXp5VtzwiHO0LAT1GrimdzUl5hhH4/kYn0V0LUXm3IyQXpbPsQgnUOKi5K2dm7cqNUSn9BDfTTzqniAn9N+LTDW55lwkgk3AN9df6G+MNr6cg0g79If5E46lvOPM8szNCylXqTilC9zcgEO6m7ewFu1xVuYy5orzDPUOqfyztdNPTDzEP8rv8anNGZMCghKeEhT5/LZfEoHhH7oVRnJEf1IwmcJZrSkpzTDe0muRbJ8hQt5MMVyBmO+wA1Y38iRa94UKT3gw/kAn2QrT+o46t0rDTPcysf59OvEBCTyj6vNfcwyUIssOTuVrD7BDv5e/oJu0d/hyCtaDBaSdFfez4dY5M2s5jzbNGH1Gn2/r3Q193JEC2xpF5ss8e/46vOkJ10rxCvzngp3y2xRq1hT8aOIIBlivkqvD3JY5h/Q361/wnM4J7JF2tHY6nJGhW/KH8mjmXmgvqKjIDPzAHBn3qJPgT5Hm6uMJOAuX1MD6EuP72KEWeWk3sx2zeah3gO8COigl2jP6JAPOPN3mLCubTql+UxmfRNnOalH9F/m7nynXmtiSG+Cv5J/VP9PLWlFw2yrIuwYq1H1Zp7I/fqvfeBVGs+D3J2HOJZnOcuztIglHtMhHqzX1AD6B3+GhiE3MckxHYf52ZdoqS/xb+WheiTv0eP53pxnoj0EXZ3hM3oT4p3cwwofq68lMWQapgoO8lPcVSsD0ukgC3YoBprTDXqKP8YHxKvhr3QYBENN2Mb1suZ1iv15QHdd5e4vr+3uKi5kL1s8yW8S2mLIBzh+Tcsd96hn/xzQr+lkzpFsKpWUfDJv5BEO6Gtec9d/np/0E35brjBf1z2fM85wtapMJeoTegP/W/0Xee1JobrsUy9TdgRxQG1+HWMqNYP9wBwPXsUv58taplcxgGXW2Zk7NeQZvsB+Xv4jvpAC6APTIR+ppzjhJ1jIcRZ1GuusPs9SznLba14HyJTuTuk32a+nuY753NQkLRpFwdHkqrfnzXx7PnDNssDzYMulbJgLIjB6MA9xhEX9noZqWdWY9bxX+17i7quXvgXAj+sIW5zWMVb0KDuBgy/bA+QLhTQcVmaSfBPbauGTCjd5JpdyTnNsY/0aF5++vLfRG+mj+afZxrNGbRYViwCH2zJRy0N0zsyvsHKpF1BGuFQG6nbu4jTvyAkN25ljgUP8wWmu8pXEZVdmATnPu0hN+DpW+Rp2cpRD17DgKfpO93uQZnKg6/IMxz3SLAXnKmP+bE4pw6+lGWQPvh7KVb3JT2dBDlsSDcXKHFH1Vbk9xXt9MK/FUPOSR9aVHiB/wHdzJ8fr44BSmpDckt/5ghvQRcvIl54FJLexDCxwgrn8tzzDft52DcOc6VDyfuC3tKA1fRXDrFQeyYk69gHfIHg9cEJu0f+kP5a/mu/KihV2EJZkhUTDF3UmU997DTesfN5V/G38iFb4Y9pUaM7DfJp54PaXCNC9jC3ggenkr8os66uZ5BFuB5iiTromPqBf4ttZZSYTNNCztNpXb+bjiC8lvB4UuR6rD9e35Q9kaB5ZlqS0XDXQJGF3/jNu0SGuvbnmJXPVf/6z9WfzR3mcs7mVj1ZrkE+SHLjWOADcnYf5ZYJxngLOsNPwAJk9Nen5nIle5sMlSdIRGXojXTbs5h08oVN8Vsmdr4P1n9lvru/UR/l3bGUDApdQ48ZVcxrm0/xpPXbNQSBdsQVc6AzgOKnr6Oh0hh36RcHP5gvhAC9tDi8jhNynD3EXUJmnxbmox/I7tJgv/lKvpliVL5hnH/E2Wga5rgW39aTXu5GkbXmYo2Qefu2p4Qkf9LtY1DvZ0FYqwyIlR4ZHelbJpiS4dmPVZW47L4Pn7vH9+WhOWOUNWZhhjsMc1X7OL82XZgjPHaovfzE/zB/lGZbYQEy0kjVP5XmlkBeKdl+O6FMmeY8O1O2salYT1hnZuZMNCvtI/ULCodccBYTku7kvP5HFY7Vgh0oUqbjThJ3M6t8Dvcd65ay1x03u0gP5IU3Yz0jH1HGSAQfZn0d4KYygvOKTrm4Amffmr/IP+CMs0+kkT3NUp9jPf9/nKteEp3e493C6P9E/y2M8q6QyyzH9H9qVo+y4hW/XQ36N3T/wgO7STRLfyGIuZZUlyw6HPGCDwldlm/BDHMg79OW5+pfy0/szE/6U4CjWM+zIp5jJRtu4T+jAy7iXrogrrpoG3sd9LNOwxE7gnVmAvzjdtK/FAS8HLxG4/S51OYOyqXiX9nKSs/687s1x3lnvfs1zgLvzQX6Fu3WajnVjFUilcBPSHMmz+oQy4QE//GUsjXwelOTqqdsfy0+KFMe1rs/mpiZ8e97yIo4nL/nWlxQEHlaCkk/zRaQxu/WU9utjUyzqsPLLAH6u5l6lOYmf0mn28KXstOalXOccZ3xLvjUPKdy7vlfR6T/PT7+1zvAQ5xjmRMoSVtjCeDOGPKrfqz/pn89rQ5vXFaDQ5WM6zNvyICd4R87lf65gnf/ITh3i4LWkhB1MQW7jLYyYS5F5fc7zr/uG0Xw5ocbzT2omJGuJ/h7bs9GQiSrJhHn25gqtjvPrVbp/Ks72qsV7zzFUab/u4gt6Nx/zCGynslguBWmmprbxXfFXEh/5io1Ol4A+V5+3fQKxwEewnkyYcBN/Lh94UX0SXXYvXeVZLzGAhwTwLg4zyzq3cY6jCv6ibnxJtKd8yV5BScLR/BKfZ6QvkQzjlNd9jON8NH+Wu4DDuuc1TgOTo/XD+vfsy105AIwjlYGsojUt6kt5W96D8kBea2N8rhD8vfVndYgFvYezDBhm4Xp+iB98UX2SvGIzeK6BXWIAdybAMh9gQ3NEdjyda17hAa45NKe7+Yda1s7cwRLLFlFXNKOb83bB7co8mK9xQViHSVYz693sTKlNK8I2cjhZ9Ln6aN79FdQC83k+X71DXt6VK9qWE3bn27P6ejb5o/lX4i699PAvX8wD9NeG9tPlozyjGXbrTP3Y8/mOLxsK6q8/kp9glnU2vaVwejE/k6c0l+QZpFe2NUwvuv77Le8zOjxNnqsVSRcuoVCWNeZY1jGOA3DHy8bK9TKh20N5Nyt8WLewrlFM8kSuS3y8PviSzOwlBoFSAn80IXSzRszr5hzwLl7Ky3+5y+D+vFPflOvcohkV0Jhnaajs0AH9Ow5xMKW7XjEjyJfiA4Afyl/23/LTtJQiyzZW1IhssnrDf236+A+/bDeQL3Ph/B3BfgY5VptnMtirzfzrvvdlGJ6e566X4QCH+ZN6hscprHOcp1nnSyx/havpucBGT7l+KE/oHKcQGS2tbk97lL/Hfr5XB/OwMh98VTaBz17tGJgLrXDfwLEaqhGKIGpI6aIQUdjMj+SD+T6J/zGuhQ/Sc+KAi9df0gPAlr6ooee0qXlW+Iv579STV17enXV1D3CP4GD+73krSySn2YmYU6P/ZTqy53PKL5fCP20OFWwHn1IhUlibWsvUO/I+/pcqvxpI4M/o8zqrL111C1AvE8un81aN60xaEs4gwlJ1JHU1YU98uMKPdy/fB+lF3PXlv//5fAR4A0tZGQBH9Yw+QafeVH/yZQTpecUb86UvBuAplhkx4nG2MaFg7s6LxaBrAbMedg/zbHBDzua6KiXkHZpooi/qv+EHdOBVqQUmT7HIxgukq/u1oRF76KSUo8hhu5Zg4sLX6IM6Vg99Wajl1coHugQPuCIw1I9yiIMZnCJZJVjML/I4Wz0dRStfgf/xldH5L3OYAbfwnRRGucg+PfCCuebL80CZcLDen3cDH9Yyq4RaiwEzvtEjxnwPBziSXGNe8B++8G3fNv30X+gMFTh1ye+uvB7L6+qfzImKJTtBFmE5pDmtebcyD9WvzAivtuKfk1Ln/5vkMKPcyYoGDHJL35bfixPuFJziV/Ty7vo8QaB4gNBeWQP+A8ewruPrry7m+CIBlV4wuIIHuBtYyiXNSTTGM6zQ8Wh+jo8B5P0J33TNjOAt0z//lbrLRv4sm7zneVPBbzM+jDRKWUhVLrKjcSPrJLP1AfMVEsKuDNCeaxASHODP6X3appNqcsKqCnvU6R2CT7DBGvA3BPBrer73oKvCT1cEgYeE/g3fwTrmMbYJRvyWeobcwZcV0uSL5tnwDqpXFa4x8VBLsYMl3sQ+fiHvn5pbuWYeYHTh06npn7uY1808w4becxWwutcKPV2/Nbf8ucxojBWURMYOYuA5fXj65t72ZQMl+QJ1gUu95iP8bP5zjqtoRjXnCI4Q9Vnu1imM6Bi+7Dvlc7eAfp//GDWf1kjK5DE9TdaLvkpfYS3g/h4KzLvzflaULEULcnQubHI8H9dD/KQeyIP5dTqhvdck5nhAvyD4s/rv9MOaMMevCmCThv2MNGbnczap82Hg2/VP9SDfkAlOK9I2JiKdHSfzh7m7AjycL28Wnj/2f27O1D/FaeA72YlqVXCG9XyYZ/yo3soBQmMKZ3hA0LzsO/vKLWk/30/LIm+haBHn1+elFM18Sev/+dfC3dlLn98PRJ5Ng7ooGqjVDhaR7uQpHtbb/B/4R9zwFRvAAQAqQ25jG/M8zoDKh6YHrz6lPn+7VAjv4gGR0rv1eW7K9SBLuuC0iwmihCc+rb/Nz/mHvqxXn1eFf56HWaLM9+Td2qbV3K5RzqrLDf1wPsyOfEwLWfJGjWmYgOZf9hh85Zu7g8O5SytcT9SzDOj0A3rxlEYvGMRcfh2EfACxzHaqQs7IzvN51h3vzQ9zB/fkJ+sf4MmXHd0+93o/cDeLdJoHzjLL9UzYAGYpEhP2AOtXegBJmZl/vt6Ud+ct1cZYJQM7sikpNZ1ylZ/TKy0T10/nKe7Pc3Q6RauOGQoTvT3n9Y3AFk/nWzF7GXH0ZYfpl/MB+JN6L3dqljFPsOEFFlVyTS9GesqroAHPf9vDHNI93K81xpKHKnKZUcMiRe+r/zf+OR/1YW3XU7l4TUDeZ4G3sp+3M2bEDQTJH9EqZpmhNvRvueOqi0MSv56P+ynCtlGEZSIJm3EO9V16uF7LGsmV6MDFwPukyJZjucAc62zpGSq/qmPszwUGTHSMys2MWOOHn3OmYb40A+jj8/+Kw3mAp/ItCZFPaSGH7M1DL3Hij+iuC9nC1W/7gODBejCTw7Ts1nyicNVEJbb8hnzUd+fdvC0PcgPfdkGm9idf4iRfnq5Kd6nlsPZwnCGh3ZphQSMV1jlJ6gb6LH/pOZ6m3/QOKfO7tEknKpFFZFjGQZgm9uS6Xnkpi8OCwx4BX2RJp1S4jTEtVXdwRt/OTiYas6xkg6Dl/CESf+4lkvl9GfSRD+QP6wibfJF9VObzBu3jvS/hVIx+Fh7iyIvc7u6Eu3yHyYdzlZoThZqQhiLn8qz+t7xD380P5wPczhyz0xL1xpc1dTdyAFjjYzzBHE+zi1ktcSOnOQOssaA1QiMtPV85qKLf1SfqHD0QkCEpKH1rAD7tkn/RX278/1L81/nZ/OrcltAwzCZnvMU5Wp5krEiYZUJyEw0Np9niHEz1Vl8amHYFJexh7eNATiisY5L/CPyPU8GU56/Q6cKX5YvSRsSD9Z68R7CdXWrUBVKTJWSf1PfoTo7mr+sB3sQp3awvABAvZ7u8cD0BwDdxOwe0k3ktMuE2Vnhad2gfCxJDDVhklv1XPMz5p/igjtWuNLZSMkEhrFqKrAmLnNTTPPAK8hcPAHcAq+zUh/ROpC2W640secxxBgz5nzSRkALpFL/JrGYB+JD+EnBYL07T8eW/Xecv5VHNs6W9JFV7mPDu6bw+Pxh8nmu68BKd2sG8P+/WFzRyYyus2kgWwXwe4U26h3dpt97O7dPDK5e/rOn7swB8Hb+nE+zA+TRv1CxFb0EMuJk+Yt4SfPZSd5kXTwm/L+/RbK5npiON01KGjZTpouv0JA8Ah/SVrPrn59sc4hZ+C/hr7OYDfFzPZGFklGluyy7JMWPA+TgNW8Asx/lp/R8c5YtXfeMv3B2c8Ne4i2XOMagdAxbZ4Df1Sy/RxyU/lrDvhbcAJ4emx0P0Z19VgrbIxdVzrOgXOeIv5J0M8+Os824BfONLXjH3XzLS49MJmGVAak27NGAHLbdyjm/SHlYE6wJUr/Bcmb1EVHI7t9ShQkIRpqG4LwsX5SwbkA8kHMovfxt4oTBtP7NsAH+JwuN6PzdwQk3CgkIdi7S6k39FZYfO70R7qZxhCPza1ISeWwW8HB28woG9LQ8AAzWCEXNcz511+UUfTpeEgY+/4N984DyJSQ9owEjFJYhUAJGFoW5QU3/XT2o3sMATCY/rVl6aJPN+9jznZ5U3sldPM1aym1ltBwW7KQyxELzxqt/+u0p+xskb1EnOUt2FpMjikF3lBW7XdYJD+kq7Q/W8tQj4ZuDfa5n79Jte1Vk3bGpTlUc1o9S/cUUcp043ylMUDub3A0vA/3iJ187nwSCuaA79Di/ptxlwmr/Nqlb4lfxtL/NiYmgXrfgXX6RqmBcCzh+sT7GQxcLRurTFxILX+aDe4o6b8lhZVqOv0s/pr3NKf+ElitX+0hUZwRL/Tk/SULRMyvoiaJ0l38AppU5xhk3ByhU5gATvSfJbc8SWcpr/KbDCUigUeCs+D9zNoVR+5S//0kz7Zy6Z7QeAn8o9+Z25NzspU7jJLbZjkeT3U/R2rQg6Fgh2AfsStoCv5kO6fO6Fnr8aKOHv4Hq+kw1u0zeywCm2+TN8+8voQd911bLDlTGABH9DNzF2pTjBytCMzuU2fzZPdrfm27yUt7HKIOE4R1+iBzh6hXbWAZ7lBDuUPKNZdWpB0ClYY4EQGmus09qm54LBIB7Jk3oj89GlnC0mUoSxIzIjB/XNyZf99vW8uOCVaelf1m79yzxKy6aGMlUTtrFQV72LE+r4RK4BMGGT9/PLOh83fZIP5JXxRj5/NTBTeSYP6yh7NMObmMsxN5J57Dmr49L/viD/Lwk9o8NKpBealoN5N0JMvCNbHBGEorG1pOQd2tSN+mwe90k9wWf9JW3XcX3ti6qVStJ+zoJ6AzsA3MmmZrVCVTFa1axuYKRZlgnOUGQl2zmqK2s503oAx/Tz9WllNWksy1HcUCJQdZvhHdaXLaBzad3vfANoP4/Hp3+eB+HfRvJGBbM5pMn00GMNeTzmtI3rOS7UqaewvIW/z3dzWB9gC+m/mH7hlTSgi998RcEtU/w0n+YcZ+l4RjvqLMf5rUvc7+WbwZQ6dd4/5F/V3jw4DWwvYuqXQ9z9dB3QdzCTT8S2sFJFTYiiZJGJbo2Zap3LZ1lmyJBv4fSld3sBAOhpvePCvB7gjMge5J1lnhnEdibs4DRjUjtdZLZYYsSW0JHnHMMoPsnf8j6lIjpSGTKuBOmQHFEnN3M/fSNxvuz1Pz1sIvM5SOCOK54u80f0s/mzHGNHdqFxbVSXci4tM+FGKhskZqAzLOnv5b2IkcifuSJ/mgKcJKSUSHl5GnjY6BhbrOVermeZjuNs5/bLxZufe9bnBVOe4ev44OU9ruI5EiiZh7iXbT7rBUpJWYTUlqRlXbdqos96m2+h5YTEUDfQH2X/4nP8X2Z7BQ7wDjomqh6ypKpGxz1WalNkMJcTFQTahTnw3BWqT/OPc6UaEyUjrEKxsrElanKdZ7kv/87L8AC6II/wQhzgZ57zkzfoMEMaQ0OqzVarrLCe23nUZKObaKY93d+IuI+bWABO82PPWSR92ilBr9RzWRp4CPJz+X0UnmXCis5pb97LxzjwQiTWS85N+1Em01LqpQeaXf7ijiDdrhUez8ywaljFDos5dzHDanbCT2jADo0Z632cBr70kqKA7+E64H0A3Mt3cFId0mlCOxWYCfPMqlXkuma0S1usaV4zWuTtuvz19J9qfr2221IE1qRBtkuxsbsyo2G+Oe/jz7yMxZ8XxC8unp30UpS73pxopP21CcsaRfWmntSmW96aaBYYC97JzUz4dgp/mRWWGT63QjMNAzLP6/5cRgol7+Y4R5ml1RFW2cH1uo/lC7uGrvb2M8+fA8lhHrpEUkpXFZk9kPBI/miWnFGEAqcJoGncsIHUaLEqO72RWUJztNqJ+KYXbjjv2QZa00P8uvrY5AkepNAxo5btGmuWZc1oU2POakvJnGY1KwFn0QUA9dKN5m42OCW7SpElyNJJgcLIwAKf9EEd/rLKAVfzZ7pY+rmQ2AK8l9/R1wrQqCZSrW3OM5dZ5/TZ6W6ubDjKBPEpJnyAr2eFTf7aFV47L1NfkS4jhUrJsu7U4yzSaYuF+pEccxcH6IujFyOIKx+jN4/kXlb5R5eAKVcRMRTAoXxcx5lIKal0hrZENg7NeqLqiVMDdvMGIjt2sg6a6MdfwLEmcIjP6us4MgWPPiz0uAaa4xktaB37jAcaq9FZtRqrU6PwvLYJlrSDqg9N6yEXlbfEdvaqRpGlyIJVUkRtVGCQT8Sib3lZxYDnzki+IMnmA8AhfYR38VQOOMskBHbAllov+lPal19ShxQM9EmKHtMpwTJrgh/jJy6L1fu3dPlS9sXkLJNseIh97GJ7HtCcvlH/kqNauYKv9jx7cR7mH7Cgwr3P87DSNP4gtS+/g84DwsJujaOE5KAYZnOssc6wqXPa4It6IycxL052OMWYGda4azrWLpeQxjqrOZkZOTe0SWig+dwhaSxrjkZwJ2f4hUv8Vq/E/U/Vek1k9iWg6MnBlCKlhUvu1CdY1//0ki3g4tzpMkDu0mBq/3Oypr+hj6R90jBGQiajoTJhsT7q4DTJWLeywJgJIB7mlvwJls9n/5euXvUBIEiHLjWAg4B8PI9wlBWdYibxKrfzOCsXXIiuDierZwTcSQf5xDTyPP+Ahy/xGL0beUjid/U5N6pWEAoiQiquslsmZMyQsU2hVrtU6NhPeV59bAkOA2c15HHexgkenK6rO9jGSYU6nZEc6qJoLLGhVQAVtdrQLs5qnkHfBnbhFR1CucketqIRtuTEjUsVyBGEY692dTc+54V9+VvAeTTj8uu9/IX8GlYycptMOiXEkxoIn2QjS59Us85xBtwBPMvD2kPhIAf6kFy68EbyknsfRPKlPudQHvNdHMiuziqgfo7reCM3Ss9b4++jyR5hOsJPMsP/wPdfhqgfzCuKgdyZ6D28oZrW9nRy1dlZYqJgyallRR3ZrHOM2zmlNc3zYvSQR3VSM5rRzRd88rt1vcSQda1pCZyMuE7nBBNtY6TWqQ22NMuGPj+tUVzqt07xyRyOhYUJtWmsEkWRjd2q00k/5n/J6fzy+YCakmQuOtjL2ReLfIT/3bdolmXaTPU9K1W35GZW3uYldjLPG1nji+ymsMg2rud78rc14J+w/3xL6GWIYB+3Z4rMKzSCHsrkb+uYnI8y9huZYelCXemwxNWOSs4Ltav+KMP7LjePS7P3FHCf/iKHWY/WKeyoxW6KoymqkfMxcqtVm3XG/dktjOh4G2dfYCoPcog/lLCTz/K0DnGIJ3gPT2BgSdVLajSWXDUjqTUeCEFLeF3JFqf555dFNQDb2MqtksJRsgj3/srYKZRFm7yZb9EaX97xkVcwdaffsO+S3z6EuIfr8kFOURlJdKSL5t1pTpnr2p6wSkUMMOiT/N3sWGOc/4bv574p5VwXnu1S4CrzCiTwoOBIDplnkntVsnKCYznTswGmZ1g+nyNLjgjmdYj9F2juecW/OKzksPZzBwdzSzUHU2qFolp0BXUWTVpFa9Hams0dPOpGYy0QL6BR84AOKgW7NK/Q53SnnuBm3c4bWNQ5dSqyG8FGzNFK2WqsFdkznuGsrL3cxD96zvcP9EZawhlZ5YyMDFuOkJPqsdrYlt2UFv6VXL6kNeAzgnv7QE136kZ+Uwd4O3vIhEaSXL1WB6oM3TAQtIKGE5xgwhbfqw9oqEf7rEaX7L55yUZ8IRH1FaUz4ADFOzWPtcpv5wFO5b3mqq//Alc4E/gYsMadOnphGT2nuiU4xEe5N39DprhrUEi2QyqlTpoq0clMNKgj4JS3CNZU2KG1F5jAef4uTwDP0ug06xzIr2WLTzFixrtyoC2vSxStZlHLms9hCcleUKPjHNM8N/Y0Ml9cgLO6oa/0hVuFCiKILBlENiZydvK0ntT4eSL8l4YGgvqJdx+cDYHl6e/uA57MAf+CjZxzqsXULCplTRNVzVO1yijhNLMMmc9Kspdf5218lsMcOu9lNF360zzw8IWPlxnAXT6Uf1XLZD7BudzI4B3cxw/ovjzfGXQ1efpL49kB/4j9mReaXS7/69/D31HmH+JndKPOYBUU6exLbapFtbhxlrHmqGFm2aZ5D9jLzZrlxAtM5RsZ8FYmnJZyOwV4jHUWVbXJbg1In3InaVKKtqulsUivW8yqaLsCswpkHpn6rvuVrOVQwjYgJxQaF8J9g4AnPBPw1vym52weLz0IuKgIsn+6ZibAw1M4+3HgLjb4GtYVOUercF+b3OaZbDnHF3NOaEkz6nQDX1THv+Vv8YP8Hj8LF0K8acVaOa08HLwgFnKZAezmkH6CFSbMM8+EszrLt03l2/t/ckV/yHT0mv7vcRb5jxyZBgrT/ppLrhEHeCge081xRntplRBhLJfoIpR21uw6a4vITY0VuYPj7rTIaZ6/Ce+PcpJHdEyFjjV2kvykVjXPKkPwmmToGAhtgPv9clVzDg1N36fMPMcugzX7lowzIQyFkoVCqBIOtxkoPKvN/CJv1z/u/bX1Ms9Xulh3/Ewf/Seg0hPBpuM4yYM8pGNaypGHDEgjGMRaQuMBjcw8ItmR+1hK+EkW+V0aXUelXOGNn5uaX2YA/7DewR3cyCRHkrZSpI/xt/I5fUF5OYOut4Mfzcf4eh7nSOZVUh0paXlED/NVeTyfBJcoFwKrdAlFI5fGg8C1RFpnGKuyDszpa5l93rV1p97Abnr94TnBrTLX6bTkwBR3GrtByGPNZ6v0poo7LQi1Kgy5Uae4/hJKGGSFW1EOHaUmIVISrZKeIUiKIG6PDXZ9BVqh0s8IusuRYfWp9IMJfxiYzeRjGhCaKPv755xqqkJhoqEq51jmKRot5+f1LJ325h41fN3F8CIvyQEu1oSuyAL0g/y3rLKl4IkcaE9enx/nzotpYD6XZHixyHCYLT4LOjQF0i4FHPvrC/pa9vGM3qvtDhnLUcKmqKTFlvCoZHTMZKeODQ+QFjmlRbb6dPOq2GLDI3oLE2Bdz6hhhjmt6iwNaEupTrgGAWggu5FlNUhDVr3k7So8rnfrLvXVzvMjf0qbdM6UHTiCqJJVSiNcGlqGPNpt0Ex37y8nG+yXTwPcS+qDQtNqYF7KDH577lMKZjRxxQqqxpE0jPUk65gmz+kJHWNL+/Ix7c1TOs0JPcRvT9PMnIbkF8GA5wSBmYc5VP9WLudCjvOdOAec4BYOX76SeS57rv/Og3yaY3qCA712cF40m/P/dCQzp1ZdbbJzSFapkSFZKaKNCU0VrYrCq1pgVue8Q7DMLFvPywkYssUEAXt4RsmmfpANKkN1aj1Reo4Z5jRhwICBJszQqVia1RYtnRYY8yUezA+aS8DtmTpP23OBUJN2qNDgJKOv168yLLMuebms80sGhCXgzAVIWFMQ6NxlEy12arvOZNCqs+xIzyqInPMkxrlCpWrBYwZErvNJfjCPaY3I5Sx8bYL4BemSZdzrQV6l8HQw/1TcaGjUqOE0z/I0m7pEse85FfmLEJE4oiWO5115Z14kWFxOIJnj3+iLzLMjxtEoTcEhjCMaFVW3DoflzmteswnmVWhpiN6NxdXu33Lc1h51GrKLTjN8WmKsPUSuySyoOCxqzHinZllQR3jVESPLlREDrWhe9+oAl5Lo3qAJZBcKIlULkUo7FL1iSLj1jrqvrvHjOqxDnD91/OWhgLP03cv7gP0kcANHuXdanAL0Of4jDSNN1DCIpANnDDzvcTZZ2ZHPKnOOp6a4yWM6xRyr+jW+YfoVt1xpenme23EFEPTv8+8mOs1mjtmpRd7MU/yLy+CfvPqjZPIQ9+Zu3s9duqhHd74kJMGj2s6W3ka6aoGxigyZheJIZ8liTeRm6InCXYRmNXaoR7eeljjib7jqVD6qNjtuInOLdYkBpwVLOaNT0bBaBuDiToXUSLY8o04TW3OSNrwLM6Dh9kspVPoBH/EWpZiEkOzse4QDyxlFpRbsNd2u5fPt89JLYy9eXN96Wuc7H5IbAfgS+znCxxGwF/K0vpY25zRDI6cKCouO0jXs1G6Kx6xpg/cwA9zhGZ5mXg/zHRzh48rksoadi+wjrgSC4DNJQsnCRBucYVPflv/kIpnk+Yseklb4kA7wBEfyUk/RT0jmIQo36DQwD24YKClB2ERGRBY8cWPVnucyS3WRRe5lkQ1u0x/mQJ6AKzp5EQSrKiwxkjTWHg2AN7PgRpvMyNlqy5ZNGbIeKZG2pcaNJFu6kaGW9btTUFbK/Aktsye3OTpCxabpIk1Mj46zkEqZZZNndZzv5iiX6vu/ZD4YMEjoLjzWXYIZ7uQJfhQQA5KSX9AdKlRXbJTRegPcRmVDGyqcprKhc9oh8WmezT/OmP0MgTUQ3DulAOSFd3J+lL7SmR8QbGiNkgWxzI169yXN4S9QxOancz9HmLnspxfXwyFWPdbtfF5bzCSukZH9abyyHSUsuwpX0dhJw7mesUNFggX+lv4zfvqy4lKPl7yXVX1Oq3qPRKs292i7TmtFY+FGE9v2JCbulG7JbNR4oqKSuNUGG2o5k2dY9oqOCpJfNvxCPqkmFbacpZcJxrJDpTaJZDod1byKPsp+jgNHePnidl87/Rf3Aqv6MQ4AD+WEnwee0ndwRDu5weuaqCgiAgeBVeQsKr6eommVDzjLzWzwO2xCbuIpu+gXpiQ9TWOAb/Z5ZPCS3sDe0b9bMMzOTzHH+3Q9T5CZ9cVq3j2i8S7eNy3Z6MpMVw8KbdeC3qsZjak1RMkStrMhsqmKQmSku1o0R2hIozmSeYxoeR8/zG3TkOnyWz/tARNdx5uADeY0r1O6mQmw6nkGHkcqPFZVCKUHaphYjsCdw5a16i2P+Jn8GgT5fdkB12lIIQsOSxQKQWQgoVIYZGhb7GWWc4a5C1XVl84NSGA8/e/7gJZPcIQlhoIngW/hFB/n+iwMGDMmk0xVmxqh1uSYs8xoVR1DrepzfJInSK5jg7foN/h23nEZDeT8cecP5lX0ATJJ9Ddzk1RbH2fMsTzD/14/NmV4HRZXC3DyIiL0T5jl2Uu57hf2jENssEXqdAzV2SZSxnSlLwjbalIOEWWoxl0o1m0NVT0CVY6zjXV2Tqtlhy9wzw+QnM6RlnVGQ605sYJ386zDqZGKxw46z2lkKJrEhPDIlc5IcszojAqQbM99Ot6zmrmLZ1y0bgxWNaaowTIlQ66BJqpRWONmtiescVh3cufLSADvmvYlXZrPrLPGCj+R5Emg4Ve1wZqeBooLin7XGkRCNsx7jjm1rGXDmkZMmOez/FlG2qPH+B3+Nb+GOOy7dLnRTcPzK4PAHskasplzupXChA3d7Xf1W4cOpq6SBZyP8xPxAR5g89KA8cLfPqbjrixzMifAmFFPBAucAoqK1PRNV6kS6tRRKExYyC3XHLEm8SABOqpeG6snoRzgHjYVrPCsZjVOeVOLajXUshsXGq1r7EkIy8apcMoO44lxaLtCcx6r40ndmw+DMv8HzWort5ixKoWIcF+HL/2MST0wUDI4xRo36ze0qeWXsu4v2XQPJBfZOQLo9P9KTxkVJwSzOeAnci0XWCSxpFDndIMVIY881sgTn1bSsFx36w9wPb/HDRk5n/8FyRnBCkfy4nK8vA/ycg8guAsYudFeNrGO596eEzzl8ug5zYaZeSGuHDOUOEBCHtCDksgHBOh27WJVA+1gTZ0bd5GudtgUtxREQwisomwYa2yro2VW66qecyfzPYaT2p/SQxewhd3c6AEzTNjUHKc00VBztdUCY60z1MBNEJOQx9G4o8EqDLKoRRortEjrUxoocmh8CyCd5KMs6zqNKMWyMygyTteShSEmYCusDQZC5/SgOq0Av6GXQgfrPy/ryBWLpXJY79O7gR/TDOQ8RzjMHo80cKEoFVL0tATUEMyz6GBZW4x9XbS8KSPfocfYReFP8Z8zTnKZBzVF8hGkLsFvLiWFisx6RLBO5jN0jOrwarqFVyKBCSLzgB5hkHAkITnCgYS7/CYhOKHUBgOdYntWtSqyMyahIqdU0nIoLYsanZq0GhbU6BwrrHJO8GlIqLxTSafzGOSmTnETe5hnrCWldmSjp2IBMdI5dULF1lARtViDaCUaD7SliUJSNVpig441z/Jb2i30mObZqyf1jBYd1B5NjVoykKQCWZIAKdfZzTHenrdO6Zw3vYDLP78GD+s83tjD171J7J/m6zdwXPDT3EYyp7fqIDfnFqpDqopkMt0IRbY2tWww0Aaz6rRB8kb9rnay3ats07/gPo7x6yYPpKbL9Xxh/rzXuUIptHdQ1zOnZc8xctR/eTXKk65GdH4w38DgQsbYg44P5pOCw5yhrTcg0rNqRDZKmSKkptffUWQpRSXtiJIhty4yM9pw49GUbALX61lgSwkcMXxJfzGHXM+yU42qWlctM6sBrapaSZOwpUJSTHYeyoyjkWMiGTfa0qqDJvfrS8yxygf5FAu5jQlR04QtUzBGnsYsOIstz8aS1vWMnuF6PsAjOneVjqWLJPD7RE4PfJS0zJ0J75zO51HgKcHb89x5lqU28lh+UL+uwjZbtiwIlbBkN6Q3cjZat2zZOq7TOs28zugEe7k+v2B4H9+S+3Rl4Hkwr7IF9F3u+4HCDq3l2VzUlt55eRFGl/uD851CfSDwYb2PT/SZ9BQOOsx3JhzkXZx0AhPOaR1CGSIoKv0xTIRCvUaAumJah2a9RdpGywli4NStnNOfvlDefIh5/rN8RreyxG42aLCtyIEyaww0stV64LFLdMUWnSvpgavlGWF7K87PyqbezC4l/wcHuJs366xGtE2jdA8A22l6L2WKotTEeYYm1zybxV/NQzyr0SXNXs+Nlz8Okqr6BtC+B3AwndmDnNGeC81aB/gu/pz/PqbN72WJgoR6vXrA6VSqyVYrteR6KZk0iKfInOQ5LeqLvC/JD3CYD7Kp88F6X7O4KPZxqQHUTPhR0JbIdZ1gT+5lmQN5KRL4sedtcTuskeZ5JA8iPqm9iAMa28CX/H72ILZroHWgyYGrUarvClCjIAgLHDWKWltDitdcNdMHioxY4Tremw9zRF8gSVY4yk16QtKGrwNX7WA7i9qt0+rUCG2qqqGhKCgqke5KCHVKQlXFadmakxmTrOnWHOr9WtUa69oRVDuUQjb0YpGUGhld1Kaq8TBOei3fpGGO9V7O8rVX3y+noff3YX5c5gg/mpkrHNa9LEwrgJsUJsAA8V/rAB9Tx48j3RNnSDbdUAhwdY1QMFRxLevMe54FtjOTO1NZ/M18jkEe42bEMf0LPu2f4qPnG9GmL/JQntdCfI5M3ArQUbTATp/kCT3FLh285BTDdz23uqzzEcVX8yD/gMMk4hbg+1jJdR5XZZMT2uCUShZ1GmUbRDSYBmcLCjmEFekxtamMo/OmrVXm2OSm3NKQNX1ac6xrP1VwmOvYyyLfxwJLGQ7mNMRsebtPaE3bqzSMGXdRAneleOKWCVa6UKOLTlhqwbMKdjPgDEfZrq9lJ1uap6OLxtnTQCPDYJSKYjlK2oSomvgxTmqOh9jQ8wMlEjzI92sXb9SHOYx8DDjK3we+hSHnOErDMZLKN8qs50f4RbbxWc54kw6Em+JiE3KnVFHW6nVnbCrVcsb2Ih/Nz2ibpJandZhv45b89/zr870dU1pgXiLrc8XWfihrDjJzyI15s8w7eYTzgshXFgQu9gaK5F4+z508oIPAP6PoiM7kX+Vvc5g9fWeCTrKDVZLGVGkslb7jnsigVJcSRgrIIndI1euMWOKMxgw4rk328jCf5nqkd6nR51X1Ya1grTKvoGpeq6BGQ+302OGJbTwUlrtwbBgnAw1UlB5FKLzmgrWTEddpqONquF4znmhxWqdO13Bk1IIkN/3GRKGIGZ9y9TwzlFz3iHUOXUbDvvT6Zv0JgnewyoiDkAPBgm/Wg17mzeziDhqGGnOaX87/e57gDbpe19PmU3WWeYZujNQ3pCucLlmctLRaoEOxoAkNe+MP5jpzNCp8in8M7OdHp+Xf8zL4TMvXFwghhy+BI63QpsLHtJzwWwymGaQuKSs8t0lcJCf4Nb4GJBo2+I887XV9vd+nR7WlRZyN5NCwpCImUoyLQk2NLCrIJftE17Th4vRWdCpuCE20wRs0YsA5DdilBeBj+Q1cz1F2alPV54TGfZ+smtyKkN1pC0diZRBdGUtOhbBtjW06hbtSZUWi7Wxyjus4x15OZKV1ppuqPgS0XYh0oqKwGgQa5zLz2WpNE31BN/I7+jbB/xpXg83/JgA3szuP8TOCPw3cQuGh7Phe4OOC27JlxPX+jJcRq7yTLQ2wxpJMkaziINIypUreVOPTHsoUFphoL2Nm8zhPaZj/Un9I38pRNi9v1c2LQL3PF4J7gYj+LxQWNWaOeS8ypOSllJLP62q8gJ4rto0zXMfDJFt8jgWRT2iLvXyBRRb5hJLV3KFJ1yF6qQ0JZd8e0qqxe/UN1RqprSieqGo3rXZjUk/qVhp26kO6FRjoDdzKaW0ZBXMaeeBFFlnXZoxY0KabsuCJsnHYhZJZSk/qUKMIqdqSgqFmNHabewisZ/iIx2xopq9PEMaI6JySiPP0sM6JMBuseciN3OhbtYBYBj7KGZ2PubPvO1SyzD5u4NP5lAb60URngW/mq/MgDY8DTd4CwkPt5ZeoPpU35hf1JlrhOTurCZe+MG0RrVuNY8kNczRq1MVYK1gLmnCjaorKf9DtfAP/8JKN+uLpYVdIxCTU+/w/iOwUSabWcos7+KIyL3aHvj8vdphcrPs/6ORxbsp3MWGXD2uHvoN/w4S9eauezpt1A0vMK1QkxKREqcJZIrJVDwT1QWEhjFocLgK8FQ1DwYYmOZMnOMt1mEVSW4nPaBsTtUw4qwETdZ54LGmoTuNYwKWGdWGTYYLc2UCrjBqdJzGx3OVMog0ZtBdxTvPqamY01RlTRnAROPv6YRB2U5A9UuQcdzBDaItF/pXFd7DUN3tMfeu7EL/qszrKTk65TNvc3skBFnmQh5TcKoCvYQEojPUwe3LkFbdsstclB5luJIUtqVE1Khka1I7izhZKBgzl3MxU5Fm9l2/Qk/kwJT+jvZfSc843tCaXcQLzH4o6p9Na0IpmLG3TGqd08yWvHHZNP9xnXQIRHQYd4aM8xW3s4idZZ4XvYlXSOjeXtTzZx3t1oU4YKbPpW2xLLQosuVXpIkp/Ri8axkRyKgVrTo10i055RmsKTrGq05C38zneqTfjPKOnvCNn2KChGIUaWncqnrixLafPc3jGjAnsUGA1btRpoHmLNjfYyzBXbVkTxtFalOjNkpBqnwL2jAClqXjAlrZxXFva8KbmWMoKfC9wUCB9QSB9HJhlT/59zukcaxzg5/QXaLgTKOopNP9IX9KAIbDFkBFv1JMMuVWVldwOYVWFI7LpexTdZ87t0A2zDLBwYUFDSV2aBa1qjn18nDd7xNM5bQ01kBdKVpfpA+hT3CRT1aiNpLXV0PTZah7iDh3WIR7twwc+dBk/6F3cw8Hco3dqxJZ+iWey0Un9FMdYYbHOCkmFiWqIsTsyEkUVJaOHV1VKwUXVtWR2PUMqBlidx6q5gxuy5Qx72MauTH5EN8caG1RJoSVmmdUZr2nAUElnyzTqQjGOHh5ohWnsjhpFhc6hBnkcGWKoOZ1hRZXT2sYTzGvcT1ffBxqKKYW2EC4KGoeiYVbLGuoct3MS0WiFNi/qbmQ+oR4XPcScjvMt3JbnNE/Hd3KSc4LkJD+nRJzjTswWScNtuZ0uV2khT2g/E2r2HRS2lFapOXFRkdlQ9ZY3hTboNJsj1qJxaJU1vsgJwR6W8gKlq4L0kK7SGCL28KU8zQ5aKZc1ZMhuDfV9ZEpH9HDCQd7XpzQcvFTmgBUOCZ7IG/h1BvyAvlfBHj7CPAuc9sCdOqXwUAMPcDSmECpYRSWKQBkOQpS+/0Wd5S6KRKPOc5aSolkt6qxCdzPRRPsUkjqKFmPMDA1b0WishrkcahpYUEPRuarGBLsLMdBADcTYE48CDy2NdEZrCeZZBkZLTiTswFM1FWcAUcFKW6TC617QmVhiyE7BbXpGf+ZCgPQz3pEA/y136mZa/Wu9kR0ec5zPq5dyC57hjyPgnfo9tjDiLZgP87QWcg9zKhqTbiNkT7efQqPiaIJIDzWMIQM5S07o1Gg+h7nJTk4ps2GZN+W3cHhasVVkwp31qmngs5DbcktbKY08Iln3b+VfkgQ/2+9pOu/5D09jv97QP8ANrOkGnY3rnZgvsE2nmNf+lMyiujSVIJzYpSotlwyFIHBt+mKL5EhTRCfUCbVaUGhDg1zRhEVN2IZ4J5+KTVYpnGNLEzpIq9WM1PfZeEapLgqN5HS1bDu6aOgIrFBf5JUVnUND2hwD2wmteJkFyGQuIhUqammIXlgn5aLikGo4R7nAQIl1k3Yzj1ngT3AcqPyIYEUTATyUn9IWY4F5G9v9Lg31UcGQN7GoXcwwo9CQ9yl5d5o3aBe7jDrmMt0LPdWIhlIDKukq2TTeMDiGhOwi64zmha197GYnpzP4sG7RD+qw7pO0OM1R8xI+wCUB/QH26HQuO9zmLK2GrHIsAW7nIG/WMcRhDgm+BqkHZKWkaolHeEqDbNjML2F2cotSRzWXN2QgryqZyNjYRZGFkGloVLD74rAUqVTv5AayQgOgc3hTCzRsZvCs3q1nWchZFkHPaBvrdG41B0qKpbHanFPjiUYeO1xVHcYmFeGImNjC4a4o5GFseDujEKFOQzaZ0ZJ2xaSMI1vs0rdW0ReDogexAVGGjKiG1PW5kx0s6BZ9GAv+Fz3sFb6WwhnBDsMZ5riDlm08wxfzWwFYZkKHmLCYHXAb4mFt8Q0c8zyhMds8zAarlVC1FFEoioySMjGjJposklDnkVpPcjZnMQ1neLOO8zRvysd0I49n8l3khXae88WgCyc+HOADCuZV1dViqUtrSe/nEJm/pv9Zn9JuTnCQgyQzZOZ5ApTWOMaTXtaABVf+qZb0VG6yn4E6Zp3MIdAcckQmdigxJaMLpuJLDjXRUKJ3cm6pnlBjRrI9JCSd1ZhkOQfapae9i4WcU+vqLXXuJA1o+8JEjJzq3DnUykGgaiJLZV6BMnvqqeTODaJRyRGrntDResKAjkCpzCBsBVgyJjAlnVGKGKiIHGuiLQ1IPcWI73PLQ/w5hhzMAE7zT7XCID+rMcGyNnUbE58C0G0Y8wjvZIYG2BDM6gnBQiqXmHCOTs4UkaFQ1KDflEg7W08Mne0qK+jsrMxLGmhZO7SLfUJ/niHfwiH+gW8DJdPDrvKiB0imIiv7VDRUYwvNueWEFg3oV7mNb2LF26floP0c9kWGwI06xmxup2Oc5qt5n77aSxI7wKJVOjRDVZtAFPXQDGHZqJ9SgshUiKydQ335a02puWxyRsEyW2ww0YyuZ8JW7mZFQgw8VMdQIwWVVrCdHUjnp6tI2Dh7Qp2adCAcyqiOIIpaUi0tPatmVRMVmoK6YvVUUEqPo2ZQ+ubArrMLHVA5p5Paoes4W8RHOMJR/q326TcM/1zwB0hV3ZTXUTVHsMyCbgCeYFObHNPtOqPT3uSoziXayyZ7FNoNComRXUyTNWjUK2o5jduIUDYeatYFM1YPc4VDY5mTdJyTeEP+Ye/M/cBSznCf4O4837nhvp5/WGRygGf4N4yJ7IUbttjSW/MP5j6Jb2ablgBzjMN8yNI+0JFpifm2PMZ2PZPbRIQ+pc9mo/nc5lV25oAhlR0MZdmjIBW2e6Zd0EyJ4S0lSwkpW5ucaFIGRhkTS40rnVv2YVUq51jwMusOntFEs3SMU7RYHbPgCTMuqp5Mkz/3e49cMlyqZSmFPV3U0pZMdcNqBG0OswtHZuPitCGwelaApQuVgUKtGlkunqMy4CS38Ra2aYEP0JHcyBLfzL/gBJugfU4NtETHqoa5w7DIp3kPDwEbNLydc+zQJuZGRuxiVg0jLzBbMwki6NtCzi8Xa9K4TFxc3UiEhrQ06jTMYU60rO1MmNNxtXzRZ5Xs1SN5b5I9se6IwP1mcDDRvTqY29lP0uaYJpG9i2Ps9VEe1Jt4Wreg7PhF/i/azxken+r9iA/pad2uswxtZQ400LuobPeIeYklWWMVFTpaiSykezZFIXrt7WonxcpCFBw1O3c0LiLlTjPeiJJmUzMkC55hl2ZockOhmR7kDMva0EjDbGSIqlRrO1wUgWtUlawmJ06s7AnpnjgEVRbZKt14H/Peclvd024jG3qPVZRylhqyggB5gDM1x7wWfAJxTCusMe9/oGc0y5v4nGZ0j5b5owRn8racsKKz7NKa2tzSIpsEB/QGJlTmNGYrC0/qOt5Il/MKTTR0X2wvIoRNcahXKZCyUpqBTBsRqDOWG22FPWQV6brcrpbdCloe1zuy5YN6yEd9rw7rzn4LeGgaBN5X4Wbvz1ZZiieW1nKNnfqsDue7CApPyhJ/gM/pl/g2DvZaeoIP5DO5F3mPqoaaUIXom7V0SkOFBmqZkywmHhQ1iEKTDaW6timHGoIioYzKKDCayHJsMlLRHPKK9tJmaqhZz+acZrwTNMdIM4xlNWxo4q2oud4zp0orydHadCJqdBGGEgZNXKM4IyMcRpHz3q0l1tV6oLEmbZQWZ5aEgtPKnFLDKVgRrlGLYy5rjFFWte400m5O8gPcAGwy5BRzPMua3pi3saIud2lLk1zUWS/nab2dFT3NlyT2ssUKp1W4Luc4lrs08rjOakZySpEkUTJwLZSqakeoRP8kfdOS6UPoViVntF/BWcEcyxl8SvvyqP5nfkR35uPcl3dq2h185xTQvUd/RzfVGbWyOs2IkprxJjfnIU24TZ/nHYDyaS9zo7brOI8IzCFg05u+nnVCE3W6Lhc00IJanmWBmYSZahpXmhB0LcSkqEe1i2wTLnJ1hsPZH9EYkkINqLGq1KkoRaRG6lS1m0rRIihY9wz9hhZUmzCSQxOXppU9cUZY0RXHuOm7fNRzfZ2RxkOnZl20k6HnNbY07daIvoXJDmVIDiXFPYBlydqQrBx5iVY7WUrKXr2Rj2pG79Zv60/xpEZqdVOiVmN2alZrui4GGua8GjpaLXMyFwj6DeIUwzzFW7SsLu0F5FWnOzmwStilZyX12AlStm7cRvaJb1ZSI0ZOrWXDUAsyoVt4W/6KN+n4Eof1Nh3keIq/OYWCBfCX/fV0muSoL9WEu5Hm6qLFQWaVeQefZoEV7+Cr+UTOs+7bpqLQqRs5TcciQ+ZAMxpn8TopUxi5YxQLzMpGsiJDKlQi1KpRIaqzECVk5MiYKIRMw3wOVbwVqZHnQUN3rLODCUMNtBHV6wSdAjRhSKNxEg0TdzFjW1mE7ehZVO7PfUqFMvo2rzDG1Y3abBh4mA3rnoAjcCjdJ339mSEpRRc9gmB3GueAAuqoms11pU/VJSK36wYNmWes61jySa7nWXXaDtqbq3mKYa4yq1Wd0Ug7Na+kYBpSA+a95BUiBypssamQLEkO0c8WVpMlnEqPI0JuqkuJLuXqjhlVpHnMqja0w/O6MTu9hWSop5ljnhNO/3n9WA8Fj0Bokpu6PuWi1v0ZGUNlUe5NclZnabhBa17kDRQ+oEeZzwZIf5uSCRu5HWuiVGhRjZdywpCJRkoXR+miVVU6nBUUtailFXIG4UZpZ8HY6tQw8VCdquS+FN14nVmNpCrNCwaaY5EBQ615kVk2vaSqlmAj2pSKJIPTIMmpGkSNtNQqQiXDznCkia2wg6EqC3SStlT6GZ+eFYrreYkipSyb6EuDA1dMsK4V3aziiWb9pD7tt3Cdvkodhe3eZFZranKgDY1V4pwG2grFLBPto2ZH0RYwZp7FRB3KZdlrTAgNEKhzn8j2rElKX5vLVtFIHkTbTDxRUxpXkZseKrF2MavTGueYz8RYS3kLn+EZfZ8/nZ/gRP4MYATnpszem3Pglpo1+0Bs0zVn2PKv+CnWSJ5WsqKRxhpzlu2s8iyVU/qirqf4nKxBLuUkT2ikkZcomlfVSOG2TnIm0g1jR3GL05TMGgTK6GnXcu9ui0vUALtzOKeGL9mFiee8ndYlT7jmBmc80pIaTLIZbd9EbiI9cao65JLGqJaK1CiadKWXCKpNLZRJ4MhFm06NioIxLV1TyUgTclqu6gvmtkoWNRTHWM6JqlpmkKpWtc4W5oTWMWMW/EZq1txtqVFq4FRlm8Wp7NR5u9axZtnGLiWF1DkdY6DGc0TKcx57gEWW0lcgE5kgaqE4qlQnBY/UWBpgico4UGJadd6iYTHN9mzZpbN6K99Il/Kb+LM8wWEMD7ODI/qSjvGvdJKRq4tRyhEMOamSt+RyNmxnni0vAGc08i2sqqrTmLewm6JF5pVsakuhOaRCiWCiTUZUddqKAWENmqhRa99h70GGgqCx1apRn+JEp6KegyvjkVJQWMsuYYtRpucYaahqeznRIo02HRoxo0HvMqgKK4g2TDXClTSFcIarM+RxQ8kmQp64cah6y50Xs9OcVTpM2o2CokAiZawskKpSKW5MhhoGWNs4I7Pm6vAOnTQ8q9t5Vk9G6zNZWXdhoAmdljK8nQXWc4ywn9ImRSMaiXk2ULZaUuet7BjQOrIxhIoJNTQZCiuLWpOl4CiWEzF02jmjJGl1VmMN3CBP8gxSx8MUvVEnfT03sqrPc1A+zh2Q79exXPbXua98EhlO5bqKFrzT6YF2Ip1iR67LFN2Yk1xIGCE2OO15kkaFhjnw2FAdOavWA1vUVoUZtyahibCKipxWS5OFSCuzZGT0IESqQQ5JJRuPNNDIm2UzWjrNWeAZF1cmQmuIlqpO4UUGGsu0Pd0kpIaxO3fGYaK4BsXOqKJ0fTZtRxcWTNwoXN2o8zBHLrZq1B4D6GnhfW9xieKiqcsiZ10crMsaq8mGuVzihim+dk5izKomWlVoRs4Jsw5wQ6eZnDgYsoOBUmNtalONiorXLTc4Qg1N1OiRwB7OoihokZV2jEtXSimyi6rDNeSqBjTIJTW5SYt8AxPOeDsr2p5HmeFxPsebAO/qBRPzPT7GSt7gGfpyioISal29yhyrbFjs5JwWNGZTE6VXdDKXWeMW7cix0rN5Tiik3IvdYAddBlWlGJotxDSomqaBPdGKYhw0kpzu21w7pVNm4pSZ0UiFJrtMpapalJ3EKeR1tRoQqoBJh7aM26zh6Dxx386ri4RuZ8jVKkRElBqKolSqNqF5zVrakmpxsSOLw03PJsC9tgaWMiRaOaOVQEOqklZFQ7WaZVZohFjTyalawoJqJhMTjYTVKLRMR5NJp6HaTGBbWgNJWefYRG77g0psuceli0Ixpc+UlEw2Ef1eHn2829fZOy94UtZzhnlmWWeQVWdZp4DelpuMuY0ZnpDPnM8CcjtzTHppZAL1JRQWuoFOMestNpWe5VlmtJSSWMybNU9n4ln156sPFcwqVTilKkg1zrKpkbYiM6Nvyg9qqyaNNdXco9CzbopKRl+gwbXIqCuhFrzudN+kK9ljTwiLzTTSujs1VNlmybiUHh/HCGdfRo1JyCJK0MtSNZGRQVPCUSIdFGExciUkZaFTpdcyE5FW9HWTDJxtpsNk44gcaKAEbVA11rOxl1VWPeNTzGlVoXXXnNOIeVmV4gkpeUtVK4KJVrVPxyhqLBWEXLSYVsoKdYIwykKhQWr6TmWFIkuV3USJwKpukDPkGVc1zEaHITY1LnCddgaUvA3nkk7rMe/tN8wuVwzzzGk5236rCystApqW21wx4jpG2q1OVYtYa37E1lnmc1MDNRoyPV1PnXdlpwXGlsycLbK4KEmHu0YdIlToEfboO26ZRtwquHOqKrJKdRRoUxuuVhb6nWNL1kaOvBkjTzWEnb3oKCKpRmVaPJOquuiQ0hFBLRlpSrHtksWlK1JpihuNIz3QphRFXdUgG4VCbY2oKJU9cuBeMSjDVoZsMjLZUsEMVHVWa5Eq3KyWPWo1YSIpUBa3Kn1ImUUuO6iaoaHTHjqCEQEEm2xGZmeBa2axHNP7F1l9Qm0cuAmHiSwRoiNUFXSJNrWhlDymZQupMJ/FxV8knOzkTRS8JBRazjW9hSYlucppW1WpqmGO9XAG817gWba0RRCydlIZ5pb2AwNCsBnzanvGkc5qq441TktRmBBK9y/aZKPSy6/3mbY8zWyDknLJ2ouy9XSMkQdKAjNjSnG62AwZexBtFJxDhTbVN+8OqBrY066faotId4GLarHVM46cIeeFVUQh0IyazKzuVWUbRVgRGRRQVLvvnHOVlKGSvc/CEq3WNGCD0AzhsZZ5ls4tE+1QxxrzHjFL0GisgTqHSGuTdc4xT7GYyS13VEZ0miPdaYhcssQkS1QqGbS9b8xGpUcxJMqY6cs3nhRHhjSKoTenXik1EJqlzWSb2gyOUdmVI82zIk+yP5FnLj/JWXc90Nlry9mqwO68watZ1HiPiqonWvEYNKdOIbLT0F2iNlGvp7KsOQYea6ani/dTLtkla9pZh/TE5ugiex8QKpQaaqWUk4hAtVhSyfRAcqeaDdBmcjZWKZFptjRgogmtGo2dkWrpNHZ1X/3p0QAhpd1zTaeJXW966iuRxqElhUdqs9K4yrYmItK1YArnD3hyNuksbiTVQUzjEo2p2vKahhi8pnO8hZHQioaxQMcg8UgNSWQlZLbRZAilpJ2ayarUEk1shoQ7h4obmmbSCyl6ygltiJ6Z6MA1SpEbD2TSViqU2fc59fWqAZXtnFSnk05VdXm7FjihyuOarS6CRT2iVb0px2y6ZlWhmLBb2wM2bWY1rsEJmQ2sXWkXYAcjPeuO2QyN1av1tJLSE1d3ajRxT0Yv6lwlskzc2TWbLI4MQq703gD3C8MFLNXSt44pZeQxKEpKztJopIaanWY1EoocMmKWDoipayYUk9Kf+R1k4JBcoyqoYcm1RGSRM1CjxsUZ457s44nW1UKJxlYj6H19TNWc7B6ztAY1GlWNnTmnhUzMkC0j67TGOsNZI2WjWkLQCA3dizwVbWnoHrDaSaVhlaG6WKqZXYbIqTBprRFlemZJuMlQvxcGiqjRuZR00JREUVzcuXExLswW1LiNM7Iyd1CAohlvaJfmvYcaJs8Ct2teYw80V0tYnXuJ3LB750GbY9JBxhzWuuZz7BENIhmpCxSWW00iPXFgzaaEWxcmrgowU4auiopSUYsa930AReGSJSP7Ok2bklElMjUOFGpdKN5SkExyVhtOjZBwpy5GcprikVtXdZaKZaKzXE0EPT3ALtSivsfXPScpCyWjKrMgd+qEIroSnoosuw7hPC0LKZyIko2E7NIME2Y09lgbhCYSZzDbeEwDQ4CW3CjUl/PSjYZ9U5rIkSpbSuTqgRqEI7N6EqOiKP2hihBE6Zucz/clF8IN4SghilUMsqoyGkNjG4SjKLWohk4pK73FpoaM6HQ2GzxWUacJFVzo7Iqz39iQJqGInBEq3vAGG3nWlYHmREoLtOxQm1vAQHheRXIvqZtSdH2ZdkqpSRwZYakqmx6Cpen3s9qfyleIzlGyZFgjJKlGL0IwUlUhE7UelE1Gok8qJU0g0kGbY7WecUcX50u2oZ7G5Z4Z4Oh/LkKllr4zuSd6NzHnkcdRw1GxaqkmaCnqJ74/a1mZGYQUJZvUWKUjgjEbmA0vZcvOXGaZnZzT0BNtp5OYJ6NrwlJ4qImK0aYabbHJOg01TWGz7x12R1ERdayIcWQpsmrpQsVBm20O+tPsKDmYKIqMCIfIJtWVnuvVulWV5RzlwEX2mEHa2+tIrUrORYdXmVWv2beROxlnL6ImMhStC6leE81IrZa0TOfIRqGBxk6VbGzhirK6kzVxRI2qzJ7EHOpIE10RDbWkXFCGGqL23cE97tc4VNSkagaKrm8YTZznTdG4AYqlppSYKJXZaSJLIhrXCIooNupcnRTjdBdkRpoU1URE9JBK1HAQIUX2svWiX+QubqeiMioopwaQkm01NRwualWGYeR1T5R0WvWcFrWmxvPa6/AeSVbndVbtWtTRZVeCoKHkWK1mcomzoE6NGlm9tOKIdWU01TX6mbVDkT0vEdmhVuois0Sxw41j0tAqcU2i0ovJhKSgBYqUMGLCisLriBalt3mkoWDdO1nXUFEUnfvj3NKoZNXYE+xSq6xzqnXiDcsjYupY+jbjzpWhRKhqy11YExVVuiiNSzYuQHF0gwzC4Z4UZvozgyzXqEXU0pVS0qjrD2lSjdqf1Whsj13saBAd0hwdvdFWzATH0Kl0cYZ7kqmnSsH9sU+Wa98p1NQ2G5VsutIFKHFxF8WWw2WSXaFVZCQpgxSqEn1lrvQhJipqE9azJ72mJgoWMZtUNRlMOKNNDdUyVliyqFsixHowNTdrAhSjkZhuDyUdLnaqp81kKChRCEqPSTrkbGvj0vRCmzCtbtpjZ8yoUUXMWhqAxrIr59R5g74qEedzm7Fmc10mw8SUERg2hZSYzYitxLOunnXjdawuS3ZshTTGVMlVY4/cM/vnSHDX10wJdTao7wyLsCjZV7WiCqdqZK/Eb1pH2E4XNeroQFuSGqHiKnmLsRoXV1eLRlZn3BGqnjTJwLXUgqrTlvpzf9XnIWXaiWirdZmye0s2cmckVxVGslpXBV10VlEhcEZSle7jc5yFxlbBQ8udN5WqDFRoddYWzMg6J1iTyVSkGqW7kHHImU5JjYOR0MQymYzZDLk6qUWZhVCjYgjLfcLa4wANoeIZRBOhaBFuOoVbldLbTHqLwkBJ406TLKEKO9I5k5CMcizjhglNVlmtnSaVovTrtTa2nZrUxmYdMbEiwml1fS93QaWvkzkdDJ2eaKQ+EE8TpaSSiuWInvopSyVL9lMrg4npeRxBSWepzkgrGiy5xgyt5NoMXFRRFkmpgYYyYpCdxlElda4UKTI6pat7Ord66hnqq/um1MiGyHCpkYVGJhoNXIVCjRuyLfJ0m1L0M5pK06ubuEYlpFYJXa8dTNIyUGjQN6JPHYe8ZmhypMJEclhOC2egTJmOCVIXimJJ2bg39y6rG6qyOFBtaqQotdSGnkUBrjZ4qz/QVDXsMb2ibOtSOqe7GDHngUl56JQjwZO+0wigeINJX+i0ilz6Ywlakx0k4YgRjZZArSogjxU9IlmDQsWBqtDEVdaspXBjgTMUXUSRanGGO3ugqJrCv0a1yEIlBxSCpEygShNDF0lqk0YTJ6kQMW5azatz17SM6dRqFJ37PLlzRHEXXdNvmkzpHl3PnlHf7CNHyT4ALZSIScmSTgJi5J5CokLaiigqNUj6SEPVuIZCjrZrOtP0S7zVSFVVW5rzAKtjqWCxLrNEZcaN0/ZE0PX5gys4QE0JV2XPWZQQXZEi2oim95Fp2iyO0kg9hqqiUFEToWaKDZJNicYlZCvDOcElJCRlD/BNSI1pVFU0i9IwJpmJhtRWtpCdahOekhAiRJCKoeWRwxOHAQZR1DlJjz3Rpmrf8hENdtXEqS5GpXMjsoLJEoq+G6iqFhU1fZ1dRZazyXD2kW2Vw1FbsJOJJyo9Jwai0wQgSpRE49KV9FhBdacucBUxiRKSIvsD31RVqIHVd/gzpaC7F32mdE2YUp1uce9rrbZWDaS+EdTCouvllafNom57bCFR3yWWoSFjh6GWogWvqTLnRXdKVTUaqQSEoynh0m9HPWM9sUvfsyr3OiByiBqZGaF6/nTFaehKQWk7nQ19g1BDoxbZUlcyanP+qMvMVinSmbYnToXG6vHM/gCVXqKQVKte3StANHLjiM5B36cbORRepCdVTxLZuKohaQMqY1esqkn0IvsNNBU3cihKpmLc9/8WNYQyG5wlpUihdLYUNW7cTpp+DwlQKF16iayonoRLh2myK+EalWF2uUhxRGKHh7aquxJy6cspxVmqOqWJzuGuf/HTHZXAtVijqfrGrCIcpattWOEGZSRStUJ9Ezsiw5qULDX+/3T925Ysx24sCpoZ4JHFpd6jx/n/bzwvfSRyVoY7rB+AyCru3U0NSeS8VGVF+AUw2IVL0rr0HxSSL/wvAv+QRAT/8M3vTAE7LkoHCcoQLmlZ5FdQFRTiAL5q00YzkYaxfuUZSgrs5WS2SgHBxUtkIpgn2v6yhNXz6xNGaTHjDQmJhvFw8b9giGXbUjd4f5C4+IUQApJaOEGVko43acP8HwLfIsn23m1a+Ba1aVW+eKn/MaUKC6VwFW/mUh5wVWJhVZOs+2PRQDraL8wdzSwi3U1XIPjGMSQlk9aKE7kqi9DyC1tQxJaz4dEUo4hmTocESBXj9k9fTKSSwVQf5llBOi0lcYkitIplJJq1vMZZtYfB4GJgRYpejhNhaOcdL4bIf/hHf+tv/C9+c+mPSOuouJlKmYgAq/Cmsf0NzMFf8SaVCtrZF5YQRUZIC+Hlga8cphKrxDyL4ZQqSQRsO5AZJkOyzOStLXPrxdTGP/oOMqKYkgi+cenNje85N0OMjnIj3kyo8yr1IvRS0QoWAmSRIINXa+p5yywBkiIFZhQZbDVokkBkLYYS4VWrC7/+URuWtcy6SKi6rWHpxUtFqW/GDKJQrFImViaFYmN+DCoRPTYOwDpTa6FdSV2hapvaWlCTQ5ghr56vKxbdPqzLizJJp1tFABOwFEUGbCFbZpdOOjZ2AOT/hf/4f6HI+I/e/IYZDH6T2HAegtXW80SUMu/sAIqiMgyeWDzYoNYklZ+EGU4tXK2r5moRUEVAPVoLJsIvrpR5E2AqEoo3k4fFwLcKUuA/pjFnUXvo8oqMZLHSSQaaex5a3EiDomgWT7uSM1Uq3ALJO24EKjIXQ1CqJJSCCgXTUFKgc2ihU3c72vgQAfWMnnkuL/f0OBnGIWkYwRNACnkL+cW9vuM/pL6gF8VQ7uzre7d6P8G2UmKUQs/4qXrgng5LXQtkSMuynN90HAi9GHU5uapHRzDoTgow2uUwlsVl7IVrxcb/W/8t8MVbf4f1rbYC/qvn+ya/UyxEQiXmQgagcCRIgSTfCBUlZDC1PW1jtj2dcnhJi2s4CQt5VqWjf+UwaTgZ6Uia7ZIcGQQDfdeSOoFmAlgHxfLxsWgIlEw5qS1k019LMvWfIacEENY2Bf0HB+oRUD77sHk9ziBElhYqQcpRqYiL6cbUUn2TsXE2h6KkcJ6ryOUUhZXWDuNWon06nBleZXDphRcDrzhcquH6OkYA3ftboJp2Eu3vAa50W9MHhJCcCkqxeQjhS5fCKTAUzGD7M4lQtdda/4zrJJLworaveKf0lWeJxU1KMP4v/RP/S1CFUwIZjh1FaWP10Iwvt+74hHypwtxaVLwDy02T53jao8iFRc2hk5wupfEOijrUOhYq4dMz8hCqrYK1CcnZBmG0TneClzIqDrLLWaEbDAEOvKITovmetgxZEJKG+A8iSIRQZSUYSzspE6yodFFQAungC+EUUQguLrVCTxazxxxeEUiGyJfTOOHseWXbs0pla8k2FIgltkQSihKSsQIp1MpQlg5nmCP01RqViAolo5YXg1Iga405VR79RyeOjAilosJ2zw7KIz2hoNMaYemKZR4qU0dSoABUlE5ckaxYFg/fHLPvtVEIVTiPAkkLEb3veVDMhuHakZxkhKWocoNky0KUuJxeTC2o8iQuiosvrLgsXFh8MSIlkay0GIK92ENyg7xRI4oA4pgIFq4Q+w9YLZk6p4LhqFgSEdklC74jaH/xbT7iCVabPfYs0YDjrFJbQlJAx6/58qpVTbdcbUPaL6qpjgi+HJAjZFub6hZQFSDXwlfYBcay4+Q77kVdcaLioBpeYi/9QxASkYJVFDZzCbwQbbODQDqYK7Ai/uJ/+I5DpoUXrpDTrI9+CIIMWOFkKk3Hfjl33mYo/l90mJduVUuctPOL1pdeaedyIV+yiqL0iu/crB5gR0GBBR1lZ5SvnUNESQTTDwIQTJDJrmOSkTGnIwgkEiwQFvlWLYE+UWHGOj6sPAgVg6EqHWzeMG4XpFMOq+FpqcK4FDIA1rGbMRdBJ2E4jBeLBsKCRTZuwJ77JVOBCovpdPvs9MA9lUzkWC5x6Bl9xAlBVWbwqh5FBeDli6GVojaV0olIi3G4x3qeyVAcUY1/BfWhcXXX4ZajJ1XpaNN6NzcxZcallbkWQFwmAgvBi0H2yMPlbiCbx3TFpVcww0xBr9h6q/Ki9J9AUt8REl5sc3lLrYJo0nkcyRcUS57kUdo23wGXLoCm6YvuRcploX0CEtllpNMLqxYTNLzYPibhUJKLVVxIaQE6Vtvx8pYGTxXY2NGKaZuodu6I9tJwNCHj5MoVDoWJLoOoIEupri0iT6Id1RJkWIitTmlGgJiXjq6uLwxU082V1fMAB15YEBajxKTR07eUmVRGJJMBrlr4isQW9UonxCaO5vh4KVBJVkhdIMXV3EMHYvi1gUDUEMMYjhcRtfQXMAf0cxV2NmTnRthEcIkuEkkmnEeUWJexqfiL/w8d/x8cffHFG28xGbcsJygq5CCS0RLE5IYFnK8dSUahvVKRYF09BnM4uECGm3QdWki1TPWqFxazzOBFmY6jikiDKJIUVagIQV8gJdOp5AbACJg3QYq8E3ECaTofGwWJ3APbZB4F90S//A/bTRlscmKJ3EkpS0hfJYorXg6Sr1HXN7RJRIkCQkLPu5oYrkXh4gLPV3uJ9blA2amS4UAeHpROQAdSpjJDOrHhBS06KqQTCMqpQNXVr7mDakAIqoiWXNPBiICdJtdZ52pVnuZPgrIH9LNKSK8GYZTfWKIQNyK97BL5ltbNXppgmiGVDAbYERkpRQZ58sSiwgpIFWdFFqUiwyQzGSeYbAFhMryc1dfQhbCYdE9aIisZzoKUzRvNbEWRksE3S8jT7qmVGxJcaIe+kyCaDckqJWEVETy3DcXwO2DdXKeBUXWWHiHotM0fb4OFxRPOqlgBw8F11DUtLgnkZSohHIPhbIOoCh+vWviWkIRyn6WIcxABGrzAFYHiJeo4qyKkiitAputgIXRCIqpNHaKEg2TCJg5WrZQFdX9daWK961T068kIL4fTqqjmAhAuBjtmqk+OSC2AulO1nPijY8jOWOfFd4BHxJ+i0keX/pFYXOkyoDg6J+ASAueEEUGbaD6GT3JZmcybkdGvO9C+5anF+6TKkpQzffuGEPwHQSPt2rloo2DXITYvxf3C0SWXmSGKPDooHR2dYDoqPPlYJxQS6Uha5okSKIIXwE27oDg4dFaraQO6pYDIHYawo7LSUeSlqOTCcneuJssFRF+x1TZXS/8B+FIisLBKkSelcyEy9qUooRARwSCFUrzD4VziQirP1XyjBp7EyueIBxFtSd9XVA+eSYcS/CspKwGf5TgsOzjV69MCgOkDSsmMhXWLq9KATlYgX4JecZG88yziJYuLPIEkgAqsOQMM3omuFTXuo6jsaDiQ66IJFhCZyJkFXtA0scRLxKvSq4KBvxi4asG+kKKDjHV4HHR3I1oNzGn56NA8SMJ0rTxH+u6mRwyxgABXGMw6wlYE2mhdGzqgYleRTm07xC0D0TBRgCfSLm0S4qbo1XRq8OQSdICFYISqxjYyIC5EvTN4I2tRrEoeLQI+m7ITb8MZN4tZ+3DRiBSWY1ezN08k0J7eYrjloEAqdKq/U5LBBxdUVmSeI6XPxTIgvGI5gVKj9e0U1sYyFZKTqqVXpVMB0CfA0o2CcMdJN3qrPJRE9MDJgXOApGEeT/S0WUF7VRAImz4oiVwJNmwmxV6t2bEphm5cOGJICwtvhOVYjKqJlivHZhjad8QJbhQy9pHCiLtWpBZKKip2wEXR4qoFoAY7U5K49E0G4yDq8lnYqFzvfTjWgcEV4ZFzH5oFUKuA7r4Xl6JreYS/IpnV+RmwhLqRUn9HZgflObkKPCiIFeGM+xyBPMugvM6mDlYzjAQhlABD6/goVCKwHIrN9GkeEops+KkhjgZbshhr8Vb6SOss08GoK7LbNqJNkw1SqiAtRWFdION21p9UHW1vBv8BKbzySIV142L3II3mq8tJ8xz2x/FypY6ScLQalgyn+F7Sycy++6vJKIVw+sU3ZJccZv5Vm8mDu9Y5Mt8M0wWimJLrfS9tndpagDpDKA3SgTpsFJpCj1EDh8lJdrHboQKR3pBIHMNoqmVi2ptFmHBFAlx1h7U7A4C1gpBNpd+XxHUCq3V6thsIasCWMF+1mbyQubFg3w5uqNJ2IWEqjUM6VUIFs7N9GOmSVWCYIYaT4RAYPRJiF/PJDn0PqJeoQ1hlkOD2Ci50Qg8RajcVDiLcre51HomrpEWIiqqzirFIMP77LHzTvGWp3vwanwfI9HbwOI47BUzV/cBeeSMTSERQcWIxhNC8eq5J9FgKu24q5ONkFsptwI1vGUKW6RNESccwqK0SEFlvlqVgpSdLrFJVX2Axk30HxWl78r1X7qU/pEKHdadZJdHkEQynBRHiUbiSMPjeL9whgEvXIYkLlxLaX0iEpTxC84cBGq3sqoD8wk7eWWKiGEocE1EGFu+OFxddye0KhhA7EQRXg3Fq86u2n0zkCQLlxtCizKkCiLJDaqhn753CF82TWkeIgtw4gtjOypQLshnVBNKAfCHhXeBVqXjJ2u+i5DL5TSOaOJBYiZ1hmqn9xrpT4jHBqBKMZcRQZ+qsQ8XRSa8TTl8M9ERSjQ8oDsxW48M6teRzkMEylLtU+a6LooUivcLwdmozd0SxCcRsVI2nBSZMLgilK/5xGuETwupuzUzekIO3SZomRXpZnEAGUo4AuXZq1UFQTLjgv0rxcmBhIUDMiIklAm0UgQiQNXw9lg4WKqAige1kHa9sbJbM2FKlovJAojLf2xG+A4z0zCAKPFiAO50yTIcjbM9QKhn+WrddaWCRuJSVp0uysKoRVOHAbu3kpeWFFaG42xwlT8Hf4sFauniK50v3yg02i0jsQXUZZ7mIrO+6XnUQwXI4vamC4iQEKt9xrZ1Yo2EiAkBx4Q8Cq8oB40+kti9VFaEsW8QRyVLiFFBMHLgcR26XUdPt1E8UYReXcyZNjyonKC6XTLds1W0dsRFw8LaSLLWVPYIXyiqfqki5oOop/yXVhWVytb8OWDF6e9Js2nU6/cYyKWSVhOTtYIGErnJXk0wqKrbtMPTiqQSogBVhVksbe+jbAA8JhEyhaFsZ0aeEyLCcKxgqRODIpPgyC0gtayJ/1NGOI3to2GcVVpTS1PsyLphBn1rL5F+otZIVrmySjdr0Jk2Uo5DeUCK9D0Iut3okYrkEX3QFrzM29d2gQj1hwSriWwvSHv9QOCRniYvi37H2yTIlZL1hcvF9FAyXD80OQRnvmzzNS6bJpaU4xWC2xw7K6/CLO7Miyoj5unHa9ymYyqLhCBbjIOBGyQjHzP2WwkuvfTm02u4UnWYTPEPcTvLITX68lF4Ejy8bB3ZRgeRBGVnhAhKlUKUS5RQgGDoSGYjKtioo2mWAgdMD1qY+ucTgWe7EcpUoHC2DdlDlDIdFsL32hQIqHLqQDC7DWVdi1xEOK/mm/HWObxWNA6sZ/NThGM9vywnwiKcpbkBRziOZ7U4aVbhKcEfG18XAQgJcLsqh7S8H3w6El8qbyU1i+eaprF0Bq+5YcLKi7lQ5owJOsBztf4fNZpnjyNHHjGV6JF83X1Fsdv0hTmSYx4FDMxzRLdLEPoJRppo7KgmSdii4ShqnTS4TOa1fotzMNDmf+pqLsEoRuB1mQWQQKunYUllYtQvCQfMK6RB1XAyaLTh3th01WtcbfuNCdWKNo0BQbk7zK25oK1STYAKKJ90jhYl/IYpUBOA2jVxrlZSnzl5kchfIqhNyxC56O3wUlk6kQu9cZdgQw+2bcLhQzV5cC6Qq6Yi93Da6C+mIVDhJCJ4o2+Ch6+oxn+XoT80EWUxeOAbbQk0+CNosOut0q0iLaSBa1V2pkWn3DEahPNlOWvTYKIR4eMLJPMQlIWp1lBnodJKd0QF5IfByuoe1whWvaEgkuua3UCDAbM879OsPqEA1aRt/Qb4iEFaFX75ACZdVyYWsyERq/Aaq9YetRU73uEnV2QQC2X6kKkFFq7rKDyVD6ySuCiqu1uFW9PBldEEkWj4NcGwtVExcN4trXT3UIYUFHp1cxMKqFLS66EVkOJAKNcgrmq++Vx2xDk6KAletMC9IX1oQV/V7Ctg5RpuJwDNMHxdzBTvVAHWZ55HgkAf0usXURYYqIEGR7axSXd9a8QlIFNscjVRHJfXCKDO1Td0bSqNYESyRpcha1WFUrEqj2+aojC9HfFW4+8M4oRXcHcPt2VrCIisi801TLjSelygWLt8hs+CD5TAS/yShQpSdSIFC700MKYWax9LyMqfBAKRaFZDYf1KxZ1C1Iw4uHsChVysk2i7S7RIKslxMVSKdWL4qVAqsU+TLf7t1EhsAkz55QSi4NTcBKW2Td6xv8JETLR4nylTiVkPr6zgc6wCsLy9/OckCgsBCQLxwmpvni18mtgsJMlG6uOvSGZglCFsVhKoYRZ4ePaZF2YB44tkrSguJ44XehSnDlU/iQ730B4y6xVo6GPPHPEuqtqHeLaR2OnlRxfb8EvzFiFAgq/2AQMEoEl2+reqjeiuhSppvL7wtmagsO1zyiR0FB9NuqmaEq6OTmg3IhBQ7m+hKYPltRcxDbNTBANLhqMClP6Zfb768XlzlCsiSTk65YoA01UzBvt5m/BK+dBCHAFYdHgUWdnLx2CLDgE50dpECdTubTiVzcwVhIpxVuGgugMILSdWLIR5iXFRY3eTabOCsyfXfPd4K8bvWJoibdrDIs1R63W8BO33TwlZWhdMiGOcoZAfoGYu6Gw4xbApG073jWOECybPWfXIzwnEiw7IYDJ90XT4lwGrMfUVlycmopGr51T16E6c+sfShhY1gOHLVW8lFl3lilZEASrv7BqVrLW8gaZ6otIrBESSndRSJ9EKaFG1S/aplEEGdaLFotPViWusiseq7xskMDLkfftOnAbRKoI2b6mvcelbZBUN1iGJ8vf7Z2oHvDdX7Ui0iKlKkXtiwl1E3L/8TPAxW0LAjhOtGvEwGL4TlF12JqxFSr0l7CyfkDYA4uCp0AfjDLxb+0lu74AvGjcS2I13BApcLRShNoMlfgDOL1OmZ0jj30qYWgslN7GAkT0Y5FCoHv7ngwBFnpt5UCcAIHPIgnXVJuJxYyMGxLrakoXWuaHGppfDDuU+EN1UzMmmMW38sBgqWfAyUMgo4YCpLjHJEqWU0JWQtxSPiNHuat/q3Uz3gNcdH/lxMrhtXy8dlcdXBwjIaCqI7cwQoCtWs3o1w8PKq4/AuxAWwqG8xoet65bkUe189VVnNqDV44mbuiheddol2fbEAUqtNKRgkrg7TaTcAZiO03uMWLLVF2C3F2ptf9Q8Uec5+sap84Vh++T6M20vcRw6x+PXeKZ/x59kJOpMkVpM2kJV4PZleiCXEXlBTKa0A8wjXERtglRcT47hziOQLKwlURGLIV1j8C6SqucAsu7mzIKp9GBOfYCaoFr5iYfHKwlKfMcKqjCsy6lxYbdQQrWYWAJYqKk+4TRWzyd2gGaxsYhphjouYCKi9y9cVUHgZ1fh/gFh8COFTsZrF2T4vhBLL6kDHVAbY11sl5RWKtKGDWqWeTJr4wqqIRh1g1qvIy3aepuNw5qWXrfTF5VVdGXBcpBLPeRBMLF0WVxfehpIrIhcCGTGlppR0HKBipyvkYKYB4SS9bLDdetsWuY86ZyRlV2Qw2rW1Dnhi1YFQlyOyxAWcln7tRkOMStxIwPIqYDm9cPnrLH612/UTPw252pXEOVWUXIBUhFgMhE4VJepPmSwrk9vVRv9JKlWIDkRkW0Q19UMI0uhSq5fI5EQIBKrreelSVvMzcvqYq2mkBPpumdxluaskLkddEqEvHyLJDeIE3qHcK45RRh7QoQO1+fQivIvKQlj7rGLKrKIUxWO8UMoqUKhVpoZh1Qe/fXy3fb1UpAxEMb5wsHWKdembgXWg07iHs6C3oxwFQ6oyLJSe8KOgXJWjCc9aiWbnNybOlA9NpAsv/sFau0qFRAGtXPfCgrgAujCmdeLCYjD8RZ1QllHZ+LWsPuHduRU8jPSNrJt9BuzKA1ar2EAch11tBUKOcWPsSkSwiRQSsFsJGE6me2qH01hnv/jznAhtqwJV08kvAj6xwGWaA24nJVsFeCysXIho20bJX/g6/zBE+05u2QGtniBUGNLpYVqJrU4wXxbDtdP/wQ64DSrirFAdElnQ66wCl8Yclq+2iMNhKEGmUcGqhEuSwWMWCxFerpJqL+6D9sRhuhQgtg+Ek+10AzfCvA+T9KqIoUd1KlaFJQKVzugjfheAO6KQdRC8GitHSFiHa2mLb3dE2MQrLCdnIIz/YurpwPsxuZUBhCUxVRXR/XTicPnE6xzdEiN2XRDTUZZQLl8PZ7fKav9zJ5JtR5djEkEbu+t+qrICgjpIFSQclIMGUjqeoFguSJ5romsVGgpukDqIqM4NuPLsLRRaoSTE61ury+eTX7gJX4hYoC4YG0IhxdrJ77Pg0LHRSeWimKAQqwhaSlzFSKMA4fT8BpsLWxIjzuUj8e2lFh1sx/nCtyT75e968WbBZQYJYxVAnWSaoB2L1w5IgVWZbVd5YSLee2jsnGh30jGD09DGuYIcsYUCG1e1YQedWl7OdjB1VkowrhIXBIqEaasFJd15uIOcWuqAy4B8ZCwfBVJu84coW7IUMEFttZtXR1Q6uSrVZWmyet9Lk4NMijqCPfFViAqssjfDriU2tXw+83NUkbBcIMKZlyXh8rJOjnoQJdq+l6XdP8WmEiV64eq6A2TVsiH7dgpmVN80ClYU8ui6X7L/OooOrekU4x5hbywbq234jApJdYzUq75PyLh44lRDYWBii4cMt60uZXmPWaK8mQ60/UHgiXQsixcZy4cHVF8PRdXhqM8jTnsMJJJxVsS9QJibq7lIDDeUmT0TNLUMDu+ltwjggdD6pvOhIaObNrcbjVAtlmSBJMrZtxlZav8rsBgyjJIjo20UEIaTrh4rBw/ClE6Yo4Jo4dXlZZzCJR6W4mXhxRQ6Yst8sjLbgin6Guu+RYVMIOokcKDDWsHjKGaek2oX/H6JIIKmubwjTtUVt5OnN1CZ4AVWmHgBGVh1KZVg0UHjdCGupd3+J00SOYGFjaXdPo982VjnxGmXQ8kQavH2sXxieKTAAmwHxIvLl4OLI1jgdUgxXeeqpajgUfGiecKV8knn6FECEUKiQJdfgOV2AouhfJAvJTs1CAgYgsGWmQ7m9tLmqveArQUwa/W2H9MbqbYJU1XZZzKSIEsBN001Kyblb8okdBIDsFwINnvJI0kRB5qqdRIQHRAUZTINjHrFRI8WjSgidSl5KbFo3uaWSEMb4UiuI+EAjH1Cj3r5ZdN1GCjxLNlp2nwFDs8iLV5lhV6GvjZxtZJJTQspq80li033BhEEU/SuLDBYLL38D7M2zOyuxWCVQber5dc+0XRGAqlT1FVty9StRN/dwuKujYVOrDLihI3yZYcr2C5aq6hkWqDVcCYvOvLOonoS+OI6iva1DoIuPTHmHT2jdnVLs/XwXNhMl4vH5VVQuJguo24mkGWUYxJ8SmWTYGdrdOMZM8Z2RWxI5W7+WnjbpJFANCe69lgy0onXXshBMT/pqqfPUY7QLan9xb9PZJHrr/f3iqKNJTDeAV/l2WSKs0xDiYMFS0Xc8XqfXDhWizhcvEKH0iaz7IQrU6cxSw1s3qjsBaNmBtBl+ylEmjglrvK1BBeJwkI1/6jevQ1IrOSra+IK2k0F6fwOj50Sjiy+iL7lYS+cgkubAlG1KC6mXoqeo/H4+fHktoULvrzMEWOM4WIDQSV9Gje0lo8ILhgvbgcKwqkg/QadctNR9DoI4bCU4Em3xLpK6HpFTkd0gQlI8EbH1oHhBpfslo+rQV9wX9eF44ClymqbeT5L9Elc5u6Y20q/4o8ZUQsGffzqsArZx18KvJnZ7sdL44fanYBePorb1fUvBSzlkekLBL0UjJK+uvtvELmZCdXxui8c20I49+u6ecQd9FkEkwcp4Jg+rQ9VgXV8uPhsFCSMhXLQoAOXgEC0PgFdtYcX3dYLSJr7ZPS0x2DgBJcXsuFjXziVekMFrhKJhQsL9GIz1haSifYW6M6KIKrQJCuIX/7mRulUQCiU8rS6sirE5i7bWrjhkw3AOuhmHQq90Ei2Sgij7AqA4DgiMxri9nARKV/cuQCtrRTTYwY+PXcXggBVYxbVbllAeqlqyQASO3MH98481IljLzi+dhe1iReI4wvfTJhFu6oHzipwkUQFQ1mvIhfFF5aXOPrEwnPwA+Hg6emtZWDhjPNT1Fdn93WrXxVVLjIcKlPm6UFIFhzpBmpJLKevNmZo/UzjZq3orQTQLkfLgMtIX/P76vAX91IaBU2lvix21Fk6S5EOt6IYBFm01UeAkD5I3T6OEM2/+AfLG0eohLCBqm7fdN5StjvQ4kmgSB11picTiezUgyFydjsPUy5VBA/VmGT355f2DQGxoQtvQctZUvBTAjRqVT0XaqJIVzhp6NtFXFng8qZDtsRX3iGtYg+nhlclCTgbiMA3l9touQsIlZTO6q8eIKQXiKbQEEK1faU3A2j/ZAbJXYungmUU/FY6/eW3wmAgRVu1S0T4KIFCQiJpucSICi+motZykWnWwgtv2YhAor3rCuvcsXwaJ4+odChP25ijJJSNGQ8lgRgDk2TP/Uk49YGAeoQMU7gALnzxn+NC5DtE0wfCtp08+DJt1KYSe68Sy9pqTY1kMxlaTaJiWKoKAsIGi+pTJkqW0VeAF4WlNHvwA+HCfxlcurrus9pAiWBHJsscDgK9SpXn6DhIrIqAWxUfZUxHuyMW1omgF2qfuJEEvF+wTrcTlaAvsqiuYaTXfiknqpIKq/MM2funhdNCOL0rQaYO0sUV2Ge6jhSqvJ0ATsjfDKtuBiq5Rrke1ULp5bCkPmbxYsBe3rwsBBIHi23iWbRFalXgYvQUoV21q7YjOSnbDiwmF18IHhRXT9nd/+ueswon+iU5SrpQKN+6/G6oygKO4t2zJqePhb98I83D6AlpnBbxZKdpYKoKdO9IqujkjbaSHxc/ilHhqIXgvfJd5yv+OPPa3ZoiOKcI2lQFHuv2IHzh4nUqT9GhDRbXmXwEpibOAmwCR+MaFLzw5mXyDWjR4FGwXoRXj9cqGOQieTLihHIo6Qeatv2JgZeSR8k8qj9aPC5biYtvJ3ZB4S/fcDW9vhCSjXbLrKMO4ZHVpMRKguZFObC0ofqrqOWEUDxM0G+spNkCBrTtYvQNpgO16TSyEitazmwALycaWOKk1T3zAAImAy//0dLdnppaOiUTR0bQdWlLWzzoC6vHv/CkplZkdwDJgNiRcf3txG7TgKjIqLYQA4k4wdWlH3Ir6yZeKAfa8wA12dHTssAaDxJ2ckCpuENhHcV2FM9ZxGqdVP8lhbMiu+848FmxfKB4+aZ2UeMJGMzejLgUVkWstrpx1NjVt78rH88XcDFR2Mj45uKNsF38OscKsOD6BpW2j3ReKN4Wi0q3YX0goROxKrS4GDBgIQ1LS6ebQIbpwPIRoEqCR432s5KXg8K7ekLQ2RZLIQyJKXk9eQBohfpAKwBsCwgck6vvBVwuVPe6rcqWcaodBVNGoJhdPEobF02S6di5Hk/dLpva5Td4rE4nadOnZkO1UQRf3l7ndEA0tq8yXsrS55CyCcBuHKhnJOtc+IeX/uhyRRwrN/dC4qCYB4COizjB5j2n2vTtjSThgm3RDafDzrGEFdFQHa8zSSBs+syhUIhWKgCij3mAhYNzNi90XS8EsRli8dWJ4H1q4PYL5SOno/X7phHt2tXx5AawGHzhH/ZF1jkZ1f9lI0W4rkiEr+rwt1YXx8zgJZQ675cXOnxFDEb11AbVp6ntIZz3mcYuq/BWHrJQBg4Kq6CbJ3RDpGugnTjQiSokBCMYuMahNNF28TRMRJUIMxtxB2EL6EZKXWEk3NImWFehxsbFbAUL0O+gYdzivDbqpft2UKusA9Y5YRk3X+dELBe+CAcuJNxKU98zkSxMu8ok7ovoiUz15dkXRmIYdtPWEmDwPDRQLpeqz0xuXDpYeCth/Me7gIgyIwAcl0Io0cjGuRv2MeTFz4T9GfGUKF5cJ2IxXTzNqnX1I+r8LywvBBqCCKUTJZXqYnjxBVlOLrWsSh7COYFpsMlPcyYubwvHsFvQQIAWS0E6dQtcKF5nz25sdEyqWg0B9QzCjHbaCLB96Ns8YpQP1XQyxtYJKmvHcpxOhcXiBbnN1Dox5nd+LCoQRV9YvLwpbOHYwT8VjOI5CGq3mbBMt965T5Hw8Qq7zSbkpo1rAPGF8IvAsqzIc5FeTLc9XRv3dVU6hsMmlt+4vG2LrCuPhUXgres4ysQ+EYk3wLdRzgsnxxA84VpoUDElLJaJ5WuIo3dHujlVPrASN16oKUricfyEJCwGqqMWK7WqlUPtRU2KUemcVz7TOHzYgR4DBRk8C8Zywe0aqapt4B2EmChXFV63r3pUfDt9g6Gd9Zg6yBClMsRAUYA9nmH8ED2hsLFy7Z3e5uUdrnRBwAkOAPcwAjyZxAguv9UGGkd/+UYpvZyxYSRl8FWnyNQFeqE1ScIGeJVJK7bN4PJhlOLqeCvTCdZXyMnA+rASPL7AA6Q/RPHmEWLhaNWOTqIuL51afPvFNyoC6C7OitsG0xfAbRajcHk521cDZOfrLb9owfLVsErd7BdZfFWJyrOcFK6OuWjHP1w8XsIBEhcuU+ioyFMghGJ4HqqLOAxsZKiwsBm+HCinCsUyBRcPEmjJUynYChsWBNAZCFIJ1uVobkzTTgD3+cZqu89wo/01ZyrEjpLE0YKjFUCXUy8vR9h0Kwi6bAUKHh5U4x9Jy4hWrugs1cWTHtVL8SJKDEhhSMwiXN+KIo9Xm9DoOiFYnswWiy+F+0QAwzUoagvIVZV2YCNcDAPV5JqiUxtC+IV/vOrmhQ2jI5/Mi99wjwCSbaIEypfbbqxVO+07F0hFu9Y7kG1EUsCLC28fSlUa26I1/lyAdDHUptBNLzXDVwOqSpConHL2aZJcHVImx1iyGXSYnOGFyatORCFUBVsF0ctV41yDQpBihZai6NViHkA8MwZuaNdRjySVLNLCqlsCwSqh5FepgbSWUbeXK6YS0EyuYLdUKdY5K0ltqbx6Zs9xlekrlow4LwQuFA6Pk7cFohyK85xSwIV2IE385T5Fcojga64hw/2sehQGiDZdyyj4DwP0jajiO666nV370bUkCLte3Ig6QuIqqgCx/V58MQ0ulsnEhXCof/Rm2AJvlxO3ry7OWtCJNZKSbpDKaAcuZYny5TY5ulAMrHpwVbZjYd8ElmfSqlcVDPMGnfyDCN+TN0PU8h+vPGyPk1Pm7jzdFI7pOBma0XKpEwhdzXLremBoLrQ5/X1wsqF7sBwvg4zoqpGyh1bGKQREeMpHphOnwjusOqFsFalZB6RRbbuHZFSQWKTLG38F9vHF926v+gAtrkMZL7bxVmghfJGl8ECRDYX3HikT4WKC2KbSL8IHqbL5qj9WXLyRKKaqYyp0Y2EzUYkMDD8+BhSl2mLFJRDLWa+4HU6hAubF8LFw075wc1V+BEwJYHR1KC2y0om2Jn+IGdnnMMl6lvNgKwbZt7tNJO0Xjw9X0VKhTWNdQuqqt9pezhHmE0BJkmwIaoJV7F6UqprvWwSbgFw9zmcDQ8gCXMF3D66Mo/FrsPhZsCCeHDTWcWj0lHVfa58adANlsSSZ3tydxsrU5TamcNC3X7xV5yu+i+2MijwhNaUFRPp1KpKaPEc2DO5DsSc4QQ9Xo5jyvptmW8sH2/TLm1XFCLuZl7D9F+/6UmEnZH+mMsvt4BMl0tVEb/XLXZRfTsDbRNqao9vhS8lXyxbMYXrSq2kajGp1HqlaPZIB0NLy7vcNFIlitCO/Ryq+uE+2Y5qTwGbgoNhHZlKQ6nSeLgEssI7F7H2JR9p6Osn4CengpP0MHGUQclRJ8qEUF7/KAC5ljwF/TYMHiSlMpvhuRiXwE2sEbhOuuvjeETt0NyK6vCrm6MkqSOSyuU0uJlU3Q+0E/sRrowN2IMs0IZz59AHZYBUC5badKL3qxqpgMfTFOocHwcZOaxDV3hWqYmQtlUmhcGFxfTz0ezTcmRoHLHqNvfuut8Mb9qv1tm0IgGTbtIuAGSisaiPDVugtCwvnaF7nbL7h8TbQLpaNYAzoEmrCpLF4Y+EkfRh1NTnE4dQ+jSwWL1jZ3E92KlGbXPYVw2bQqrz4IHQkwM1+g6Ej8rZw6Mjm32iQkucvDBzIR/nUYc1fumGxToucThBOWilR9c4L52iuyi4xD1Km4fpW2tU56o3qQ2Ayj6yerpgdaQaiuiFG9cZpYmtD8jbSNxbfBBbA73M1E1AFE37RTu0DHxCHF3fLQd3On1Il2hzi6XvCqcXjzWScZFheALb79haumjmAE8QicRomldvurFs0v9T+opPV6Z9n+iBsrb5yjzVJHYtZFQu7te8o6NzxHx/a8mFw6e2F9oOLQgUQu0FktiWkj4Ku3u9ohSDxtHE4ExMB2ehSMG24PwlpiKVnbPXhBLBZzCIS75aZmDxOnshzKIiH5HIdWZMt8lLrLM2AdeOlOlvZDmTNMOgQPS6wxNUgE1Z0fK36vWPObMTwqgIHCxu2mO1thjYC3RBeOLUQ8faN7ka+/OZC0VD64u4jjGoK4dzWLjDnRAj6LF1IdKZRN3o4LqTaULUlIX2/GYNQCfbSVRUEvVqRTio51y8+DDtM+yAzvEuAL8LFjtrtXUyZUW8lri4dePkgO8wx2MyGRtPHmLbFYAdkk96aDd6xHbRYrfixLNd5EbEaZ8spIrE4J0gPrz4DgWhEMHJs5qrReVwFIaIsjGfvxl8ETiyKCwvpg+KiUN5Y2N4QiFOXjsNLxoLZI98GkC/pYJ6uaKOtLpqsiOaqNx86COY+KYfJd6V3x7BjUdgFsW5mB0RiJbJCTVAoLr5wMSmnjuRAhBwn1eq1dg964RsXvpHYUrmFoQ0hs3MBgE4szCryUIGUIKS/6k/A1Q7B/SQNP831oDJa/PbC8cJpvJ4bslE45fgvbNxULaqgF2uAJBcdpe1itPxcnfKHIGpwZ8dU79Podi8qGJUsh7+Z1fuKzApvj6F177w5usg9WSPfYEUsFGQhfSTQWhvqm+JeUYe6UGzPNZV0ykYTzm3CdttyBi66ie5khRfX1mK5zfdZBuVDDpOpVB7XFmz1RGEj5XPJhBMk/IJBu0LuIQFxbukEq0dBbUY8R3/P0dsVHw3vnJnri20ekzi07waG/Wr+ndLC8iQRIGGLNWBM54JIUBvA9MFvjJy3D1d6QE164UbggJHYdCyTt4lFAF4OHx8kAtsJVHWsBBbL2SVUL8oCZbM1JMawrzzbdfjFeoKhHJ0qkmDClXKt6VWHvPj5J7Rhg1Q0suilws3h3zLcmbwZWwpYpBdSaXCBKFZHxzGOdfACYhmVzt4gJpMoRQ+1erBFwwYLo1BhswR4KMZZ+gdXGYdJ4ga8RENVf/nNVHHRPkhYWcW3lF2UgKopOBJpFTPcoIwUJ1nMEi8nAzcKSajcTtRxXmAz8I/i4Xmnq6QvrSO25OxqeFO97+C5fB9GaK9kEHyoQmijIzEN8Vy8y84gtsHAjebOdDkLA2lRxJ3ZPAC6A4Ps6miqcAnlTi5ssUfHP0R7JUaeE7O7ZHX1HeJc//zpB2wDqT+1/LYq9T6h0sJBNqhQCITAAGsK2XY7aFFcmHj7i6w/aLC3HD7igZPLVFYggi2/n5pPEF2eRAHhwAijzbhRDhYSBwfhxd2aiTossjoItx2GCkQWMdNAlLGE9r1DcEFVnYWB8OIB3Jy+BTMbG/PyYWGBIi4n5NWU4564d+JPq9r75kJ0BndxHFZ6zDrmS0MPIIjQXQlzIwHQl7dhI+U6SPeySYbFMivIYroIkqeSUonprkLEggLV2d30KjIG0uMUVKqQ2eSrGn7eQRtk1aFcHbzGnyowsGskJsVoDBWEDpTsg7kd/cKbdBCM3dJCYKGKDG2H06EDRmedrOCOyp5RNnn1hCYzuCtkcpht3Qxa7CthTlUXtepWVFG4/FbCdRSwTSNV7Xgi0MkLQKlXUV8ELaiWzaWFrHQKvHhV3/BC4ihsJ8rSBVlKrKJa2dsxIX0jrrp4ZrJIBwzTbQ7x1NNuYK0FGDCLSH4PRkdLp12DfHO7bSEKrBfLxB1Zon0YDGwGL56OonEnkjTQZRKLhZwZJAcBMWGVg8VUsQKwfCTGPYKQmQLSnz6AcLRKdOgMOorYjSi5QpRLaDMJsApQZzL7MoJALW0H4BXnHAAoIQB2GAsR4gl0SlAg2pesO4+R1Dd22+gWTsTwa/WlA4tObgcP5IulFwjzWHDvB+/xMKcQAFgR6aiY1A+UUdWZ9TlWiVlLRnJjVanApmS0QNlCcJ3QmalVAl5da0IwrwJpI7vowqO2Qt/f09mIScA9NfeicVBOL961CBYB4eq/1YbtKNirUFcG68QkYo0kRK0+PVTn8owllIapA0ABM4wqLpwmPFlGhH3czAX+qwAwYKsyeYJiWJZBEfZBwMZRcDuro7yzh9XdCAOgN0LECRa5YBE1IRAid0r1CdMYbjIfflEjJMN1BouwFGwTKNtdy/mNYNc3f5AGE4W/cFpVQ1TKRnJZPO0+4/DVYQRQmZ0tc/nm6YgEEGFLuLSZls2u59vgNZtSxgArHLCVLRkXYspD+GO6O1Vfhw92mUph991AyDi4eLrM24Sb6iSbqrv9dj3S1ABsiaU2fhj7pCaJCbK1XEjcJxcc1fDvQdt/HauCVTNnZWw2StORTSMK49MOzhCI5WMctAFdonl14pHqkMWTwOkBfhLVnmWN+Bd32c1SCkZXqfroIztRtC1jAmgHoYcXNaC2jcBuOjifyVozIkK7IaxTcZCV0Yu9QN8Q04ebgN2UsG6JNHoZdiAS4AkogKEAz8psw6gCvvDHUHUYi4T0smpFM/A+EOsUgEWeDpPJnsg/dEA/Vf+gG/13yZLAg8ApX2Giq5jlG4DrcAElnjkIw5YVOw40wqCj9jgZOYUMBDcuEqhWBI1BSd/KNtm+POR0CxMR2zXpHFE/JUA7RYRv8WTqThHnzSR8SFZvL/iQBa/YXgWTV88snbaBQ2AdK2neXuxmUlpgCyjRAEO2SQ8eY92O9uhKGRJ5GGGyzGIKyWL0VIUG/HLhKPBHMso8tXAQvJMNqQiu5NJidqquXOFNtfsnzGjFrQtL9kGQvS+lCr3AXjoOFoVdknU85lCfw6gRYLkYGL+NBw1uoo0bFsCRrdP2nj0K8VYh63aQ3o4Z8b+wwUremxOFyHE46mGvqiPkm193uFxIz5iGNXhkMNmZEjqbYFCnJ4fuCGoDcBNPZgWUZtgEJ5YPFvvGS6KgoOldXN6h08d5T/IWcBxRpeTDN+o4i37R8upBwyCnh6A6vLGLEA8vxf2MHkKNqXZDQ7pwO2BurNpcLl0owy8ZxZuL5YXNV1so0mRxDR84+2Ih7ClCEm9fWrzGSsJjQBa4KVwKEMnHUc/gU2nymsJPTCyPtskjA+C/aFbdZ6lbHdClAm4HtjmAh7C1eoEoing76VrOFpe58TS2MWKX7mOq8iZo8iRimkx2kg4M4bCBXRth65DZzQPaUPZpV+QPet0z707eRdlUMTpWF32EHdSJVXTdF11+DGpoLMK7R26VKvSwMVCWqdjPVTCIDLJmvjzJRc1MaaNOdqil1LeIk4Xjp8xdeiMF3t15yLX1Iqt0Gwy41cGTGekLPSZJBMpsMxiHopbg6HA4vGkcJ7fh9n8HfrDAXgrVBQlQ2SPMzpruDPO+UWerz0yj13CDNY2PuIARQwGHeRDbYXOdd79Bp2BpOxzBUosM2dpAOthMngZ7H/ZJ8rBDWaQzDP2AoSiHy9kkxz4V8ElYtX9ErLNcO5++558vFOQgoIRdL0CnwlKdK2R3koEfRUTyAHjzC8dhEPVS1XKJJZPBVjAC4Wqr2mK3sQRcHIj4LkyoccPEt1Adk3HhDaCcOg53YkaruYDkzYUbaWYrABAGVnN6ew33PgRx8duTwNkmmQ5uvXCQZYJpt/e3ryY2oTmLrfsNdBD7eO16eU+LwR85AJ8HSwAolrNZhcGhCiAd2Cfa60WyGYBPP57gLb26S2dB5qNC6F+w0FdWw8CYn++hgoA4naAYYAE4ET7kweV4mD8/MODnyAJ1zOALB6XwRNU/sF9VXQEc34pToPAM2ZpkBjPrIAcRlUUo+5lZ5OEA6M39C6ibWZ+Hm9hlQAsj272EcqjQPghyOekS3wovFCauilUAvHA4sCmGECK2/mR1Tdnes07ax2rfEJPC5cMwo1pZUCGR9bP74XDEOREdWopuvAplcUZtv4QWfcM+lZX6dgjUYyDoUB1RBFpwVuwlUIzTRlKBVgxsRBdlFIR6Yl46PMZlhJGtAwYxLI/OA61gyIx2ouyF3ISin1uKv+Uhlh1+O1AOHuMs7zafKyVQZiEyBoEanDJttNQNJW3MLMSSEVUUyVC6EfOOdfDDR/c4lbCx1j7F2j5JzVxHB5uFD4kX/zBxc/lNIVkVvLErARdvLiCxWmNmSL5awkw7WgbbEYl+VGkdO5s0ihf++OKNru2Xk/Tq1gKmGoMrN5V53BXiAf86eOrpqJ/9NbiAh43UXv7GzB/b1woHZri9bwPgkiuZPjSASJ+WlrftyIdDD5ObYkkluudj/PxWg2REQLatcF9KhJ+xVT/3ZymIJUfcJ3hKClepxyxuG8eNeLxorMklUDFFg0UBR5uBKrZRFUDj5XKyL66ey/RAXVafTi2ns9UEcqPtxoVq45Mq0subQOfYAtcTAe8CYSS+1fOGmz2ja39ZT74eEzGMM0pIdhaAkJ0MgmW5g5OFqFVknxgBNE0Cs1jaZ6NX6ifatJVcqKf7Bz+PlR7mTs83O54isNw2zw0fdHBaObAgTwohTBbHDzgqm//PD3nTvZTXxGO0BoXP1xw6vKjdA1WxG9uuFfm0KfzcBqAt0zM6E4DMUAQl6hbjRIXT2ECe9HJfnol2SCMosaWgL8ESKayClgE5eWEobXxO15/J+UgLfhDB/nJ9UHgY1x2OWUmMq2j7Hjd2EhCDyxpfQIIHV7WnF0dKifbeLqH5O+2NdywE7UQ6YCzAUj46epDobI0Jo1kKmm0Y1OVfgU1/9WCAT5NdbKfMNrBuqLPPB8eTSew9gqhGEpaJbSN8M6hS3HxE1PgAC4DgLpirIVcOiWwcRF3w2MJHVy9sGyd1xT3DwL4v8AEtRiNE1opmuwuBiDckAUUlcYrhjWg6lvsJwmWeylAYRwqXU6GabIM+cJJ2cON6UMDOgEK76wuPqLo1VqJRCheE6rPTOWt3N9MYxuLNBhBQ2bxfE5YlIpEOhiNoQ9jjTJU+aMdNFVimxY2EuU4ncGW1zJzCcO57miDUQbYUvMViBqueyxcfwW3NKfCp1nu7BcqtiO+qxHPMsrF8EORSFGK5T1GV+Pj5PJd2Y0IBH1AusXp8Qnc/35k16giJwG7UX5BmcviUAQ94PceK2eG5Z2hNgWPsiB5DsVOXUW3F1wwqtmKvqDJZARb1arpmPzm2bC6oAqO3Q/MI0e1M+2DgwSd7G4kjtexS8UkVadziqPXdmxfrQObGwN+JeLjuTaZs0z6M7IjG5AE4xsywY5Meu+J351ibwqgK7QdG4HM2Ffp4kh+rzfG7fg7UQsxPMpd3+2C0IVsfgZ8de3rZk0QGVYiie9jnRi87lmV20ZyWLTpUk6ihYjz8jgeUSJuRxzHKW80tpSGPPl/qWaiDzGER3bAGC1jC7uxFV/vGtZJaApIzPuIj8l6E7YXTCUxqoLubYHGwbK5RTQgmih9ueB+SfJjCw3Iq9Og5cAzKjWQR1Swmn0Hj7AIUGqBEuJjsHJ/8JdnqC0JN+GyJ8mT4EguFjmputA9CC7+r5aZdESTgcNfvrQLFj6z94VnyKW3n9G7XT4om6tHLycEMKpn9Wa1aLSrt5DwhYAU6mI0EKaieiV1MxNWTBNrXuzTI9RhHjJaUbO8K/dYt+IcVimGS4fTSs46IrSd4s9OK2mtUKm2i/1tTZaAhNgXbHHBYzBQusXqUFhTY9J9unMwfQR0bwXgeY3Ob1CrDVkrOPPWxym6DswDNSgSjXrCQCGdHNiE1Ndnw+Cmis/EGF+j3/KRvIFuRjZY6qQKPdq3hkcYXMILPllj0AP6pEH7114MIdcDdHHGORqmemeHM0kAwZaJkYWWrNaPnfw3EdQP4oc6ONaxbjdv5BQ8JBe1J1qqhtn/pkZNC/d6f24kfexioJxlwIiV+tYQdAZ1UAPdKDGM4y5Fqk80Y199OUxJYaTmJUsFGFm23XivULIKZXAx/brDTOQemacLn+fSyph5fxmZHKZC4OnsciWwLTKgiG5nqPDqrLU8aWPLzyDWJ382zNYCmdR/CxF9iSz+mZ9dDmfRjZSSeFh45CEvNpvk5oD84C58roKHR/o1uHAut7u/bQqfDIpt3GjiSy8J5OEIuNjWpH8oYw4+mrit4TjJ3M9IbbcBBg09E82XQFeCve/+ziAtkoLqk6wFXQ8tHJ6Kn9nqQ4SHJtiRdGMMuEMEqLHUOFsRWLfkxFvlcpPNfLSZ+KAnukZvqUcBx+ugeuQ3QNb8XIAt04MADdhoru+OfQ/xhnmksCwhhPO3YZuTN5x2WH5P30RRjXbTMKkS7ebQP2Yx5MGeSP/Of4rilfJaCu8Vu2lvboh83/jnOxgIKUHQ33Bux2KDvE+jTR/soP4WxR5QtHLFab4jfs71+GTjC5icl/kxe8DgYjDvQEMk+FGFhe6IxqigGWYcBtXDnhGaUBNl9frVtLRJ3n6o+MB+SLNXMv6SR5Yh5YGzHt67/5kYY6/NuoFVOsIV+4xpAiKfQFtMuyi33gYTCoHqrubOzjox2zpifdvj4jUHHQLwxtgbhljuHusXho7XvM7eJV/1oIKJ9AX+1/R+7ihZdwg2+DNDZNcXwB8D+Ds3nbfoFOgxJImP+EA+fioIjBf2gpoDG5r0rcfAzVSOoCQbWfGaOZcXMgOvfHcp42zZdvW2lu2II2dnFHNWGGHRbxSRyROxtiESA2WCPemknA0CYQXHySdvCrvEl8gMJ4jOg6sc4XcKsioGFsy9DBlUwufyiIVyQgc5lZcubFj+uvg8tjx4qLNX5AVHsBK6mhVlPCcqY0ZG7Wh+dHvSZC0zL0vSsX9c+R6H7IG09D/JTcKPBX7J6gNzr0A50e66WUZBjeS8rpviDiF/3zKwGaJoPPnflp6UfQKWjptgC7F4MsxuewUF9WpemaEJUTW46s6JjOAnztNcXAVHRD733ttrjQY8Fh0dVAblhtrZ9iweVcRCDzc5ueKrnR1DRytdhCw55BegC3gkqkB1zB47+cnoRTfMWvjpawYFfKqi2IEOiZ1PNpdYcV8n2BYA7tTJnWIkSnoSlrO7fH+I3/yWwwphFdpnOZ+09pzh7vw8NBgAq1C8nXXOdx4P1zPpoeMYj5Zz/W48jGYbuPkqkDzUUAzn3qRgVcNifIfWHxfwZBnsQQmnOqLa4DZBImYwgWCF04vfI8GPQjeztihjzSpcqO4znUZ8fdDM5iJ/xqf38nF5dUJs/RkufUqzZQwDGl7ApZUPda++EMfFpQld2mubT8Hp2brcXBM4TLNGHHTtesrsFCJA0EH8Xx2qJUxcbbZT3Gff9uu+nvfmMhvrfn4a7UAaZdAWmJy/N/TTJo8HKag0C5ZMDPnRt32ndz+EyJFP611T/pw6QUGxAtSkodQ0/aIYV/JGGuXUWAzUbwaF0tgSV4JrRbQ/IaT6M6GmQCbIj+lq91LZcKmKBTWFnj7BjkM3Z/G0S9PsWHbjLftpBIrCqKcRC9bnd3suWczSGtM5F5YefmVZHm02FgO6EGT4t/XIwxqJsrBI6g65NsyBTnmu85/IoD87QTjzP8U9WNwp+tGFdceFfq4NAwOq42A9g0zm5ZUXrfDhcwKQq4B7vV9/pbTnVRfe8t57aGs9t+qNOG7bP3H5tUIvVYJAec7CPLKhPnTM2R25/5DFc9AzfG/UWreau9TXWDAW0opcquBRGVYhTzwpyT+Xb3ki/2Wj9qhuqfCbqnf3MB0B/5hcxBSWx8CYxXksmLmxfvZfGLZsLSZjIKU+mIW87o4FpZoYOGkXLObWrnoc28mVRXcO1Mfevevkp9392/C840DMZtufFPqf0w38Lf26QNsh4xnNsOBpNf2sDpx/ynvmxelX/9s8x3k+tODnCGunl3Pqad/rjYvWpYNXfxJ/qARor0Q52FFWk1HjemKA3PW7g3Q6eJ8VwIgTEBMXSVlieQcPc7F3KPqfXD4Dm6bqHXeuf4WpXAhTYtP+egQRzepk59wJPfNJzUKoGQ2oTRQbY1NGmWndcCU16bOVa1Tm++1PYN1uIM3f/TFNmdPGIgp452wOtTAHl574eoxjNvYjhCrG9CdCHmYZb9NwJHSDy3O+DMX4Ax5+d84vkzQfm+bQg1bIy4reLmWum13wmcS3K7DlIzxw6WnwA5XmWKJaKn35k6iJjgrVRfVXOmKr7sDGEizmj5+zRI1L91w/xzNL7AGhDeYqYwXpUA/iNCrqttDsQ93HLf/hn/Xpbf/JYobDyYaTMHSOg84T78zyhxQ1T/RSofmq5tml6Vm1v0+cR4kP7oGmo/LEO1Tx2uvQUYpgN91TEXX43zNvhNv3V6vHT/m1F2kCg6oEgf2Ddp2yeEga/OuHPBx75Cn7oC21tN7uPHdz02X2TgqjPmQCjG7IZZfVQ9Jn9Jzrzt4ugYRe1HHSqrSlEfw7VD6HhubAHz545IR5khOPO2HCQ4ZzzIVvz0GUD/QC8s9bmCwxi/oyN5ypobKDLnHxQJ/507ewEi5nR+3lrrmHZj8aheod/+Pb8LcGeW6BmPFUPHWce0FPUVZz+mo3a0fyJo2vXoV9Ezscp5N9ib/6g0c/fa3q65y4k//XbfJ5rqxnaBqqmoGPMkR1kNWrSfEj5mWJNf9VcPz0HXzsUdLoCAYVk1CzpqWieQw32D4b641vyQMUdk8WHgva5KSxKYbRsjkS0NnCsemYLcGqrIcW0k1W/+R7MDjWVpR8SXkMtjzxhqgdOUxFz6z/dC/43mc0vjBuPfc9T7vYYB26vd1J0PeayPyX/gFhz4qDkD27PTzM4e2dKuI/IC/yRfM4KGxrCA7bzA7n8rMzxNLV52je0n824j/MxQa8ueaKvlaHC8qc6KdpwRFcF/FmQzyVp/ouL2JxjPO61mA75sflpAnz5cV3sFDp3GBcP2oarE9f8wdkcnRwwyF+zTj9MuB6fIeboGqk3ssOlDGG6A34MPzpxfPyVqZ9X/XsSWHzs9+Zy+FnT5q8FMru2pwJ2d/JP4l8/ZukDHY6TH7ude7Len3xi/2jRf87/PoueXv436ab9oH+v14fB9uMYQfdUf3CNmuOX4Z6G8HzciMSangdqB4BhUqsa48RTtfW8dQaWDQ33Gv/ZXr8+qD/XZZ+Zbly1+Zj6EGBHLDeAOtor+eHwZIO4flCymimEPZOzpxMlf8HQLfKQnwruEVn0Ore7rzZq4PCfxdye6XhQmr60R4H5jAx/z96bnTczuxGNcs7mORv9EU0+rp6fBdGfc2oj/tTRz5TiX0vuuQDRvg1Dzvl9ZD13WD/w+dzRsdJ6XtVDYZM4hoF9q7QCYJRmHhdzJzXT74BmC6PJrn7kUp9KxgR/w2k/QNAn3UgaUuewucOeIX4b9tRsZYkPA+CnNuVHvzEwags6n53sQWSHA0iJ6ebjt7FETZ2gAV4eCtdvEcijtH16vblM/3Uj/6g3P9nt0y58+r/PSY6yPr3A03LO5UGx5uDRL2HXv+5QYtbW0wHN+o6HvPowwf792H+Kws8j0wwAHyibFUaLPJ9nOANdTpKE5yeyhy2HmZuQv6Q0/j+exu+7//OsHs71jMPjOVsldwaMnklAV7xTBLLJnvE4d4xxywMACG5C4SR8PurU6fyfo9bq2T/xjFt+tVB4Bj2/ppz9bVpA8pzKH/iBP8Sw5xE/cx7F0x/MCrcYfoqMmeH9tH6/wQj8n7UTPt9pSEn8lFqoT8nKX3/25xvMhfRQhmc9x7PMjx4vTPBha/0Cw93+6n66F45sphv+5wvWT0n+g//x//hhfumtqUf81CVPB2sPr378UDpPNZ7O82OdOGNEN+9wfiJZbaz+sXZWtxQg46cO+4w99VwO4MdenQ8Dvu+Hms9eRjVu+0MS/BhF/GCvQx0cu85Pjenm9vfwrl032hWg65vPVT8b6UPs9//x+jEkz+c7TzuoT11avwVsP0UAPxwBEJDA8/RE/NSbM8esR9jZy7bfUEfQ9dUw2/+p7KYEET8P4/cQDf+6tH6urqHv8KMkmOcypXLYUI/EKX/aQCIQ1fVi+4GThZ9BHRDyhz3Y/0MQUTHZvPgp5UaX9nPOfu6vn7XaS6v473XMX0e0f3VeGhrGvBJOlTvvSnwIW93H19wAT+Px+778Fxfx933jD4r2tKy/PpdVM7r8NRH4Vfb4g3EMaPyZ281Z0oDgZFjOadMlCR6FKsKP6YMg6gyA+Yy4PozPz7mvf58AD1/9g7aN/d3zqTTIRHRMzoAin1SdJ4XkuQVagt75U5od3zz1pGZAkVPAyz80bE1U88f44zlafu4DD2iJIuTp1p668QNn8vdPNXjBTLqfS3bqAn84xZ/X8YPi/GALs1z8UyH8G3fwDxPhXxV/64JZ+BDNP33AM37/kPU4PVM7kT19069LaXgl8kP00i/kegKUCmT5AQD7Gz88m/8fbTR+l7U/J9QzLBzy66dNN5+S1n7unPgsAzXJgYBR40aHsUDz5xx66CDPIAJPXza9wacq4s+j+jCWxny1rQHKNn0At+Tt14Fm4/cPM3eQ//Vu+TMz+DxcfrY5Hzpk15d8msDpGn6pPfmrMPRnyq6PcOBxk6yPZyB/lZEzi0cn+vBHPjw2A0+j4WlH/atCecw//TTS/6Khw0aJv+Bs/bt8+dcyaPrFg756RDDzyp8epE+lNFsF3l6e/NGczzU4oDI6ruW3fEdzzDehwZ+szx8IeGr/ZyX9PkyH+e3ntdowanys5v2UWfAnUuoHEvzsVvPh6Hygiucif07KXzc1H7LJT//2lGJP8fnzp35OEfzMEepX8z0REx8o7tcM6l93DAGcf+GLfCZ1z2fHryKBzcps/rw/U+wHJP83bkH8//vnh2nbqeIPJ9K/hmEP9+G5RloJQwYe6Wx3f0XPkJSzLfTpMvm5ZfDpF/VhK3+mlP+7tR5MsD5cCk9UUPWmfAxan/Xnf//lX4X3Qx+xnwiIGemift8EU3Ri+m78AOrP6//F9nzmnP/bs3yQ1afX9PNWf6FWQFuw/ay7Z/+z4zm7TJ4b1v7FRnmGjqNNGHj7KRr/VUD9qp9+w9L/5lUTPyS2uTd/SZn80X3jEZEoscdKDaiBZJ5MjB/aTlNsCs94vVdzsfdv02zdHjR44qBb1d8C3ehxDqvcxyV2327eiKJvmPCu5GGxWlv21Jof8jhHgvE5PmBsmCM8me1cH82vYVfH2qjcVhCFx5nm+dkK6LzzYS54mr++Ok73MPUQlgvt7IOnQsOeblo2W3jdpJMJF1DBbgHnI1F7/D1qxiC32gfZkwoym48Pc+J8CGxjpsyf+g9wP8sPHFBtekPwAe03Hgt7z89+EHPYlgYFmB5/PvtBtZak6bbV6J5G/R/zqPqIaT68ZyY/19Dce/Vwlh6mjzlWRSZwQFajHxNSNfilH3CoZsPGw+t/en/+9Ixj6zoS9BbPBB7WR4us/VRU89bqR4fy+4V7kHsY7TDkoQMNI8scv8MyP40DplEe1+6OCsDHU/Zzj8WDVz6fQMZDfBBtoTlZspttWk2A/vUdPgesf5kU+PP6Z17sRlfRm7If/ABPh00Pj2mVApM/S9jnOa8GtKimZ6sL9vd8bMOPxP9D9AugnPcz8+d2u9d9wMROrrZxWHOpnaMo/PFpC58ZQCQ34Jv3CJn5kZtHT2Tsx/T7p+U0cUwQN+Xq7/czk4XN2ff16M6MkR1Us5VaxWXBHWPEeuZN/5uA/cEr3IRuuz0Hho4J3o9koStWPrGAoR54ng4SmBR6d/ryD+uY7bp1foHTBHkeXAG/T8V/twNPJ3dP1wDuZ5PysAMBDog9a6rVHUO1eOCpIQwU8EaZOL9U0T+Ps6UoDVGMDHE8NGk/zdpTaNOwi2BVzc9Zhqsamh+aMG+7DoHtwnn4gAaM0/wUmOdfP3qTHzjXvmmUgGJPsc54uT6ART0TBz9yhELVLM1HsYwCcX5Q6jluezEdGGdWz6c46DKWNQT3GrTuDHrnf38GHBQ/2J79CepQz95mXLk7c/PDmnqWXP9c/gECfpUFBeD5Gf389+f3fmsJapi49bgiJKr/aB+s3mhVzMHpEcbzWJpKMHd/dbqbD+ntI36MS0mwcNMtQ+ABALeZEVETDXXc1drBxo3Em1CctlApHhg3p6z0hnCw2w+t/YWfbsp+VJrNFxzECK7nBwSnxnnKuPB8og/sZxcDZ25SPogQ3dMOg9vHsjGVnj2HfM8nC0b1lel4qg8Xtx8q4u5lz3FhmdGXUc2v8CZxaLa3qPScNrsvMxs3no5Jn5OgftWC/dJPy6bmCiBvuOdLOOyAnxm6cwM+JM5Enoym7LT9cA+pAQs30uAep036jE7tWWv9sITyAZxDxk8cFPorGYflhbJA3+hsErALzm/sTgXH9vZyAU3XsXi32lesNmMskNUBLbP07zb2mOCXAltjOxXaaV0JzPOLSHfGZ1itmXf/pN2JPJNOjdzzECPG7fOnv2pNTyEc1jC2N3p510HyCaNDn0YFo4Zmqh9XXCSM7dDt3ya0dktCuurbNjCYUC+77Jc7gECHcwhnytuD074gXa256LnO2kpsXGWnAF9k7cSeXKvFgnAPj6LaR4vlJu2codgWjoPwblcMbhDvj/qPIHf9g2zWKQJg762H9Nsx8PTNtnY/ADaPs8I3CoG34bcvH46n5ucwOxbI/lNuG0YECsWqjk7axpxXwuarubej3yPKYngTPBC308EyBBdQRGG5GN6sFqVhox0OT3tZY5swD8YkF51TdNPelAvZhl+oOrqeaGSAxmmrJ3t6rj0eSBcOUek3hQMwAco3Ox8Y7vp9T8ruQX+yjY0cz4SmV+959TF1zZvV5n4w5YPEHwQPmgy2Ebh5t3yodeObQDlpnKmMzwdILBYCmzE3cB82cPG4SNYUNP1DUqjN3Vo4lovwHnFk1/0HpbLwN9483ioUoPI/ThR2bW+5f8yHGcCHMdS3XrtN7YkelguF01SHT7F6cIPoM8OwNwLbMQe0XdjN1nbv08TGaQ0+3hNjX1N+HpfowLHVrhvVisApLY0uk0fU4cPy3cbxHj9TnNpYAE+lNoiNA+HbYvF4qzMAHrPE0+dDJ9qg3woQeIP4RgJ4TwF+EHNR3/0GmFOe36h2CuX2bfGG8A3Ss3EPisr+Y5Ol8e7eecxYwA2yZkmovqWnxqZ8pp1+js+ycHPh/lwoG7RwmvrTO6iZN/xT5kL57ZsgsXG8fKtK+IfgqR9q7xljgWbZbQQ27Ig2Ueg9+d3eQwa3247lPSQqcffj637C71Hq3iDWlHmmUbghsEoHBwUjx9vgRuGoyuwwmW9vdtmYRD3TmUJxoabaMN3XAk8R4gb91vI3aOBuWxe1icumaJ46ZPu2MPAHfR6cOeoL1eE5szjOXCeBG8RB4I2CJzdoT71zmLNMqMLGGy9sBrrQht944aQ3a+62b6xZDAeF4B+s8XXrPaqeD/RqZHizHcbKxNP67SmrjnNo081xuE1L9wDJXfxtng468vP//+EXN7/Rn4k2s0nfKtxYOJDfKCS/DSR2ZReXFje2iQ792igL99yTgQPgYDUgM/Dz8W7sAAU78O7zi5c3jhffEN9u/13zOfjh4I2+Am9c/NOkZJTNN3JOhN5dUbut5FoNzDfCpcILt03iQK3+cY/7t8UDe3Hb6OY2BjjsGn8jBmQ7c9YlDp6qLPA9AFjCOAbuDqXDBfqm8LbRHsxNv/gfIPmN00cJExvnMcXmwsbGzb5Z9PTRpO1i66i/G+CD5st2Rd6B3Td+2qD0RjBQ/rurUAvFgz+4x7Xz4jcPFv6uf/xfuClvdO+6QcHB43cnc42m7uAm8YYhF3u5lTFGii1R2a2GmXp8A0jvLk4pv/uO7XqnyG+QxI13Z+x+YKfTQxULxEb01wdI3waJ415i8j3R1O8Kap7XM3B4wDbjv2kD5dX2Vtikj4OGmFW8cffxTnrDbOcDzo7HvP4udfenz+B0GIHCNwotBf0bAv3fs63b+4W+Rx0b3om/AQh/I/F+yisCG6vnfnULuPDfFonCqu5lbTOwsbCn2Tm+5iwJb74R/nGx2y4k3nwDhA+Oyq6Tf8YxELUB/4fvsoQbf3yaR3kymjS18A1y+Y1o5GTyxMBgmbN02xuzerwkmTgdmXoqAOGNhzdfrmmqxrphCtS7WXns2NqEvCEVlomNboIHtu2v3nr7HuphP5icN9PfVOdQdHCzjWhUoenf3LiHSi+0nmT7hfdHI1czydDnxd5YcP9M4Hh/9Qas/unQCXA1Pct/jyVGoqaSAG5EN8c2gyfxzxQT7ycOrXt3fCN9UyKMv/GMo/80ouTj9sfoFOrbCeP70WTgePH0wWszdYrqUm0jYNwT0/Ht745w54J4CKe3v1FjBWXIZ36EPfrF72dC4cC3AzdfUxPX3JBTH7sf0J9GjFDugumJydxYExzYJ0rfqppytrG4bwfLG4lvuB08hlm6Z1rxhwF3hNtn6idsBXf7GU5lDgRuX3o72C6edvGyOnpqbN2KbzzYBR9Ix8TBmjPgjcDB9/yuxoGsi9HARqDveCFg3p0xBKLYSUJ6MuZG/Fvp/wdt9HhQnyp7YWPhzXIbigHhrmcTbwJJGj58EAHxD4xEmxq8sXCPBw/4RuGCjQrdjQhyI7F9+I0b3RItEDfI0OE/DKavvnXdDz5dU5C1sHJwZ1r2PzCujk5GIfE3y9djz+ZifXRjZbJfCUh/PyMn7vFLDfZS6gc4aNp0Mo9zwh7iZqt1Cv01Ewec+MaFzjr6nr35MT7z2z86CIH+nlyG7UcW2X6n3dSNwwnhwn8PJ3Nm6tgQ1rSFml8r5HztXs7PSI4G/0zmSGuRNh+1wkn+D36QzWz5Ev4B8T+PcgFtVhg6uHBTFm+cVs/y/flYPVNrTf924MZBNh23/nQjhPCfaWEa3ftTk4CNROAbSeHGf/mN5P+0BU673VgAtxdZp32uSZdvX2NZ9z8jPQ0c0IlvmOXjpa6kq5I3rKwby4mb/pwwgvG3gw0NJTp6hewUWMw04bGu7BZqfoVvXAMSafY58TeIa5pQIHozjDvSaRPHyRyeI97zGVm+YMjfuPAekKcBnsT3nCeC8I3Hm3XPtu3nS9ScFWNDN29hj6i/acfCAdDZ70q8cc+x9mCL1UjYHOd9Bx8sGH+P+/9B0DS+P9xOM2Zdm/BWDvJMFHs6h6IPv/HF27uLq4Z+XG5b6mvoUrsddHSoEm8voUdF+IbZdXgrXA++cSC4UruFBn2zDTUS/J7BULT07/xh4Q3ibpbT0PGOY7zNzrycjejQ97Yq7M9JEdh4Zo1duv0zY+pEQdXLw/wHx8XsBrGrcn64js+THVy0q/xwofiCuxaZHuZn1PPGqxkJFIz3AD+PuP/RwBe+cbfxVzd7k4jw2KT6M0P95hsEE/93I8hTp9bcN4dy4LCrdTnwP+Pa2S5Tpl2IH/eEzsN9WF8WT4HVqQtm+CQ36e2bB1u2cXgm0kRELf7daARyQKc27jfa2/ogED6KWfM1HDyYPKNE6IO25eyHdtC4x76qf+0nq+CwH1Ky6gmq6JaZU6r1aKMANMQs9lyvoxtQ1gEQ3h8WsVAMbCQawNZck3JDV49b3/O9Y6Blz5JI/D1Q9Bo0gkNoOTj4bxCF15wDn7H27P6ebTZ68ecXj+5Qs4UDD/+FTYujUIn/BwU8d2Bj8+CHjfQzEwycMTgZvta4iLUqcnvNdaGWsT/mL12cnZ4QNBfg6LSTJfbE1VQn9w3LL3w6tMbGKjYtjXCybANyKfxYU/Tdm9PdNqum/0ZP2dvpbkMVMyiaw9CpGo/Swh6yl8ag7htrOJNNymg7giRM3g7ZB8AasVvDVU10WqxHlIjo8wnBexxeKZQeijnKF8+IF++5QH55wHxCVPrWbynO/3yeOgcG1ocScmN9SJT9q5rZw+nmkYlC4kayTwgm/geYW8NNCiU/xxzQ1tA3l43jYLXBadeTJMo1iuLvblBoGMHd9iQOvCdoNrDBiXnqynrj/sVPIwMbl40QTsxR1X184sYCXA7BNy4DG+lD+aa88N8gkjXsYY6o1TiN/g84MhoUvlDeDkANt6rIZ46zJ/3wHz6a45Y4lynZdIUK6bLxjQM0CehjD8KizpSQ4WYi3Fg4Ld6uvg2DdEH84zcv9B3d85X5Xj9eenOcd0vLKe/63/3pGfYUgN/A/Ls+14NnY5855+tJcIB9+gT4/Uf7i+9BmT/y+LHr1NyGk7rdne+IPHtQKp/Ot/+Yc3Q70zPDjka6IcD711AzZi/e1YBUzkmzcPxQTzl5Gax/Gm6qUkyHO55L/hC+HxMbPgdkYLefj41/bJlV/ChD2bc4D1LN1OqryYNEouieJD5wbKOffS02Zzw/00qWOx2dOYPpv8de64Bt6+jG9wOFNTv4eZ6FnGlmzQsv/EuK9Ck5PT1RfXhBNfOQLvSe5dLfI/oibchuuEqV+L9nTBN+/Gc+0m7sDrlse/yxAn9st8bO+VOrNq+wyV/JbXFThflTk0XbeKNr2Nz7UX2M2Yw9xtADhMrFfFzEWI8LKYtToA9UTcj3Y6OC8OmJxk/MJ8NbuDv1NjrrpZeIf0wm6QKruFjnqcAaXXgMwzoJaXzJ3IVbh9EckwfRI5Ff8gHOCxjWJ2hPemnXU43N6zPEwpDua455DfDjDzrwgM3xecUPOygeTHegue5B/PkTPZ4PNx2sB/31/x0A4M25FYMzguMAAAAASUVORK5CYII="},{"uuid":"a44aaf69-213b-4f68-96fc-304a19e9cdae","url":"data:image/jpeg;base64,iVBORw0KGgoAAAANSUhEUgAAAgAAAAIACAYAAAD0eNT6AAAACXBIWXMAAAsTAAALEwEAmpwYAAAKT2lDQ1BQaG90b3Nob3AgSUNDIHByb2ZpbGUAAHjanVNnVFPpFj333vRCS4iAlEtvUhUIIFJCi4AUkSYqIQkQSoghodkVUcERRUUEG8igiAOOjoCMFVEsDIoK2AfkIaKOg6OIisr74Xuja9a89+bN/rXXPues852zzwfACAyWSDNRNYAMqUIeEeCDx8TG4eQuQIEKJHAAEAizZCFz/SMBAPh+PDwrIsAHvgABeNMLCADATZvAMByH/w/qQplcAYCEAcB0kThLCIAUAEB6jkKmAEBGAYCdmCZTAKAEAGDLY2LjAFAtAGAnf+bTAICd+Jl7AQBblCEVAaCRACATZYhEAGg7AKzPVopFAFgwABRmS8Q5ANgtADBJV2ZIALC3AMDOEAuyAAgMADBRiIUpAAR7AGDIIyN4AISZABRG8lc88SuuEOcqAAB4mbI8uSQ5RYFbCC1xB1dXLh4ozkkXKxQ2YQJhmkAuwnmZGTKBNA/g88wAAKCRFRHgg/P9eM4Ors7ONo62Dl8t6r8G/yJiYuP+5c+rcEAAAOF0ftH+LC+zGoA7BoBt/qIl7gRoXgugdfeLZrIPQLUAoOnaV/Nw+H48PEWhkLnZ2eXk5NhKxEJbYcpXff5nwl/AV/1s+X48/Pf14L7iJIEyXYFHBPjgwsz0TKUcz5IJhGLc5o9H/LcL//wd0yLESWK5WCoU41EScY5EmozzMqUiiUKSKcUl0v9k4t8s+wM+3zUAsGo+AXuRLahdYwP2SycQWHTA4vcAAPK7b8HUKAgDgGiD4c93/+8//UegJQCAZkmScQAAXkQkLlTKsz/HCAAARKCBKrBBG/TBGCzABhzBBdzBC/xgNoRCJMTCQhBCCmSAHHJgKayCQiiGzbAdKmAv1EAdNMBRaIaTcA4uwlW4Dj1wD/phCJ7BKLyBCQRByAgTYSHaiAFiilgjjggXmYX4IcFIBBKLJCDJiBRRIkuRNUgxUopUIFVIHfI9cgI5h1xGupE7yAAygvyGvEcxlIGyUT3UDLVDuag3GoRGogvQZHQxmo8WoJvQcrQaPYw2oefQq2gP2o8+Q8cwwOgYBzPEbDAuxsNCsTgsCZNjy7EirAyrxhqwVqwDu4n1Y8+xdwQSgUXACTYEd0IgYR5BSFhMWE7YSKggHCQ0EdoJNwkDhFHCJyKTqEu0JroR+cQYYjIxh1hILCPWEo8TLxB7iEPENyQSiUMyJ7mQAkmxpFTSEtJG0m5SI+ksqZs0SBojk8naZGuyBzmULCAryIXkneTD5DPkG+Qh8lsKnWJAcaT4U+IoUspqShnlEOU05QZlmDJBVaOaUt2ooVQRNY9aQq2htlKvUYeoEzR1mjnNgxZJS6WtopXTGmgXaPdpr+h0uhHdlR5Ol9BX0svpR+iX6AP0dwwNhhWDx4hnKBmbGAcYZxl3GK+YTKYZ04sZx1QwNzHrmOeZD5lvVVgqtip8FZHKCpVKlSaVGyovVKmqpqreqgtV81XLVI+pXlN9rkZVM1PjqQnUlqtVqp1Q61MbU2epO6iHqmeob1Q/pH5Z/YkGWcNMw09DpFGgsV/jvMYgC2MZs3gsIWsNq4Z1gTXEJrHN2Xx2KruY/R27iz2qqaE5QzNKM1ezUvOUZj8H45hx+Jx0TgnnKKeX836K3hTvKeIpG6Y0TLkxZVxrqpaXllirSKtRq0frvTau7aedpr1Fu1n7gQ5Bx0onXCdHZ4/OBZ3nU9lT3acKpxZNPTr1ri6qa6UbobtEd79up+6Ynr5egJ5Mb6feeb3n+hx9L/1U/W36p/VHDFgGswwkBtsMzhg8xTVxbzwdL8fb8VFDXcNAQ6VhlWGX4YSRudE8o9VGjUYPjGnGXOMk423GbcajJgYmISZLTepN7ppSTbmmKaY7TDtMx83MzaLN1pk1mz0x1zLnm+eb15vft2BaeFostqi2uGVJsuRaplnutrxuhVo5WaVYVVpds0atna0l1rutu6cRp7lOk06rntZnw7Dxtsm2qbcZsOXYBtuutm22fWFnYhdnt8Wuw+6TvZN9un2N/T0HDYfZDqsdWh1+c7RyFDpWOt6azpzuP33F9JbpL2dYzxDP2DPjthPLKcRpnVOb00dnF2e5c4PziIuJS4LLLpc+Lpsbxt3IveRKdPVxXeF60vWdm7Obwu2o26/uNu5p7ofcn8w0nymeWTNz0MPIQ+BR5dE/C5+VMGvfrH5PQ0+BZ7XnIy9jL5FXrdewt6V3qvdh7xc+9j5yn+M+4zw33jLeWV/MN8C3yLfLT8Nvnl+F30N/I/9k/3r/0QCngCUBZwOJgUGBWwL7+Hp8Ib+OPzrbZfay2e1BjKC5QRVBj4KtguXBrSFoyOyQrSH355jOkc5pDoVQfujW0Adh5mGLw34MJ4WHhVeGP45wiFga0TGXNXfR3ENz30T6RJZE3ptnMU85ry1KNSo+qi5qPNo3ujS6P8YuZlnM1VidWElsSxw5LiquNm5svt/87fOH4p3iC+N7F5gvyF1weaHOwvSFpxapLhIsOpZATIhOOJTwQRAqqBaMJfITdyWOCnnCHcJnIi/RNtGI2ENcKh5O8kgqTXqS7JG8NXkkxTOlLOW5hCepkLxMDUzdmzqeFpp2IG0yPTq9MYOSkZBxQqohTZO2Z+pn5mZ2y6xlhbL+xW6Lty8elQfJa7OQrAVZLQq2QqboVFoo1yoHsmdlV2a/zYnKOZarnivN7cyzytuQN5zvn//tEsIS4ZK2pYZLVy0dWOa9rGo5sjxxedsK4xUFK4ZWBqw8uIq2Km3VT6vtV5eufr0mek1rgV7ByoLBtQFr6wtVCuWFfevc1+1dT1gvWd+1YfqGnRs+FYmKrhTbF5cVf9go3HjlG4dvyr+Z3JS0qavEuWTPZtJm6ebeLZ5bDpaql+aXDm4N2dq0Dd9WtO319kXbL5fNKNu7g7ZDuaO/PLi8ZafJzs07P1SkVPRU+lQ27tLdtWHX+G7R7ht7vPY07NXbW7z3/T7JvttVAVVN1WbVZftJ+7P3P66Jqun4lvttXa1ObXHtxwPSA/0HIw6217nU1R3SPVRSj9Yr60cOxx++/p3vdy0NNg1VjZzG4iNwRHnk6fcJ3/ceDTradox7rOEH0x92HWcdL2pCmvKaRptTmvtbYlu6T8w+0dbq3nr8R9sfD5w0PFl5SvNUyWna6YLTk2fyz4ydlZ19fi753GDborZ752PO32oPb++6EHTh0kX/i+c7vDvOXPK4dPKy2+UTV7hXmq86X23qdOo8/pPTT8e7nLuarrlca7nuer21e2b36RueN87d9L158Rb/1tWeOT3dvfN6b/fF9/XfFt1+cif9zsu72Xcn7q28T7xf9EDtQdlD3YfVP1v+3Njv3H9qwHeg89HcR/cGhYPP/pH1jw9DBY+Zj8uGDYbrnjg+OTniP3L96fynQ89kzyaeF/6i/suuFxYvfvjV69fO0ZjRoZfyl5O/bXyl/erA6xmv28bCxh6+yXgzMV70VvvtwXfcdx3vo98PT+R8IH8o/2j5sfVT0Kf7kxmTk/8EA5jz/GMzLdsAAAAgY0hSTQAAeiUAAICDAAD5/wAAgOkAAHUwAADqYAAAOpgAABdvkl/FRgAAs09JREFUeNrsvWeXJMexJXgtsqo1Go1uNLQGIUmCEJRPzNuZ3X+9Z2dndnZ2Ht+jeCRBEgQIrbvRjW6gdVWG74dwr/T0dB3uITLNzslTqSorKzPC77Vr18xJCAEODg4ODg6O3YqGPwIODg4ODg4mABwcHBwcHBxMADg4ODg4ODiYAHBwcHBwcHAwAeDg4ODg4OBgAsDBwcHBwcHBBICDg4ODg4ODCQAHBwcHBwcHEwAODg4ODg6O8WIv9onL5ZI/LQ6O8YK0S2P8NK+ri/675k8yXl/ICyw/9cfVpbVcb43ncFSOxWLBHwJHfQLAwcFRFdgb47Lw3LdwXNRj6vVMsDeJg0kAWgvQKyBXjy3l9aXj0mo/9YvtPiYKHBxMADg4tjp04LWB+J687Gs/97X7bRfzOfsWMkAW5UC/kJHdLy3grGf5OtAfADiUlwPjtu2iP0d/ros8qL/JwcExVQLAEhQHRxfL5dIEehtg7wM4Zvw8Lq+fkNePa/cdM57vuuivvzBAXv20EQQ4AN6U9lsN+NXlnudiPn5Xu9zR7jswnn/gIBRHxGCxWDAx4OBgBYCDYxSgJwPoFwbIK1A+LkHddTmp/TwJ4JS8nNQuOik4IcHwOICWiFr5t29KgASA7ySQHmpv+UhmJ6Lv5eO22BdCnNVumz6CYwAeVNwfwGkASyGEIht35edwxwD729rllryo23e0n67LXZ1YLJfLA0M9UMSAywkcHAlBsdsBu0yArABw7Ehmr4P9vpG5HzdA/ZR2OSOBUl3OyPtPG5cFgH0iWkqguwPgKgBBRArUdXOesAC8zbznCvM5FLNeOH6q6/r9xySZIADn5WdzTAixkNn9UhIX/XILwA3jvhsaabhlkIW7hpJwoJMCVgo4OJgAcHDkZPdmZq+ycDNbN8H9DICzAO4DcL+8fr987p7M2lsJZFeI6JYEOtN057oN2N35PuDPzYwpgghQ4H7y3D4thDgF4IL8TBupJhxKsL8uFY3rAL6X129YSIKpKtzVSIGuFLBKwMHBBICDYw3wVYa/Z2T3ekZ/WoK6upw1AP5+AOfkZV8C/W0A3xLRFZmxtth01SMT/GOyfpFJBCiCCFDEzxgSoG43xvUTQogLAB4AcFISgwMA1+TlukEQvpMkQV1uGoqBrhIcys+/ZULAwQSACQDHboG+CfjHLGCvA74C9wfQydnqckZm9YcArhHRtwC+hbudLnRxgb9JAnz39wX/VBIQUw4IkYDYSwPgASHEAwDOCSH2JJjfQFcuUZdvNZKgEwKTFNzTCQGXDDiYADAB4NjeLF+v3yvAV9K9ntWrTF4B/YPyckzW6K8Q0dcSSFxDcIQn4++b/U9ZASipAujXbWRA/TwphHgYwAXpMbgH4Bt5UaRAVw6UWqBKCYoQ6D4CVgc4mAAwAeCYcZav6vjKla8y/DNGZn8eXR36ggT6i/K+PSK6DeCSBPxDuKfgCfiH6aRk/KkKgIsc5Gb/MUQgxRAYSwBiVQDXbXV9TxKCh4QQJ+X3dhXAZUkKrsjLVUMpuKEpBKrrYAlgyeoABxMAJgAc8wD9Paxkfb1+f9YA/IsAHgLwMIALRNQA+E6C/TVLdu/6qQ+tqUEAhAfUQwSgbxZLGQQgFfBTCQAcwO/7eU6SgrNCiFYSgK8BXJLEQCcESiFQJQNlKjxkMsDBBIAJAMc0QV8Z92yAf14C/kUAj8jLCSK6B+BTadQzx9T6fiKgBPhAv3XcX4oAIOKxFPBPLQHEAD484J6qBJikwPezQddtcAHAk0KIY+jk/6/k5bK8XHUQgjtMBjiYADAB4Jgm6N9vZPgPy8ujAB4kIqCT9L+Qi7lIBH4fAfApAK0F0FMIADJJgO/3fNm+C/hjwR8ZBMC2b0GsAhBLAMyRyISu4+AxdCUDoCsTfCkVgq8NheA6kwEOJgBMADjGA/1jBuifQ+fQfxCdpP8ogMcAPA7gFBEdENEncgFvLaBvA3iXzO8De58K0DoAvrWAuIi4zwbmsdJ/LAFwPUYRpMHnA4AB9EggAC4fgI0A2EDfRQT0+84LIZ4SQuyjKwF8DuALqRBcwspceM0gA/eYDHAwAeDgKAP6+kAeZeLTQf+8BvqPSMB/EsBpIvqeiD7AyqlvA/xlBvDnmP90soDAYzFkAGL9xM0B++g1IuYxktJKJOgDbsnf91iqGTCWCCwchEB1GDwnhLhPAv2nkgx86SEDykSoDITcTcDBBICDIzHbV2N2TdBX0v6jGuifkaD/IVaT9ZYOsPcRABFQBEKgH9P6BxdRkMDuk/r79v2HTniKvN83F8BZGjCIgk36txGA2JZA286HqSUBGylQ109pZOCGJAOfY1UuuGwhA2pcMasCHEwAODg8oK+G8xzDql1P1fSVW/9xAE9I0L+fiG4S0ftysW0DlxQCYGb7sQQAWG3MEwPy+s/WAfpA2gyAWhmnT+63Zfiuxxrb4xHkYOF4PEQAQoRg4XiO73JaCPG8EOI0Ol/ApwA+k4RA9w1cx6q9UJUIeOgQBxMADo7lcqlvsKPq+mexquk/rGX5TwO4SER3JOjfwPoe8qkEwOYHcBEAHdjbENjLVjNbxu+r+cPxuE8NqA36MWTAm/UbP5vA7zU2RUC2aIZIQWPc5yMAPk9ADAFQykAD4IwkAyck8H+sqQNfoysTfIuum0D5BQ6kKrDkVYCDCQDHLoG+yrqUi/8kVg7+C1gZ+Z4A8AyApyUAfEhElxNB35XtLwOAH1vnby0Zvc/0ZxIIX1kAHvCHgwiEIrcE4HquL+u3Pdf3O43j+TZioBSDmJKAyxOg379AvC/ASQaEEBcBPCsJ4McaGVCeAaUKfI/VxkXKOMheAQ4mABxbC/yNI9tXdX1l5HtKAv8FIroq6/p3DFBPJQA+UiAct62mP7m4x4B968jUW6TX+UNu/1jwKEUAfLK/SwkIKQWNQ1mI6RSAoRI0HkKwcNx2dQWkqAHqckII8aycN/ANgI8AfCJVATVv4KpDFeDyAAcTAI6tAX7TyX8GnaFPSfyPSdB/Wmb7APCBHM6zdAB+iADYsnzzvmD2b0j5Lqe/DexNcgDY2/9sIJ463a8PIUjJ9mNA3nc95AuwqQoN7PMEfF0CjaYSNJEqgEsJaCLVgIXj9gJdW+Hzcq39RCMDX2BVIriGrqR11EHA5QEOJgAccwd+3dSnavsX0Un8T8pM/zkAD8hs/wOZCZkg7yIAInA9yfmvAb5LCYgB+1CfP+Df4hcOohCTzdfwBVBAIfCpBk0kIXCRAxsxiCUFG+UBDyHwGQIXDjJgXncRAnV9X3YRXJDZ/weSDKgSwWWsvAKmaZDLAxxMADgmD/pqQVT1fSXzX8DK0PeUBP3niGifiP4uF0Qz218mZPxLB+i3cLv6W3n8m+Y+E+CXiKvxu8DdB/qtB8Rzav19nhMj/8c+J9Yb0ATIQGiqoI8ULByE4MgsKNWmBu6uAVd74CJDEdBVgQeEEC8IIQ4kEfgAqxLB11hNHVR7ERwwEeBgAsAxZeDXjX2qhe8Cutq+MvT9AJ3Mf4eI/oZO8nQB/zKQ8fuIgKv+b2b5JuinAn7sbn4hyT+mb3+DLAj/CVxDAVh/YNW6ZwN13+/6sn4fMYjZPCiGEGyUBAx1wOYDCAG/SxFYeH6eFEK8JDsIPgbwvqYKqPLAdax8AmwY5GACwDFJ4D+hAb8u8z8L4AUAjxHRN7KF7zAS+GPlf6fpT4Kkbyc/gc12vhbx0/xitvKNyfaFA9RT+vxrAwMlPEYOskAZqkAMCQDc/gCTECzg7xRotC4DnykwtgwQIgJ70idwUaoA7wH4EOvlATVTgIkABxMAjskB/zkJ/MrU97wE/oeI6FMi+lQD8KUn088hADbQt7n6fS1+Jsi3EUDvyuZFAsjHGvxcCsJQJCB2c6BYUkAJ5IDg31/ARwxsUwV9rYIbXQMeMpBKAFzKwNFjQognhBBPoWsbfE+qAso0eBkrwyATAV6He+EyEwCOHOBvsN7Kp4x9j6Nz8j8P4CUA54joAyK6ZAF8HwFoLYQgJP27QH+JQFsfwvP8YzP+DbDXgD4F8FPn/I+1+FOiKkBI7ByQwJtCCmJKBaHZAfr1RQQZcKkCC8SZAxfY9AgshBAPoZspcB3A3yQR+FgqBMowqLcQskeACQATAI5qB5ue8ZvA/wy6+v7LRHQfEb2HbuhJmwH+IULgkvht/f1LSzYf09qXKvGHwN5l6MvJ8ENRajRw7PbAfYgCOcgBZZCCFJ+ASx0w71/AMS/AoQrEAn4UCZDXL0jD4PcA3gHwd3Q+ARsROOT2QSYATAA4agC/cvWfM4D/BQn890tj39UA4C89mX7rIAFCvz8R9EOZfwrgKwm/DQA74Df5pYB86rS/ECHIJQCpv5vSVUCe3/epCPoQIHgIQAohcHYNRJCBhYMMmNdd5QEfITgvDYPXJRF4zyAC17DagIiJABMAJgAcvQ4ufXOe01iZ+3Tgf4WIzhHRuxrw9wF/G/DroG9r8wuZ/GJBPwXwfT9TB/PktPhNTeoNzQpIJQgh8Pf+jCAEfciAaydBs0Sw8BCBXBKgiMCLQohrAP5qIQKqa+CeJAI8WZAJABMAjuiDSi1a+gAfZe57BsCLAF4B8DARvSvn87uA3gf+S7iNf60F+FtLhh8y9qUO8/EBvmtufyzYh4A+tfY/tUjxAuQQgxgfgJUoWAhByhChkGHQVAgaCxFo4J8LkEoCFkKIi0KIF9G1C/4VwLuSCCizoD5QiI2CTACYAHAEgV8Z/NQGPRck8D+tAf9TRPQhEX1pAfsQ8MeY/5aaxO8a8GPu0pcK+mb93ibpx5KAFLDv0+I3RTJAPZ8f4zMIkYKQSrAxl8DYNwBw+wJCBkHAPS/gKPuXZMBXEmhigd+8CCEeFUI8i65TQBGBjyURuILVxkNsFGQCwASAw/r9mkN8zqMb4PMUpNQP4AUiuiTH9aYAv6/fPwX4bVv4+giADfSPwNyzZa8vw08x7cVu6FMK2Ida2KnC78TuMeBSAkKEwGk21MiA+fwQGdAJQGi0sI8I+OYDpBCB52TnwHtYlQY+Qbf50FV0rYPsD2ACwASA4+h7VYvKcXRy/wPotuN9Ap2r/xUArxLRARH9WWYRS6wG+fhAP1b+byUYh3byMwFen9i3RMSmPRbQN7P8NhPwc0x+pUb71vx9GuD3U70ALsIQQwganzrgKBXYyMDCuO7zCmxc5N/xdQYsEonAnvy5L4T4oRBiH8BfJBF4H91AoUtYNwryzoNMAJgA7OiBoxaufQn896Pbne9xdHP6XwLwI+ns/w90tcRlwqWVJME56EcD/dD8fjPD14f2tBbQ12+ngH4ok0/p308B4SGIwNBRYl+BWNk/lhD4NhoKkQGbEqBn/6YS4NtmuDFUgdCgoL1Q9m+5nBJCvC6E+A7An9DNEfgAnVFQjRe+hdUWxFwWYALABGBHDhq9n1+v8z+Drs7/IwBPaAa/w4LAvzRMfS4CIBwKgKvOvwbkAdB3bdDjA3Tbc9pEsC4l+8/RBFgK+G3RBJ5LEeqAbS8BkwzYiIPLH9AgomNAB3utPFCKCOxpRsHPALyNdaOg8gfw/AAmAEwAduBgUYuHaut7AF2d/2l0df4fAniJiL4mog+xKfUfIq3mbwK/a+e+JezSvq3Ob2b7RxfNPxCT6Yey/FjATwX7Gtn+2JMAh1AFUu73EYKYuQM+ZaAxBhGFSgQ2Y6CtTdBUBFxEIMYTsGdeF0I8K/0B7wL4Mzp/wMfo/AFqkJDqFuCyABMAJgBbmPWr8b2qn/9JdHX+V9HJ/Qsi+hNW+5CHgN/M+A8TgN8n84dc/UckQIK+gFvO7wP6vt/pC/Y1Zf+h9wKoCfyp4O8D9pJkQO0gGDNR0DZW2FYeiCUCe5GKgEkEjgkhfiyEWEo14C/opgp+ilXb4G0AB6wGMAFgArBdWb/u7n8UXZ3/ZQCvAXiiaZo/ygXABPhU+d8F/KH6fmtRAQDHIB9Htl8D9EsA/thGwLFiSOCPIQQlyUCMKqCXDczsv0HYJ+AjAtFlAMt9Z9u2fQ1dWeCP6KYKfoBu10G9W4DVACYATABmnvUfQ9fTfz86d/9TWNX5f0REV4zteWMA/xBu138I+G1tfeZgH8Ai+Qckfl9dvy/o1yYBqSA/dw9AzHNLg39fMhDrF2gcqoBZEgDWxwa79hLwEQGTFOwlEAK1/fAFqQYof8An6LoFrks14B6rAUwAmADML+tXPf33yaxfuftfBfATInqQiH4nT3IF6rGyvzX7l8C/DAC/bWa/TfIPAX8o2y8N+inA3ifj37VBQH0VgVhCUIsMxKgCrvLAwqIOLAJEYGHpGkhVAva02yeFEG8IIa4A+AO6ssAHWDcJqtkBrAYwAWACMJOs/xS6TXsexmqK32voZvd/SkSfWkDepwKYdf6jer+sKbq27XUBf0x93wf0Mdl+DdCfgwegBjGgyr9bywNQigy4SgRNQBXQ2wkXDiJgMwm69hEgjQj4fAF7vuwfm90CTwohnkQ3N0C1DX6MbszwNciRwqwGMAFgAjDdrH+hZf2qp/8H6Nz9rxPRKdnTf0/L+F1gbwP+tTq/Bvy+HfxigV9gfZe/Utm+D/RzRvWWJAEpQL2NcwD6KAKlwN9GAHLIQIoq4GsdjCUCevugzR9gIwIuUqDuPy5nB9wG8Ht03QJ/x2p2gFID2BvABIAJwMSyfjW/X2X9z6Az+f0EXWvf34nokiXLj6n9b2T/mtzvqveHgH9pZPt9gT+FEKSCOXsAyqsEY3sAYtUAIH5nwlQi4Npq2EUEGg8JiFUBXGqA7g1QswPeQVcWeAfd7AClBnCnwJYQgD3+CGf95ZOW9Z9GN9DncXQ9/T8C8AYRHSeiX6Ob+HUId29/VL+/zPpdm/oI+A1/ucDfRoC+eV/KHP5cEpBKDmLBe8r7APjAVCT8jgg8RzgeE5HPN59Lic8h41gi7ScZxxoF/rdGHvcK3FuZeDVEJIzfXVj+3sI4vtX5pUiAEEIsAAipBqjHWgvo+wZt7QFo5RyQq0KI14QQD6NrGX4A3eyAz9F5A24ul0ulBvAUwZkGE4D5gr9i+CfRbdf7ELpa/8sAXkfn8H+fiL42QD5U57dm/Uad39faZ2b/5ha+Nqnf3MJ3iGy/pNRfywNQGthjX4sqvZaIeJ5IeCyWEPQBfxcZIOMxYVECWuN3zN83iUCjnSeKCDQGEWi0+9Tj6rxrpDK3kK9n2z+jdRCJPY0ECEkEfg/gYSHEf9ZIwDvovAGXIOcGLJdLNggyAeAYEPzNaX6PoXP4/wjAm0T0EBH9FnLEpwP4Qxm/LvebBj/XQB+znS9U4y8F/DVBfxfl/z5/gxJeK0QOfMCfQghEBkEIEQUb4Mfc32qqgEkEhOYR0ImAXipYwl4OaDXSsBRCKG+ATiJaB+jbFIE9+X6+IqJrQog3hRAXpcp4P1adAt9KNYANgjMM9gDML+tXkv9ZdEa/J9Ft3POaBH9XX7+rzc/8GZP127bqbQcE/lC2v0vy/7a1AYaeU7IVMPSc2MecWw0jziew0UHgMQu6NhXSFYK1YUFat4A+OdDlATCNgbo34DkhxIPoDIJ/wGpuwDdSDWCD4PCY0AuXWQGYF/jrRr9HADyLrq//TQDPNU3zB3RO3VDW72r/M939tnq/72LKjH2AP6bunwv0Y7v/h5T/a28HLBJfYyplgNBzUkoE+v/m8gM0PRUBM5PXSYAwfupqQGuUBRbY3EdjzQNgqAC6GvA+EV1q2/bnMvk4j67j6EN0ewpcQ1cSOGASwAoARznw1yV/ZfR7EcCPAfxUzvD/gwX0Y7N+E/zNKX8uud809Y0B/H2y/VJzAHKJQCpAz8VsNbdugNQ5AbGqgC37bwooAuY0QVeXwMbugkbLYKgjQFcD9Mf2pEGwBfBbdOOE30M3WvgK5MZCXBJgBYCj35erTmC1be9FdEa/VwC8AeA12d532QD8kPxvPtYKIWzjfUMb+Oi3oWUb5uY8wnIpCfxjgP4UdgGcAikYuhsg5f5YJSDlNiJUgRhjoE8RMPcTaIUQJLPwRjtfGu21hEUVIOM8XShibpgEXWqAef/RfXKeyENCiH9B50M6i2742MfoNhb6frlc3gF3CUw6mABMF/xVze4EVpK/Mvr9lIieIKLfoBvqc9An8zfAX3kAbKBv28gHjqy/dZCAIYB/6pn/kBsATaEEkAr6fYHfBdx9wT8E+CWJgG4GPCoDCCGgdQzohj6ygLkiAguDtC+EEK1mEjSNgSFC0ALYI6JL0iD4lhBCkYDT6AyCqiRwh7sEmABwpIG/GuxzSrJr1dv/EwA/py5+bcn6QzX/tZ8Oud831MfW3meT+83+/TYD+PtK/kNm/kPN/h8jk8r5mymqQG4ngA/kaysBMX4A12eZQgSgPbbE+gwBs2NA9weYXQHCUAtadDK+ep3WogbYzLzCRgbkWvRDIcT/oZGAE+hmBnwL4Jb0BXBJgAkARwD899DV+8+gq/c/ha63/w0Ab8k5/l84wN+W4R8kZP0m6Pvk/mVEnb8W8NfyANRSA2pm/btWAiiR9fchA31KAqlEoHX8DZtREMbvm2WBhXEbhhrgMgTuBwiBKgm8DeAxIcR/kSTgDDrD8ifofAE3ZKvgIa/yTAA4NoGfsNrBTw32eRbdHP+3ALzSNM3v0e3VfWgAu8/856v1H2Kzxm+T/W1yv03ibz2AHntfaRIQem5pNaAUGSgN7qmvRQVfN3Y2QEwJAAWz/hiwB9I7A/oSAdd9ZsfAkT9AcoBQWUAnAvprKzWgdQwQspUGNtQCIvqciK63bftLdLMC7pMq5oeQg4Pk9MBD9gUwAeBYgb8+1e9+AI+i28Tnx+gk/4tE9K+Q23JqmX0M6JuS/9KR9buA3+fu99X5QyDfFgb+0nX/Xd/+N/fvpCoCfUsAMcAfowSkgH1f8LcRgSZADvT7lAHwyB8gz21bWQBwewMa4/xdiI5NLAJgv2e5rV7ju6Zp/lX6Au6XSsApmdh8CeA6eHogEwCONfBX9f7zAJ5A1+L3OoBfENEtR73fJAFOImAAv2uwjy/rV3K/a0EA/HK/DfhRIfufagdACqjmAu8U9gKIeR8xakCfCYG5MwGGMAO6rpvZvYscuPwBRwqB1i1AOrA7zl1dDTgaL6zOdc84YVjAX2C9S+DXAF6UvgClBJxA1yp4FStfAJMAJgA7C/7K7Hca3WCNp7Aa7PMmEX0gd/DTwT5F8tdr/fpPAf9wHxGR9fvk/pTaf23gn7v5b6pSae6+An3NgEC/EkAfMuBTCfqqAHAQAZ8/wFUWEEKIhoiW2DQJxnYKCMMb4FIBNsBf3U9Ef0PXKvifNCXgmHzuN+hGCLM5kAnAzoL/cXliXMSq3v9zAK82TfNbyG03ETb86YqAafQrmfWbJCBG7h8K+Oe88c8czX8lFIESoN8H+FPIQG0VwEcETNA3P1+zLNA41AAY56mNDOjkQ5UETIPgPuwdA+ZFtQp+L6cHnpEqwHGJPZfRmQPvMglgArBL4K/Mfvdh1d//GjrJ/ykpn91LAH9f1p8C/rFZf6rcLzJIwBitf1OZ/DeVGQAhcE/9W7Xlf9f9Nf0AqS2BMeUA06SXWhZAphqwMH6qkgCwMgiGBnxZL03T/FoI8TMhxCl0Xqdjkkh8DWkO5A4BJgDbDvzK6X8CndNfmf1eB/BLIrpPmv0UqMfI/jbwtxn9XJP9+mb9sWSgL/BPsf9/yMx/zIx/KrMAUoG/b9bfB/xjsvzYx3PKAqlqABwkAA41wDXd03m/XNteF0L8b3INVOWARpKAO+AOASYAWwr+5mQ/Nc//DQC/IqIDuf+2Dv4+t38I/F0b+SyR5vB3Zf195P7aJCD02NBEYIjMfyomwJj3M7QSEHNfzOZA6An+riw/5vGYskCuGqCrCjAUAZ14iAgS4CoHKnPg7wG8LIT4z1iVA/ax2lqYJwcyAdha8FeT/dQWvm/JzP9LIvrMAu56r7+z7z8g+XvH+cp+/jYz6/fJ/bWAv+bmP6VAfxc2AMo1AdZQAkr4AULkoKQ3IEcFgAH6rrJAtBqAbnMg83dtWXyoJGBTDfe119KvCyL6K4AnDCVgX/4d1SHAJIAJwNaAv2rzU5P9XgXwUwA/a5rmb+gmZbkkfxfwH8Dt8g/V+zdq/oGsP8bkFwvotYF/DNf/WHP/p0IOaCQloLQKEAL0EMDHKASxRCB0n6ssEPpMySAPup/Al82bJYE9TQ1oHcqgSQT0DoHPiOhW27b/jHVjYCNJwE1uE2QCsC3gfwZdm9/T6Dbz+TmA17XJfgceAnCYAP6hmf56i8+yZ9bv6gLYhex/zu1/qX36pd5rrBIQA2AphKBUSaBkCSAE9DEqQAj0bcTA5w1YOIDc9AVAu71nlAT2EecLUPddbZrmj3JyoO4JUFMMb8rxwUwCmADMEvyPoevxV21+PwbwSwAvN03za3ST/Q7grvmb15Xkb5v2F5X1a5J/iVp/iUx/DrP/p5T91876a+0eKCKfX7Md0AXWpVSAVPDvqwLEgL7+uz5vALAqCbjImm1+gBojDK2c4PMBmK/1vewQ+KkQQi8FkLaWMglgAjAr8F9omf9DWLX5/YqIntPa/Mys3yb36/cvNfBfm/EPt8vfNdbX1cdbI+ufevY/pbr/mPP/S6gCJSYBhkA/BPAp6kBJFSBGIeijAvh+N6QGwKEG6LdNcBeec94sCUBuVayvE/uOc33fvI+I/g3Az4UQigAs5Pu4hG5WAA8MYgIwG/A/poH/8+i28f0HrcfflPxD0r9u9tNB35T+Xbv4uSR/kZDlp2T9c2j/Gzv7n9Pwn5IbCeUqAb7H+5oDx/QCpDyOHmpAA7uPgDQlAGqTL0dJoLF8Tof6c7TpgS7lQLiOA7k2/lwIsacRAPX+1G6CTAKYAEwe/O8D8LAE/9cB/CMRPST7YH2g7yoD2Fr8bLP9+0j+fbL+UnI/Z//Tyfr7qAE5PoOhhgLVVgGGaAcMEQKbGmC2EJpEAYgvCZj+AGg/hccc6Dr/9DbBXwP4qRwfrEhAg25g0PdMApgATBn8j2vg/wN0Pf7/SETniOg3kZn/RhnAAH9b1u8a8BNy+9fI+vtK/DX6/uee/U+pFTDlvZT2A0xJBehbDsj1B/hAP0cNcBoELb/vOkc3VEWNBJjntrUrwFACfgPgDYMEKCXgex4dzARgyuD/CIAXJPj/ExGdIKLfeYDf7PVfk/4N2d/s73fW+xMl/xSjX60SQN/rJbP9ks7/bcr+U1SA0PspoQLkdgTEAH9s1h/K9IH1rX1jwT+27h+rBoQMgrCQAVeXQOP53NVmYzZPQAxpV22CvwPwY0kCGo0EqDWXSQATgEmB/1kN/N+S4E9E9CcH+Nvc/janv8/stzHZzzPYJ1Xyr5H178K2v3N0/tciGqlegBhSMOQGQaXKAX22Cc4tAZjPCRkEzWx/DejletRok4TNEgBgMQcaHQKxSp/6nT8BeEUI8S8WJeA7JgFMAKaW+b+ogf+SiN4NgL8p/aus3wf+rpp/qN7fR/IvmfXPcdvfXDIwpAIwtVHANRSAHNDvQwbG3Ca4hhpggnyoJKBeo7Fk9ebna5KBtY19DDUg+riVUwNfFEL8s+VYYBLABGBU8D+Gddn/LQD/TER3iOgDeRLciwB9vd6vg79vR7/U6X6lJP8plQA4+5+vClB7LkCtPQJK7BRYuv4fowbklgR8vgDfsWEqAzZfgKssYE4NfBfAc5IEbDyPjYFMAMYE/4cl+L8pM38f+Jsuf1P2N+v9Nqe/1eyn2nfg35/bBfR9+/vHzv5jJMUa7v8ShKAUyE9RARhCBehjDCy9OdDQKkAI9H0RUxIAHL4ASynAdZ5DJwOSBPiOE1c54ANJAv7JssZxdwATgMHAXx/vq7v9/1nK/rngf2AB/EMEnP4Os19svX/srH/Ksj8rAHUVgFiw92XzKY/5DIAxwJ+S9ZcgArUMgSklAfNxwD8vYIHV1uJwZfgeEuA/kLq19QVJApYWEiB4YiATgNrgbw75UW5/0mr+KeB/4Bjw43X6W8x+ter9Y/X+l1IDaoL+1Hb9m1oXgO89pZQGxvQBhLJ8H9gDmx0APsBHJvingH5sSSDGF2AzBzYx57v0BCQfr0T0Hjpj4D9hs/x5g8cGMwGoDf6nNfB/HV2f/0ki+qMH/K1mPwv422R/oRGDHLNfbNvfHKT/qQ/9qbXz3650AeQoAKmgXwv4Y7N+IL0tMFcNoITvyjY4yOcLWFMKHC1/e4YioFQCGL8Tc67rJOCvAF4TQvyjoQSASQATgFrgv4/Vxj7PoRvvq4b8uPr8D2A3/NnA3+b0t/X320x/cIB/br1/m9z/sc9JJQhTVABqvl7pXQKHUABCAJ8K/MB2dAOEfsdWIgiaAyWg62bBQ9jNgsJDAqKOG5lwvWmQgKM1kLcSZgJQEvz3AJxCt6Xvs+g29vkHOd73Nx7wdw34UYa/mNn+Lrd/qtmvlOQ/5KY/U6v/l1AA5qoClNwPIBbo+yoAKdl+DPCnZP0liEDOJkGpoJ/iC7ApBroiYO4Z4Pou96DtIeBpEbR9n6QpAb8H8JYQ4h8MxbRFt5XwIZMAJgB9wJ808L8A4Gl0W/r+ioie1Db2cYG/dZMfzfB3CLvL3wr+Hqd/jtmvj+Q/553/hlQAxlYBcl+fKv+tPtl/rgJgA+8UgjCVHQJ9wJ2a6cNDGny+ABhAf0QQ5GZAoWPjENreAYoFWH6PQscDEf0WwC+EEL8y1lMB4JZUAqZqpGUCMAPwPwHgPICnAPwIwC+1LX29Er8t+w+A/6Er67eAf4zTv6/knyv97+rY3xqkYAwloM/f6DMgKKf+X1MBSAX+GCIQe70U+IeA3vdc0wcQIgUxJEC4cMdRDjDfN5nfndxK+BdybT001lEhlQAmAUwAkmIhwf8cgCcAvArg5wBesezqF5r0t0wE/yMpy9Pm53L6l6r3T0X630Xj35wXq5zMPxfsYwF/aAUgN+v3AT6Qtz1wbinARwp8HQJmm6Dtcz6MIAGUQCiJiP5NCPFLdCbse9oaKwDchjGZkIMJgC/71+f7Pw7gJQA/BfB60zS/hn26n3d7X4/b3wb+oR7/0k7/KZKAMRWAXDKQCt5zHACUk/HnZP6u59dqCayhANiy+xzwr1UKyDUH+joEjkiABPSlY0thHwkghxLg8gMQADRN829t2/4CwF25Rqs1uZUzAnhQEBOAJPB/FKv5/j9rmuZ38uCKafMzwf+wB/j72vxyzH616v+5hGDOCkAtFWAuSkDMe6xlCEwpB0ylJTBECmLmAQDD+AB85kD9to0ErM0KMAYGpSgBMceUun63aZrftW37U5n135XrsCoF8L4BTAC84O+a8verpmn+BuAG1iX/Qw8JMDP/5UjgP3T9v0/Wvy0z/8eW/GsRByr8fvpOCaw1H6C0AhDK9IG68wBSgd73XJcPwDcrIJUEEABykACbCqDfd6Npmnfatv0VgDtawrbUlADuDGAC4AV/1ev/OjrH/1cArnhAf4nwbP/DQuAf4/QfS/Kv3foXem4tBWBMFWBKakDKeyhtBuyT/ddUAHyAnkoEaoF/SR+AjwTo0ZcE6EqALdsn2E2BBOAqEX0lOwPuYFUOaNGNDOYZAUwA1sDfbPdTvf6/IqK7RPQJwsN9XLP9zSE/tcC/Vr1/irv+lQT+mj3/u2T+S/lfpmIGLKEAAPEtgHqmn3p9LPB3kYFQR0AfEkDQTHuGEnDgUQKOfso1+6RBAlQSdpPbA5kAmJ/DSXTtfk+ja/f7BRGdkcMmNqR9C/Af3Wcx/OmgP0fwn/PUv6mpACXBfioLGBV43ynlgFLZfyzoxygAKURg7F0CS/2sRQLUz6VGAsjjCdDB/4ggyL1Z3hBC/MIgAWrNPGDg4+xf9fqfQ9fu9wqAnxHR00T0v+B29pv9/ksASwnkS9in+i1HAv+Sc/9r7fo3lgKQSwZSADgXqOeSoYTeZ245oMRkQBeQx9wfUwqIyfJjHqu1UVCf/QFiXq80CVgaP5UaQJIEmNK/fvvAvI+I/gPAL4UQP8PKGKh3Bux0e+BOEwDp+D+GleP/JXSO/1cDg35sZOBQAvkh3Lv6LQuBf0yb3zYM/ylxO5UM9FUBxlACar7+WFMBYzP/UIbvAu0YMpBTCqgxICi2DRBIq+Xn1P9dJMA1MCiHBNhw6tCiBJigDwsxABH9u1QBbhkkYLnr7YE7SwA009996Bz/L6Bz/L/VNM1v5UGiMnyznm+b7W8z+7k29hETB/+pm/9qAP8Q0v8cZf/SewDUzPxDZKBmKWCoAUG5bYApoN934mAOCRAeIrC+ZaB/WJBNDbjbNM1v27Z9E10n121o5YBd7gzYSQJg7O73IDrH/2sAftE0zXtYTY6yzvLHZgnAZvY7hHtXv7YC+O+C+S+XCIyhAuSC9dyNSTXKAUNtEhQC/VLAH5P1x6oBQ5sBfYpAHxIQSy6XDsA3LzCu326a5j05KOiGoQTs7O6Bu6oALLBu+vshOtPfNQCXYR/l6ywBWHb204FfzBj8hyQBpYlATra/Da7/2gSCKry/El0BtbL/GgrAEK2BNTsBUkyIfUiA2nLYpwKESgD65TIRnZXlgBvoSgL3jASNCcCWZ/976Cb93Y910999cmepQ8QZ/5Ya+C8t4K8A3bbBz5zAf26T/0qRgRgwHbLWPxVloMaOglPpCpjKJkEmsPrUgFKTAUcnAXB3BbQaETh6XW1GQGMB/QMbESCiD9BtIfwzGOWAXTQF7hQBkNK/bvp7EcCbAJ6TG/wcRlx8g358rn/brn5Dgv8Uxv/WVACGUAFSAHAX5f/Y/6VkGSAE9jnZfyzo11IAgLqTAfuOBS5NAtSWwLZdBJeW19JnBJDmCjyIUAGIiH4vNw66rpGAA3R+gHaXSgE7QwCMuv9DAJ4H8BN0G/z8Du56v22a36FlxK8P/AXcW/rWBP+SUv9Uhv6Msc3vnIb+TL0EkJPxpxCCMToC+gA/MKwfIMXdn0IGbM+NJQHA5lbC+k6Dtu9deQFI6wyg2IvcM+ANjQTcUcndLvkBdoIAyEl/C6wm/alhPz8jovexMv3ZgH8Ju+M/lPkfZfcS+IcCf3b+l8n2eehPv/exi8OBTKCOAX6gfEcAkO/mL0UGepEAmak3GglwzQcA1mcE6ERAvaaNBNwmovdlKUAnAYcAbshywNZPCtwVBWCBzWE/PyWie0T0dSD794G/OdRHr/0vtazfB/6YEPjP1flfCvh58M8wRIEyX2ObygBjKQA54J/6GkBcOQBwbyXcGiOA9XkBOiFQ81ZcSsChQQr0UsDXAC4IIX4qScBNrDoDbmu/ywRgxtm/3u//CGS/PxE9Juv+S4Rlf9t9etZvtvqZ4N96wN+2q99cwX/Kdf8pyf9DgX2pv0EDvK9a7YFTKwP4gH8IBSCkDpSaCRC6z7WLoCIBrVQDEPAFmEAPxJcCGiJ6B92kwDdMErALfoCtJgBS+ld1f7XD32sAfkxE/w5Pjd8C/D7Hv63XX1iu24A1drzvEOA/lgKQSgRqqgA1QL8k2I+hEpTeBbAUKRARzx2rDJAD/HNRAHL/TmgrYfN5raEaUOAYMTsDzLkA+n2NUg6I6DeyFHAVwHdYtQe2y+Xy3jaXArZdAVBz/s8DeApdv/9Pm6Z5V5N6DmJIgAT/Fvatfc1ef33KH7BZApgy+PcZ9zulfv85T/yb84JTQvpPzfhTyUCOKjCEH6DWHgG1dwcsRQLMlsCFWkstI4MbuNsDDy1+AF0laLA5KfBvbdv+FMC3kgQcdQZgizcN2loCoM35vx/A4wBeBvAmEbXohv3YhvwsPZm/PtvfVft3DfqxAb7YAvCfS91/qqC/a9uRip7EQET+Tgk/QM3sH6izR8BQ8n9pEiCMTH+DEFgGBdnKAHpnwKFDCTCJwKG87xsielQI8aYkAUemQFkK2Mr9AraSAFjm/KuWv2eI6H86gN42yU+Z/mzb+NqMfwL+QT8uAJ8S+A+95S/X/eetFtRoC5yCH6BE9p9KBEqYAscaDNSHBPgeN1UBWzlgaRAIZQpcarsHNsZP0sBf+QH+LIT4R4kVigTcw2rToK3zA2yrAqC3/D2DruXvNbnJT6ju71IETODfqPVbHP++QT+m8SUlE9/G0b+1yUCJbH+Muv/UVYISs/9jXrOkH6BEl0CJMoANyIF6o4FrKQA5fxcGCYgaFITN9sDWpgBg0/0PS/a/4QeQmwa9BuAbdKZAc1wwE4CJZ/96y5+S/t8ioi/ll3kYCfyxpr/Ydj8f+Ke4/ucy+rcmESgJ/FMB/W0tB/SV/VMAPjXrT1UF5tAWONXRwK77YkgA4G4P9B1X+nwAkxTYygAE4BYRfSmEeAvAFUkCbgM42MZSQLNl4K9G/epb/L5ORPcT0adw1/k3tvH1TPqzmv7gb/cLZfu1RveWVgBSzIC5hsAYMhBzn+/+0GMxjwP2mQ2xYJb7u9tEDEp9fujxPeYcIyLhuPQ9J2V/jNjzcC5rh+v/sM1DCa2z+m1zKuvSGNnuM3svARwS0adEdD+A1yWGPCwx5ZjEGFYAJgj+JP8fU/p/lYh+HQB9W93fBf4bY34djv+YAzv1BJ6zAjCUCpCrBMRm4Vz7L5PRx/xPJc2BYw0KCpUF+rYIphgBx1IAQu/T/M7NQUG6L8DVGaD7AJbYNAU2minQ5gdYUwKI6Hdy18BL6NoDb6DrHFOjgreCtG9TCWCBbpe/c1hN+3uDiN7DqobjGvPrIgStg1X6TH9A2P2fyujnDv671PNfEpzntMiUbP/rQwhKkoEUIhC6b8jZALkjgof2AgDhaYGhVkEy1liTBLRY9waY3QAb4C9v3yOi9+SAoMsArkGbD4AtmRK4FQTAMu3vBwB+QkT7RHQJcQN/Yur+Kaa/WOlrLvI/Cl8vmfWPDfy73Pff5/+rORfAB9ixz5nabIC+A4KGIgEpCgQQ7xUA7KZAkzgsLZ+77gcwWwNdA4IuAXhcCPETSQLUfICtmRK4LQqAkv4fxGqjn5eI6H/Bv8GPDu76sB/vlr7YbO/z1f1D7X4pWf5UFIBUctCXCAwJ/EOA/i7W/Etl+KnZfqoqMBVTYN+hQCkgPIUyQEp7IOBvDTS7AtTPRpsPoJcDnEoAEf1RCPErAF+hMwV+D1kKkGoAE4CRs39d+leu/9fljOcDB+DbdvrT+/2D4K/NB0h1/IdAfergP9XWvyGAf8qb/AxNKmiA/4Eyfzc34x+CCNRSA0rtFDhmGSClPVCRANUVYG4W5CIB5nXTE6BfXwI4IKJ3hBCvA/ga3XyAmwDuLZfL5dy7AmZNADTp/wxWA39eI6JjRPQN3Nv6bgC/Jv37pvyZYO/L6nMBf2ryfy3w54E/81YMYt4TFf4bY5gCU4lASS9ADPCHsv6xSECKAhFSNYKmQLj3DdjwA0jC0Gg4YBsMdPSYxJKnhRCvoTMFqvkAB3MfEDR3BUAf+KNm/b9kcf0f+lQAx5x/qwIghDAn/AHxpr/U1ropDv8ZQwWYA/CXAOltKw2Uqv2nZvkpGX8u2Kdm/ilqwDZ4AVL+ru09m8eL1xSo+QFa47PTFYCj17OMCjazf7MU8AfZFfAluiFB32FVCmACMEL2r6T/swAeA/AiOuOf7vpfRqgANoe/S/63TfZLMf0B0zQA5oB/bj9zKqiXbPWb2qS/XfUC9K3957xGLR+AL/OPAf1YNSDWFxDrBahNAnIVANf/aHuuWR5oAwqAuq7AfunJ/vUSwRKrroDX0fkBrmJVCpjtgKBZDjXQev5PA3gI3Ta/Pyai09K5aTX52QiBzP4PYR8msQb6kXV/n+kvV3KfysY/MeSgjwoQSxDGGPqT81zX7+7q8J+an0vJ77DUcKDc47zP+SV6nuOlfvYpMwqPmgp4hgRpM1nMddpcuw8tHV+HPuwgoktEdBKdyfw5dFvMnwawJzGJFYCBQo37fQDAk+iMf68Q0e8DX+Sau18DdPO5vszfBuzQ7rddn6oCAJTpBMgB+9Ssf46O/yFBfhtMgCUUgpyRwdtmCERitl9DCSihANg8ALF+AL110MzsjxJgOVBIVwR0hUCVAvTWwD/JHQM/x6o18I6GIUwAKmf/atzvWXQ9/8r495XxRbjYnW+DH+tGP7K+lDrsJ3Q9VQEoTQLGAP85bfE7te19p6YY1NgEqDQhGLszYA6GwBokIPXvAHGmQCDODyAAtHI+gGvDoCU22wBdHQJ6m+AduVfAa+j8AFfQlQIO5jgbYI4lgAWAk+iMf08DeBXAw0T0Mfx9/qbxb4lN6d+8LGGX+3Pr/n0UgJJlgCHAP2XuuU8aTVUDSs33T5WeSwL0NuwVUPN/6LN/QJ/nhEoDyDymU/bHyL0+ZIKR+lhKadFXKrCVBUJzXMzR717cQLdXwMdE9JDEnqclFp2U2DSrmBUB0Ix/9wN4FN1GDT9umuZtBJz+FvAPSf+uXv4+df8pKABDgT963E5ZUMcGfgb7cf/XKREBgXwFq48vYGokoM8aI9DfD+BSaFtD9dXbwJeImxmjSMCf0XkBXpBYdD+A4xKjZhOzKQEYxr+LAJ4F8EMp83znYW/mMCBd+m8Dl6Um/7tq+il1/9wTotQJWgr8S7n+a078K/F43+fXeo1tIwV6DNUi2LcrwPf4UBMCfY551/NrlwNSXzd0n3k95Ac4ul8I0RrbBusDgnSfgCoHmKWAA6x3AuiegO+ICEKIHwL4AquuAFUKmMV5PicFoMH6xL8XAbxIRH8KsLZWA31Vz19aGKFt2p9IYJi5WX+O1J8K+qVZeao8OTT413D9s/N/HJWg72uUUAViHkeP47l0V0AptTFnrSkp/8esq16FVlvD28Cav9T2dmmx6RHbwBeJPS/Ky+MSm47PCVdnoQAYE/9U298PpfHPluFvSDxY1Xpa2Hf8c0n/tul/Jev+uQCfmhmNte1vSTIAzKPPf26GPVfQxN57n3kBVOC5cx4T3GcqYMpnnaoOmPfFKBlAeD6AShpdHQK2jYNUV4Ce9bvMgGpM8FdSBfgMXVfA9+hmA8xiQuBcmEqDzmRxHl3b30sAniKij+Bu9fM5/nVzn63nf2k4/0PZb5+6/zaN/o3JUGqA/xT6/IfO8kXCZcp/Y6jPeMg5AblqgEC+kbZWybG28bikH8D2u8JQfV2zAcw28cMYbJEY9JTEpCclRp2cC7ZO/k1qbX9qq9/nAfxIbvYT6vc/+kKNoQ8u2X8Ju/TvavkD0uv+UxjSMRT4l5BDcxfc1Ow3B1iGAMAxAXfK71P0/M5KKCc5w4JyicGcSUDKYynrj7kO29RbsxTgWvePEkTLrrCuxFKRgHfQGQKflxh1H4BjEruYAPQMc6vfV4joDBFdRlzbxtKhANiyf/PgCUn/OVl/3xOk74k4BfCfSstfX+AfAkjnHkP8PzW/w6m2BtYiATXWnFLG49TrtvZtOFQAmwIQ1R5IRJeJ6AyAV6Qa8KDErMmX2CdNAGRLxTGs2v5+AOBVIvpjIOtf29ZXa/Fw9YPq0r8ISHGu632z/tokoNSJCPTv96+Z9c8N+HfNJFjz/x2bCJRWA2rNB5hjO2DoMwity0oFWDqSPNMQaPrFfIrAocSkV7HeFnhs6m2BU1cAVNvfgwCekeB/iG4rRh8rOzS+SO+kP6xL/7aDwyU55Z58YqATEomvm5tV1AD/nGyrBvCXBivuCqj/edQaFNTnGJzKfAAkrjl91p6SfgDfdd8avbaeW0oBthkBNuw4hF9tvkVES0kCnpGYdXrqKsBkCYAx9OcxyaxeIKK3Eaj3W1hbqPYDuNtJcuWn1DJAbPtfajYce2L3UQFybw/l/u8D/GMBEhOCcVSBFCKQowbE/L2aJCCWGIiM9SWWMOTsPFqiFKDfBgJeMFjUZB/2yLbAF+TlMcxgONAkCYBj6M+rRPQF1tv+XOYM1fNvEgPnJj9Gv2iOxJSa9ZfO8rfB+V96l78xgZ9Bf1qf4dATA0vvGjjFjgBUXINS16OYddq21vvGwOulgBZhs/mBxKhXJWZNfrfAqSoADbrd/s6hG7DwAoAn5Lz/kCtzgwjAv8ufVSpCeek/5UTIJQNTN/+l3Fcq6x8L+DnqkIExiMCQakAMMRiCBMxlDcoqBSDOEOgC/kOPCvAxgCckZqnhQCemirWTe1OSKe1r2f8z6Jz/HyFii1+Nuem314x+2DT+tZYDpYT0NCfzX+pCMhb4l876GfiZCAyhBkyJBGCia1AN+d9aCrDMBrB1DdiwxFcKUF0BH6HrCHgW3eC6MwD2p6gCTJGVLCRjekDL/h8moi8js3+b9L/EpuljCb/0H5KXhpLdciW2FHafysBLgH+u5D90dhfz9xj4xyMCJb7DkqShREmgNglI9QGUXoNqlSNj12qzFGDDBnM2gIklPhXgSwAPY+UFUCrA5LwAk3IoWmr/Kvt/F37Tn+1Lc27wg3UTiCkF2aZNAcNI/7XH/5ZQAVLBf9vG/Jb4/WEZdebGJFOtW3q+Dxro90Mjcn2Pux6zjf4F0sYDxz7mejx2Q5+Uz6jmxkApGwbpY4L1x0gCc6t93uTAjYWGM41FCWg0EvCuEOJlAB8A+ArAdQB3lsvlckobBU2tRWEj+yeic3LSkq/f37bVr2vMr8kAbeDex2wytfG/IWYfWwscC/xL1vq3FvhrLCq+15woORiSCMSQAESCPSKBOgfokQisvvdQ6qfrNYH4fQJC123/H7C5Y6AgIp0Y6D/1nQJbbZ8Ac48AGwG4DOB5IcQPAHyMbp+AGxpeTSImUwLQsv8zWvb/MhG9h3DbX2jKn3kB0o1/JbP+1Cy99LjfGDBP+V/HAv+SY11nBf6LxULol137+xP5rvuWBGLvL9EGmHLeD70m9V1DbddzDIEh1TiEO2Yp4D0AL0ssuyixbVIdAVPyAKjs/5zM/n9ARPcDuAK/AUMf+uOr96+1fGjZv036d5UB+pCAIev+KSdy3+t9wH+qM/5L/n4V0J2iAjGx9zbk9z7GngF9BwL16QzoszbV2BPAW+v3rOsC6KRg2IfDbfgCDPPgYQCbrkgM+wHWOwIm4wWYBAGwZP9Py+w/VPv3mfxs/f5K+re1AuaO2RQ9T7QUMoCMkzFlYRka/EsvqCUzwUkA/5RBfybvuQQRqEkC+pwztUnAGGtTn8QqdVy5nhBu4AQ8u8V6MMhUAd6VKsDTU1QBpqIAqL5/NfXveSI6q2X/tg96bTqTJfs3pzoJuI1+scY/EXFATUH6rzn0pyb4l97Wt/aiz6A/n/9lKBJQeuOgnHMOPc/vsdamvkqq7TNuPYqA67beHWDOCTBVgI0uNOPnFYllz2M1HXAycwFGfxOWvv+nALykZf8t3HX/NuI+M/sPTfzrowbEAmwf1l3S9Ncn2x9y6E/JRXiyWf+2gP6E/7+hjo2SvoCpzAUYYm3KWUtz1+3QhEBXxh+DO2tkQGLZSxLb1HTAScwFmAILabCa+f+oZErnZPZv7cn0ZP+2QQ5m9m+bABVii6UktVwfQB+5DT0XgLmB/+yy/m0H/okSgSHUgLmQgD6JSp+1qeY8ABjrvGuN902ENTGkdWCOSwVQ169ITHseq50Cj08Bf6dAAPbQ7Z18AcCT6Fr/PoS7rhLLwjam/zna/mzSUgnjXwkSMGTrX+xitW3gP1rWv2vAP7H/v68aMDcSkHOO1xwNPBT42xI623evJgTahgOJHirAEkArMe0FiXEPSswbvQ1/VAIgd0k6JhnRI5CjE4noa4RHL8Zk/ynz/nONf31aWaYir/UlCGOC/xALOQP/dhOBmgSiNgnIIe59/QC116q+uwTGrt8hXHCNj3epAE7Mkpj2kMS4hyXmHRt7p8CxFYCFZELntez/U88H6duwwZb9C0v2LwJssI08cGrt+ofEkzAV9PssCENO/Su92c/oWT8D/2Q/n9okUiCv66VvJ0AJ4C+5DsWsfSXaA2M2Cwq1BQoHlvhUABdeKRLwqaYCnJfYt5sEYLlcNjL7PysZ0TMAHtcIQOv5MPtm/6ngPvaufznPr7nrX23wr525jQJuDPGT/6zGLAlMhQSUHlc+5i6BOW2BJVQAW4KqCMDjEuselth3TGLhzikACwAnsRr7+zwRXUKglgL/5j42xhYa+hPT9tdHTqvdAlhLPmTw56x/V9WAbSQBff+XEgOBSu4SmEsEotoCteFAPhXA1SXgxC6Jcc9LzHtAYuBoKsAoBEBr/TuDri7yFICniegDhLf6VczL9mXYsn8RyP5LZvyl5bSUk3XqU//GBP/BJX8G/ll/jqLHcTZVElA7gSmxdqX8vyUVAZcKIAIqwNLAIqf8j1UZ4AN0Q4GewgS2Ch5LAdDH/j4K4DkiugP7Fou+uczm1L9S2f8QJ08f5jzU1L/UBacm+NdelIuCFkP3VnymNcnmUCSgxPm/Td0AooAKYGKOb/+Zja3rJdY9J7HvHEYcDzwWAVBb/j4I4AlJAN5BuN1PgbpJBvpm/zlO0lIkIPfE6iPd5Wb7uYtaCfAfagHnrJ/VgKGOPVH4XEol7aLn2tGXFITUgBrg7yNquSqAiUleDJNY95zEvgclFo7SEjg4AZBtD8ehmf+IaA/AHUem7+u7NOc1619USva/bbP/Ywd85C4cKa+3M+DPEL21n/XUSECtczU3ken7+fTdurxEK2DsRkFIwCcbli0B3CGifaybAY+P0RI4hgKgzH/n0c1Gfo6I3kec4c/Gskxzhm3uf0z2n3LApLSl5JxINVWAuWz4Mwvw56x/Z9SAKZGAof0ANbL/EluXl+jcilUBXDgTMgFau9mI6O9SBXhMYuEoZsBBCYBsd9gHcB9Wc//PA7gK9z7MNvlfBLJ/UTD7LzlLW2QsCkP3/qcuLFMC/0HNfgz8O6cG1PShDEECcteCmjMBUtfL1HWtlAogPCqAwOYusz4fWysx7wGs9ge4D50ZcFBMHloB0Hf9ewSd/H8lMfO31f5dfZookP3HylCxj8cy5tjnI+OEKQm8UwL/bQUejml9F1MmAbXO7RLlzBojzEus4SLwt31zZoQDm9qQOkBEV9F1BDyCkXYJHJoAKPPfBXR9kE9pc/99H5he028d0sxRZm/M/K+d/Y+x81+OCjDU8J+ai+Do4M+S/3RJwAxKAjWO/9qTAae8llXZFdDAChum2HBHGBjVhhJaiX1PSyy8gBHMgIMRACltKPPfQxL8CcA9uPv+Y02B+pe2HDj7z5GvhjDP5Mp9fcFfDLj4DQ7+DLWsBoxIAkThczUX+GPXopy1bKhppjkqgA1rQqY/334B9zoIPJoJMPhkwCEVgD2sJv89BuBZInrPA+627N829lf/ImCwOQyU/efIZiUYcy15sIbjn8Gfg0nAcOdJrZ0CU0hFzV1MUxO0XBXAxA3AbTjXh9TFJK5LaYB/VmLiAxh4l8BBCIAx+U/1/ivz3zKCBOgfuPn4GsBLIwawadoYI/uPZbpjZ/9j1/0nC/4s+c+XBAz4vdU4fsci/FOaapqSWNVSAY7u10x+tjKBORjIZwDUL1ckFqqZAINOBhxKAVC9/8r897Q0QLQB0G9NdgX71o1tgLGJxIOidvZfY+JfLlNOyRx2DvwZSlkNmDEJKFEKmPPaljMK2EUGzJZy2wRa2+Z0bYgMWMyAxzFQS+BQBEA3/z0mCcD7gWzfnLW8MebXBH1L61/rYnMZB0TJ7L/UYtMH6FPZMgZe4Bj8OZgElD8HS64PU1jbSqgAwoERMMAchhHd5g9QLYEC7n0BTALwviQAj2FgM2B1AqBt+6t6/58kogZu85/p8g9NALTVYfpk/8g4oMZiyCVYcd/FSgy4sFVfyFny314SMND3OiQJmFopYOoqQC4xsJEBW0ugT8EWcM+3uScxUZ8JMIgZcAgFQN/45xHJdD6Evy7iav2ztf+Zg390ucaX/aPAAVOaIfcdEDSHLX+HXFg56+cY4zse8vif0lbBNYcC9VUBYh8TjqTxKLG0DAay3W4jzID6RbUEPoLODDjIBkFDEABT/r9IRJdhN0/EmP9sk/30Lyj0RcYoA6LAQddn+M/Udv0rvciJCSyoDP5MAqZEAmqfT2PvEpi7FpZQAUL/V+gxW0ugeX9rwaloM6DExIsYeCZAVQJgyP8PAniCiG4j7PY3gT7G/GdKNLEHZJ+d8Upk/zndAX2JQOoCIgouRgz+HEwCpn0elt4lMGet6+Nz6rvGuwiHD3Nck2lTzIA3ATwpicAgGwTVVgDUxj/n0O169BQRfYDNGoj1Q9EkFPgyf6P1LxX0c7Lkmsw45fl9T8TUzyblc2Hw52ASMBwJKLlBUKnEY8i1LnWtjv1srD+NlkDXvjNmGcC3z42aCfCUxMpzEjurYnRtArCHbrDBeQCPSlZzA+7eSVuG7/IHWMcAB6SdUq5/YPiNf0pl/7WkfwZ/DiYB0yMBJTL/mipA6nNz3f992sB9LYGuFsAW7jK3a6bNTXRqud4NUHUmQDUCoI3+VfL/40R0w/OhiIBEYmvxs5n/UroAROEDKZURl9j4Z4yFZuwFkMGfY9dJwFjn+BS8ACUTt5TBQGv4YpgBbYZApWTbklffTIAnoJUBauJ0TQVAd/8/hK7970OEd/hTH5pNEVhaviCR+MWWzv5LAvKUsv+xXP9VF2Nu8+MY8bgY4jyZyjbBpf/PIVWAWDxxlQBcuwf6drpVBOAjdD4AvQxQzQdQkwAo9/8D6FobXPK/yzAB2OV+6xAgz5dU0gfQR/av0Rs7ZIYxhPRfHfwZ6jhGPkaGMPJNZQvwqc48ya3/myBvkgGbCmDDNl+Z+47M+h9FVzo/DWCvVhmgCgEw5P+L6OT/63BL+6ZkEur9t03+E44vrqYPIOXgn3v2z+DPwSRgPiRgG1SAIdqfY+v/LjOgb2t6czJgqPVdLwM8jq50fh8qlgFqKQBK/r8fK/n/A8S1/5kT/toI1oXILy3ngEg94HYp+2fw52ASMD4J2BUVYIypgK5k0pb1A3Zvm7Dgm9M0SESfSALwkMTQE3MjAEr+P49O/j8D4Bb87kgT8G1bLtoG/gDuXf+Q+eWnHPyc/e/2Qs7BJGDsYBWg7vbnttut5TVtSoA5zM7V6aYnwXfQ1f4fxqoMsD8LAmAM/7kA4DEi+g7+tghT/ncxqlDv/5Dmv9rzsVHpRNuZ7J/Bn2Pix9CuqABDroFDmAFDMwFsirU5E0A4lAK9DPCYJABnUGlvgBoKgJL/z2JV/38f/p7+kPxvZv4+NWAo81+q9FWL+U4t+2fw52ASMDwJmLIKMKU1sKQZ0Jb1A3b/GpBWBvgYXRngIlZlgOLdALUIwCmspv+dkZJGaEBCrPzvGsqQC/q1gL8PA55y9s/gz8EkYD4kYK4qQCwhqGEGjCUDNv9ZnzKAfrktsVO1A55Chb0BihIATf4/g07+f5SI7sIu92/IJZHyv7DI/y6ppjTzRYTiUIIBTyH7n8qCx+DPwSSg7GvPQQUQPd5bjbXQ+zlITLIlrGvKtaUM4CIDQu6b8whWZYD90mWA0gqAav87qwgAgE8RuSMS0uX/mJp/n61+cw+4sQYA1V5gxEgLGIM/xy6QgKmch7VVgD7vqYQZMLct0PV4bBnANRTIioWyDPAYKrYDliYAavMf1f53noiuWFiOa4c/IE3+j2n5S/3CSzHfVMlLFDi5x8r+R5f+Gfw5toAE1DyPaiqiU1kLS+8JEPP+XR0CrjIAYB9uZ1PIv0U3SE/5AE6hsA+gGAGQk4r2sZr+9xARHcJf67fNRxa+DzJB/i/d+z/0AKCS7XhjZAUM/hxMAuZ7npVQREu9nzEGAiGAKbllgBD2md0Ah+h8AA9IAlB0c6CSCgBhVf9X43+/QFj214FdOEgCkC7/5zC8Pix3yua/KWT/DNIcHOOeU0P7fsZeE3PWe5HxujFlAOHAuhA+foF1H8AxibWTIwALrO/+9wgRXYZd/m899/uGA9lYWYwk04fBTsX4UorpTiZz5+yfg1WA0UlEToY/lzWxz/+FnpjjM/nZjIBWDCSiS1IBuCCxtWg7YEkCsCclivslAdgHcAB7n79L/rDtrpQq/+fIO6UP8ponMWf/DP4c208C5qgCzG1H1ByfVE4ZAA6g170BNkPgEsChxNIHJbaeRMF2wCIEQKv/q/7/h4joJtyGCHOqn5JL4Mj6bb2XJeX/Kc3/zznJx8j+Gfw5OKZNAoZSAVL/9lz3BQg934dZa6UBB+ZZLxJLH8JqHkAxH0ApBUDv/z8v3+wXiGv9012Qrov5gbfI2zin5vz/UiddKTduKvPnBZeDYzePyZJK4NzXxhTwdw0FsmX9Mc5/V2v8FxJTdR9AEewuRQDU+N/75Js8L2cZ++r85gfndUUWcv/3lYn61rZynlsjY4j93cll/wz+HDtGAmqoAGKA95P6O6XW0RJruo8M5HYDhDDO6Q+QWHpeXor6AEoSANX//yARkZH9+wYiuNr/4FEBxpL/+0pcOQd9rrt17KE9DP4cTAImei4VWi9KzkfJXUNT1+raZQDX5nSt47GlByP1oUCEdR/ANAiAUf9XBsDv4HY/+loiXK1+tg2AUlnjnCSuoVl7jeyfwZ+DScCwUVIFqKUmzqVEGgrftvRRWGao2s5OAHm5rhGAYj6AEgqAqv+fRmdSuEhEXyFu3rGrJcLXYgGHDFN7A6Dcg1oUPgFKqBezy/45OHY8pnRO11pr+m4MVGptz7ntw6mYdsDQQKCvsTICnkYhH0ApAnACqwFADwC4Br/BQTg+IKv076j/l7idyx77SFs55KGGylAr+2fpn4NVgPFKAX1UgBrZ+ZhrZc76XgRjLJglPKQAEXh5TYL/AxJrT0yFAOzJN3MWwAVZq2jhb/9rASwtu//5ZgHY7u/7hc153OVOZOMM/hx87M5SdZhLGaD0FsExGHZ0n8TAZQRethJbL0isPYEC8wBKEYCTWO0AeCMgdwS3+3UwJJfUUmvr3xqz+MeW/0tnEFXJBoM/B5OASZ3DY5QBSqw3tbZLj9l9VoSA3UESbD6AGxoBKDIQqCnw+6r+f79UAL6Ef5xvqAUiZg6AyPzCcg/Y3HGXNd3/JRYLHvHLwbHbx3LpNWKMboDSa2VOQunqCPABf0yirCsAX0oCcD8K+QBKEoBz8s1dQ0RvIyIH/2hlgpwvveQMgJIHWy7rnuoiMWkywcGxZTGX83HoMsCQI4GD90nsiiEENh+ADTOvSYw9NxUCoAYAnZFvag/h/v+Q418gzjABrLcE1gbaseZc55KXUgcyhl5sOPvnYBWg2jk3ROI0FZIxRMLVRoB8DL7FYOZSYuw5rIyAveYB9CUAygB4H7rpfweRGb7e/x/DjAB/q0Xu7SEGXIy1BfAsQZTBn4NJwCwVhz7r1xBrZs5an1sKiPUBAN2+ALHKuJAYq08E7OUD6EMA1ACgk/LNPADgG8T3N/r2Ua5Z/885SGowx5qvWfJ9zNZHwMGx5SEqndNDvccx1sxa7YCu1w5l/fBgog1Dv5FYe5/E3n2JxYMTgAarCYCqBfAS4ichAZtOyLUP0VH/j535P5f2v9TXqSX/c4bEwbG7x/jUOqdKEoEh2gGtmKT5AMzfi5mJY1MALmHVCXBKYnA2jvclAMewGgF8CsBteFyMsHcC+KSSFMCf0kGcI2Gl/F9TOMGKEgkGfw4mAZM+R2uXAfqsnVPzTsV62eDARF8X3W0Dc3sZAfsQgAWA45AtgETUwO30h5Hxhz4cl3tyaGDMrWWlHuRTY/+cFXFwbPcxP7W1p2TiNQWMiO0AAOyDglyqQCuxVrUCHkcPI2BfAqA6AO4HcGj5Z6wDgKQs0kZ8IDlf8JD1/6FbWvqy3jEzAg4OjmmDee5aMUYHVZ/JqUP5AFyEYAPYjZZBmyKgv8ahxNzenQC5BICw6gBQLYBXHPKFzwToJAGO+f+59f9cGarWwVv6RJ/rFqSc/XOwCjDPbH9Su4Uivwsg9vk5PgAblsVgoG9YXiux9pxGAPaQaQTsQwDWOgCI6DLiNzwIPSfE8krKOqV7WedW/x8NgBn8OZgEjHoOTK2EWmIN7ZvclVZPYicEAn5T4NH9EmuLdALkEgBlAFQE4AziDYCuLN71hYvEDzzmdg0wnmv9f7LqAAcHx9ackyV2Bcxdh4faFAgOVSD0P4a2DLYZAc9oBCDbCNiXAJySb2KBPANgjBIgUL/ePef6vxjh8+Dsn4NjXudCjTW0bzY9FR9ALUyJ3QoYSDQCSsy9Dz07AXIJwEIjAGeJaInIqX6aARDIMwCW2tRnajWsEifLlBYSBn8OjmHPiamc/3NbQ0tjiu01fPI+YDcC+soAS6xmARxDphEwlwDsoWs/UArALfidi656RxvBlBBJDPoythpy0tD1/ykuGBwcHNONMdaYuayhJUcBw4FxLfy+OFdH3S1NATiOzJHAfQjACXR9iGex6gBIkfed5j/P9KTYL3moXQCReJDOwfBT9T1y9s/BMdq5MZkSYI+/XXpfgNoDgXyYlrIBnkkarkjsPT00AWg0BUANAbrmePOhcYeI+CBKHKBi5BMqh0ykEpgh/mcGbw6O3cvo57qXypCfY6wRMAX/nL46IrquEQDVCpiM57kEYF9TAE4AuAv/dr+uLYDhuS9EAmqrALlqwFiMfPLtf5z9c3BM8hypsXZMYV+AXCWgD7b4wD+EeSkYegddB4DC4Kw9AfoQAPXH9+B2L7o+DBe4txjGAFjygBry4OWFjYODScA2qBJTXktrGQHN/W9icdHVTbcnMfjk0ATgmGQdZ4goZq6/bQRwTPaPgBJQ0wCYe+DVHGIxZibAAM7BMV/AndN5PKW1tJQRMKQCuEYC+zoBBFbTAAcjAAtNATiDdfk/5mIyoZTsvmTPfWkD4JROzklt/8vZPwfH7M6ZMfYQqb2W9jUC9nlPPkIQmgDoutyVGKyGASW3AuYSADUF8DSA6/DP9fe1+G3M+7cwoBoMrfaBPMRJyaDKwcExNcVhDh6Gmtm/Uw1wdAK0kVhpmwh4HasSwOAEQG0EdANhA19KX2QKixvjwGIDIGf/HBzbdu7skhFwTLzog4vm4zewKgEMQgBUC+CRAkBE32lvyLWzUSijby0fRJ8d/kp1AMydZJQ6oRnEOTjmm5VPdX2ZwppboxMgZqfANqQYwL9TICT26iWA5FbAVAKgtgE+Lv/ocQD3EK5VuGQOF7spfeCMvYvVVBcCzv45OFgFmDLxn/OuqiLif3JhYhuBq/e0ZFwNA0raFTBXAVAEYB9pLYCpLRI+5WCKHQBTOJEYdDk4OOa+tkx5Tc3pBICR1ceSApdSoV5nH+vjgKsqAHoJ4BT8PYwC4fpHLLiX/hLHml41xATASZzEnP1zcMzuXCq1rkx5TR2iEyDlveRgpvmaJ4ckAPtKASCi0CYGR2/ScPe3GQdP3z0ASp4AU/o7tXcRYyDn4Ni+jH2o3QentqbWVgNiMcw5BM/ASi++yl0BlQcgeRZA3xLAvQjwd2X6G5sHZbQADsVoOTj75+Dgc2r7iNBQCZarFdA3HTBmyN690RQAADcRN98/RvaP+RLGZpxi4gcrBwcHxzYCcc2/UXKDtVQDeMgUH8LXGxoBGEwBUHMAbiFsgBARH3Zq21/uFy5GPDDH+LtMJjg4OHiNqpfZ5zwv1djue/5trOYAVFcAFjoBIKJbgaw/JvNvE8hDzhe3Sy2AYuzXY6mSg6NOZJ5bo68JBf/mnFsBXWVwV/IchaMSg/U5AEnDgFIIAGGzBHDb8sZiXIt9ZZNY5jQF0J0SIDI4c3BwbMt6NaVWwFwy4/s7IQ8ANAVALwFEzwJIJQC6AnAcwAHSDAs+lgMPGyr5xe9yCyCTAg4OjiHWEm6v9vvdYmv8IWw9kFisKwBVCcC+/GMN/Nv5wvMP2qId+UAXMzw5J/WeWf7n4Ni5c4zX1vRoI/52zDAgdWmwagMchAAcl9dTHP21a+RzGJbBGTcHBwfHvNfWGkpBDjYq/F1gVQKoRgAarJcAUiYWxf6jKZK/GPhL4uDg4ODYPXKRgzWp94WM76GOOb0EEI3r2SUAInK96Y3rgSmAoQ9UbMEBtPVsmuV/Do5hIuNc2zXVcoqYEfOefNMAXdeFxOLqJQBdAdiHe2CBL5N3fkiOf7jWlzrVXfoYRDk4OHYtE9+2XVNFT6yLVQD0n/tDKAD6IKA2gulEjwEu9EGL3C9lS08sDg4ODl5rymFCjf1nQuOAQzjbYn0QUPU2wH3YWwBjdzXKBWs+2Pn/4ODg4LVibv9HLN6l7KKrtwLqCkA1AqCXAe4ib4c+MeEDoNRwB7FLJwjX/zk4ho2C59zc1qrSQ+Om9PnkYKeQWKzL/9UJwD66XYhKZPI5rYTMQDk4ODi2f83b1jU/Fvdi8PUeVgbAQQjAHoBlxj8Ww3DmlEUPsaGEmPHJwMHBwWRgzPVzSp9DzKZ3qVNxgc4DMKgCsMDKAxDzxn3yRekDQMzwRODg4ODgmNc6XovApO4XcE/D5cEUgMPAG07NXOc0LlLwCcLBwcExi/VwqtiSYgz0/e6hBv5VCYBOAtoMqaLElzPURkAM7hwcHBzbv6bV3BCo5Hv0lQCUAkC1FYAGQENEdyNkClHpH+ZgAsLBwcHn9Bw+91o4KABAYrEu/1chADAUgKXlTc61/s4nR2ZwCyAHB597W0qIpopnJta2WvafhOk5JQD1R9rEDzo07IAPOg4ODg4G5l1XCVI74ZZYr/9XUwBg/IFSbR6CD04ODg4Ojh1Zo0tho7DgchUFAIYC4HuDYgu/MA4ODg4OjjHISghnKYcE5JYACP5BQBwcHBwcHBz1CYMqASSTgNwSADwEIPTGmSBMi1lycHBw8Joyrc845fNeGthcRQGwkQAODg4ODg6OcSMLkxv+3Dg4ODg4OHYvcgkAy0EcHBwcHBzTiCxMbnr8oUXC75DjOked4M+Yg4OD15T5fsYpn/cilwSkEABhXBZ8oHBwcHBwcIxKwtTePMkTeVMVAPXibeBNUYF/ioODg4ODY9cAPfa55EjQqygA5h+K+Qeowgcxly+Mg4ODg4PX6JrYSBZcrkIAhKEANIn/gFnfID44OTg4ODh2fI0k2Ov/sZ+FKgG0GKAE0KIbPLCw/AM00wOKgTkzlsslf3YcHHzubSPRmCqemVjbSEw2y/PFFQDFMlohxPHAh9SXEOyqSsAsnIODg8/p7czuS/4+AYDE4uoKgDAUgKbQP0wDHshzaUfkk5WDg4MJyPDvjSb6P/pwVSkAy9oEQIH/IYA9yz9OPT5UmtEBS3xCcXBwcMxiPZwqtsS+55A/YE9isq4CVCUASwD7kSDv+2epwhdPfIJwcHBwMKmo/J6o0v8ZIggm7u5ruDwIATiEfxAQZX6AhHQH5NwP0pRWSiYSHBwc2wLiQ6yfU8v4YzAwFVcXEpMHJQDHEmUMZPxjc8+kGbA5ODgY9HdnDU3Bsr6lcfXYsaEJwAGA44iT9ceU6IcyGJLj506crNyOxMExbBQ85+a2VvVdY2lGn08MdpLE4oMhCIDK/g+w8gCYF5/UEStn0wQPPGbmHBwcnMXz/1H6vZi4GSqNm5d9iclKBaiqACgCEOP4d/1DTaEviDI+ZD7YOTg4OHitycGE0oo2eV6XEnBWEYBqCoBe/79nAfGQAuD9x4mIenwpczyAaYtPLA4ODo65r8dViIUF60J4SR6cVTh+D+s+gCoKgKr/3xNCIJKZmP9wM5MDYu7gTEP+PvsAODiGiYxzbdC1YALr6FzxowkkxNbrEovvYeUDGEQBsMkTIUYTMnJQ5H0xHyht2QHEwcHBwTH8mp6DNTEtfz4sjFEA9NuDKgB35fWcFr5aTnmawYHE5IKDg4Nj3mtraTIRMgGGiMFyCAVgrQQgWUbIqJDyTzUjf2E0w5NhUu+ZywAcHDt3jvHamh5NxN8OKQj6c1qZlA9CAFQJ4C7srYCAvVXB1x2QOv1oqA2BSrYq9nmtmtMAGbQ5ODjjLv16u7a2xpYBYrJ+H37aWgDvYr0EULUNUJUA7gA46XnDQHzvf+nhDlMaNsQzDTg4OHaVFGzL2loKY1Ln/FMAY09KLFYEoFobIAwF4LYQ4lQgq4+pbTQe1lQi+y/xfMr4YmkLTt7k1+MyAAdHncg8t7ZBMcxZW6eGHTHYl4yjQogTkgCoEkCb8oZTCUCrEQClAISkj1gnZC7DogJf6FigRTN/fQ4ODlYE5rhGDbkRUWzff04p/DSA21gvAVQlAKoEcBvAGaTV+GNr/6mySW3iMOTByqDNwcHBZGK4v1FyRH0fbIuR/M3HzkgsvotVCaC6AqAIwDGkGRbMv712vzH8YOytIhmIM4PLABwcfE5tCfEohUPmgB/9flcZPMZYf0wjAIOUAFQb4G0hxCLw5oC4aYApRkHffds2C4AGPECZCHFw7A7wDZVgDbXV+1hrf6qU73qObwqgE18lBqsSwGAeAKUAAGnTAGONgVT5i6PIL6X0STTVdhXOWDg4OPvfhfbqXByo8dmm7qLr6qobpQRwC6tdARvEjTIMuft9rshUJrarrYAMvBwcHHNfW+bUApgyA6AJ/I2YwXk65h4MSQCEoQDcg98HQMab9SkAfaT/0gfunFsBJ0MIWAXg4Jj1OTSl9WvOLYAu3PVhYhOBq8ckBt/CqgtApLzpPm2AtwHcEEKctbCSBpsGB4r4QHwMqNQezUPKPhjoYK19sjOQc3BsT7Y+lfVlCmtubgdA6nNNTPMpAfo2v+TAVggh7gNwA6s5ANUVAGC18cAd+cfPOD5EV3ZPEc+fykFT8++PaQQc7DNkFYCDYxbnTo01ZCgD4Jzwog8umo/fB+AmVmr8MvUN9yEAt+Ufvz9CsoBH5lj7xzJaAcfyAozNohlYOTg4pqY2bMME1JLY4msBBNzlcSBcUr9fJuGDEwBlPLgB4DjCtQrbP9hY/tnQB0uVDtS5dAKUOnC3OZPh4ODsf7ogu+0dACktgCYJQCKWHtcIwMFQBKDFqgRwUwhBkW9eMSDXP4sAIZhLJ0Do51yY/BQUDw4OjnHO4ymYD6ewlpboAPBh2lpSbFHAnUm0xN6bWG0G1Kb+c7kEQFcADgNA7vtQnMMQAgdEabaYwyBrn7BbBbisAnBw7Oy5Mre1tM/zU1vdY3DRRRwODQVgUAJwR2Mfx7HuUrT5ARrE7RsAhDdNyGFqoS8s5eBhIyAvbBwc23KO7JIBsHYHgAvcYzDP1wLYGBh73MDgQQmAmgVwE8B3QohzDtmi0S5A3OYGMeA/FbCjCr9XaiIgTej/5eDgmCaYl1yvaGLvbSqkKLQ3ju3+xpNMQ2LudxKDs1oAcwkA5B9T7OM7ABdg2dwHaZsE2ToBYtlX6eyfenzBUwXQSdT+WAXg4Bjt3Jhy/T91DR3aAJiLQ74OgNhNf8zXayTmfqcpAIc5H3ofAqDGAX8H4BQ2hxWE2gIbC2kI9UUObQQcciLgGD4ABmMODlYMhn79uayhpQyAPvD3KeQu+Z8k5n4nMfju0ARAzQK4ha4EsEB6J4CPATWRzK+EWWMuoEgTOJmL/A1WATg4qpwTUzn/57aGlsYU1x4Avsw/tQNgoRGArBkAfQhAqxGA7+Ufd9YrDGBPKRP4Mv9cmSbngI3tTBjqpB3CvDKHBY+Dg8G/7Dldopw6FGEJOe6H/jxcSkCqvO/DR/X4UmKvIgBtzgfYlwDclm/iBoCTsDsYmwhy4JN9KPOLoZ4Ha62BQFTwtaasRnBwcPA5mbJ2UsHX6qNa9MGWGBLgel4IM3X5/4bE3ttjEAC1K6AiANeEEBcjmI5P/qcESYUqHZglZbg572K1TZkPB8euZ/9jrRWl6/+lyyRU8bMM4ZnLFxfEUIm11zQCkLwLYAkCoGYB3JBv5nyAtcTMBtC9Ao3jA4zZNKhk9j/VXaxqv59B/kcmARwM/rNUB6bmoSqdPOVgjNXwZ2BZDAa6MFPh63mJuWonwIOhCQDQ1SBUK+A1dHsTwyFhNHCPBE6ZAVC6C6BvD2tNH8DYNazJKgccHByDnNNDeKj6vtcxBwCl/G1fZm8zADYOLIXE2mtYtQAucz/EvgTgnnwT14UQbQDUAbsRMGUHwZIMrwYwl/QBzHEBYRWAg2Mex/zU1p4x6/81MCJ2Zz8b6MNHFiTWXpfYm90B0JcAtFhNA1Rv5gTcZQCnIuD5Z13SSq4a0JcRpr72XHwAJZk9kwAOjuGOdZrIWlBqHRuj/l9rAFCO+78JYOhJA3PvItMAWIIAHGA1DOiqEOIhxz/dOLJ5c1Tw2ocpZZGYWkuqXFPCBzDWCTbkLGtWAjg4duMYn0ryVEo9KLne59T/FYS5Xtc1Jh9w752jDIBXsZoBkLUHQAkCoIyAqhPgKoCLxj/WwD8dEK5/FOHOAPT4wnIPlBpEYCrtgLmmHgZtDo5pgHapc3qo9zinvVRSlQCKUAMax302NUBdFAFQHQDZBsC+BABY7QnwPYBvhRD78E8xOrpIZ2RoBGKNUcCp0wD77guQI2lNqQzAGRIHx24f20N5p2qumTlrfclRwM7WP4mFUbgpMfZbibnZewCUIgCqE+CGfFOHABaWbL9xMJyYDYJ85sCm4IE8hpO1xAlZs5Y1CtlgEsDB4F/tnBvbOzUEORlSyW08GX8KvsVg5kJi7LdYtQAu+3x4fQmAmgioWgGvAjgXkDOi5x0DTh/AUAcyFTrgYg/iKZGM1PfAoM3Bwedj7Fo5VOv0GLsAKuiK7QQwa/+usvn9EmOvYdUB0Pb5EEsRgFvoXInfCCEeQXggUBOhCtT0AVDlgy10kE+lDDBZ0GYVgIOz/0mQiaHl/5w1dewhQK7fj9ndL5T9Hz0uhHgUwDcSa3vtAVCKAEBKEqoT4AqA+xDf/hcC/hgfAKGOCbCko7VG338JNpvynMGzDiYBHAz+kzqHh5D/qdB7Lb1WxuJObP3fthVwE0iU75MYqzoADvt+2KUIgDICXtUGAjWBf3yhyfuu9gc4WJHvQJlrO2CtMsCsQZRJAAcfu7OMsXZQHbr9L5Tx2zDs6D6JgYsIvGyEEAKrDoDeBsBSBEANBPoenTnhOoCzcLcxNEgzTLj2BShxO/fAKbGz1VBlAGR8JpNRAXgh5WDwr5b991kvS6xXtdfKEolc1kwAC2bFbpTnwsuz6Gr/ygDYawBQDQJwU765y7JWkeIDsMkdLpnFJ7WUUAJyDIE5LS6lF4QpOHK3QnXg4JhJRj3W3x9K/s8lD33X9tQEMIRToam4rjK5Wf+/PDkCsFgs1EAgZQS8jM6tGOMDIE8PpO/+vpJSSYNIjex86DJAaRVgLhkVB8c2HatU4Pyutd5MUf7PeV0TO83pftFYZtkl0JcQn5XYqgyAhxJ7R1cAgK4X8TakEVDWKkLzAGx1D8A9Icl1EKVKWzlKQO4BM0Q3ABU6oCefsTAJ4NhR8B/7fB1iYFoJNaCvepGqClDg/dom3pqPLRDXAQCsDIC3UaD+X5oA3MGqE+BbIcR5+OcBwMJ6FpFsyfUFxG4aVEoqSmW2NRyuQ7D23L/DJICDwX8Y8C+Z/Y+xLpVaR0us6T4SECQNmqoNCwlYwK6GO8sCQogL6KT/K1gZAJclDq5SBEAfCPQtgK8BPGqoAD6DwyIgm7iklpSDoaRENEQZoLbENVsgZRLAwcdkVZKxy2tjzGfhmv5n/o5vH5wF/MY/ffrfIxJTVf2/d/9/UQKg+QAUAbgkhLgPcf39jRyY5KqHmB90E/mF1yoDUCITrTXnOvf9lzrpRlMBmARw7Aj400DnaI21peTs/1jllXq8/77yfwizYjDPNf9f1f+vSYw9KFH/L6kAAF1NQvkAvpEsZc+iAiy0nwtslgGc85MjywCpjHZKI4Fr/40h2n6YBHAw+I8P/kOtAVNdE2uNAPa+lsPUbiMJuvq98GT/C5lcKwNgsfp/aQJg+gC+FkI8hHAnQKgd0GRWsMguvrbBUqa/nIMv9+Qc6iBHpUVmGxZgDo45H3tDZf9TXhP7qhp9MMdq5oO7LGA1zAshHgbwFVYGwGL1/9IEQMis/wa6aUVfAXgMcR6AUDsg4PcHxB4MOeyvxIFXU/7faRWAg2MLY47Zf+r7G2K79BLrfar8b3P++/xtrqxf3X4MXf3/Klb1f1HqCypGAIx5AN9KBWAP4YFANlckHL9Xqhug71CgGkw35+Tpo1yMnVGwCsDB2f+8z9WpzkfpO/ynpPy/0fUmx//6OgL09r99SQCuSWwtVv8vrQAAq3kAaiDQt7CPBbY5/9V7WcDfFRAjyeQyvJJEIJXpjjnusnRmwSSAg8G/LnAPuTPolMx/JYE/lQTkyv96rd+mDCwcGHmfxFC9/r8secCVJgAtVhsDfQPgCyHEc3AbHWwGQZdMAqSXAajHF55zItQY95tDOvqc4LVfY44LMwfHlI6xqZ3LQ5r/+kj9OSQg5vGQ/G8bgOfCwKP7hRDPAvhCYqmq/7clP9SiBGCxWLToygBHPgAhxAnEmfway+6AfcsAiPyic4E+lfGOKXkh8fOgkRYwJgEcuwj+Q752re3ShzL/1VqncxJJl/yv4xewvvufEwOxPgDoNDovnar/H0iMnawCAHQtCkc+APnGTyDCCIjN1oiYMkDMUKDSGwP5Hh/b+FJykcktdzAJ4GDwr3MubMsa0WcNLNH7X2Lr38bynMaR9S8QNgDql5MSO9UAoFso2P5XkwDo7YCXAHwmywCuPkdTBkktA4SAeMhdAUsw3ympAKUWISYBHAz+w4N/33N+G9fAHBLgIx6x8j8QVwZX5r+nAHyOrv5fvP2vGgGQEsVddD6AK+h8AGfhngWwVh6ILAM0jv2WCfksb2pmwCkz/KEWNSYBHAz+w5yHU8r+hzT/pWLEhhlQYpFL0gc23f/BuThy/v+XWM3/v1da/q+lAEBKFWos8Ffy+qkA60kpA5hfTINysn/qgT+EGXBuKsCuLOAcDP5TiCln/6X+txLmv9jfjbndOPDU5f5P6f8/gc7x/5XE0JvovHXFoxYBUGWAa4grA5hdArYyQGMB/wb2OkxKGWDK+wLUOtl2RgVgEsAx0WNmV7L/GmvfkPP/fS1/ZvIKT2a/gL/0bZP/L0kMLe7+r0oApFRxD6t2wM8tZQDXKERdKnG1WeiSilOagb0kMLeBQHNWAZgEcDD41z0vOPvvt36H8MFLUCxYZcWsFPkfwAOSAHwjMfTurAiADLU74FV0tYzvAZyBvxOgweZQoFBtJTQTYGwzYOr7mZsKwCSAg8F/WuA/9ew/93mlgL+v+c+nRptY5drwzin/CyHuScy8KjH0sOT0v6EIgCoDXJdSxqdysMEC4RKArQyg3+fL+lMz/b4HEEXeV2tBGFIFqLnt6GAkgIkAx0jHxRDnCWWe20Nm/9TzuZTwXkua/2ABfRODbOr2Av55/7r8/wyAT7Ea/1t8+t8gBEDrBlDbA38uhDgD/+6AR3URzVmpP8/8kEkrAwBhT0AKUahZDijBiIdQAWikha86QDMJ4Bj4WBjrHBhi7HeN7H+I1r8UbNjAF61jzTvYxzP730YAzmO9/a+a/F9bAQBWQ4FUGeA7dPONbeAe/HDgniRIkcBfa0+AUipAjS2J+6gAfTMPJgEcDP7DEfO+iiNn/2HcAMI1fxPHFpYk14Zzp9GVyr/ASv4/qCX/D0EA1OZA19CVAT4x9gZY+OQQjTmZmb/+xbhmAvgOtj7O/9IqQOzBP8R+2DG3mQRwMPjPA/xLt0JPea1LXatzfQCu3n9gs3Xd1fvf2DBQCPE8gE+wav+7XTP7r04AjG6Ay+jaAU8irh2QHOqA+YHqX45ZLkglA333hc5hu6knS+kTpDRwMwngYPAfFvxLv9ZQ2X/uPiolzNgpoO9KQAF3R5vN/OdMdtHJ/6fQ1f8vS8y8u1gsljUP0GaAk0ANBboipY3LQoiLcHcB2AgBwe4ZANwDgkKTAWPr/6kHXJ95ALWZ8RClgDEWRiYBHNsO/qh4vg7hdyqR/ffp/+/zGFlA32b+M3HK2+9vgP9FyB10JVbeQIXZ/2MQAH0o0FcAPgbwbOADOfrAImYCuMyAMV9kH/BPPShL1cdSyUDuIjKVrgAmARzbDv59XpMKnct93kdppTM1+y89+Icc2X93Y9385/QByOfFkoBnJTZ+iU7+rzL7f3ACYCkDfCqEaAEcg9sHEJJOTJnFNiMghuH1Bf8aKsBY87H7nvypi+AQ082yAIKJwHYC/4TBv2bdP+Vvj7XvydSm/9lwA7Ab0G3mdVcp21X/PyYx8ROs5P8qs//HUACAVRngqpQ4PpaGh5APYAFgYWwQpH/ogHsyoEkGbCpAqa6AsfYGKDEfO9X3MMTCNioJYDWAs/4ZgH/u65dcH6awtpV0/duy/zVfmUWRhi0Z1bL/BfzzbhqJhR9j3f1/OMQBOxQBWKLrZ7yGbsDBx7Lf0VcCcDGrkBkwtiUwlvnNZW+AXIacsmD0yQaYBHAw+Jc7N0pL/9u4tvVpA/e1/gF+859NqfaZ/85LAqCG/9zFAPL/YARA9jEeoDM2XAbwmWQ65xGujbjMgNYBQUZLIDm+6L5y0ZgqwNCGwNzFZ+zFk0kAg/+cwb/2+TfFfU/GyP59oL+GIUbrHzx4tAiBvnY5L7HwM4mNN1C5938MBQBS0riNzuDwJYCPhBA/CDAk0wy4gLsOswgwNlMZKD0hcCimPPSCkPJatQaUjE4CmAjMC/i3APz7GmxrbX+eoibMIfu3Sf6uDrOjnBZu/9nC6P1f+JJaiYEfYWX+u42B5P9BCYAxGlgNBQKAfdjNgAsXGbB8kPoXZvoDMLIKkDKTYEwVIGUByVmgZk0CWA3grH/C4F9D+h8q+w/13o+Z/es/GwfWeBNXbG7/q1/2JQZ+IjHxO3S9/+1QB3Az8PmpzwT4HKvJgL6NElwtgc5xwBYz4BAqQJ8SQGmmnCvzpWb+NODiNxkSwERg57P+McA/9/k1dz0dey2rnf2b5j/APwa48WT/NvPfcxL8P5eYOJj5bywC0GI1E0CVAS7AXjNZBJiUa/KSbXOG2ipAyuPIODFKy2a5C0qJqWCzJwGsBux01j8W+A917qWAbsraVIIM1Gj/C2X/tmTThT+u7N+qAEjs+0hi4TWJje2QB/KgBEBKGwfQZgKgq3ucQ9x4YGXys33wa6xtYBUglRyMNRRoyG2CmQRwMPiPC/5jS/81N/4ZOvs3DYAbiaixg23ICHhOYp8++vdgSPl/DAUAWG0Q9K2UPt4XQrxoYUm+D3JhYWEL+Ccz9VEBYp6XI5tNdShQqVbAsUgAlwR2APhHkPznDP61tjovuZaVLAHkrOsx2b9NbY7FLF3+fxHA+xIDlflvOfR5NDgBkJsb3AVwHauZAIcATiC+JdBlulgrARRUAWzPI5QtAeQuLCWHAuUuKCUXt1JZPasBnPWPmfXXAv+a52oqERjCzByztsYMdyuV/dv6/UP4ZD5+UmKe6v2/jgE2/pmKAgCszIDfSAnkfSHEy3CPBl67T5NaFsaXt/B8mSVUgJzrtdoCkXHS9M38cxeznSEBTAS27vOdGviXJPKlWppLrGElHf+ls38YwG/eNjHJh18LiXXvS+z7BiOY/8YmAPoGQV8C+FAIcQLAnocE2IyApuNyw6iRoAI0I4A/Ff6JjBM6RSmotUFQ7GI5eV8AqwFb9ZnWLEGVAv/Sdf++u/6VLgHUIAFNYvZvxRbE7fpn4tmexLoPsW7+W45xXo1CAIzJgJekFPKxbItYwD0X4Oi6ZdayzQuQogLUUARSTqC+i9QQJ8/YJKD2osxqAGf9Q5DNocC/dhIz9ammOaOAQ9n/Ru3fsuufq+9/ITHuY3m5hIEn/2UTgMViYb30VAFMM+DFAHvy7RWwKKQClCYCsSfQ1CYD5i40UyUBo6kBTARm9bnVPrbGAP8S/0utyX9DJDDkWONzs38X9oRwqxFCPISC5r++uDxWCUDfJlhNBvwIwJdCiCd8mb+hAtg2X3C1bPhUgNJjgUvX//s8v8YULSYBGYDGsD75z2pbwb/mQLOhev9LKpcEv6HQ3GDOhS8mBnlxS2LbFxLr1OS/e0O3/k2CAGgqwC10U5A+A/CeEOIp+OX/JvI+nbGF5gKQRwVIIQKEOmOB+xhwSslp20YCWA3grL/E8bBN4B+bOAyxVuWsr7bn+NZ2X/ZPCLf7xdyny/9PAXhPYt0ViX3LMc+5UQmAbHu4h64N4it0xojLshTgIgF7hVSABuO1AtbeJKjmeOCS8mFokahpDhxVDWAiMJnPozaJJNQD/5T/bQpjf0tn/aVaAJtC2f+eB/wfRDfw50OJdddl9r+7BEDGoWRC36Cbi/wegOcQUU+JVAEaiwrQeE5SCqgApWS1nPp/aVmt70KSslDltjVtpRrARGASwD9m1l8C/Gvt+FeiXDk1979P9T3CBCP7bzKzf9tjz0tsU61/tzBS69/UCIDaJfA6uraI94UQ36HbJ3nh+FBjVQDXlo1wqAA12gJLDtXoO0I49wTbdhIwmhqwi0RgAv/vEMfJlMC/5kZmKf/3UJ6lUNufLfu3qQG2MkBM9m+qAOclpr2PzgNwXWJeO/a5ODoB0FoCb2oqwN+EEC8hfptg130b5QCN4dXaFTBVwhpLXkuVClMWoLFJwOzUgF0gAhMB/iGOjTHBv8+5PWaZsu9amrpub6gDBjaYiWRO9q/k/5cA/E1imxr8M1rr39QUAKUCqMFAX0gV4HuLCrCIUAGs8j/8/Z012gJzwH+o4RolJLVaJIAGWoQnqQboQLkNZGBC/8tQWX/u8TvFmf9Dr02lSEBy2x/Cu/7pff+p2f/3WvZ/DSPs+jdpAiCZ0CG6oQiX0bVJvONQAWxqQKg301QBGov8k8Mea+4NkFvDKzFfe0wSUFoNGCNb3GkyMLH3TJi25D8V8J/L2hSz/sau5+a+MWb2H4MtzqE/Wvb/jsS0yxLjDqeQ/U9JAQBW44G/lUzp74YXwDdhKUYFaBC/UVBpQ2Bse2AfuQ2JJ9VUSMCUSwKTIQJTJwMTfG9Dfu99JP+pgn/J2f+xZCAnmfJdD63rettfCDNc2b8Pm1Tt/+8S077FiGN/J00ADBXgkqYCvOhRAVwfvGsus276C7UFljIEjlkKiD0ZYwF16BHBMQvrEGpAid+vBrhjgO7Yf38i3zWhbL2/NPjnnMdjrEk16v8xxj9yYIJv/5lFKOvXsv8Xtez/0tSyf6CrXUxqXcPKC/A5usFALwJ4kIguaR9uK38usarBCMnQhBBiqX1Brby+8ZO6J6svfmkcSEK7LSyPxV43X8f2uOt5JX6aJ7br78U8joTHEHgPSLgfjsdCv5vzWjV/vyogB8h10decaAxJ8kpL/jXAv09CkpPp1yYBJcsAZttfYyR75kY/DRE5PWgW8H9QZv/vSSy7NrXsf3IEYLFYiOVyaaoAfxVC/CMRXbEQAHV9aXms1b4cIa8LrLZzbLX7Wu1LbzUQVLdhXA8BaAqwxoB/zkImMv4uMoA+hgQgghiEgDz0WaR8VltLBLYEyKcM/EOBf63Wv1Ry0Hfjn1QSUEr6hwHowGa518z+Xa7+0LhfV+3//8NEa/+2D2qyKgCAS0KIRzwf+B7W92b2jRK2DQcK1fpzpk2lnFy5bLnGVMCh5gKk1D9TFt6SWRwK/z2O8sA/dNZfw+w3FPiX6PsfcsOfPq1/FCAIFDn0R9X+zdLyngf8H5EJ7LtYbfozuex/kgTA0RHwVyHEs3C3XGxcDEOgq7/TNwgiR2pK6QoYSn5LkdtSTrYSJCBlUUxdhIcyCeqvwWRgONAfkrz1PeaowPlQCvxjEwAqvAb1SYhy5//bjH8xY37X+v8NLAleJFb9derZ/1QVAF0FUFsFvwvgcyHE0wEVwKYKuNo2jr5ojd3FGgLRU7IqNRp46NnbQ5KAvmpAjeyOVYF5Z/u1VKKas/5Lg//QCUjprD92sx+v8U+u+RsT/mAf8rMXwJqj+yVGfT6H7H+yBEBTAW5qKsBfhBCPAdhHXFdAYxkRbF4I8RMC+5QCSuwSGFuLG3r2to+Np0qcY6sBNYCGycA0PsMax0Fu1p9bIhsK/GutPaWz/tR12rbWkwcfFsbMmBDu7EuM+ouW/d+cavY/ZQUAWO0RcE1jVO8JIV6NlGL2YDdx2EweQP9SQI1dAvsO5xhq9nbu7aHVgDGIAJOBcT+rWt97yaw/9VwpDf4115VcEhCT9feR/hGBCw0Sys4Sm97Tsv9rmMjM/9kRAEMF+AYrL8BxAKc8X4w5Iths12jg2DRIMkNzHGRsKaCPGkDoV8svOXs753/KvZ1yXwk1YGwiYAIcE4J6n0dN4M89PnPuKz0ACAnrCnquPb7nlOwACM1v0aV/3yY/TQA7fHhzSmKTqv1/M/Xsf+oKAOReyfew2inwPXSlgNciWJmtTzNUClgYEwL79p3GHNhDGnFKsPEhSUCuGlCTCJQG7V0jBDTQZzkk8OeQ1jHAf05rTknzn2/in2vwTwj4zdr/a+ik//ckVl0HcE9i2GSjmcGCcYhu7+QrAD6WKsBNIcTFCFa20JhcaFqgTf4vUQqYwolY2wyYIlH6ZM8xNg7KAaKaYL1thGCI/6fmd1hjg5/Uen/K+TY181+pxKOP9O/rALBO+dOy/0UIZ4QQF4UQN2X2/7HEqlsSuyYdkycAi8WiRbdd8HcAvkI3V/lPctDCXiRDM79QX+1HN4mULgUQxgP/McyAJSTQPmpAzOMlQKQmUJPjMnWgH/JzqUUWxpjx3+e8mlr3Uc4al7L+xEr/tp5/Z83fkTC6ksw9iUV/ktj0lcSqA4ldTAAKxBLAbQBXAXyKbm/lz2TLRUwpYM/C9mwSkF4KaCyLjOt6rEoQc8CX6AiYEgnI7QgoseDmLvo5wDU0OFPkZS5/p/ZnXPIYoMxjNAboc5W1ocC/5FqTuj7aNmmLkf6tG8IZ676pDu8hTvp/GsBnEpM+lRh1GxNt+5slAdBUADUi+AMAbwshHsV6W6B3FoBjlrPNGGiyyFApIFaWK9kOiEJSXikS0NcXkFISmDIRGIMMlADwqSoNVOi7GAL4a0n+Kedb7ba/2mtNat3fRQ5C0n9jwwINI2JmAKi2v7clJqkNf2aR/c9JAQDsbYHvCiF+bEoyni+tcbBBmyxkKwW42kly/QBDdATEnKB9TswhtgxOlWdrEIESZIBd/8N8XoThgL9v1p8D/qHzr5TxuFSWn7LWxSoANvXV7O4ye/5tsr+p+jaeZHKt5Cyx52+YUdvfbAmAYzjQn+VufmcRZwjcw+aEwAbhQRC+vQL6+AFyT8wpGXJSF6iYkkAJNSDl8aHIABOCep9Hje8y9/Hc4zqlBBDKiue4xvSp+9sG/kSt8Ua2v4cI4x+AsxJ7ZjP0Z+4KgGoLvIuuxeILdC0Xbwshfgi3IXDPI/OklAKsDDPASEvtDTC3dsChWwNLZPxTmBi47aSgpkeh5PNLAn+JrL/U+TjH9r/Yun9IoY2V/hcxWCIx522Z/X8hMenu1Nv+Zk0AZChDoGoL/IsQ4pLFEOgrBZi1HvKwQ9emETX8ACH5f2qtOaVJwBRaA6cwH2AbSEHN/6FWv/8UW/7mBv45a1ipur/Ns+VSAHTpP7SnzBquCCGeFkJcltm/avubjfFv1gRAmivuYb0t8I/SEHgi9ku0PMc1KbAhosayV4Dv+pQVgKFJQK2ugNpmwD4gM8TAoLEJwpDvhwp8HzWBv0/WX6oEgC1ZW3Lq/mvXZTSO5M7W9x+DD+r2Cbnd7x+xavv7Ht3Qn9nU/uesACgVQO0W+CmAd6QS8GP4zYBrYG/s8ewrBZDnoMv1A6QqACVP0CFIwDZ1BfTtOR8CoGngy5D/T63f21bX/5jgn9r2F6MAhNZVWzLmGvNrmsIXDlJgNf9JjHkH3dCfTyUGzTL7ny0BsBgCVVvgbSHEQwEGt/aYHPqwh7iuAFvJINcPgAInztAkIIUQpDyWqgbMsT1wl2r9Y3w2U2z3yz3O+/T+T2XYWCkFILbub27zG3L97xnD4TZc/uZjQoiHhBC30Q39+RAzNf5tgwJgMwS+C+A/hBAvYHM2QOiLtu0XkNIamOMHmIICkLpApEp1MZlMjV0D+xKBmqqAD/h2FfCHUAn6uv5L1f+RcD7kzL1H5rk9RQUgpe4f0/Lnm/MfKgPsS2z5A2Zu/NsKAiBDnxD4MTpTxrtCiJ8YX6KN2ZkqgNkG4pweZWkNDPkBaMIKQJ9JXSnXa+wV0GfBLmkGrAFo20AMSv8/Oa/T97tOfWzsWf9TSChKKACEtLq/0+RngP0e1qV/Fy6sYYfElHcB/BkzN/5tDQHQDIHfA/gawPvoDIGHAB5EoBMA67WgPYcC4OoWSPEDxDL3MU9YZN4Xe72EGpCSbZUmAmOaAac6rW/oMcSllYHSx83Y4377nNNTUQBS1lJbmx85Mn1z1r8PD0wV4EGJKX+UGPM1Zmz802NvC+REfULgZ+gMGg+3bfufmqa5Jh9XX6aQF9f11ri90K6rCwAIIlrIQRCtdiDqB4OQ97fGdXXACu1gFtp95uOu59X6iYj7fO/Zdh2Ox2JvI+I+3/2xj7keT31eCARK1wu3pXxAA/xun82jxpr4lwv4Y4F/SiLjA3yXmhpb97cpAKYa4Lu+ALDftu0rAP4fiS2fYYYT/7aWACwWC7FcLtWWwd+gm8p0AcAjQojXiOj3GggrkG+N6y2AloiE6FB94SEB0H63MYBNB3wbYNoeD4Hr0CQAEcTA955t12NIge/2UERgCDIwBCHYBcAvCfpTAv6+StvYtf/UOQC+rJ4cj9s8WCHwd23z65oWq4x/r6GT/t+W2PKNxJrZGv+2TQHAYrFol8vlgZRl1GyAB4UQDwO4SERfOzL8PTPD10iAsCzQQgNydJ4TCCFEq4G7rga4ABUBwI0B4SEVgJysv5QagMBzahGB2OeUAHQXwGwLMaCRXmdI4I8B9tJZf062P4QCkEIMYh83Jf5Q3d/l9bIBvVUFEEIo6f8/sN7zfzB36X+rCIAM1RVwDatSwENCiH8hom8t4G8qAa4yQKOpAAtjcSaLCiAMNcC8DstrAPMrA5QC/tjsv0/m31f+T832S2X4PnCaGjmgibwmFXhOra1+hyACc5X/Y01/tuw/WPfHSv7fw6pLzEcIjgkhXgLw37Ep/c/a+LeVBMBRCngAwEUhxJtE9G8ekN/T7yciyKxeVwf0UsCaKiD9AEu4ywAIkABgfmWAWFUAyFcDYohBDhEooQqkgHANyX8b2wap8u9R4cdygD8X7FPAPSc7rw3+oXY/E/xt7/UI8C11f5+BWw1987n910iAEOJNdF1lf8IWSv/bqAC4SgEXZCngcSL6zKEEbFw3SgE2EtBoi7kp/YdMgTGAHwvCUykDpNT/Xdl9bvY/BVNgH0IwxYx+yooBFXzuttX+5yr/wwB/p+kP9ro/ecB/Lwb0sZr297gQ4jt0Pf9bKf1vJQGQJOBwuVzewaoUcFaSgP9CRFdhl/sVWO9r10N+gPUzt1MNYGT8PhIgLKrAtpcBck2AczAF5qoDIbCZOzEYo/4/Z9PfnME/R/5Pcfwf7c0C91z/UN1/H/6e/5NCiCcA/Fd0436V9H9nsVgcbhtebh0BkLGUco3aMfAcOlPg60T0r4goA2jgvactxLa2QAXkQh6YyhSoH9RmSSCnPXAKZYDQfS4SE6MMxN4uQQRSH+tLBvoAeQjMxiYINJHXrQX6fYC/FNiHAHUqJCBV/neBv36fPunPtjeLbda/PtTNHPC24fTHuvHvDXSmv7exGvhzC1tU9996AqCVAm6gG9rwdwDnhBAPAniViN6GuwygX/Y1P8Ae1k2BsCgDZmugqzMgpj1wbBLQVwGILQPUzP5rlgBSAb6W7L8NXgCq+DulSwAs/5fZAtjX7rfh+Id/xK85xl2f9rcfAn2spv29KoT4DCvp/2uJIVsn/ZsMbBtJgLlXwN8A/E4IsY/NKYG+mpA5KthlNtFHBS9gH/8bOuhLnFhjSXmpI4FztkLtOzLYN0UwNPVtiImB274nQN//t9R3kXM85N6Xcjt1BHDKxj9TBX/zeVbHv7YRm2/tXWjgn9TyJxXifQC/Q+f634pZ/zupAGhxiNVeAZ8AuA/AA23b/kvTNN9h09WvS/9rw380P8CeJYsTBplydQboqoDZHjglJSDl7wHlhwINUQYYakhQTqY/p/a/morEnOv+KVn+EApALSWgL/jb2v1Cjn9bz/+eAf57AeDfl5c9AMfbtn0ZXcvf2xIrrmILZv3vNAGQrYEH6LZsvATgJDpT4HkhxFtE9Gu4/QAmEVCTAs1FWDgWaVtngA3cTRKQ2hVQgwQg4TEfIQhdTyEFuaA/RT9AHyBPAcVSZGEIVWKMnv+p1f1LAP6UwT9W9Qw5/lNMf8Gxv0KIt9C1+/0R3dbylyRmbK30vysKgPIDqA2DvgLwHoD7hRAPAHiZiP4Cy0RAedk3snwT+Bc+MqB1BgD29kAbCTAX8DFIQE0FYKwRwTGAP5QfoDQpGBO4h3xftev+2zLydwgSkNoO6Bv042z3szj+bQSggbuM6/IAqLr/S0KIL9FJ/+9i1fJ3b9vBfycIgIwlAL018LQkAf8HgIe1UcG2jX/WygES1M0OAJ0QAMa44EB7YMygoNgZACVIQCrJ6KMATK0tMHZQUAisS84HqEUOpgryua8x137/WoBfkwSkPic06CfU7ufq89dNf/qwHxcZWCMGQoiHhBAnAPwPGC1/2HLpf6cIgCwFmK2BZyQJ+N+I6Du4OwE2ygGW+QCHsHcGAPb2wNCMgNiRwS6QRiYZCP0OkFYScJEDnxrgUgdct0sQgb5kIJUQ5AB6LBjWJgrc9leODJRo+8sF+BqgnwL+zl5/Dfhtk/6sWb8E/5hhP3q//w/Q7fK30fK3TdP+WAHAUSngECs/wAlFAtq2fbNpmn91EABbOQAWJUC4CIAE9UZrKexLAmIBPua5qa+XqwDkGgG3tQywy22BU2z7m/JWv6UVgJqdA0OBvznpL1b230O3xe9bAH6Pruf/fazq/oe7IP3vHAGQJGAp/QDfAfgSnSlQKQFvENFvPQQA5k9NCYBFCbAu6gYJMDsEYklAjuTvyxhr7Q+QowDschmASwB5v79t4377KgBTmP4XC/5kAf9Qr7+e+ZvO/r3QRQjxOjqzn63uvxPS/04SAEkCDpfLpZoP8JkkAfcJIe4H8CwRfeABf1MNcHUG6M/fk8TAtpjrg4KGIAFDmwJTFICh/AA5qkAMGUglBKmgvs0lgLlt89sX+Ptk/alAP3Xw9w362YO93c/m+Dd3+Nt3gP+zQoibAP4dXd3/c2zxqF8mAPZYYn0+wCkAZ4QQ/zsRXUdXC4rtCBCG299ciJcwBi7J5y9nQgKA/JJA7vWaRCA3+59LGWDqJYCx5P+hdvvbZud/NfC3DPpRj9na/fY08N+HZ2MfrJcFLgghzgP4v7Be99/6fn8mAOsqQKvNB7gM4JhUAs60bfvPTdP8HusufeECf40Y6I8vQgt5JRKAwmSgD/iP1RFgA+rS2X8s0JcuAwyR4U9BIRhrzG8s6I9JBKY8AKgk+C9gb/czs3xbrX/fcv1k27YvoXP8q37/y9iRfn8mAG4S8D26mc/HpRJwum3bnzZN82vEzQLQVYDYxVmoA78wCRizHbDvTIChFIAxOwKG7AaYi6IwhWl/cwD+PuA/RPtfH/A3SwCuQT97MvsPgb4+6W+BbtLfWwB+g8749x52YM4/E4AwCdBNgV9IEnASwGk5KfDfHCoALKoADBIgIj7ftjIJSFUEYn8HEcQgVQ1IIQI+QJ9bR0Bt499USgLb5vwPPadkF0BJEpD63Njn9AH/0Na+eq//ngXovQqAnPT3VwC/RbcnzBdyzb+7a6Y/JgCbYQ4JOgbghBDiFIDXiej3iDME6jMC1G3bfABRkAQQ7OOFYzsA0IMojLlHQAzIlygFhIC8ZikgBtimUg6ggV9nStJ/abDvC/h9s/zUn7bZ/rng73P928b8Bo1/QojXhRCfA/g3AH/BDg77YQLgVwGEnA9wG50hZE8jAf8ZwItE9DfY6/7WBdhSDtCnBNq6AnJJgG8DoRpmQESCf592QF9GnwLycysF5AD6XHcO3IahP6UUgNz2vyl0AJQG/z0LCbBl/vsWsN83SYAQ4kUhxA0A/4rVJj/K9He4K8N+mADEk4BbAL6Rn8txSQL+C4AniOizQCZvzgiAxxJQiwSkZvJjmAFLtAOmKAAlSgE5ZCAn8+c5AOWUgKFc/ynAP0bWXxr0bWBfGvzXJvkR0b4ny7cZ/vaFEE/IMb//Fd1GPx/Jtf0Wgz8TABsJUKbAG+imQikl4KQcF6zGCNuMgNZJgFo5oDQJgIcElO4I6Av+Nc2AMQrAlLsCUsB9jF0AaysOpZWAMV3/JRSAucz+923pWxr8XfV+n/R/XgjxGID/BuAPWE3623nTHxOAeBLwtUYCTrRt+09N0/xBPmaCv3PxNTYDEoVIgGsrYdfWwrFkwAUmtScD5poBU26nKgAlSwEhUC5VEqgF1GMQh5oqwBT6/lMAvQbQp4L+UOBvjvjdD2T++s99dK3cLwP4f9FN+mPHPxOAJBKwlJMCv9MOvOOSBPxCaw8EPFsBW0iA6KkE6LsTmiRAfz/m7RQzYGlVABnkoBYR6KMA1Mr++5QEamb6QxOOIcb+TmXkb4msv2a27yMEPue/a1e/VPDf84B/iAQcb9v2TQC/Ruf4V2N+2fHPBCCLBFzXDsBj8gD7udw4CIj0A8A9LdBHAoQxLEhn360FIM2OgFRQrw3+ufL/1OYCpHYGpJKCFGCfmwlwG81/Y438HZIEmEDfOJ7jAn99ql8M+Lum+7nq/gsAx9q2/Rm6zX3+HcA76Nr9rjP4MwHICdUe+K12IB4DcFwI8TM5IyApA0skAa36HQCtYxdBgt8MWIoE+F7L9VhMtj/kgKBcVaCEArBLZsDa5r/SPoAxN/3pSwiGqv/HmP9cu/rZLjHg75P6zU6AfSHEz9D1+P8bup5/bvdjAtBLBVCdAXfQ7RnQaAfbPoCfGYOC9AXaWRqIIAFLD3kwSYCvI8BUBmLJABIIRK4aANQZEFRKAajdCjiUGXCqysIUpv/tsvQfA/o+sDfr/xQJ/tbtfD3g79re1xz083MhxEdYb/e7Cm73YwJQiATckgfU0UErhNgD8JbcQti1yIoIJSB6MbSQAPM6RZKCEEhMsRTQlwjkKgA+QM8F+9SSQKnMf8iSwVw2/alBBGq2/tXM9kNgXxr89Z39fBP9Fi4FQAjxlhz087/QtfupDX643Y8JQBES0EoScBPG1pRCiP8E4A0i+l3qQi3BnCwn7zKwgKmMvrUAuwgoAyIR5PuWAvqCfwkiEKsKxJCBlMdSgb6vIbAGYahJHoYw/qUAfAzIDwX8seBegwyE2v500NeJgLmLn29jnwb2Vr9gf7/+UwjxhhDiMoD/D12734foev1vSvBnxz8TgGIkQN898IgISBLwGhH9MXHhJUs5wAb+DVbegJaIGu13Wg3YXbMChAU0UzP90O8A420VTA7ikUoOYshAqgIwZvY/BYNgLRVgrq1/Y43+zQV/n9PfJAONzP518N/D5pa+Prd/Cvj/WAhxDcD/RGf84939mABUJwH3sJoDYJKAV4jor6mLl4UEmIvJEitvAEkSoP+O8hy4ZgUAmz4Agt0s6AKfvvsCDLlRUK4qEEMGSigAOfV/SviupgrsYygAc6r/jz3/PyT5O3v8NfA3s35CeGMfHfxDBsBjEvxfFkLc0cBfH/Rzj8GfCcAQJGBN5hJC/AuAF4jovcgsj3QpQAP0Q8v3YoK36g4gz6wAV4eAb2jQEPsC9NkoqC8R8D2nLxmopQCkgPoU2wKn0vpXS/bvC/ylAb8G+Fud/tjs8Tc39Gk84O8a7+vq/VeZ/wtyrfyf6Lb2ZfBnAjA4CdCnBepKwD8DeI6IPkheJdc9AYcG614aioDaWMgcGKSfrDGgn+ILiFEF+qoBuUQg9NxUcpAL+qVNgVPM/sdQAXZp5n/JrD/0nJQxv4TwgB+91z8k+6eC/74Q4jlpvv4f6Kb8/R085Y8JwAgkYCmVgO/Nk8MgAb5FhBwkIGYBa7XHW8esgNaR5TaOTDV08tTeJbAPEQgRg1RVIJYMpDyWSgpSgX2qQ4GmrADMvf5fMuv31fvXiECgx9/W4tcEwN/XAaBk/+fk5j4m+H8vM3/u9WcCMDoJ6FZsIf4J3TbC71pOLtuCQQAOJAkgqQTAyPxtaoDtdVSHwAJxvoCckkAI9OEA4xLgH5v1l2wLjLk/FvBTpf5tVQCo0POmUP+fm/s/p94P+J3+JvA3GtAvAOxJ8F9EgL4p+78ohFhI8NdH/DL4MwEYnQSofQPWFmNJAmKNgYYQYG0RPDROcPOgNzsETIk/1hcQs4/AlLsBXKBech5ADR/Ato4F5h3/plMCiJX8Ab/Zz9Xm11jAf0+Cv2/3Ph/4vyL3UVGZP8/3ZwIwWRKgO/KXkgT8mIj+FFiYNk5MKeurkoCuCJjg38DoEMC6OVA/qU2QB+JLAlMqAQwl/099ONBYKsBc6/9jjv0NAfTQWX+o7S/W7EeWzN+s9y8QHvDjavW7g25nv9+j29lPZf4M/kwAJkcC9IW4RVeX/0cAbxLR7yMXgTVCYJkauLFzoOV1WnnCunwBfUsCJdQA83EgvRUwtxzge04MGehDCFIVgDmrAFPN/nOy/dzsPxfkS2X9viw/pd7vMvuZCoA+3c8E+ejsXwjxuhDiOlZuf73mz+DPBGCyJEBYlIB/hH1ssJn9q9sH+nXNF0Aa6Ns8Aa0FzH2+gJSSQK4aAA9Ip/gDgOkMB/LdH6MC1PIBDKECTCX7LwH6fbP9MYE/hgzkSv5AuN7vm/Bngr9e9w/2/Mvxvpex3ufPhj8mALMgAcoYKDTgVSTgF3IDIZcpkBwnb2OQgEPYRwhvAI9WEgDsdX5bScCmLOSoAakqQE6m39cEWMIAGMryd1EF2ObsPwa8h8z+Y7L+PpI/YbPH35T7XU7/Pc9l3yAGx+TGPp+hG++rwF/v82fwZwIwKxKgKwH/AODnRPTvngWH4B4YZJsV4Dr5lxqIp5YE+qgBsYQgFfxzywG7qAKUVgNKkgrO/vNJQOpufiUkf1u932X2W1jMfmbG76r7HxNC/Ezu6ve/0M32/0AD/wMGfyYAcyIBAquxwToJOJBKwL8DuGs5MQ8sioC6n4w2QZc5sLW8hlkeCJUEAL9BMKXujwLgX3My4FxUgFRQH0MNqDH/nyf/lcn6AY/kT51caMr8vnq/DvZ7HvC3be2rZ//HJfj/Dd2Wvn9Ct7HPZfCEPyYAMyUB+thgRQAOARwIIe4KIX7RNM3vsBorbNb+XZtx2DoEzJq/Xg5YIwNE1MJdEtBBJmQQjFUDQkCPRPCPKQeUUgFS1YGUx0LAVlIJSCEQtUhDqdn/Q+38N7Xsv1rWD7/kb8r9Zr2/cTj9Y6T/PQBn2rZ9E53c/28A3ka3pa/a1Y/BnwnA7EnAWilAAv3dtm1/2jTNOwCuOk5y8hACpQYoP8Ch8fyl8VNXAfSSgEC8QTBGDYAHdEupAKkKQaoKEAL+kh0BQygBUwP4lN8Zsu9/jtl/k0AGfFm/z+hnSv6x4O/rANgHcL5t25cB/BrAbwD8BcAnAK6Ad/VjArBFJEBtJbxGAADcadv2l0T0BRF9hk3JHwFloLGQAGCzJGAjELb6v0sN8IG+aRocQgUA6ngBcu7LAf0pKAFDxRC7/g0J+lPJ/psEMmDN+hF2+dsk/4UB/jYCEHT9CyGeEEI8hq7H/7cA3gHwmUyGbjP4MwHYRhKglIA1EiCE+BWAU3InQUpRAYCj8cEm2C9hLwmsAbnqEjDUgAabRkCfGmA+d2wVIEQaSpOBVNAvqQT0VQSGzPhLZf41sv9SU/+GyP5zsv61pMEC/C6nv03+V/V+F/B7ZX+5o99JAP8Nq+l+XwC4JsH/kMGfCcA2kwBdCbgH4LYkAW8Q0X8EAL9xKQHoavtmlg/YSwLmYwsiWgohGqzPMohVA2CoATVVgFyFwPV4CeAvYQLs0wY45yFAUx7/O6X2vyaBDLhq/crUB6yc/TGSvzncx/y57yEBR8Y/OeDnBoD/is7pr6b7XQdwR4K/AAcTgC0kAWK5XB5KlqtIwKGmBPwSwK/krIC7HiLgJAhal0ADe0nA5QsQUk0QWG8XjOkUAOLKAjFAn5Pd9ykBDLk5UJ9WwG1oA6w5/ncK7X8xz+sD/LFkwKz129r7XJv52BQAW70/JfvfQ+f0/7kQ4hN0Tv8/omvz+xpyrj+AJYM/E4CtJwEADmWboNBIwD1JAn4uhPh50zS/lYyYPBcb46dAScBGAnRQV2oAZEkgplMgtiyQOvTHBWq1RgPXaAfMkf5jgDJmoRxSFRjaBDjV0b+lNwGKkfv7Zv0+8Dclf7PFL2bYz4m2bd9CV+dXTv+PsOrx59G+TAB2jgio0cGqJ/+IBAC42bbtW03T/BWdKcZXDjg07jvEZkmgwabs7yIBNjXA1ymQWhZIBfMh2gNDZCCkDqQQghRi4AP6bTMBzsEAOOTufylyvy3rt9X6Q07/kMs/J/s/37btK+iMfr/Fyun/DXjADxMAJgFLgW5q4JonQJKAXxDROSL6MEIFsJEBXQ0ws39dzg+pAbo3wMz0c8oCNYkAME5XQAyo11IDYhWBsTP+vll/TvY/VgmgL/AD8XJ/jax/YRj9bKDvzf6FEM8KIc4D+H/QbejzNwCfyqTmFtjpzwSAScBGm6DyBNwCcEMI8Qt0uwn+R4QKYMqBsQZBX0mgjVADdBCKKQuY5KC2ClCKDKSoAzmkIFcNmJoiMLXWv9KgX4MENA5iEJL7Y/r6KTLr9wF/SvavzH43Afxf6Mx+fwc7/ZkAcHhJgDBIwG1JAn4mhPhV0zS/wbovQDf6OVUALStoHAbBxqEG2DoFhCQTrQHmobKAjQiICipAjCrgu50C8jHAn6sE9FEDSigEU5v/v60lAB/Iu+4Lmfxis36C3ejXRIC+zfmv6v0/RWfw+3d09X7d7MdOfyYAHBYSIAAcyJKAbgy8ha5Wdr1t27eI6D0iumQBf1MFaDQwVwvDUmYISzkKuNGAGwbw60RAwNhDQDMJ6nMDSCMxZlkAnvtKEYG+qkCsEjDURMBSZsCaCsE2TAAca/pfau3fbAHWs/6F9hxfb78t69dr/Wb2v2+53zrpTwjxkBDiBXRy/++wqvdfxmor30Ne7ZkAcLiJgOoQMJWA7wFcF0L8DMAFIvob1o1/LmOgvigcqoVB8wboRKI1wN+8vQbYmhpggn5j/FsxpsAcIgAPeJeq/5cqAfQ1A7rAfopmwF2a/pfT/58D/ECcyY+wuXOfjQToWb9L8rcpANa6vxDiJSHECQD/Hd1c/3exmuynxvqy2Y8JAEcECVgabYIHkgTcAHBNCPFTdDsK/lYSBBf4bwA/1ocHNQAONTVA30K4wWY5QCcFtrKAaQ50+QNSiEAIEKcwD6BGCaAE2E9pDkCtrH8o0O+T7acCf0qdH3DL/eZWvmbWb4K9DehDE/+OCyHeFEJ8BeB/oJP830c33OcaeKwvEwCOLBKgfAFmi+ANqQS8IYT4maUk4GsR1EsDuhqwkPsJ6KDvmhfgKwu0xi6DOhHQ/QEpRGBsM2Bq1l9qHkAJ+X8odYAqPL/GwJ+xSEAq8AOWOj/Wa/2AW+4P7uTnqfXHZv665P8ndLL/X9Ht5HcJ68N9GPyZAHDkkAAArVYSOJAk4Ca60ZlXhRBvAXiMiP6YqQSY3gD9+WYpQL8sjcVKdQu49hUw/QG1icAYSkDK/TmgX9IUOBVCULLlbwjQHwr4bXV+E+j17H8B/2Y+C03184F9VOYvhHhNnuP/Hd1Uv/fQSf5qJz+u9zMB4ChEBA4Nc+BdjQRckRLcPzRN8wd0XgGbEuAaCGKqAY02N0BXDGxKgLmj4NF9Dn9ATSKAQNaeszlQDSUgBfT7Av5USgBDjwAee/OfWsAfGujj3cVPSv6xWb+rA+C+tm1/AuBDrIx+H2Il+d9C5/Lnej8TAI6CJED5Am5gfVbAd+iMNj9p2/YNIvqGiD6wKAFNpBrQqkVHDv9ZGkRAZf/mY60B6qWIgHCQAB/gp3YGxJCBvkpAqhoQC5xjzwao0QVQiwjUHv9LkWSgFPATNlv6Ngb8GHJ/E5n1b8j/QojnhBAPohvn+0esBvsol/8dsOTPBICjGgnQSwJ6m+D3kn1/I0sCvyKi32BlENQBP1YNOJRyoRogZIK+6QEwuwTUT5tRMIUI2LJ/83dLtQX2UQJC98cA/651AUxB/q+R7ZcCfmDd4Edwu/v1nn7SMn4T/FOyfvXzuBDip0KIywD+TwB/Rmf0+wLAt1i5/FnyZwLAMQAROFwul/qWwsoceA2dL+An0iD4PhF9bYC/C/h18Ndr/8okuLSoAaY/oMG6P8BGBNqCRCB0f2qJAJh2GWCOg4CmKP/3Af2awK+b/mKA32n2M+T+xpHhB01/QoiHhRDPo3P3/wGbRj+e6scEgGMMNcDoErirqQFXAVwWQryJlUHwwKIGLLFZCtCvH3UCGGWBFpslAbIoBGtEQgehCCIArLwFMX4AszxQQwkA6s8FyAX9oTsCpjIAaOjd/yiBENj6+GOBXz8XdTJgXl+r92vAv/CAfIz0vy+NfvcA/N+SAPwd60Y/3sKXCQDHiCRA31pYVwO+l9LcZakG/JKI3iWiy4grBeiqgCIJC6zKAofavgImGTAnCbaO7N5HBAD7QCGbHwDwlwdKgX9K1r+rA4Hm5v6PJQF9sn0YAJ8C/I1BApxDfTR3v0/uj5L+hRAXhRAvotu+9w/y50foxvlew6q3n41+TAA4JkAElhYScFOerN8A+EoI8QaAZ+SmQnexWRKwlQIabJoJW7mALTWToEkCWkMFaDxEgAAIi0egsRCB1PJAaKhQSQ9A3zbAFNCfw1bAY/gASrv/U7L9FOB3KW4u4DcJgAn+a61+GqDHSv/q/uNyE5/bAP4bulr/3wF8LteR78G9/UwAOCZJApRBUGXfyiD4nZTsvhZC/FgI8XMi+oiIPsdmm9+hRw1Qi9BayyC6gSC6P8AF+uo+dV3A3jUA+TgsGw4tjN8D7AZEmypggmhp8E8xBcYCfx/QLy3LUuXfnUoLYArowwR4GwmQ54nK8uEBfl0FcAH/kdlPk/sbA9Ab+Ov8G9eFEI8LIZ5GV+P/EzqH/8da1n8LXW8/Z/1bFLQ+vZVjG2K5XKoF4DiA+wBcAPAYgOcAvArgJ0R0gYh+L+W8Q40A6D+TLhKwW0MJ0O/Twd8kAsK4rsBdaKoA9Pu16zrgmwAoHIDoen7sbd/9IdWhJHCPfQKXngI4xva/BH/Nv0+2bxvpmwr8a5m/It4ZF5vsf1JOFb2Kbob/X7Fy+F/Rsn42+jEB4JgZEVgAOAbgJID7ATwE4CkALwL4EYAfaXMDUsD/UAP1PkQA2n3QHjOv+4iADuZiIDKQSghKAv/UT9i5tAD2AX1ykAAf8C8s1xeIm+gXA/yhmv8GCdD6+v8ss/730O3e9zW6IWO3OetnAsAxfzVgIdWAMwDOSzXgWQCvAHgNwKNN07yNrlxgAn0METg0AD6GCLQWFaA11AFYFAIhyUBrgGGMKlCCDJTO+mNOwLmepFPbACgV9GOzfSXz2/bdMPv5XTX+JgH4zS19Q8BvPn62bdsfAfgS3UCfv6Kb5vcFug6iG+BaPxMAjq1TA/alGnAWwEUATwL4AYAfSjUARPQ2OiPhYSQRaI3HQ0RAWAiBqzQQWx4QDvAvTQZKqQG1gb/WSU0Vf6/m9L9SoG/epgSZ3yX1uwb6+IDfrPfHAL9q7fuh7OB5GyuT32dY7+tnhz8TAI4tVgOOATgN4AEAjwB4Gl1Z4IcAXiSir4now0QSYLsvhwjYVAHAUhKwqAK5JYIYMgDjtXPBvwbwj3USDzkGOOX+xvO8GNCPlfhtGT+wLvPbsv2+wL8x5z8E/kKIZ4UQDwN4VwL/u+hMfl9hNc3vHmf9TAA4dkMN2ANwAp1J8EEAj6IrCygi8CQRvUNE3yDNFKiXBFxEQFgeExYy0MJeHvCpAoC7RBBLBmz3x6gDfbP+ufsAatf/fVl/ar0/BfR1iR+R2b7ZRmub42+285EH+JuIjN9GAi60bfuKzPLflsD/ITr5X7X23QFv4MMEgGOnSABpaoAyCV4E8Di6boGX0JUFzshJgreQ1hUQIgKtQxGwEQGzJKBPCGwtqoB+GwEyYLsdUgdSCEEp4J/biUoFnuPL+lMG+Ljud8n7OujDkt3rtxvLdZ/MT46MvykI/AsAp+Qkv5tYtfV9gK6n/zI0kx94mh8TAI6dJQK6SfA0gHPougWeBPA8OqPgD4noHhH9GZ0/wFUSaBOJQGshAy6zYGvJ/r1dA5ZLChlIUQdiScMYwN/394fYCjgm40+Zz++7PwX0YyR+U9q3Zf8bFwP0m0Tgb+CW/FWd/zg6qf+v6Or8n6Kr81/D+hhflvuZAHAwETgqC+jdAsof8IIkAj8goi+I6JNIBcB2u7U8fkQKtDq+TRWwlQUAt2nQRQYkF9ggA7bSgIgkBEDaLIHS8wCGVAlq1/5je/hjAN+U9jceN+R9H+jbTH2AW+63Zfu24T4LBxmIrfcv0A3kekoI8Ri6Pv6/oGvrU3V+3d3Pcj8HEwCODRKgFi7VLaD7A55B5w94FcATRPQBEX2VSQJchMAkAkv42wVDBCBEBo5AXZIBBNSBWEIQQxZib08F9EuRAUq87cvufYDvzPIl6jdwewB8oO8iAL72voUF+H2AnwT+QohHhBDPoavz/wVdnf8jdHX+K9Dc/QBalvs5mABw+IiAWqCOATiFzh/wIDp/gCICrwB4iIj+Jo2CscDvKgO4hga1DjIAuMsEqWRgI+OPUAdchAAZpCCVCMzRBEiRzw2BPSIA35Xl+xSBWNB3mfrgAP0G7uE+Lvk/iggIIR4UQryETtr/qwb8anb/dcgRvhL4We7nYALAkUQE9rBqGzyHlVHwGcjSABGdJaL30MmMoYzfBvZLhxqw1i6oEYFQx4AP/G3dA1a/ADZbDGOmC6aSghAxcAH/1E5cSlQFCOm1fxfYwwL45AF72/2+QT42ad/p7DeA37WRz8KhCCwiFIHzQogXhBDfSeB/TwP+y1jV+e+BR/hyMAHg6EkEdH+AbhTUicDLRHQfEb1rEIEcEuAiAqYq4BskhAQyICJIgI0QAP6SQMrcgRDw9zlRc3+37wZAKQbAnF33jh4LAL4L/CkB9PUMf8Phb8n2vTv4ZYL/eSHEi0KI79Ft0WsCv+rn5zo/BxMAjqIkQC1wan7AGQsR+AGAlwwiEFsOaD0qQAvPzIBEMmCdKNiDECCCFPiIQoqpMAfsS3cB9DEApuzC5wJ/E+xRAPDhAH8f6JPm5Hf19jee7D8G/NVtBfw3JPD/XQN+5ey/AdnPD67zczAB4KhMBPYtROAxSQSel0TgLIAPiOgy4roDWo8yYJsc2FqIwNIA7RiDYEgdQMR9yCAFLsCOlfr7EIUi60cEKYidxZ8K9kBY4kdElh9j9NOfp0v8rsvatr2RhGCDAAghLgJ4Tkr9f0Pn7v8I3cx+E/jZ4MfBBIBjUCKwMBSBB9B5BB5Dt+vg8+jKAxeJ6CMi+iKSALiUAfO2aQ7MJQN6R0FIHfCRgShSAMkMEghBKvAPvRdATIbvBXwJqkgE+1jQtw30aTwEIAb0zTp/EwB736CfNQIghHhMCPEMOln/PQn8n0jgV1K/nvHzIB8OJgAckyEC59B1DTyGbqDQs5IIPEZEl4jofYS7AlIJgM0UaJIBwD5HwNx9MMYrgMjbgH3IkA34beQglhTUBv8QCQiBvQvkbb/XOIiFD+hd2b4N4KEBNyygjgDo27L9WALgdP0LIZ4XQjwkgf49dCN7P5W3v8Gm1M/Az8EEgGOSROA0Vu2DjwB4AqvywNNEdIeI3pGLWWhAkAv0bcOCXPMCjm5rLX62rgFgfaqgq40w1SfQejL42JkBG25u4T+BS53cTtDXQN0G4AgoAz4nf+MAfx/QwwP45vQ+082vDwRygb5e498Y8hNBBmx9/yeEEC8LIU6gG9qjZP7P0A3wUe18Nxn4OZgAcMyBCCiPwHGs5ghcwMow+LRUBZ4jomPSMPhtJAFwmQFjyYCpDgD+rgFXy2DrIAIhJSCkCgD+FsNQ9J0wSD0fB+Ja9WKBPibThwPwff37KsuHBfBTQD/G/GcjAA9IY989dDP6P5QEQBn7rmDVx38XXOPnYALAMUMioOYInAJwFp1P4CF00wWVKvAcgPNEdIWIPlCLHcLmQJsPIDQ90Dda2FQHUoYItQ6gbyOyfp9RsE0E85xxw33G9IZ+v/G8Rgj0zd+3gbuZ9QMew5+R5ftG9zqn+iG9/q+u7wshnhNCXEDXIfOBlu1/KYH/W3ST+9QAH3b1czAB4Jg1GVDZj9p5UPcJPIyVafAZdOUBIX0CVyIIgG8DIatBEOGhQUe3HYRAwD9YyKYUuIhBbMYfaxSs1RlAEWSAAtdjJX/zfptnwObk37gYI39tWb5rqI/L6Odz/rtuX5D1fZJZ/kdYmfq+xnp9X9+hj/v4OZgAcGwNEVALo2ohPC1VgfPougceQVcieApdieCCVAXel4tiCgFwmgIdagBg7xqAQyGwgbwIKAVwkAWbIhC7kVAq4PctAcQSgtBgH9vjDezOf9cY3w3Z35LhAxZXv/H7iwABiLmYBOCYBP3zMtv/UIL+5+hq+5fl/d9hVd8/AO/Qx8EEgGPLiYBuGDyO1cZDulfgMXQlgqelKtCgmynwTSYBWAaUAJ8x0Nc6aI4JDpEClz8Alt9zgbdtw6IS4J9KAvTM3UUKzMcajzrgc/jbwD40q9+V6Ye27KXIrH+DAAghHkTXu9/KbP9jrCT+r7Gq7X8vs/27YGMfBxMAjh0lAwuHKvAAVh0Eqp3wGQAPyg6C99FJpilkQESoAi5CAGyaBYHN9kGBVeuhTdL37TAY8gUA4T0HBls7PNm9Tynwjeo1CYIJ9o2FGJjOflgyfJf873P7p2T9p2W2fxKdnP8RVu17ysn/rSPbZ5mfgwkAx84TAbWo6qbBM5oqcFEjA0+gKxOcJaKbkgzcRF45wGUI9BEC36wAFylwTQa0/T4Q9gOkDg8qDfo+EuACfliy+7XHLTP9XWAfM9nPl/H7NvSJAX8F+qclsH8iM30F+kriv4ZVbV9l+7wzHwcTAA4ODxlQpkG1AdF96IyDF7AyDz6KzjPwJIAzRPS97CK4hXBHQBsJ/LEEwDdJ0LzuBH/LroM+lcAF+n0nBFLE/TG9/RuEwTIMyNUB0ASuNwkEIIYI+EoA6vop6eK/T4L6p+hq+srBfxmdxH8NncSvNuY5AO/Kx8EEgIMjiQjoXoFjWJUIFBk4L8nAQwYZOEVE14joI3Ryq2smwNID9KGdBEP+AJchEIHHgIiSgGOPgVzAzyUEZubuyvxdKoDL3Od6LKbOH7ONb2zrX4NuUM8zQohzkliaoP+Nlukr0L+DVQsf1/Y5mABwcBRSBWLJwGOSEJwiontE9AlWuxP6SgExBMDM9kMEIAT+MaAf8gSUUgFSs39fzT+GDIRIQOy2vfBk/D4CYLt9XgjxlBDimAT9z9FJ+y7Qv4VV+54Cfc72OZgAcHBUJgPHLWRAGQgvSkLwiLw8KBPVr+XGRHcRHg4U0x3gIgW2x30qgY0g+DoGbIpBCPRTCEDK5j8UQQJsPgFXvb+Bf/te345+sbV/lfkfF0I8BuCiEKJBJ+N/ia6Wr6R9ZeQzM/27DPocTAA4OMYlA/tYDRo6hVVboSIEFzSFQPkHjhPRXakOfIs4c2COAhBzgYMgAP65AKnKQNL6EJnpux731ft9BCDmEqMA+Gr9D8gs/7jM3HXA/0aSAAX41y2ZPvfsczAB4OCYMBk4gdXkwTMaITiPVWeBIgQXZB37GhFdkou+rUXQVx5AIhFAxm04SEEMEfDdHyP9+7J/FwEosZNfSAGwZf4m4N8vd9o7J/0TV9D15evmPSXrfycBX7n37zDoczAB4OCYHxlQ3QS6OnAGq3KBTgiUSnBRqgZ7RHQLwCUi+hqbpYJUM2DfLYVT2gT7Zv8hFSCmzQ+VCEDI/LcQQjwM4CEhxCl0Ev23WEn5VwzAvy5B/4aR5d+T3zmDPgcTAA6OmZIBvZ1LqQNqp8LTGiE4q6kEihgoc+GDAPaJ6BDAFUkIbiOuJbCEAmADdl/tv+9sAIpQCFwtfzEegBQCEGr9OykB/4IQYk9m6t9gZdZTQK+Dvcrwb2K1495Rlg/egIeDCQAHx9arA2rbYlUuOGmQAtNLcF5TDE5JleAQwLdEdBWr0kGuEXAXCUCKAfB+OV//AQn2hxLAr2hgb9budbC/jZWsf1cDfM7yOZgAcHDssDpgKgSKFNiUAqUW3G8oB/cDOEZESwlMVyUxuAv/YKBUH0DIBAj0KwXEbgEcmvQXe9sc9nNcAv15dAN4FjJDv25k8te167bMXgd7PcPnLJ+DCQATAA4OKyEw5w4oU+EJSQj0iyIHpzXV4KxBDk5KtUBIELqDrpRwC6vJhaHsvy8J8N0fawLMBX9b1n9K1ugvyM91X26ZeyiBWwf577Rs/qYB8vrlDlamvaO+fEm4GPA5OJgAcHAkkQKTEOik4Lh2OWkQhNMGOVDX9cfUfQ2ABRG1GohdAyCkgtAiPBMgdmtgkQD2KcCvg3wjM3iSysgJdFP1Gg2QFYjf1MBcv++G8ZgCeDVX/66W2SuwP9Sye5b0OTiYAHBwVFMJdGKwj/USwnFNNbBdTmLde6CIg7qunqdeZ18C53EAh1JN2JMZMiT4XXMRBOlVuB749+6XtXUXwJ+TfxNS2TiUWfueBONGUzjuamRGyfG3tOu3NUC/47noQH8gL2uZPWf3HBxMADg4xlYK1IS5hUEMlGqgk4R9i5JwQrt+TLvsG7dtF/V3FrDvhqe/L33bXWVQXGoX226ISwm8B1rW7bqYz7mrEQIzcz8wwP2e9ncOjfclOLPn4NhCArBc8tbYHFsZrr3kdZKgKwj72n3mZd/zU72Wq1de39nORgDMHRRtMw4UIB8YIG3+tF0OjAz+0EI6zAuHJxaLBX8IHNmxxx8BB0f18IGZbWrdwkEYTOKwsGT1ZuYP+AfpAP5BRrAoAaZasLQAuQ3QzftcWxtzcHAwAeDg2PowZwQgQBR8s+7NdjoY111b8wLxmw/5NkeykQcODo6JBnsAODg4ODg4djAa/gg4ODg4ODiYAHBwcHBwcHAwAeDg4ODg4OBgAsDBwcHBwcHBBICDg4ODg4ODCQAHBwcHBwcHEwAODg4ODg4OJgAcHBwcHBwcTAA4ODg4ODg4xov/fwAHnhIg2IQLzgAAAABJRU5ErkJggg=="}],"object":{"uuid":"da66c047-c0da-4a53-90dd-589c4e53e868","type":"Group","name":"MagicZoneGreen","visible":false,"layers":1,"matrix":[1,0,0,0,0,1,0,0,0,0,1,0,-0.33236499558859744,0,0,1],"up":[0,1,0],"children":[{"uuid":"1921b779-fac1-42ec-b40a-a1af729bf6fa","type":"Group","name":"PortalDust","layers":1,"matrix":[-1.0000003044148624,7.619931978698374e-8,1.2979021821838232e-8,0,-1.2979033855407715e-8,-1.8384778810981241e-7,-1.0000001522074553,0,-7.619930580271816e-8,-1.0000001522073967,1.8384778922002538e-7,0,0,0,0,1],"up":[0,1,0],"children":[{"uuid":"9ec4a1d9-3f3d-49a6-85fc-577cef6084a2","type":"ParticleEmitter","name":"PortalDustEmitter","layers":1,"matrix":[1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1],"up":[0,1,0],"ps":{"version":"3.0","autoDestroy":false,"looping":true,"prewarm":false,"duration":5,"shape":{"type":"cone","radius":1.21,"arc":6.283185307179586,"thickness":0,"angle":0,"mode":0,"spread":0,"speed":{"type":"ConstantValue","value":1}},"startLife":{"type":"IntervalValue","a":1,"b":1.5},"startSpeed":{"type":"ConstantValue","value":-1},"startRotation":{"type":"IntervalValue","a":0,"b":6.283185},"startSize":{"type":"IntervalValue","a":0.15,"b":0.2},"startColor":{"type":"ConstantColor","color":{"r":1,"g":1,"b":1,"a":1}},"emissionOverTime":{"type":"ConstantValue","value":25},"emissionOverDistance":{"type":"ConstantValue","value":0},"emissionBursts":[],"onlyUsedByOther":false,"instancingGeometry":"780917d8-bd1b-4d63-8aca-f79e3211f964","renderOrder":0,"renderMode":0,"rendererEmitterSettings":{},"material":"769df3ee-4567-40b7-8da4-473fb149f350","layers":1,"startTileIndex":{"type":"ConstantValue","value":0},"uTileCount":1,"vTileCount":1,"blendTiles":false,"softParticles":false,"softFarFade":0,"softNearFade":0,"behaviors":[{"type":"ForceOverLife","x":{"type":"ConstantValue","value":0},"y":{"type":"ConstantValue","value":0},"z":{"type":"ConstantValue","value":0}},{"type":"SizeOverLife","size":{"type":"PiecewiseBezier","functions":[{"function":{"p0":0.8495575,"p1":0.8495575,"p2":1,"p3":1},"start":0},{"function":{"p0":1,"p1":1,"p2":0,"p3":0},"start":0.49871457}]}},{"type":"RotationOverLife","angularVelocity":{"type":"IntervalValue","a":-3.1415925,"b":3.1415925}},{"type":"ColorOverLife","color":{"type":"Gradient","color":{"type":"CLinearFunction","subType":"Color","keys":[{"value":{"r":1,"g":1,"b":1},"pos":0},{"value":{"r":0.59607846,"g":1,"b":0.050980393},"pos":0.4587167162584878},{"value":{"r":0,"g":1,"b":0.047058824},"pos":0.9518272678721293}]},"alpha":{"type":"CLinearFunction","subType":"Number","keys":[{"value":0,"pos":0},{"value":1,"pos":0.41690699626153965},{"value":1,"pos":0.7580224307621881},{"value":0,"pos":1}]}}}],"worldSpace":true}}]},{"uuid":"cfe42db9-925f-4bd2-bc92-3d15a4e2b795","type":"Group","name":"GlowCircle","layers":1,"matrix":[1,0,0,0,0,-5.321248014494817e-8,-1.0000000532124802,0,0,1.0000000532124802,-5.321248014494817e-8,0,0,0,0,1],"up":[0,1,0],"children":[{"uuid":"94cac5fe-52a9-431d-ba8a-19fe44a2cdc1","type":"ParticleEmitter","name":"GlowCircleEmitter","layers":1,"matrix":[1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1],"up":[0,1,0],"ps":{"version":"3.0","autoDestroy":false,"looping":true,"prewarm":false,"duration":2,"shape":{"type":"cone","radius":0.01,"arc":6.283185307179586,"thickness":1,"angle":0.06981317007977318,"mode":0,"spread":0,"speed":{"type":"ConstantValue","value":1}},"startLife":{"type":"ConstantValue","value":2},"startSpeed":{"type":"ConstantValue","value":0},"startRotation":{"type":"Euler","angleX":{"type":"IntervalValue","a":0,"b":0},"angleY":{"type":"IntervalValue","a":0,"b":0},"angleZ":{"type":"IntervalValue","a":0,"b":6.283185},"eulerOrder":"XYZ"},"startSize":{"type":"ConstantValue","value":4.1},"startColor":{"type":"ConstantColor","color":{"r":0.45882353,"g":1,"b":0.28627452,"a":0.4509804}},"emissionOverTime":{"type":"ConstantValue","value":1},"emissionOverDistance":{"type":"ConstantValue","value":0},"emissionBursts":[],"onlyUsedByOther":false,"instancingGeometry":"f40b6ee0-aa01-46e0-b05a-d938b54eec83","renderOrder":0,"renderMode":2,"rendererEmitterSettings":{},"material":"6d9283b7-81c2-4063-84cc-f696054ce6f6","layers":1,"startTileIndex":{"type":"ConstantValue","value":0},"uTileCount":1,"vTileCount":1,"blendTiles":false,"softParticles":false,"softFarFade":0,"softNearFade":0,"behaviors":[{"type":"ForceOverLife","x":{"type":"ConstantValue","value":0},"y":{"type":"ConstantValue","value":0},"z":{"type":"ConstantValue","value":0}},{"type":"ColorOverLife","color":{"type":"Gradient","color":{"type":"CLinearFunction","subType":"Color","keys":[{"value":{"r":1,"g":1,"b":1},"pos":0},{"value":{"r":1,"g":1,"b":1},"pos":1}]},"alpha":{"type":"CLinearFunction","subType":"Number","keys":[{"value":0,"pos":0},{"value":1,"pos":0.5014572365911345},{"value":0,"pos":1}]}}}],"worldSpace":true}}]},{"uuid":"c86a5eb7-2571-4e87-b5fd-e68a0f965b0a","type":"ParticleEmitter","name":"MagicZoneGreenEmitter","layers":1,"matrix":[1,0,0,0,0,-2.220446049250313e-16,-1,0,0,1,-2.220446049250313e-16,0,0,0,0,1],"up":[0,1,0],"ps":{"version":"3.0","autoDestroy":false,"looping":true,"prewarm":false,"duration":5,"shape":{"type":"point"},"startLife":{"type":"ConstantValue","value":1},"startSpeed":{"type":"ConstantValue","value":0},"startRotation":{"type":"Euler","angleX":{"type":"IntervalValue","a":1.5707963,"b":1.5707963},"angleY":{"type":"IntervalValue","a":0,"b":6.283185},"angleZ":{"type":"IntervalValue","a":0,"b":0},"eulerOrder":"XYZ"},"startSize":{"type":"ConstantValue","value":2.8},"startColor":{"type":"ConstantColor","color":{"r":1,"g":1,"b":1,"a":1}},"emissionOverTime":{"type":"ConstantValue","value":2.5},"emissionOverDistance":{"type":"ConstantValue","value":0},"emissionBursts":[],"onlyUsedByOther":false,"instancingGeometry":"780917d8-bd1b-4d63-8aca-f79e3211f964","renderOrder":0,"renderMode":2,"rendererEmitterSettings":{},"material":"7442c205-fb42-4fb9-baec-82a192b81351","layers":1,"startTileIndex":{"type":"ConstantValue","value":0},"uTileCount":1,"vTileCount":1,"blendTiles":false,"softParticles":false,"softFarFade":0,"softNearFade":0,"behaviors":[{"type":"ForceOverLife","x":{"type":"ConstantValue","value":0},"y":{"type":"ConstantValue","value":0},"z":{"type":"ConstantValue","value":0}},{"type":"ColorOverLife","color":{"type":"Gradient","color":{"type":"CLinearFunction","subType":"Color","keys":[{"value":{"r":0.6156863,"g":1,"b":0},"pos":0},{"value":{"r":0.101960786,"g":1,"b":0.10980392},"pos":1}]},"alpha":{"type":"CLinearFunction","subType":"Number","keys":[{"value":0,"pos":0.004592965590905623},{"value":1,"pos":0.5014572365911345},{"value":0,"pos":1}]}}}],"worldSpace":true}}]}} \ No newline at end of file From 20731b1c602516ea5159dda215860b832a2b3051 Mon Sep 17 00:00:00 2001 From: Mikalai Lazitski Date: Mon, 8 Dec 2025 11:42:36 +0300 Subject: [PATCH 07/62] feat: implement Three.js JSON conversion for FX Editor, enabling particle and group data management --- editor/src/editor/windows/fx-editor/loader.ts | 942 ++++++++++++++++++ 1 file changed, 942 insertions(+) create mode 100644 editor/src/editor/windows/fx-editor/loader.ts diff --git a/editor/src/editor/windows/fx-editor/loader.ts b/editor/src/editor/windows/fx-editor/loader.ts new file mode 100644 index 000000000..114b88c71 --- /dev/null +++ b/editor/src/editor/windows/fx-editor/loader.ts @@ -0,0 +1,942 @@ +import { Vector3, Color4, Matrix, Quaternion } from "babylonjs"; +import { readJSON } from "fs-extra"; +import { IFXParticleData, IFXGroupData } from "./properties/types"; + +interface IThreeJSObject { + type: string; + name?: string; + uuid?: string; + matrix?: number[]; + visible?: boolean; + children?: IThreeJSObject[]; + ps?: IQuarksParticleSystem; +} + +interface IQuarksParticleSystem { + version?: string; + autoDestroy?: boolean; + looping?: boolean; + prewarm?: boolean; + duration?: number; + shape?: IQuarksShape; + startLife?: IQuarksValue; + startSpeed?: IQuarksValue; + startRotation?: IQuarksValue; + startSize?: IQuarksValue; + startColor?: IQuarksColor; + emissionOverTime?: IQuarksValue; + emissionOverDistance?: IQuarksValue; + emissionBursts?: IQuarksBurst[]; + onlyUsedByOther?: boolean; + instancingGeometry?: string; + renderOrder?: number; + renderMode?: number; + rendererEmitterSettings?: any; + material?: string; + layers?: number; + startTileIndex?: IQuarksValue; + uTileCount?: number; + vTileCount?: number; + blendTiles?: boolean; + softParticles?: boolean; + softFarFade?: number; + softNearFade?: number; + behaviors?: IQuarksBehavior[]; + worldSpace?: boolean; +} + +interface IQuarksShape { + type: string; + radius?: number; + arc?: number; + thickness?: number; + angle?: number; + mode?: number; + spread?: number; + speed?: IQuarksValue; + width?: number; + height?: number; + depth?: number; + boxWidth?: number; + boxHeight?: number; + boxDepth?: number; +} + +interface IQuarksValue { + type: string; + value?: number; + a?: number; + b?: number; + functions?: Array<{ + function: { + p0: number; + p1: number; + p2: number; + p3: number; + }; + start: number; + }>; +} + +interface IQuarksColor { + type: string; + color?: { + r: number; + g: number; + b: number; + a?: number; + } | { + type: string; + subType?: string; + keys?: Array<{ + value: { + r: number; + g: number; + b: number; + } | number; + pos: number; + }>; + }; + color1?: { + r: number; + g: number; + b: number; + a?: number; + }; + color2?: { + r: number; + g: number; + b: number; + a?: number; + }; + alpha?: { + type: string; + subType?: string; + keys?: Array<{ + value: number; + pos: number; + }>; + }; + keys?: Array<{ + value: { + r: number; + g: number; + b: number; + } | number; + pos: number; + }>; +} + +interface IQuarksBurst { + time?: number; + count?: IQuarksValue; + cycle?: number; + interval?: number; + probability?: number; +} + +interface IQuarksBehavior { + type: string; + [key: string]: any; +} + +interface IThreeJSJSON { + metadata?: { + version?: number; + type?: string; + generator?: string; + }; + object?: IThreeJSObject; + materials?: any[]; + textures?: any[]; + images?: any[]; + geometries?: any[]; +} + +export interface IConvertedNode { + id: string; + name: string; + type: "particle" | "group" | "texture" | "geometry"; + parentId?: string; + particleData?: IFXParticleData; + groupData?: IFXGroupData; + resourceData?: { + uuid: string; + path?: string; + type: "texture" | "geometry"; + }; + children?: IConvertedNode[]; +} + +export interface IConvertedData { + nodes: IConvertedNode[]; + resources: IConvertedNode[]; + materials: Array<{ + uuid: string; + type: string; + color?: number; + map?: string; + blending?: number; + side?: number; + transparent?: boolean; + depthWrite?: boolean; + opacity?: number; + }>; + textures: Array<{ + uuid: string; + name?: string; + image?: string; + imageUrl?: string; + }>; +} + +/** + * Converts a Three.js JSON file (from quarks) to FX editor format + */ +export async function convertThreeJSJSONToFXEditor(filePath: string): Promise { + const json: IThreeJSJSON = await readJSON(filePath); + + if (!json.object) { + return { nodes: [], resources: [], materials: [], textures: [] }; + } + + const convertedNodes: IConvertedNode[] = []; + const usedResources = new Set(); // Track used texture and geometry UUIDs + + _convertObject(json.object, null, convertedNodes, json, usedResources); + + // Add resource nodes for textures and geometries + const resourceNodes: IConvertedNode[] = []; + + // Extract materials data + const materialsData: IConvertedData["materials"] = []; + if (json.materials) { + json.materials.forEach((material) => { + if (usedResources.has(material.uuid)) { + materialsData.push({ + uuid: material.uuid, + type: material.type || "MeshStandardMaterial", + color: material.color, + map: material.map, + blending: material.blending, + side: material.side, + transparent: material.transparent, + depthWrite: material.depthWrite, + opacity: material.opacity, + }); + } + }); + } + + // Extract textures data + const texturesData: IConvertedData["textures"] = []; + if (json.textures && json.images) { + json.textures.forEach((texture) => { + if (usedResources.has(texture.uuid)) { + const image = json.images?.find((img) => img.uuid === texture.image); + const imagePath = image?.url || image?.name || texture.name || texture.uuid; + + texturesData.push({ + uuid: texture.uuid, + name: texture.name, + image: texture.image, + imageUrl: imagePath, + }); + + resourceNodes.push({ + id: `texture-${texture.uuid}`, + name: texture.name || imagePath || `Texture ${texture.uuid.substring(0, 8)}`, + type: "texture", + resourceData: { + uuid: texture.uuid, + path: imagePath, + type: "texture", + }, + }); + } + }); + } + + // Add geometries + if (json.geometries) { + json.geometries.forEach((geometry) => { + if (usedResources.has(geometry.uuid)) { + resourceNodes.push({ + id: `geometry-${geometry.uuid}`, + name: geometry.name || `Geometry ${geometry.uuid.substring(0, 8)}`, + type: "geometry", + resourceData: { + uuid: geometry.uuid, + type: "geometry", + }, + }); + } + }); + } + + return { nodes: convertedNodes, resources: resourceNodes, materials: materialsData, textures: texturesData }; +} + +/** + * Decomposes a 4x4 transformation matrix into position, rotation (Euler angles), and scale + */ +function _decomposeMatrix(matrixArray: number[]): { position: Vector3; rotation: Vector3; scale: Vector3 } { + const position = Vector3.Zero(); + const rotationQuat = Quaternion.Identity(); + const scaling = Vector3.Zero(); + + const matrix = Matrix.FromArray(matrixArray); + matrix.decompose(scaling, rotationQuat, position); + + // Convert Quaternion to Euler angles (in degrees) + const rotation = rotationQuat.toEulerAngles(); + rotation.scaleInPlace(180 / Math.PI); // Convert radians to degrees + + return { + position, + rotation, + scale: scaling, + }; +} + +/** + * Recursively converts Three.js objects to FX editor nodes + */ +function _convertObject( + obj: IThreeJSObject, + parentId: string | null, + convertedNodes: IConvertedNode[], + json: IThreeJSJSON, + usedResources: Set +): void { + if (obj.type === "ParticleEmitter" && obj.ps) { + // Convert particle emitter + const nodeId = `particle-${obj.uuid || Date.now()}-${Math.random()}`; + const particleData = _convertParticleSystem(obj.ps, obj.name || "Particle", json); + particleData.id = nodeId; + particleData.name = obj.name || "Particle"; + + // Extract position, rotation, scale from matrix if available + if (obj.matrix && obj.matrix.length >= 16) { + const { position, rotation, scale } = _decomposeMatrix(obj.matrix); + particleData.position = position; + particleData.rotation = rotation; + particleData.scale = scale; + } + + const node: IConvertedNode = { + id: nodeId, + name: obj.name || "Particle", + type: "particle", + parentId: parentId || undefined, + particleData, + }; + + // Track used resources + if (particleData.particleRenderer.texture?.uuid) { + usedResources.add(particleData.particleRenderer.texture.uuid); + } + if (particleData.particleRenderer.material?.uuid) { + usedResources.add(particleData.particleRenderer.material.uuid); + // Also track texture from material + if (json.materials) { + const material = json.materials.find((m) => m.uuid === particleData.particleRenderer.material?.uuid); + if (material?.map && json.textures) { + usedResources.add(material.map); + } + } + } + if (particleData.particleRenderer.meshPath) { + usedResources.add(particleData.particleRenderer.meshPath); + } + if (particleData.emitterShape.meshPath) { + usedResources.add(particleData.emitterShape.meshPath); + } + + // Process children + if (obj.children) { + node.children = []; + obj.children.forEach((child) => { + _convertObject(child, nodeId, node.children!, json, usedResources); + }); + } + + convertedNodes.push(node); + } else if (obj.type === "Group") { + // Convert group + const nodeId = `group-${obj.uuid || Date.now()}-${Math.random()}`; + + // Create group data with default values + const groupData: IFXGroupData = { + type: "group", + id: nodeId, + name: obj.name || "Group", + visibility: obj.visible !== false, // Three.js uses 'visible' property + position: new Vector3(0, 0, 0), + rotation: new Vector3(0, 0, 0), + scale: new Vector3(1, 1, 1), + }; + + // Extract position, rotation, scale from matrix if available + if (obj.matrix && obj.matrix.length >= 16) { + const { position, rotation, scale } = _decomposeMatrix(obj.matrix); + groupData.position = position; + groupData.rotation = rotation; + groupData.scale = scale; + } + + const node: IConvertedNode = { + id: nodeId, + name: obj.name || "Group", + type: "group", + parentId: parentId || undefined, + groupData, + children: [], + }; + + // Process children + if (obj.children) { + obj.children.forEach((child) => { + _convertObject(child, nodeId, node.children!, json, usedResources); + }); + } + + convertedNodes.push(node); + } else if (obj.children) { + // Process children of other object types + obj.children.forEach((child) => { + _convertObject(child, parentId, convertedNodes, json, usedResources); + }); + } +} + +/** + * Converts a quarks particle system to FX editor particle data + */ +function _convertParticleSystem(ps: IQuarksParticleSystem, name: string, json: IThreeJSJSON): IFXParticleData { + // Convert emitter shape + const emitterShape = _convertEmitterShape(ps.shape); + + // Convert particle renderer + const particleRenderer = _convertParticleRenderer(ps, json); + + // Convert emission + const emission = { + looping: ps.looping ?? true, + duration: ps.duration ?? 5.0, + prewarm: ps.prewarm ?? false, + onlyUsedByOtherSystem: ps.onlyUsedByOther ?? false, + emitOverTime: _extractConstantValue(ps.emissionOverTime) ?? 10, + emitOverDistance: _extractConstantValue(ps.emissionOverDistance) ?? 0, + }; + + // Convert bursts + const bursts = (ps.emissionBursts || []).map((burst, index) => ({ + id: `burst-${Date.now()}-${index}`, + time: burst.time ?? 0, + count: _extractConstantValue(burst.count) ?? 1, + cycle: burst.cycle ?? 1, + interval: burst.interval ?? 0, + probability: burst.probability ?? 1.0, + })); + + // Convert particle initialization + const particleInitialization = { + startLife: _convertQuarksValueToFunction(ps.startLife) ?? { + functionType: "IntervalValue", + data: { min: 1.0, max: 2.0 }, + }, + startSize: _convertQuarksValueToFunction(ps.startSize) ?? { + functionType: "IntervalValue", + data: { min: 0.1, max: 0.2 }, + }, + startSpeed: _convertQuarksValueToFunction(ps.startSpeed) ?? { + functionType: "IntervalValue", + data: { min: 1.0, max: 2.0 }, + }, + startColor: _convertQuarksColorToColorFunction(ps.startColor) ?? { + colorFunctionType: "ConstantColor", + data: { color: new Color4(1, 1, 1, 1) }, + }, + startRotation: _convertQuarksValueToFunction(ps.startRotation) ?? { + functionType: "IntervalValue", + data: { min: 0, max: 360 }, + }, + }; + + // Convert behaviors + const behaviors = (ps.behaviors || []).map((behavior, index) => ({ + id: `behavior-${Date.now()}-${index}`, + type: _convertBehaviorType(behavior.type), + ..._convertBehavior(behavior), + })); + + return { + type: "particle", + id: "", + name, + visibility: true, + position: new Vector3(0, 0, 0), + rotation: new Vector3(0, 0, 0), + scale: new Vector3(1, 1, 1), + emitterShape, + particleRenderer, + emission, + bursts, + particleInitialization, + behaviors, + }; +} + +/** + * Converts quarks emitter shape to FX editor format + */ +function _convertEmitterShape(shape?: IQuarksShape): IFXParticleData["emitterShape"] { + if (!shape) { + return { shape: "Box" }; + } + + const shapeType = shape.type?.toLowerCase() || "box"; + + switch (shapeType) { + case "cone": + return { + shape: "Cone", + radius: shape.radius ?? 1.0, + angle: shape.angle ?? 0.785398, + radiusRange: shape.thickness ?? 0.0, + heightRange: 0.0, + emitFromSpawnPointOnly: shape.mode === 1, + }; + + case "box": + return { + shape: "Box", + direction1: new Vector3(0, 1, 0), + direction2: new Vector3(0, 1, 0), + minEmitBox: new Vector3( + -(shape.boxWidth ?? shape.width ?? 1.0) / 2, + -(shape.boxHeight ?? shape.height ?? 1.0) / 2, + -(shape.boxDepth ?? shape.depth ?? 1.0) / 2 + ), + maxEmitBox: new Vector3( + (shape.boxWidth ?? shape.width ?? 1.0) / 2, + (shape.boxHeight ?? shape.height ?? 1.0) / 2, + (shape.boxDepth ?? shape.depth ?? 1.0) / 2 + ), + }; + + case "sphere": + return { + shape: "Sphere", + radius: shape.radius ?? 1.0, + }; + + case "hemisphere": + case "hemispheric": + return { + shape: "Hemispheric", + radius: shape.radius ?? 1.0, + }; + + case "cylinder": + return { + shape: "Cylinder", + radius: shape.radius ?? 1.0, + height: shape.height ?? 1.0, + directionRandomizer: 0.0, + }; + + case "point": + return { + shape: "Point", + }; + + default: + return { shape: "Box" }; + } +} + +/** + * Converts quarks particle renderer to FX editor format + */ +function _convertParticleRenderer(ps: IQuarksParticleSystem, json: IThreeJSJSON): IFXParticleData["particleRenderer"] { + // Convert render mode + const renderModeMap: Record = { + 0: "Billboard", + 1: "Stretched Billboard", + 2: "Mesh", + 3: "Trail", + }; + const renderMode = renderModeMap[ps.renderMode ?? 0] || "Billboard"; + + // Extract texture from material if available + let texture: any = null; + if (ps.material && json.materials) { + const material = json.materials.find((m) => m.uuid === ps.material); + if (material && material.map && json.textures) { + const textureData = json.textures.find((t) => t.uuid === material.map); + if (textureData && textureData.image && json.images) { + const image = json.images.find((img) => img.uuid === textureData.image); + // Store image path or data for later processing + texture = { + uuid: textureData.uuid, + image: image, + }; + } + } + } + + // Extract start tile index + const startTileIndex = _extractConstantValue(ps.startTileIndex) ?? 0; + + // Extract material type from material if available + // In quarks, materials can be MeshBasicMaterial or MeshStandardMaterial + let materialType = "MeshStandardMaterial"; // Default + if (ps.material && json.materials) { + const material = json.materials.find((m) => m.uuid === ps.material); + if (material && material.type) { + // Map quarks material types to our format + const materialTypeMap: Record = { + MeshBasicMaterial: "MeshBasicMaterial", + MeshStandardMaterial: "MeshStandardMaterial", + // Fallback for other material types + "MeshLambertMaterial": "MeshStandardMaterial", + "MeshPhongMaterial": "MeshStandardMaterial", + }; + materialType = materialTypeMap[material.type] || "MeshStandardMaterial"; + } + } + + return { + renderMode, + worldSpace: ps.worldSpace ?? false, + material: ps.material ? { uuid: ps.material } : null, + materialType, + transparent: true, + opacity: 1.0, + side: "Double", + blending: "Add", + color: new Color4(1, 1, 1, 1), + renderOrder: ps.renderOrder ?? 0, + uvTile: { + column: ps.uTileCount ?? 1, + row: ps.vTileCount ?? 1, + startTileIndex, + blendTiles: ps.blendTiles ?? false, + }, + texture, + meshPath: ps.instancingGeometry || null, // Store geometry UUID for now + softParticles: ps.softParticles ?? false, + }; +} + +/** + * Converts quarks behavior to FX editor format + */ +function _convertBehavior(behavior: IQuarksBehavior): any { + const converted: any = {}; + + switch (behavior.type) { + case "ForceOverLife": + const x = _convertQuarksValueToFunction(behavior.x) || { + functionType: "ConstantValue", + data: { value: 0 }, + }; + const y = _convertQuarksValueToFunction(behavior.y) || { + functionType: "ConstantValue", + data: { value: 0 }, + }; + const z = _convertQuarksValueToFunction(behavior.z) || { + functionType: "ConstantValue", + data: { value: 0 }, + }; + return { + force: { + functionType: "Vector3Function", + data: { + x, + y, + z, + }, + }, + }; + + case "SizeOverLife": + // Convert size value using _convertQuarksValueToFunction + const sizeFunction = _convertQuarksValueToFunction(behavior.size) || { + functionType: "ConstantValue", + data: { value: 1 }, + }; + return { + size: sizeFunction, + }; + + case "RotationOverLife": + const angularVelocity = _convertQuarksValueToFunction(behavior.angularVelocity) || { + functionType: "IntervalValue", + data: { min: 0, max: 0 }, + }; + return { + angularVelocity, + }; + + case "ColorOverLife": + // Convert color function from quarks format to our format + const colorFunction = _convertQuarksColorToColorFunction(behavior.color) || { + colorFunctionType: "ConstantColor", + data: { color: new Color4(1, 1, 1, 1) }, + }; + return { + color: colorFunction, + }; + + case "ColorBySpeed": + // Convert color function from quarks format to our format + const colorBySpeedFunction = _convertQuarksColorToColorFunction(behavior.color) || { + colorFunctionType: "ConstantColor", + data: { color: new Color4(1, 1, 1, 1) }, + }; + return { + color: colorBySpeedFunction, + }; + + case "ApplyForce": + const magnitude = _convertQuarksValueToFunction(behavior.magnitude) || { + functionType: "ConstantValue", + data: { value: 1 }, + }; + return { + magnitude, + direction: behavior.direction + ? new Vector3(behavior.direction.x || 0, behavior.direction.y || 0, behavior.direction.z || 0) + : new Vector3(0, 1, 0), + }; + + case "Noise": + const frequency = _convertQuarksValueToFunction(behavior.frequency) || { + functionType: "ConstantValue", + data: { value: 1 }, + }; + const power = _convertQuarksValueToFunction(behavior.power) || { + functionType: "ConstantValue", + data: { value: 1 }, + }; + const positionAmount = _convertQuarksValueToFunction(behavior.positionAmount) || { + functionType: "ConstantValue", + data: { value: 1 }, + }; + const rotationAmount = _convertQuarksValueToFunction(behavior.rotationAmount) || { + functionType: "ConstantValue", + data: { value: 1 }, + }; + return { + frequency, + power, + positionAmount, + rotationAmount, + }; + + case "GravityForce": + const gravity = _convertQuarksValueToFunction(behavior.gravity) || { + functionType: "ConstantValue", + data: { value: -9.81 }, + }; + return { + gravity, + }; + + case "TurbulenceField": + const strength = _convertQuarksValueToFunction(behavior.strength) || { + functionType: "ConstantValue", + data: { value: 1 }, + }; + const size = _convertQuarksValueToFunction(behavior.size) || { + functionType: "ConstantValue", + data: { value: 1 }, + }; + return { + strength, + size, + }; + + default: + // Copy all properties as-is for unknown behaviors + Object.keys(behavior).forEach((key) => { + if (key !== "type") { + converted[key] = behavior[key]; + } + }); + return converted; + } +} + +/** + * Converts quarks behavior type name to FX editor format + */ +function _convertBehaviorType(quarksType: string): string { + const typeMap: Record = { + ForceOverLife: "ForceOverLife", + SizeOverLife: "SizeOverLife", + RotationOverLife: "RotationOverLife", + ColorOverLife: "ColorOverLife", + ColorBySpeed: "ColorBySpeed", + ApplyForce: "ApplyForce", + Noise: "Noise", + GravityForce: "GravityForce", + TurbulenceField: "TurbulenceField", + }; + + return typeMap[quarksType] || quarksType; +} + +/** + * Converts quarks value to function format + */ +function _convertQuarksValueToFunction(value?: IQuarksValue): any | null { + if (!value) { + return null; + } + + if (value.type === "ConstantValue" && value.value !== undefined) { + return { + functionType: "ConstantValue", + data: { value: value.value }, + }; + } + + if (value.type === "IntervalValue" && value.a !== undefined && value.b !== undefined) { + return { + functionType: "IntervalValue", + data: { min: value.a, max: value.b }, + }; + } + + if (value.type === "PiecewiseBezier" && value.functions && value.functions.length > 0) { + // Use the first function segment + const firstSegment = value.functions[0]; + return { + functionType: "PiecewiseBezier", + data: { + function: firstSegment.function || { p0: 0, p1: 1.0 / 3, p2: (1.0 / 3) * 2, p3: 1 }, + }, + }; + } + + return null; +} + +/** + * Converts quarks color to color function format + */ +function _convertQuarksColorToColorFunction(color?: IQuarksColor): any | null { + if (!color) { + return null; + } + + if (color.type === "ConstantColor" && color.color && typeof color.color === "object" && "r" in color.color) { + const colorObj = color.color as { r: number; g: number; b: number; a?: number }; + return { + colorFunctionType: "ConstantColor", + data: { + color: new Color4(colorObj.r ?? 1, colorObj.g ?? 1, colorObj.b ?? 1, colorObj.a ?? 1), + }, + }; + } + + if (color.type === "ColorRange" && color.color1 && color.color2) { + return { + colorFunctionType: "ColorRange", + data: { + colorA: new Color4(color.color1.r ?? 0, color.color1.g ?? 0, color.color1.b ?? 0, color.color1.a ?? 1), + colorB: new Color4(color.color2.r ?? 1, color.color2.g ?? 1, color.color2.b ?? 1, color.color2.a ?? 1), + }, + }; + } + + if (color.type === "Gradient") { + // Convert quarks Gradient to our Gradient format + // Gradient has color and alpha as CLinearFunction objects with keys + const colorKeys: any[] = []; + const alphaKeys: any[] = []; + + // Extract color keys from color.color.keys (CLinearFunction) + if (color.color && typeof color.color === "object" && "keys" in color.color && Array.isArray(color.color.keys)) { + colorKeys.push( + ...color.color.keys.map((key: any) => { + if (typeof key.value === "object" && key.value.r !== undefined) { + return { + color: new Vector3(key.value.r ?? 0, key.value.g ?? 0, key.value.b ?? 0), + position: key.pos ?? 0, + }; + } + return { + color: new Vector3(0, 0, 0), + position: key.pos ?? 0, + }; + }) + ); + } + + // Extract alpha keys from color.alpha.keys (CLinearFunction) + if (color.alpha && typeof color.alpha === "object" && "keys" in color.alpha && Array.isArray(color.alpha.keys)) { + alphaKeys.push( + ...color.alpha.keys.map((key: any) => ({ + value: typeof key.value === "number" ? key.value : 1, + position: key.pos ?? 0, + })) + ); + } + + // Fallback to default if no keys found + if (colorKeys.length === 0) { + colorKeys.push( + { color: new Vector3(0, 0, 0), position: 0 }, + { color: new Vector3(1, 1, 1), position: 1 } + ); + } + if (alphaKeys.length === 0) { + alphaKeys.push( + { value: 1, position: 0 }, + { value: 1, position: 1 } + ); + } + + const convertedGradient: any = { + colorFunctionType: "Gradient", + data: { + colorKeys, + alphaKeys, + }, + }; + return convertedGradient; + } + + // RandomColor - similar to ColorRange but selects random from range + if (color.type === "RandomColor" && color.color1 && color.color2) { + return { + colorFunctionType: "RandomColor", + data: { + colorA: new Color4(color.color1.r ?? 0, color.color1.g ?? 0, color.color1.b ?? 0, color.color1.a ?? 1), + colorB: new Color4(color.color2.r ?? 1, color.color2.g ?? 1, color.color2.b ?? 1, color.color2.a ?? 1), + }, + }; + } + + return null; +} + +/** + * Extracts constant value from quarks value + */ +function _extractConstantValue(value?: IQuarksValue): number | null { + if (!value) { + return null; + } + if (value.type === "ConstantValue" && value.value !== undefined) { + return value.value; + } + return null; +} + + From f85ea1a76011ec996b5aa1430c385801e38987e9 Mon Sep 17 00:00:00 2001 From: Mikalai Lazitski Date: Mon, 8 Dec 2025 13:16:43 +0300 Subject: [PATCH 08/62] feat: enhance FX Editor layout and properties management with state updates and matrix decomposition logging --- .../src/editor/windows/fx-editor/layout.tsx | 30 +++++++-- editor/src/editor/windows/fx-editor/loader.ts | 61 ++++++++++++++++++- .../editor/windows/fx-editor/properties.tsx | 24 +++++++- 3 files changed, 103 insertions(+), 12 deletions(-) diff --git a/editor/src/editor/windows/fx-editor/layout.tsx b/editor/src/editor/windows/fx-editor/layout.tsx index d1be08690..b786b7cff 100644 --- a/editor/src/editor/windows/fx-editor/layout.tsx +++ b/editor/src/editor/windows/fx-editor/layout.tsx @@ -109,6 +109,7 @@ export interface IFXEditorLayoutProps { export interface IFXEditorLayoutState { selectedNodeId: string | number | null; resources: any[]; + propertiesKey: number; } export class FXEditorLayout extends Component { @@ -128,6 +129,7 @@ export class FXEditorLayout extends Component { - this.setState({ selectedNodeId: nodeId }, () => { - // Force update properties component after state change - if (this.properties) { - this.properties.forceUpdate(); + this.setState( + (prevState) => ({ + selectedNodeId: nodeId, + propertiesKey: prevState.propertiesKey + 1, // Increment key to force component recreation + }), + () => { + // Update components immediately after state change + this._updateComponents(); + // Force update properties component after state change + if (this.properties) { + this.properties.forceUpdate(); + } + // Force update layout to ensure flexlayout-react sees the new component + this.forceUpdate(); } - }); + ); }; private _updateComponents(): void { @@ -182,7 +194,7 @@ export class FXEditorLayout extends Component (this.animation = r!)} filePath={this.props.filePath} />, properties: ( (this.properties = r!)} filePath={this.props.filePath} selectedNodeId={this.state.selectedNodeId} @@ -215,6 +227,12 @@ export class FXEditorLayout extends ComponentError, see console...; } + // Always update components before returning, especially for properties tab + // This ensures flexlayout-react gets the latest component with updated props + if (componentName === "properties") { + this._updateComponents(); + } + const component = this._components[componentName]; if (!component) { return
Error, see console...
; diff --git a/editor/src/editor/windows/fx-editor/loader.ts b/editor/src/editor/windows/fx-editor/loader.ts index 114b88c71..e9eeb9044 100644 --- a/editor/src/editor/windows/fx-editor/loader.ts +++ b/editor/src/editor/windows/fx-editor/loader.ts @@ -279,18 +279,72 @@ export async function convertThreeJSJSONToFXEditor(filePath: string): Promise= 1) { + pitch = Math.sign(sinp) * Math.PI / 2; + } else { + pitch = Math.asin(sinp); + } + + // Yaw (Z) + const siny_cosp = 2 * (qw * qz + qx * qy); + const cosy_cosp = 1 - 2 * (qy * qy + qz * qz); + const yaw = Math.atan2(siny_cosp, cosy_cosp); + + console.log("[_decomposeMatrix] XYZ Euler (rad):", roll, pitch, yaw); + + // Store rotation in RADIANS (not degrees) because EditorInspectorNumberField with asDegrees expects radians + // The component will automatically convert radians to degrees for display + const rotation = new Vector3(roll, pitch, yaw); - // Convert Quaternion to Euler angles (in degrees) - const rotation = rotationQuat.toEulerAngles(); - rotation.scaleInPlace(180 / Math.PI); // Convert radians to degrees + console.log("[_decomposeMatrix] Final rotation (rad):", rotation.x, rotation.y, rotation.z); + console.log("[_decomposeMatrix] Final rotation (deg for reference):", rotation.x * 180 / Math.PI, rotation.y * 180 / Math.PI, rotation.z * 180 / Math.PI); return { position, @@ -379,6 +433,7 @@ function _convertObject( // Extract position, rotation, scale from matrix if available if (obj.matrix && obj.matrix.length >= 16) { + console.log("[_convertObject] Group matrix:", obj.name); const { position, rotation, scale } = _decomposeMatrix(obj.matrix); groupData.position = position; groupData.rotation = rotation; diff --git a/editor/src/editor/windows/fx-editor/properties.tsx b/editor/src/editor/windows/fx-editor/properties.tsx index 5d360f898..7551a889a 100644 --- a/editor/src/editor/windows/fx-editor/properties.tsx +++ b/editor/src/editor/windows/fx-editor/properties.tsx @@ -29,8 +29,27 @@ export class FXEditorProperties extends Component { + this.forceUpdate(); + }, 0); + } + } + + public componentDidMount(): void { + // Force update on mount if a node is already selected + if (this.props.selectedNodeId) { + this.forceUpdate(); + } + } + public render(): ReactNode { - if (!this.props.selectedNodeId) { + const nodeId = this.props.selectedNodeId; + + if (!nodeId) { return (

No particle selected

@@ -38,8 +57,7 @@ export class FXEditorProperties extends Component Date: Thu, 11 Dec 2025 13:35:28 +0300 Subject: [PATCH 09/62] feat: integrate VFX system into FX Editor with enhanced particle behaviors, geometry handling, and new types for improved visual effects management --- .../editor/windows/fx-editor/VFX/VFXEffect.ts | 96 ++ .../fx-editor/VFX/behaviors/colorBySpeed.ts | 73 ++ .../fx-editor/VFX/behaviors/colorOverLife.ts | 78 ++ .../fx-editor/VFX/behaviors/forceOverLife.ts | 36 + .../fx-editor/VFX/behaviors/frameOverLife.ts | 36 + .../windows/fx-editor/VFX/behaviors/index.ts | 19 + .../VFX/behaviors/limitSpeedOverLife.ts | 36 + .../fx-editor/VFX/behaviors/orbitOverLife.ts | 100 ++ .../VFX/behaviors/rotationBySpeed.ts | 61 ++ .../VFX/behaviors/rotationOverLife.ts | 32 + .../fx-editor/VFX/behaviors/sizeBySpeed.ts | 51 + .../fx-editor/VFX/behaviors/sizeOverLife.ts | 60 ++ .../fx-editor/VFX/behaviors/speedOverLife.ts | 92 ++ .../windows/fx-editor/VFX/behaviors/utils.ts | 166 ++++ .../factories/VFXBehaviorFunctionFactory.ts | 189 ++++ .../VFX/factories/VFXEmitterFactory.ts | 596 ++++++++++++ .../VFX/factories/VFXGeometryFactory.ts | 150 +++ .../VFX/factories/VFXMaterialFactory.ts | 366 ++++++++ .../src/editor/windows/fx-editor/VFX/index.ts | 13 + .../fx-editor/VFX/loggers/VFXLogger.ts | 38 + .../fx-editor/VFX/parsers/VFXDataConverter.ts | 570 ++++++++++++ .../fx-editor/VFX/parsers/VFXParser.ts | 123 +++ .../fx-editor/VFX/parsers/VFXValueParser.ts | 187 ++++ .../VFX/processors/VFXHierarchyProcessor.ts | 294 ++++++ .../VFX/systems/VFXParticleSystem.ts | 21 + .../VFX/systems/VFXSolidParticleSystem.ts | 538 +++++++++++ .../VFX/treejs3dobject.particle.json | 870 ++++++++++++++++++ .../VFX/types/VFXBehaviorFunction.ts | 32 + .../windows/fx-editor/VFX/types/behaviors.ts | 138 +++ .../windows/fx-editor/VFX/types/colors.ts | 10 + .../windows/fx-editor/VFX/types/context.ts | 17 + .../windows/fx-editor/VFX/types/emitter.ts | 20 + .../fx-editor/VFX/types/emitterConfig.ts | 50 + .../windows/fx-editor/VFX/types/factories.ts | 37 + .../windows/fx-editor/VFX/types/gradients.ts | 9 + .../windows/fx-editor/VFX/types/hierarchy.ts | 43 + .../windows/fx-editor/VFX/types/index.ts | 42 + .../windows/fx-editor/VFX/types/loader.ts | 14 + .../fx-editor/VFX/types/quarksTypes.ts | 379 ++++++++ .../windows/fx-editor/VFX/types/rotations.ts | 14 + .../windows/fx-editor/VFX/types/shapes.ts | 18 + .../windows/fx-editor/VFX/types/values.ts | 27 + .../editor/windows/fx-editor/animation.tsx | 2 + editor/src/editor/windows/fx-editor/graph.tsx | 322 +------ editor/src/editor/windows/fx-editor/index.tsx | 28 +- .../src/editor/windows/fx-editor/layout.tsx | 41 +- editor/src/editor/windows/fx-editor/loader.ts | 690 +++----------- .../windows/fx-editor/particle-generator.ts | 191 ---- .../editor/windows/fx-editor/properties.tsx | 5 +- .../properties/particle-renderer.tsx | 9 +- .../editor/windows/fx-editor/t-318 (1).json | 1 - 51 files changed, 5961 insertions(+), 1069 deletions(-) create mode 100644 editor/src/editor/windows/fx-editor/VFX/VFXEffect.ts create mode 100644 editor/src/editor/windows/fx-editor/VFX/behaviors/colorBySpeed.ts create mode 100644 editor/src/editor/windows/fx-editor/VFX/behaviors/colorOverLife.ts create mode 100644 editor/src/editor/windows/fx-editor/VFX/behaviors/forceOverLife.ts create mode 100644 editor/src/editor/windows/fx-editor/VFX/behaviors/frameOverLife.ts create mode 100644 editor/src/editor/windows/fx-editor/VFX/behaviors/index.ts create mode 100644 editor/src/editor/windows/fx-editor/VFX/behaviors/limitSpeedOverLife.ts create mode 100644 editor/src/editor/windows/fx-editor/VFX/behaviors/orbitOverLife.ts create mode 100644 editor/src/editor/windows/fx-editor/VFX/behaviors/rotationBySpeed.ts create mode 100644 editor/src/editor/windows/fx-editor/VFX/behaviors/rotationOverLife.ts create mode 100644 editor/src/editor/windows/fx-editor/VFX/behaviors/sizeBySpeed.ts create mode 100644 editor/src/editor/windows/fx-editor/VFX/behaviors/sizeOverLife.ts create mode 100644 editor/src/editor/windows/fx-editor/VFX/behaviors/speedOverLife.ts create mode 100644 editor/src/editor/windows/fx-editor/VFX/behaviors/utils.ts create mode 100644 editor/src/editor/windows/fx-editor/VFX/factories/VFXBehaviorFunctionFactory.ts create mode 100644 editor/src/editor/windows/fx-editor/VFX/factories/VFXEmitterFactory.ts create mode 100644 editor/src/editor/windows/fx-editor/VFX/factories/VFXGeometryFactory.ts create mode 100644 editor/src/editor/windows/fx-editor/VFX/factories/VFXMaterialFactory.ts create mode 100644 editor/src/editor/windows/fx-editor/VFX/index.ts create mode 100644 editor/src/editor/windows/fx-editor/VFX/loggers/VFXLogger.ts create mode 100644 editor/src/editor/windows/fx-editor/VFX/parsers/VFXDataConverter.ts create mode 100644 editor/src/editor/windows/fx-editor/VFX/parsers/VFXParser.ts create mode 100644 editor/src/editor/windows/fx-editor/VFX/parsers/VFXValueParser.ts create mode 100644 editor/src/editor/windows/fx-editor/VFX/processors/VFXHierarchyProcessor.ts create mode 100644 editor/src/editor/windows/fx-editor/VFX/systems/VFXParticleSystem.ts create mode 100644 editor/src/editor/windows/fx-editor/VFX/systems/VFXSolidParticleSystem.ts create mode 100644 editor/src/editor/windows/fx-editor/VFX/treejs3dobject.particle.json create mode 100644 editor/src/editor/windows/fx-editor/VFX/types/VFXBehaviorFunction.ts create mode 100644 editor/src/editor/windows/fx-editor/VFX/types/behaviors.ts create mode 100644 editor/src/editor/windows/fx-editor/VFX/types/colors.ts create mode 100644 editor/src/editor/windows/fx-editor/VFX/types/context.ts create mode 100644 editor/src/editor/windows/fx-editor/VFX/types/emitter.ts create mode 100644 editor/src/editor/windows/fx-editor/VFX/types/emitterConfig.ts create mode 100644 editor/src/editor/windows/fx-editor/VFX/types/factories.ts create mode 100644 editor/src/editor/windows/fx-editor/VFX/types/gradients.ts create mode 100644 editor/src/editor/windows/fx-editor/VFX/types/hierarchy.ts create mode 100644 editor/src/editor/windows/fx-editor/VFX/types/index.ts create mode 100644 editor/src/editor/windows/fx-editor/VFX/types/loader.ts create mode 100644 editor/src/editor/windows/fx-editor/VFX/types/quarksTypes.ts create mode 100644 editor/src/editor/windows/fx-editor/VFX/types/rotations.ts create mode 100644 editor/src/editor/windows/fx-editor/VFX/types/shapes.ts create mode 100644 editor/src/editor/windows/fx-editor/VFX/types/values.ts delete mode 100644 editor/src/editor/windows/fx-editor/particle-generator.ts delete mode 100644 editor/src/editor/windows/fx-editor/t-318 (1).json diff --git a/editor/src/editor/windows/fx-editor/VFX/VFXEffect.ts b/editor/src/editor/windows/fx-editor/VFX/VFXEffect.ts new file mode 100644 index 000000000..8661964f3 --- /dev/null +++ b/editor/src/editor/windows/fx-editor/VFX/VFXEffect.ts @@ -0,0 +1,96 @@ +import type { Scene } from "@babylonjs/core/scene"; +import { Tools } from "@babylonjs/core/Misc/tools"; +import type { IDisposable } from "@babylonjs/core/scene"; +import type { QuarksVFXJSON } from "./types/quarksTypes"; +import type { VFXLoaderOptions } from "./types/loader"; +import { VFXParser } from "./parsers/VFXParser"; +import type { VFXParticleSystem } from "./systems/VFXParticleSystem"; +import type { VFXSolidParticleSystem } from "./systems/VFXSolidParticleSystem"; + + +/** + * VFX Effect containing multiple particle systems + * Main entry point for loading and creating VFX from Three.js particle JSON files + */ +export class VFXEffect implements IDisposable { + public readonly systems: (VFXParticleSystem | VFXSolidParticleSystem)[] = []; + + /** + * Load a Three.js particle JSON file and create particle systems + * @param url URL to the JSON file + * @param scene The Babylon.js scene + * @param rootUrl Root URL for loading textures + * @param options Optional parsing options + * @returns Promise that resolves to a VFXEffect + */ + public static async LoadAsync(url: string, scene: Scene, rootUrl: string = "", options?: VFXLoaderOptions): Promise { + return new Promise((resolve, reject) => { + Tools.LoadFile( + url, + (data) => { + try { + const jsonData = JSON.parse(data.toString()); + const effect = VFXEffect.Parse(jsonData, scene, rootUrl, options); + resolve(effect); + } catch (error) { + reject(error); + } + }, + undefined, + undefined, + undefined, + (error) => { + reject(error); + } + ); + }); + } + + /** + * Parse a Three.js particle JSON file and create Babylon.js particle systems + * @param jsonData The Three.js JSON data + * @param scene The Babylon.js scene + * @param rootUrl Root URL for loading textures + * @param options Optional parsing options + * @returns A VFXEffect containing all particle systems + */ + public static Parse(jsonData: QuarksVFXJSON, scene: Scene, rootUrl: string = "", options?: VFXLoaderOptions): VFXEffect { + const particleSystems = new VFXParser(scene, rootUrl, jsonData, options).parse(); + const effect = new VFXEffect(); + effect.systems.push(...particleSystems); + return effect; + } + + /** + * Create a VFXEffect directly from JSON data + * @param jsonData The Three.js JSON data + * @param scene The Babylon.js scene + * @param rootUrl Root URL for loading textures + * @param options Optional parsing options + */ + constructor(jsonData?: QuarksVFXJSON, scene?: Scene, rootUrl: string = "", options?: VFXLoaderOptions) { + if (jsonData && scene) { + const effect = VFXEffect.Parse(jsonData, scene, rootUrl, options); + this.systems.push(...effect.systems); + } + } + + public start(): void { + for (const system of this.systems) { + system.start(); + } + } + + public stop(): void { + for (const system of this.systems) { + system.stop(); + } + } + + public dispose(): void { + for (const system of this.systems) { + system.dispose(); + } + this.systems.length = 0; + } +} diff --git a/editor/src/editor/windows/fx-editor/VFX/behaviors/colorBySpeed.ts b/editor/src/editor/windows/fx-editor/VFX/behaviors/colorBySpeed.ts new file mode 100644 index 000000000..3d34e8b9d --- /dev/null +++ b/editor/src/editor/windows/fx-editor/VFX/behaviors/colorBySpeed.ts @@ -0,0 +1,73 @@ +import type { Particle } from "../../particle"; +import type { SolidParticle } from "../../solidParticle"; +import type { VFXColorBySpeedBehavior } from "../types/behaviors"; +import { interpolateColorKeys } from "./utils"; +import { VFXValueParser } from "../parsers/VFXValueParser"; +import type { Color4 } from "../../../Maths/math.color"; + +/** + * Extended Particle interface for custom behaviors + */ +interface ExtendedParticle extends Particle { + startSpeed?: number; + startColor?: Color4; +} + +/** + * Apply ColorBySpeed behavior to Particle + */ +export function applyColorBySpeedPS(particle: ExtendedParticle, behavior: VFXColorBySpeedBehavior, currentSpeed: number, valueParser: VFXValueParser): void { + if (!behavior.color || !behavior.color.keys || !particle.color) { + return; + } + + const colorKeys = behavior.color.keys; + const minSpeed = behavior.minSpeed !== undefined ? valueParser.parseConstantValue(behavior.minSpeed) : 0; + const maxSpeed = behavior.maxSpeed !== undefined ? valueParser.parseConstantValue(behavior.maxSpeed) : 1; + const speedRatio = Math.max(0, Math.min(1, (currentSpeed - minSpeed) / (maxSpeed - minSpeed || 1))); + + const interpolatedColor = interpolateColorKeys(colorKeys, speedRatio); + const startColor = particle.startColor || particle.initialColor; + + if (startColor) { + // Multiply with startColor (matching three.quarks behavior) + particle.color.r = interpolatedColor.r * startColor.r; + particle.color.g = interpolatedColor.g * startColor.g; + particle.color.b = interpolatedColor.b * startColor.b; + particle.color.a = startColor.a; // Keep original alpha + } else { + particle.color.r = interpolatedColor.r; + particle.color.g = interpolatedColor.g; + particle.color.b = interpolatedColor.b; + } +} + +/** + * Apply ColorBySpeed behavior to SolidParticle + */ +export function applyColorBySpeedSPS(particle: SolidParticle, behavior: VFXColorBySpeedBehavior, currentSpeed: number, valueParser: VFXValueParser): void { + if (!behavior.color || !behavior.color.keys || !particle.color) { + return; + } + + const colorKeys = behavior.color.keys; + const minSpeed = behavior.minSpeed !== undefined ? valueParser.parseConstantValue(behavior.minSpeed) : 0; + const maxSpeed = behavior.maxSpeed !== undefined ? valueParser.parseConstantValue(behavior.maxSpeed) : 1; + const speedRatio = Math.max(0, Math.min(1, (currentSpeed - minSpeed) / (maxSpeed - minSpeed || 1))); + + const interpolatedColor = interpolateColorKeys(colorKeys, speedRatio); + const startColor = particle.props?.startColor; + + if (startColor) { + // Multiply with startColor (matching three.quarks behavior) + particle.color.r = interpolatedColor.r * startColor.r; + particle.color.g = interpolatedColor.g * startColor.g; + particle.color.b = interpolatedColor.b * startColor.b; + particle.color.a = startColor.a; // Keep original alpha + } else { + particle.color.r = interpolatedColor.r; + particle.color.g = interpolatedColor.g; + particle.color.b = interpolatedColor.b; + } +} + diff --git a/editor/src/editor/windows/fx-editor/VFX/behaviors/colorOverLife.ts b/editor/src/editor/windows/fx-editor/VFX/behaviors/colorOverLife.ts new file mode 100644 index 000000000..cf8fc2140 --- /dev/null +++ b/editor/src/editor/windows/fx-editor/VFX/behaviors/colorOverLife.ts @@ -0,0 +1,78 @@ +import { Color4 } from "../../../Maths/math.color"; +import type { ParticleSystem } from "../../particleSystem"; +import type { SolidParticle } from "../../solidParticle"; +import type { VFXColorOverLifeBehavior } from "../types/behaviors"; +import { extractColorFromValue, extractAlphaFromValue, interpolateColorKeys, interpolateGradientKeys } from "./utils"; + +/** + * Apply ColorOverLife behavior to ParticleSystem + */ +export function applyColorOverLifePS(particleSystem: ParticleSystem, behavior: VFXColorOverLifeBehavior): void { + if (behavior.color && behavior.color.color && behavior.color.color.keys) { + const colorKeys = behavior.color.color.keys; + for (const key of colorKeys) { + if (key.value !== undefined && key.pos !== undefined) { + const color = extractColorFromValue(key.value); + const alpha = extractAlphaFromValue(key.value); + particleSystem.addColorGradient(key.pos, new Color4(color.r, color.g, color.b, alpha)); + } + } + } + + if (behavior.color && behavior.color.alpha && behavior.color.alpha.keys) { + const alphaKeys = behavior.color.alpha.keys; + for (const key of alphaKeys) { + if (key.value !== undefined && key.pos !== undefined) { + const alpha = extractAlphaFromValue(key.value); + const existingGradients = particleSystem.getColorGradients(); + const existingGradient = existingGradients?.find((g) => key.pos !== undefined && Math.abs(g.gradient - key.pos) < 0.001); + if (existingGradient) { + existingGradient.color1.a = alpha; + if (existingGradient.color2) { + existingGradient.color2.a = alpha; + } + } else { + particleSystem.addColorGradient(key.pos ?? 0, new Color4(1, 1, 1, alpha)); + } + } + } + } +} + +/** + * Apply ColorOverLife behavior to SolidParticle + */ +export function applyColorOverLifeSPS(particle: SolidParticle, behavior: VFXColorOverLifeBehavior, lifeRatio: number): void { + if (!behavior.color || !particle.color) { + return; + } + + const colorKeys = behavior.color.color?.keys ?? behavior.color.keys; + if (!colorKeys || !Array.isArray(colorKeys)) { + return; + } + + const interpolatedColor = interpolateColorKeys(colorKeys, lifeRatio); + const startColor = particle.props?.startColor; + + if (startColor) { + // Multiply with startColor (matching three.quarks behavior) + particle.color.r = interpolatedColor.r * startColor.r; + particle.color.g = interpolatedColor.g * startColor.g; + particle.color.b = interpolatedColor.b * startColor.b; + } else { + particle.color.r = interpolatedColor.r; + particle.color.g = interpolatedColor.g; + particle.color.b = interpolatedColor.b; + } + + // Apply alpha if specified + if (behavior.color.alpha?.keys) { + const alphaKeys = behavior.color.alpha.keys; + const alpha = interpolateGradientKeys(alphaKeys, lifeRatio, extractAlphaFromValue); + particle.color.a = alpha; + } else { + particle.color.a = interpolatedColor.a; + } +} + diff --git a/editor/src/editor/windows/fx-editor/VFX/behaviors/forceOverLife.ts b/editor/src/editor/windows/fx-editor/VFX/behaviors/forceOverLife.ts new file mode 100644 index 000000000..e71756db2 --- /dev/null +++ b/editor/src/editor/windows/fx-editor/VFX/behaviors/forceOverLife.ts @@ -0,0 +1,36 @@ +import { Vector3 } from "../../../Maths/math.vector"; +import type { ParticleSystem } from "../../particleSystem"; +import type { VFXForceOverLifeBehavior, VFXGravityForceBehavior } from "../types/behaviors"; +import { VFXValueParser } from "../parsers/VFXValueParser"; + +/** + * Apply ForceOverLife behavior to ParticleSystem + */ +export function applyForceOverLifePS(particleSystem: ParticleSystem, behavior: VFXForceOverLifeBehavior, valueParser: VFXValueParser): void { + if (behavior.force) { + const forceX = behavior.force.x !== undefined ? valueParser.parseConstantValue(behavior.force.x) : 0; + const forceY = behavior.force.y !== undefined ? valueParser.parseConstantValue(behavior.force.y) : 0; + const forceZ = behavior.force.z !== undefined ? valueParser.parseConstantValue(behavior.force.z) : 0; + if (Math.abs(forceY) > 0.01 || Math.abs(forceX) > 0.01 || Math.abs(forceZ) > 0.01) { + particleSystem.gravity = new Vector3(forceX, forceY, forceZ); + } + } else if (behavior.x !== undefined || behavior.y !== undefined || behavior.z !== undefined) { + const forceX = behavior.x !== undefined ? valueParser.parseConstantValue(behavior.x) : 0; + const forceY = behavior.y !== undefined ? valueParser.parseConstantValue(behavior.y) : 0; + const forceZ = behavior.z !== undefined ? valueParser.parseConstantValue(behavior.z) : 0; + if (Math.abs(forceY) > 0.01 || Math.abs(forceX) > 0.01 || Math.abs(forceZ) > 0.01) { + particleSystem.gravity = new Vector3(forceX, forceY, forceZ); + } + } +} + +/** + * Apply GravityForce behavior to ParticleSystem + */ +export function applyGravityForcePS(particleSystem: ParticleSystem, behavior: VFXGravityForceBehavior, valueParser: VFXValueParser): void { + if (behavior.gravity !== undefined) { + const gravity = valueParser.parseConstantValue(behavior.gravity); + particleSystem.gravity = new Vector3(0, -gravity, 0); + } +} + diff --git a/editor/src/editor/windows/fx-editor/VFX/behaviors/frameOverLife.ts b/editor/src/editor/windows/fx-editor/VFX/behaviors/frameOverLife.ts new file mode 100644 index 000000000..9267ddc46 --- /dev/null +++ b/editor/src/editor/windows/fx-editor/VFX/behaviors/frameOverLife.ts @@ -0,0 +1,36 @@ +import type { ParticleSystem } from "../../particleSystem"; +import type { VFXFrameOverLifeBehavior } from "../types/behaviors"; +import { VFXValueParser } from "../parsers/VFXValueParser"; + +/** + * Apply FrameOverLife behavior to ParticleSystem + */ +export function applyFrameOverLifePS(particleSystem: ParticleSystem, behavior: VFXFrameOverLifeBehavior, valueParser: VFXValueParser): void { + if (!behavior.frame) { + return; + } + + particleSystem.isAnimationSheetEnabled = true; + if (typeof behavior.frame === "object" && behavior.frame !== null && "keys" in behavior.frame && behavior.frame.keys && Array.isArray(behavior.frame.keys)) { + const frames = behavior.frame.keys.map((k) => { + const val = k.value; + const pos = k.pos ?? k.time ?? 0; + if (typeof val === "number") { + return val; + } else if (Array.isArray(val)) { + return val[0] || 0; + } else { + return pos; + } + }); + if (frames.length > 0) { + particleSystem.startSpriteCellID = Math.floor(frames[0]); + particleSystem.endSpriteCellID = Math.floor(frames[frames.length - 1] || frames[0]); + } + } else if (typeof behavior.frame === "number" || (typeof behavior.frame === "object" && behavior.frame !== null && "type" in behavior.frame)) { + const frameValue = valueParser.parseConstantValue(behavior.frame); + particleSystem.startSpriteCellID = Math.floor(frameValue); + particleSystem.endSpriteCellID = Math.floor(frameValue); + } +} + diff --git a/editor/src/editor/windows/fx-editor/VFX/behaviors/index.ts b/editor/src/editor/windows/fx-editor/VFX/behaviors/index.ts new file mode 100644 index 000000000..368005c38 --- /dev/null +++ b/editor/src/editor/windows/fx-editor/VFX/behaviors/index.ts @@ -0,0 +1,19 @@ +/** + * Behavior modules for VFX particle systems + * + * Each behavior module exports functions for both ParticleSystem (PS) and SolidParticleSystem (SPS) + */ + +export * from "./colorOverLife"; +export * from "./sizeOverLife"; +export * from "./rotationOverLife"; +export * from "./forceOverLife"; +export * from "./speedOverLife"; +export * from "./colorBySpeed"; +export * from "./sizeBySpeed"; +export * from "./rotationBySpeed"; +export * from "./orbitOverLife"; +export * from "./frameOverLife"; +export * from "./limitSpeedOverLife"; +export * from "./utils"; + diff --git a/editor/src/editor/windows/fx-editor/VFX/behaviors/limitSpeedOverLife.ts b/editor/src/editor/windows/fx-editor/VFX/behaviors/limitSpeedOverLife.ts new file mode 100644 index 000000000..5fbfb373e --- /dev/null +++ b/editor/src/editor/windows/fx-editor/VFX/behaviors/limitSpeedOverLife.ts @@ -0,0 +1,36 @@ +import type { ParticleSystem } from "../../particleSystem"; +import type { VFXLimitSpeedOverLifeBehavior } from "../types/behaviors"; +import { extractNumberFromValue } from "./utils"; +import { VFXValueParser } from "../parsers/VFXValueParser"; + +/** + * Apply LimitSpeedOverLife behavior to ParticleSystem + */ +export function applyLimitSpeedOverLifePS(particleSystem: ParticleSystem, behavior: VFXLimitSpeedOverLifeBehavior, valueParser: VFXValueParser): void { + if (behavior.dampen !== undefined) { + const dampen = valueParser.parseConstantValue(behavior.dampen); + particleSystem.limitVelocityDamping = dampen; + } + + if (behavior.maxSpeed !== undefined) { + const speedLimit = valueParser.parseConstantValue(behavior.maxSpeed); + particleSystem.addLimitVelocityGradient(0, speedLimit); + particleSystem.addLimitVelocityGradient(1, speedLimit); + } else if (behavior.speed !== undefined) { + if (typeof behavior.speed === "object" && behavior.speed !== null && "keys" in behavior.speed && behavior.speed.keys && Array.isArray(behavior.speed.keys)) { + for (const key of behavior.speed.keys) { + const pos = key.pos ?? key.time ?? 0; + const val = key.value; + if (val !== undefined && pos !== undefined) { + const numVal = extractNumberFromValue(val); + particleSystem.addLimitVelocityGradient(pos, numVal); + } + } + } else if (typeof behavior.speed === "number" || (typeof behavior.speed === "object" && behavior.speed !== null && "type" in behavior.speed)) { + const speedLimit = valueParser.parseConstantValue(behavior.speed); + particleSystem.addLimitVelocityGradient(0, speedLimit); + particleSystem.addLimitVelocityGradient(1, speedLimit); + } + } +} + diff --git a/editor/src/editor/windows/fx-editor/VFX/behaviors/orbitOverLife.ts b/editor/src/editor/windows/fx-editor/VFX/behaviors/orbitOverLife.ts new file mode 100644 index 000000000..1438b885e --- /dev/null +++ b/editor/src/editor/windows/fx-editor/VFX/behaviors/orbitOverLife.ts @@ -0,0 +1,100 @@ +import type { Particle } from "../../particle"; +import type { SolidParticle } from "../../solidParticle"; +import type { VFXOrbitOverLifeBehavior } from "../types/behaviors"; +import { extractNumberFromValue, interpolateGradientKeys } from "./utils"; +import { VFXValueParser } from "../parsers/VFXValueParser"; + +/** + * Apply OrbitOverLife behavior to Particle + */ +export function applyOrbitOverLifePS(particle: Particle, behavior: VFXOrbitOverLifeBehavior, lifeRatio: number, valueParser: VFXValueParser): void { + if (!behavior.radius) { + return; + } + + // Parse radius (can be VFXValue with keys or constant/interval) + let radius = 1; + const radiusValue = behavior.radius; + + // Check if radius is an object with keys (gradient) + if ( + radiusValue !== undefined && + radiusValue !== null && + typeof radiusValue === "object" && + "keys" in radiusValue && + Array.isArray(radiusValue.keys) && + radiusValue.keys.length > 0 + ) { + radius = interpolateGradientKeys(radiusValue.keys, lifeRatio, extractNumberFromValue); + } else if (radiusValue !== undefined && radiusValue !== null) { + // Parse as VFXValue (number, VFXConstantValue, or VFXIntervalValue) + const parsedRadius = valueParser.parseIntervalValue(radiusValue as import("../types/values").VFXValue); + radius = parsedRadius.min + (parsedRadius.max - parsedRadius.min) * lifeRatio; + } + + const speed = behavior.speed !== undefined ? valueParser.parseConstantValue(behavior.speed) : 1; + const angle = lifeRatio * speed * Math.PI * 2; + + // Calculate orbit offset relative to center + const centerX = behavior.center?.x ?? 0; + const centerY = behavior.center?.y ?? 0; + const centerZ = behavior.center?.z ?? 0; + + const orbitX = Math.cos(angle) * radius; + const orbitY = Math.sin(angle) * radius; + const orbitZ = 0; // 2D orbit + + // Apply orbit offset to particle position + if (particle.position) { + particle.position.x = centerX + orbitX; + particle.position.y = centerY + orbitY; + particle.position.z = centerZ + orbitZ; + } +} + +/** + * Apply OrbitOverLife behavior to SolidParticle + */ +export function applyOrbitOverLifeSPS(particle: SolidParticle, behavior: VFXOrbitOverLifeBehavior, lifeRatio: number, valueParser: VFXValueParser): void { + if (!behavior.radius) { + return; + } + + // Parse radius (can be VFXValue with keys or constant/interval) + let radius = 1; + const radiusValue = behavior.radius; + + // Check if radius is an object with keys (gradient) + if ( + radiusValue !== undefined && + radiusValue !== null && + typeof radiusValue === "object" && + "keys" in radiusValue && + Array.isArray(radiusValue.keys) && + radiusValue.keys.length > 0 + ) { + radius = interpolateGradientKeys(radiusValue.keys, lifeRatio, extractNumberFromValue); + } else if (radiusValue !== undefined && radiusValue !== null) { + // Parse as VFXValue (number, VFXConstantValue, or VFXIntervalValue) + const parsedRadius = valueParser.parseIntervalValue(radiusValue as import("../types/values").VFXValue); + radius = parsedRadius.min + (parsedRadius.max - parsedRadius.min) * lifeRatio; + } + + const speed = behavior.speed !== undefined ? valueParser.parseConstantValue(behavior.speed) : 1; + const angle = lifeRatio * speed * Math.PI * 2; + + // Calculate orbit offset relative to center + const centerX = behavior.center?.x ?? 0; + const centerY = behavior.center?.y ?? 0; + const centerZ = behavior.center?.z ?? 0; + + const orbitX = Math.cos(angle) * radius; + const orbitY = Math.sin(angle) * radius; + const orbitZ = 0; // 2D orbit + + // Apply orbit offset to particle position + particle.position.x = centerX + orbitX; + particle.position.y = centerY + orbitY; + particle.position.z = centerZ + orbitZ; +} + diff --git a/editor/src/editor/windows/fx-editor/VFX/behaviors/rotationBySpeed.ts b/editor/src/editor/windows/fx-editor/VFX/behaviors/rotationBySpeed.ts new file mode 100644 index 000000000..c795c9211 --- /dev/null +++ b/editor/src/editor/windows/fx-editor/VFX/behaviors/rotationBySpeed.ts @@ -0,0 +1,61 @@ +import type { Particle } from "../../particle"; +import type { SolidParticle } from "../../solidParticle"; +import type { ParticleSystem } from "../../particleSystem"; +import type { VFXRotationBySpeedBehavior } from "../types/behaviors"; +import { extractNumberFromValue, interpolateGradientKeys } from "./utils"; +import { VFXValueParser } from "../parsers/VFXValueParser"; + +/** + * Extended Particle interface for custom behaviors + */ +interface ExtendedParticle extends Particle { + startSpeed?: number; +} + +/** + * Apply RotationBySpeed behavior to Particle + */ +export function applyRotationBySpeedPS(particle: ExtendedParticle, behavior: VFXRotationBySpeedBehavior, currentSpeed: number, particleSystem: ParticleSystem, valueParser: VFXValueParser): void { + if (!behavior.angularVelocity) { + return; + } + + // angularVelocity can be VFXValue (constant/interval) or object with keys + let angularSpeed = 0; + if (typeof behavior.angularVelocity === "object" && behavior.angularVelocity !== null && "keys" in behavior.angularVelocity && Array.isArray(behavior.angularVelocity.keys) && behavior.angularVelocity.keys.length > 0) { + const minSpeed = behavior.minSpeed !== undefined ? valueParser.parseConstantValue(behavior.minSpeed) : 0; + const maxSpeed = behavior.maxSpeed !== undefined ? valueParser.parseConstantValue(behavior.maxSpeed) : 1; + const speedRatio = Math.max(0, Math.min(1, (currentSpeed - minSpeed) / (maxSpeed - minSpeed || 1))); + angularSpeed = interpolateGradientKeys(behavior.angularVelocity.keys, speedRatio, extractNumberFromValue); + } else { + const angularVel = valueParser.parseIntervalValue(behavior.angularVelocity); + angularSpeed = angularVel.min + (angularVel.max - angularVel.min) * 0.5; // Use middle value + } + + particle.angle += angularSpeed * 0.016; // Assuming ~60fps +} + +/** + * Apply RotationBySpeed behavior to SolidParticle + */ +export function applyRotationBySpeedSPS(particle: SolidParticle, behavior: VFXRotationBySpeedBehavior, currentSpeed: number, valueParser: VFXValueParser, updateSpeed: number = 0.016): void { + if (!behavior.angularVelocity) { + return; + } + + // angularVelocity can be VFXValue (constant/interval) or object with keys + let angularSpeed = 0; + if (typeof behavior.angularVelocity === "object" && behavior.angularVelocity !== null && "keys" in behavior.angularVelocity && Array.isArray(behavior.angularVelocity.keys) && behavior.angularVelocity.keys.length > 0) { + const minSpeed = behavior.minSpeed !== undefined ? valueParser.parseConstantValue(behavior.minSpeed) : 0; + const maxSpeed = behavior.maxSpeed !== undefined ? valueParser.parseConstantValue(behavior.maxSpeed) : 1; + const speedRatio = Math.max(0, Math.min(1, (currentSpeed - minSpeed) / (maxSpeed - minSpeed || 1))); + angularSpeed = interpolateGradientKeys(behavior.angularVelocity.keys, speedRatio, extractNumberFromValue); + } else { + const angularVel = valueParser.parseIntervalValue(behavior.angularVelocity); + angularSpeed = angularVel.min + (angularVel.max - angularVel.min) * 0.5; // Use middle value + } + + // SolidParticle uses rotation.z for 2D rotation + particle.rotation.z += angularSpeed * updateSpeed; +} + diff --git a/editor/src/editor/windows/fx-editor/VFX/behaviors/rotationOverLife.ts b/editor/src/editor/windows/fx-editor/VFX/behaviors/rotationOverLife.ts new file mode 100644 index 000000000..d0bde6d3f --- /dev/null +++ b/editor/src/editor/windows/fx-editor/VFX/behaviors/rotationOverLife.ts @@ -0,0 +1,32 @@ +import type { ParticleSystem } from "../../particleSystem"; +import type { SolidParticle } from "../../solidParticle"; +import type { VFXRotationOverLifeBehavior } from "../types/behaviors"; +import { VFXValueParser } from "../parsers/VFXValueParser"; + +/** + * Apply RotationOverLife behavior to ParticleSystem + */ +export function applyRotationOverLifePS(particleSystem: ParticleSystem, behavior: VFXRotationOverLifeBehavior, valueParser: VFXValueParser): void { + if (behavior.angularVelocity) { + const angularVel = valueParser.parseIntervalValue(behavior.angularVelocity); + particleSystem.minAngularSpeed = angularVel.min; + particleSystem.maxAngularSpeed = angularVel.max; + } +} + +/** + * Apply RotationOverLife behavior to SolidParticle + */ +export function applyRotationOverLifeSPS(particle: SolidParticle, behavior: VFXRotationOverLifeBehavior, lifeRatio: number, valueParser: VFXValueParser, updateSpeed: number = 0.016): void { + if (!behavior.angularVelocity) { + return; + } + + const angularVel = valueParser.parseIntervalValue(behavior.angularVelocity); + const angularSpeed = angularVel.min + (angularVel.max - angularVel.min) * lifeRatio; + + // Apply rotation around Z axis (2D rotation) + // SolidParticle uses rotation.z for 2D rotation + particle.rotation.z += angularSpeed * updateSpeed; +} + diff --git a/editor/src/editor/windows/fx-editor/VFX/behaviors/sizeBySpeed.ts b/editor/src/editor/windows/fx-editor/VFX/behaviors/sizeBySpeed.ts new file mode 100644 index 000000000..faeb75d2b --- /dev/null +++ b/editor/src/editor/windows/fx-editor/VFX/behaviors/sizeBySpeed.ts @@ -0,0 +1,51 @@ +import type { Particle } from "../../particle"; +import type { SolidParticle } from "../../solidParticle"; +import type { VFXSizeBySpeedBehavior } from "../types/behaviors"; +import { extractNumberFromValue, interpolateGradientKeys } from "./utils"; +import { VFXValueParser } from "../parsers/VFXValueParser"; + +/** + * Extended Particle interface for custom behaviors + */ +interface ExtendedParticle extends Particle { + startSpeed?: number; + startSize?: number; +} + +/** + * Apply SizeBySpeed behavior to Particle + */ +export function applySizeBySpeedPS(particle: ExtendedParticle, behavior: VFXSizeBySpeedBehavior, currentSpeed: number, valueParser: VFXValueParser): void { + if (!behavior.size || !behavior.size.keys) { + return; + } + + const sizeKeys = behavior.size.keys; + const minSpeed = behavior.minSpeed !== undefined ? valueParser.parseConstantValue(behavior.minSpeed) : 0; + const maxSpeed = behavior.maxSpeed !== undefined ? valueParser.parseConstantValue(behavior.maxSpeed) : 1; + const speedRatio = Math.max(0, Math.min(1, (currentSpeed - minSpeed) / (maxSpeed - minSpeed || 1))); + + const sizeMultiplier = interpolateGradientKeys(sizeKeys, speedRatio, extractNumberFromValue); + const startSize = particle.startSize || particle.size || 1; + particle.size = startSize * sizeMultiplier; +} + +/** + * Apply SizeBySpeed behavior to SolidParticle + */ +export function applySizeBySpeedSPS(particle: SolidParticle, behavior: VFXSizeBySpeedBehavior, currentSpeed: number, valueParser: VFXValueParser): void { + if (!behavior.size || !behavior.size.keys) { + return; + } + + const sizeKeys = behavior.size.keys; + const minSpeed = behavior.minSpeed !== undefined ? valueParser.parseConstantValue(behavior.minSpeed) : 0; + const maxSpeed = behavior.maxSpeed !== undefined ? valueParser.parseConstantValue(behavior.maxSpeed) : 1; + const speedRatio = Math.max(0, Math.min(1, (currentSpeed - minSpeed) / (maxSpeed - minSpeed || 1))); + + const sizeMultiplier = interpolateGradientKeys(sizeKeys, speedRatio, extractNumberFromValue); + const startSize = particle.props?.startSize ?? 1; + const newSize = startSize * sizeMultiplier; + particle.scaling.setAll(newSize); +} + diff --git a/editor/src/editor/windows/fx-editor/VFX/behaviors/sizeOverLife.ts b/editor/src/editor/windows/fx-editor/VFX/behaviors/sizeOverLife.ts new file mode 100644 index 000000000..80790bd36 --- /dev/null +++ b/editor/src/editor/windows/fx-editor/VFX/behaviors/sizeOverLife.ts @@ -0,0 +1,60 @@ +import type { ParticleSystem } from "../../particleSystem"; +import type { SolidParticle } from "../../solidParticle"; +import type { VFXSizeOverLifeBehavior } from "../types/behaviors"; +import { extractNumberFromValue, interpolateGradientKeys } from "./utils"; + +/** + * Apply SizeOverLife behavior to ParticleSystem + */ +export function applySizeOverLifePS(particleSystem: ParticleSystem, behavior: VFXSizeOverLifeBehavior): void { + if (behavior.size && behavior.size.functions) { + const functions = behavior.size.functions; + for (const func of functions) { + if (func.function && func.start !== undefined) { + const startSize = func.function.p0 || 1; + const endSize = func.function.p3 !== undefined ? func.function.p3 : startSize; + particleSystem.addSizeGradient(func.start, startSize); + if (func.function.p3 !== undefined) { + particleSystem.addSizeGradient(func.start + 0.5, endSize); + } + } + } + } else if (behavior.size && behavior.size.keys) { + for (const key of behavior.size.keys) { + if (key.value !== undefined && key.pos !== undefined) { + const size = extractNumberFromValue(key.value); + particleSystem.addSizeGradient(key.pos, size); + } + } + } +} + +/** + * Apply SizeOverLife behavior to SolidParticle + */ +export function applySizeOverLifeSPS(particle: SolidParticle, behavior: VFXSizeOverLifeBehavior, lifeRatio: number): void { + if (!behavior.size) { + return; + } + + let sizeMultiplier = 1; + + if (behavior.size.keys && Array.isArray(behavior.size.keys)) { + sizeMultiplier = interpolateGradientKeys(behavior.size.keys, lifeRatio, extractNumberFromValue); + } else if (behavior.size.functions && Array.isArray(behavior.size.functions)) { + // Handle functions (simplified - use first function) + const func = behavior.size.functions[0]; + if (func && func.function && func.start !== undefined) { + const startSize = func.function.p0 || 1; + const endSize = func.function.p3 !== undefined ? func.function.p3 : startSize; + const t = Math.max(0, Math.min(1, (lifeRatio - func.start) / 0.5)); + sizeMultiplier = startSize + (endSize - startSize) * t; + } + } + + // Multiply startSize by the gradient value (matching three.quarks behavior) + const startSize = particle.props?.startSize ?? 1; + const newSize = startSize * sizeMultiplier; + particle.scaling.setAll(newSize); +} + diff --git a/editor/src/editor/windows/fx-editor/VFX/behaviors/speedOverLife.ts b/editor/src/editor/windows/fx-editor/VFX/behaviors/speedOverLife.ts new file mode 100644 index 000000000..d11eb767e --- /dev/null +++ b/editor/src/editor/windows/fx-editor/VFX/behaviors/speedOverLife.ts @@ -0,0 +1,92 @@ +import type { ParticleSystem } from "../../particleSystem"; +import type { SolidParticle } from "../../solidParticle"; +import type { VFXSpeedOverLifeBehavior } from "../types/behaviors"; +import { extractNumberFromValue, interpolateGradientKeys } from "./utils"; +import { VFXValueParser } from "../parsers/VFXValueParser"; + +/** + * Apply SpeedOverLife behavior to ParticleSystem + */ +export function applySpeedOverLifePS(particleSystem: ParticleSystem, behavior: VFXSpeedOverLifeBehavior, valueParser: VFXValueParser): void { + if (behavior.speed) { + if (typeof behavior.speed === "object" && behavior.speed !== null && "keys" in behavior.speed && behavior.speed.keys && Array.isArray(behavior.speed.keys)) { + for (const key of behavior.speed.keys) { + const pos = key.pos ?? key.time ?? 0; + const val = key.value; + if (val !== undefined && pos !== undefined) { + const numVal = extractNumberFromValue(val); + particleSystem.addVelocityGradient(pos, numVal); + } + } + } else if ( + typeof behavior.speed === "object" && + behavior.speed !== null && + "functions" in behavior.speed && + behavior.speed.functions && + Array.isArray(behavior.speed.functions) + ) { + for (const func of behavior.speed.functions) { + if (func.function && func.start !== undefined) { + const startSpeed = func.function.p0 || 1; + const endSpeed = func.function.p3 !== undefined ? func.function.p3 : startSpeed; + particleSystem.addVelocityGradient(func.start, startSpeed); + if (func.function.p3 !== undefined) { + particleSystem.addVelocityGradient(Math.min(func.start + 0.5, 1), endSpeed); + } + } + } + } else if (typeof behavior.speed === "number" || (typeof behavior.speed === "object" && behavior.speed !== null && "type" in behavior.speed)) { + const speedValue = valueParser.parseIntervalValue(behavior.speed); + particleSystem.addVelocityGradient(0, speedValue.min); + particleSystem.addVelocityGradient(1, speedValue.max); + } + } +} + +/** + * Apply SpeedOverLife behavior to SolidParticle + */ +export function applySpeedOverLifeSPS(particle: SolidParticle, behavior: VFXSpeedOverLifeBehavior, lifeRatio: number, valueParser: VFXValueParser): void { + if (!behavior.speed) { + return; + } + + let speedMultiplier = 1; + + if (typeof behavior.speed === "object" && behavior.speed !== null && "keys" in behavior.speed && behavior.speed.keys && Array.isArray(behavior.speed.keys)) { + speedMultiplier = interpolateGradientKeys(behavior.speed.keys, lifeRatio, extractNumberFromValue); + } else if ( + typeof behavior.speed === "object" && + behavior.speed !== null && + "functions" in behavior.speed && + behavior.speed.functions && + Array.isArray(behavior.speed.functions) + ) { + // Handle functions (simplified - use first function) + const func = behavior.speed.functions[0]; + if (func && func.function && func.start !== undefined) { + const startSpeed = func.function.p0 || 1; + const endSpeed = func.function.p3 !== undefined ? func.function.p3 : startSpeed; + const t = Math.max(0, Math.min(1, (lifeRatio - func.start) / 0.5)); + speedMultiplier = startSpeed + (endSpeed - startSpeed) * t; + } + } else if (typeof behavior.speed === "number" || (typeof behavior.speed === "object" && behavior.speed !== null && "type" in behavior.speed)) { + const speedValue = valueParser.parseIntervalValue(behavior.speed); + speedMultiplier = speedValue.min + (speedValue.max - speedValue.min) * lifeRatio; + } + + // Apply speed modifier to velocity + const startSpeed = particle.props?.startSpeed ?? 1; + const speedModifier = particle.props?.speedModifier ?? 1; + const newSpeedModifier = speedModifier * speedMultiplier; + particle.props = particle.props || {}; + particle.props.speedModifier = newSpeedModifier; + + // Update velocity magnitude + const velocityLength = Math.sqrt(particle.velocity.x * particle.velocity.x + particle.velocity.y * particle.velocity.y + particle.velocity.z * particle.velocity.z); + if (velocityLength > 0) { + const newLength = startSpeed * newSpeedModifier; + const scale = newLength / velocityLength; + particle.velocity.scaleInPlace(scale); + } +} diff --git a/editor/src/editor/windows/fx-editor/VFX/behaviors/utils.ts b/editor/src/editor/windows/fx-editor/VFX/behaviors/utils.ts new file mode 100644 index 000000000..917724716 --- /dev/null +++ b/editor/src/editor/windows/fx-editor/VFX/behaviors/utils.ts @@ -0,0 +1,166 @@ +import type { VFXGradientKey } from "../types/gradients"; + +/** + * Extract RGB color from gradient key value + */ +export function extractColorFromValue(value: number | number[] | { r: number; g: number; b: number; a?: number } | undefined): { r: number; g: number; b: number } { + if (value === undefined) { + return { r: 1, g: 1, b: 1 }; + } + + if (typeof value === "number") { + return { r: value, g: value, b: value }; + } + + if (Array.isArray(value)) { + return { + r: value[0] || 0, + g: value[1] || 0, + b: value[2] || 0, + }; + } + + if (typeof value === "object" && "r" in value) { + return { + r: value.r || 0, + g: value.g || 0, + b: value.b || 0, + }; + } + + return { r: 1, g: 1, b: 1 }; +} + +/** + * Extract alpha from gradient key value + */ +export function extractAlphaFromValue(value: number | number[] | { r: number; g: number; b: number; a?: number } | undefined): number { + if (value === undefined) { + return 1; + } + + if (typeof value === "number") { + return value; + } + + if (Array.isArray(value)) { + return value[3] !== undefined ? value[3] : 1; + } + + if (typeof value === "object" && "a" in value) { + return value.a !== undefined ? value.a : 1; + } + + return 1; +} + +/** + * Extract number from gradient key value + */ +export function extractNumberFromValue(value: number | number[] | { r: number; g: number; b: number; a?: number } | undefined): number { + if (value === undefined) { + return 1; + } + + if (typeof value === "number") { + return value; + } + + if (Array.isArray(value)) { + return value[0] || 0; + } + + return 1; +} + +/** + * Interpolate between two gradient keys + */ +export function interpolateGradientKeys( + keys: VFXGradientKey[], + ratio: number, + extractValue: (value: number | number[] | { r: number; g: number; b: number; a?: number } | undefined) => number +): number { + if (!keys || keys.length === 0) { + return 1; + } + + if (keys.length === 1) { + return extractValue(keys[0].value); + } + + // Find the two keys to interpolate between + for (let i = 0; i < keys.length - 1; i++) { + const pos1 = keys[i].pos ?? keys[i].time ?? 0; + const pos2 = keys[i + 1].pos ?? keys[i + 1].time ?? 1; + + if (ratio >= pos1 && ratio <= pos2) { + const t = pos2 - pos1 !== 0 ? (ratio - pos1) / (pos2 - pos1) : 0; + const val1 = extractValue(keys[i].value); + const val2 = extractValue(keys[i + 1].value); + return val1 + (val2 - val1) * t; + } + } + + // Clamp to first or last key + if (ratio <= (keys[0].pos ?? keys[0].time ?? 0)) { + return extractValue(keys[0].value); + } + return extractValue(keys[keys.length - 1].value); +} + +/** + * Interpolate color between two gradient keys + */ +export function interpolateColorKeys(keys: VFXGradientKey[], ratio: number): { r: number; g: number; b: number; a: number } { + if (!keys || keys.length === 0) { + return { r: 1, g: 1, b: 1, a: 1 }; + } + + if (keys.length === 1) { + const val = keys[0].value; + return { + ...extractColorFromValue(val), + a: extractAlphaFromValue(val), + }; + } + + // Find the two keys to interpolate between + for (let i = 0; i < keys.length - 1; i++) { + const pos1 = keys[i].pos ?? keys[i].time ?? 0; + const pos2 = keys[i + 1].pos ?? keys[i + 1].time ?? 1; + + if (ratio >= pos1 && ratio <= pos2) { + const t = pos2 - pos1 !== 0 ? (ratio - pos1) / (pos2 - pos1) : 0; + const val1 = keys[i].value; + const val2 = keys[i + 1].value; + + const c1 = extractColorFromValue(val1); + const c2 = extractColorFromValue(val2); + const a1 = extractAlphaFromValue(val1); + const a2 = extractAlphaFromValue(val2); + + return { + r: c1.r + (c2.r - c1.r) * t, + g: c1.g + (c2.g - c1.g) * t, + b: c1.b + (c2.b - c1.b) * t, + a: a1 + (a2 - a1) * t, + }; + } + } + + // Clamp to first or last key + if (ratio <= (keys[0].pos ?? keys[0].time ?? 0)) { + const val = keys[0].value; + return { + ...extractColorFromValue(val), + a: extractAlphaFromValue(val), + }; + } + const val = keys[keys.length - 1].value; + return { + ...extractColorFromValue(val), + a: extractAlphaFromValue(val), + }; +} + diff --git a/editor/src/editor/windows/fx-editor/VFX/factories/VFXBehaviorFunctionFactory.ts b/editor/src/editor/windows/fx-editor/VFX/factories/VFXBehaviorFunctionFactory.ts new file mode 100644 index 000000000..b0cf724b1 --- /dev/null +++ b/editor/src/editor/windows/fx-editor/VFX/factories/VFXBehaviorFunctionFactory.ts @@ -0,0 +1,189 @@ +import type { Particle } from "../../particle"; +import type { SolidParticle } from "../../solidParticle"; +import type { ParticleSystem } from "../../particleSystem"; +import type { + VFXBehavior, + VFXColorBySpeedBehavior, + VFXSizeBySpeedBehavior, + VFXRotationBySpeedBehavior, + VFXOrbitOverLifeBehavior, + VFXForceOverLifeBehavior, +} from "../types/behaviors"; +import type { VFXValueParser } from "../parsers/VFXValueParser"; +import type { VFXPerParticleBehaviorFunction, VFXPerSolidParticleBehaviorFunction, VFXSystemBehaviorFunction, VFXPerParticleContext } from "../types/VFXBehaviorFunction"; +import { + applyColorOverLifeSPS, + applySizeOverLifeSPS, + applyRotationOverLifeSPS, + applySpeedOverLifeSPS, + applyColorBySpeedSPS, + applySizeBySpeedSPS, + applyRotationBySpeedSPS, + applyOrbitOverLifeSPS, + applyColorBySpeedPS, + applySizeBySpeedPS, + applyRotationBySpeedPS, + applyOrbitOverLifePS, +} from "../behaviors"; + +export class VFXBehaviorFunctionFactory { + public static createPerParticleFunctionsSPS(behaviors: VFXBehavior[], valueParser: VFXValueParser): VFXPerSolidParticleBehaviorFunction[] { + const functions: VFXPerSolidParticleBehaviorFunction[] = []; + + for (const behavior of behaviors) { + switch (behavior.type) { + case "ColorOverLife": { + const b = behavior as any; + functions.push((particle: SolidParticle, context: VFXPerParticleContext) => { + applyColorOverLifeSPS(particle, b, context.lifeRatio); + }); + break; + } + + case "SizeOverLife": { + const b = behavior as any; + functions.push((particle: SolidParticle, context: VFXPerParticleContext) => { + applySizeOverLifeSPS(particle, b, context.lifeRatio); + }); + break; + } + + case "RotationOverLife": + case "Rotation3DOverLife": { + const b = behavior as any; + functions.push((particle: SolidParticle, context: VFXPerParticleContext) => { + applyRotationOverLifeSPS(particle, b, context.lifeRatio, valueParser, context.updateSpeed); + }); + break; + } + + case "ForceOverLife": + case "ApplyForce": { + const b = behavior as VFXForceOverLifeBehavior; + functions.push((particle: SolidParticle, context: VFXPerParticleContext) => { + const forceX = b.x ?? b.force?.x; + const forceY = b.y ?? b.force?.y; + const forceZ = b.z ?? b.force?.z; + if (forceX !== undefined || forceY !== undefined || forceZ !== undefined) { + const fx = forceX !== undefined ? valueParser.parseConstantValue(forceX) : 0; + const fy = forceY !== undefined ? valueParser.parseConstantValue(forceY) : 0; + const fz = forceZ !== undefined ? valueParser.parseConstantValue(forceZ) : 0; + particle.velocity.x += fx * context.updateSpeed; + particle.velocity.y += fy * context.updateSpeed; + particle.velocity.z += fz * context.updateSpeed; + } + }); + break; + } + + case "SpeedOverLife": { + const b = behavior as any; + functions.push((particle: SolidParticle, context: VFXPerParticleContext) => { + applySpeedOverLifeSPS(particle, b, context.lifeRatio, valueParser); + }); + break; + } + + case "ColorBySpeed": { + const b = behavior as VFXColorBySpeedBehavior; + functions.push((particle: SolidParticle, context: VFXPerParticleContext) => { + applyColorBySpeedSPS(particle, b, context.startSpeed, valueParser); + }); + break; + } + + case "SizeBySpeed": { + const b = behavior as VFXSizeBySpeedBehavior; + functions.push((particle: SolidParticle, context: VFXPerParticleContext) => { + applySizeBySpeedSPS(particle, b, context.startSpeed, valueParser); + }); + break; + } + + case "RotationBySpeed": { + const b = behavior as VFXRotationBySpeedBehavior; + functions.push((particle: SolidParticle, context: VFXPerParticleContext) => { + applyRotationBySpeedSPS(particle, b, context.startSpeed, valueParser, context.updateSpeed); + }); + break; + } + + case "OrbitOverLife": { + const b = behavior as VFXOrbitOverLifeBehavior; + functions.push((particle: SolidParticle, context: VFXPerParticleContext) => { + applyOrbitOverLifeSPS(particle, b, context.lifeRatio, valueParser); + }); + break; + } + } + } + + return functions; + } + + public static createPerParticleFunctionsPS(behaviors: VFXBehavior[], valueParser: VFXValueParser, particleSystem: ParticleSystem): VFXPerParticleBehaviorFunction[] { + const functions: VFXPerParticleBehaviorFunction[] = []; + + for (const behavior of behaviors) { + switch (behavior.type) { + case "ColorBySpeed": { + const b = behavior as VFXColorBySpeedBehavior; + functions.push((particle: Particle, context: VFXPerParticleContext) => { + applyColorBySpeedPS(particle as any, b, context.startSpeed, valueParser); + }); + break; + } + + case "SizeBySpeed": { + const b = behavior as VFXSizeBySpeedBehavior; + functions.push((particle: Particle, context: VFXPerParticleContext) => { + applySizeBySpeedPS(particle as any, b, context.startSpeed, valueParser); + }); + break; + } + + case "RotationBySpeed": { + const b = behavior as VFXRotationBySpeedBehavior; + functions.push((particle: Particle, context: VFXPerParticleContext) => { + applyRotationBySpeedPS(particle as any, b, context.startSpeed, particleSystem, valueParser); + }); + break; + } + + case "OrbitOverLife": { + const b = behavior as VFXOrbitOverLifeBehavior; + functions.push((particle: Particle, context: VFXPerParticleContext) => { + applyOrbitOverLifePS(particle, b, context.lifeRatio, valueParser); + }); + break; + } + } + } + + return functions; + } + + public static createSystemFunctions(behaviors: VFXBehavior[], valueParser: VFXValueParser): VFXSystemBehaviorFunction[] { + const functions: VFXSystemBehaviorFunction[] = []; + + for (const behavior of behaviors) { + switch (behavior.type) { + case "ColorOverLife": + case "SizeOverLife": + case "RotationOverLife": + case "Rotation3DOverLife": + case "ForceOverLife": + case "ApplyForce": + case "GravityForce": + case "SpeedOverLife": + case "FrameOverLife": + case "LimitSpeedOverLife": + // handled at emitter level + break; + } + } + + return functions; + } +} + diff --git a/editor/src/editor/windows/fx-editor/VFX/factories/VFXEmitterFactory.ts b/editor/src/editor/windows/fx-editor/VFX/factories/VFXEmitterFactory.ts new file mode 100644 index 000000000..ba28dd113 --- /dev/null +++ b/editor/src/editor/windows/fx-editor/VFX/factories/VFXEmitterFactory.ts @@ -0,0 +1,596 @@ +import type { Nullable } from "../../../types"; +import { Vector3, Matrix, Quaternion } from "../../../Maths/math.vector"; +import { Color4 } from "../../../Maths/math.color"; +import { ParticleSystem } from "../../particleSystem"; +import { SolidParticleSystem } from "../../solidParticleSystem"; +import { CreatePlane } from "../../../Meshes/Builders/planeBuilder"; +import { Mesh } from "../../../Meshes/mesh"; +import { Constants } from "../../../Engines/constants"; +import type { VFXEmitterData } from "../types/emitter"; +import type { VFXParseContext } from "../types/context"; +import type { VFXLoaderOptions } from "../types/loader"; +import { VFXLogger } from "../loggers/VFXLogger"; +import { VFXValueParser } from "../parsers/VFXValueParser"; +import type { IVFXMaterialFactory, IVFXGeometryFactory } from "../types/factories"; +import { VFXSolidParticleSystem } from "../systems/VFXSolidParticleSystem"; +import { VFXParticleSystem } from "../systems/VFXParticleSystem"; +import { VFXBehaviorFunctionFactory } from "./VFXBehaviorFunctionFactory"; +import { + applyColorOverLifePS, + applySizeOverLifePS, + applyRotationOverLifePS, + applyForceOverLifePS, + applyGravityForcePS, + applySpeedOverLifePS, + applyFrameOverLifePS, + applyLimitSpeedOverLifePS, +} from "../behaviors"; + +/** + * Factory for creating particle emitters (ParticleSystem and SolidParticleSystem) + */ +export class VFXEmitterFactory { + private _logger: VFXLogger; + private _context: VFXParseContext; + private _valueParser: VFXValueParser; + private _materialFactory: IVFXMaterialFactory; + private _geometryFactory: IVFXGeometryFactory; + + constructor(context: VFXParseContext, valueParser: VFXValueParser, materialFactory: IVFXMaterialFactory, geometryFactory: IVFXGeometryFactory) { + this._context = context; + this._logger = new VFXLogger("[VFXEmitterFactory]"); + this._valueParser = valueParser; + this._materialFactory = materialFactory; + this._geometryFactory = geometryFactory; + } + + /** + * Create a particle emitter from emitter data + */ + public createEmitter(emitterData: VFXEmitterData): Nullable { + const { config } = emitterData; + const { options } = this._context; + + // Check if we need SolidParticleSystem (mesh-based particles) + const useSolidParticles = config.renderMode === 2; + this._logger.log(`Using ${useSolidParticles ? "SolidParticleSystem" : "ParticleSystem"}`, options); + + if (useSolidParticles) { + return this._createSolidParticleSystem(emitterData); + } else { + return this._createParticleSystem(emitterData); + } + } + + /** + * Create a ParticleSystem (billboard-based particles) + */ + private _createParticleSystem(emitterData: VFXEmitterData): Nullable { + const { name, config } = emitterData; + const { scene, options } = this._context; + + this._logger.log(`Creating ParticleSystem: ${name}`, options); + + // Calculate capacity based on emission rate and duration + const emissionRate = config.emissionOverTime !== undefined ? this._valueParser.parseConstantValue(config.emissionOverTime) : 10; + const duration = config.duration || 5; + const capacity = Math.ceil(emissionRate * duration * 2); // Add some buffer + this._logger.log(` Emission rate: ${emissionRate}, Duration: ${duration}, Capacity: ${capacity}`, options); + + // Parse life time + const lifeTime = config.startLife !== undefined ? this._valueParser.parseIntervalValue(config.startLife) : { min: 1, max: 1 }; + this._logger.log(` Life time: ${lifeTime.min} - ${lifeTime.max}`, options); + + // Parse speed + const speed = config.startSpeed !== undefined ? this._valueParser.parseIntervalValue(config.startSpeed) : { min: 1, max: 1 }; + const avgStartSpeed = (speed.min + speed.max) / 2; + this._logger.log(` Speed: ${speed.min} - ${speed.max}`, options); + + // Parse size + const size = config.startSize !== undefined ? this._valueParser.parseIntervalValue(config.startSize) : { min: 1, max: 1 }; + const avgStartSize = (size.min + size.max) / 2; + this._logger.log(` Size: ${size.min} - ${size.max}`, options); + + // Parse start color + const startColor = config.startColor !== undefined ? this._valueParser.parseConstantColor(config.startColor) : new Color4(1, 1, 1, 1); + this._logger.log(` Start color: R=${startColor.r}, G=${startColor.g}, B=${startColor.b}, A=${startColor.a}`, options); + + // Create VFXParticleSystem instead of regular ParticleSystem + const particleSystem = new VFXParticleSystem(name, capacity, scene, this._valueParser, avgStartSpeed, avgStartSize, startColor); + + // Set basic properties + particleSystem.targetStopDuration = duration; + particleSystem.emitRate = emissionRate; + particleSystem.manualEmitCount = -1; + + // Set life time + particleSystem.minLifeTime = lifeTime.min; + particleSystem.maxLifeTime = lifeTime.max; + + // Set speed and size + particleSystem.minEmitPower = speed.min; + particleSystem.maxEmitPower = speed.max; + particleSystem.minSize = size.min; + particleSystem.maxSize = size.max; + + // Set colors + particleSystem.color1 = startColor; + particleSystem.color2 = startColor; + particleSystem.colorDead = new Color4(startColor.r, startColor.g, startColor.b, 0); + + // Parse start rotation + if (config.startRotation) { + if (typeof config.startRotation === "object" && config.startRotation !== null && "type" in config.startRotation && config.startRotation.type === "Euler") { + const eulerRotation = config.startRotation; + if (eulerRotation.angleZ !== undefined) { + const angleZ = this._valueParser.parseIntervalValue(eulerRotation.angleZ); + particleSystem.minInitialRotation = angleZ.min; + particleSystem.maxInitialRotation = angleZ.max; + } + } else { + const rotation = this._valueParser.parseIntervalValue(config.startRotation); + particleSystem.minInitialRotation = rotation.min; + particleSystem.maxInitialRotation = rotation.max; + } + } + + // Set sprite tiles if specified + if (config.uTileCount !== undefined && config.vTileCount !== undefined) { + if (config.uTileCount > 1 || config.vTileCount > 1) { + particleSystem.isAnimationSheetEnabled = true; + particleSystem.spriteCellWidth = config.uTileCount; + particleSystem.spriteCellHeight = config.vTileCount; + if (config.startTileIndex !== undefined) { + const startTile = this._valueParser.parseConstantValue(config.startTileIndex); + particleSystem.startSpriteCellID = Math.floor(startTile); + particleSystem.endSpriteCellID = Math.floor(startTile); + } + } + } + + // Set render order and layers + if (config.renderOrder !== undefined) { + particleSystem.renderingGroupId = config.renderOrder; + } + if (config.layers !== undefined) { + particleSystem.layerMask = config.layers; + } + + // Set emitter shape (pass matrix to extract rotation for emitter direction) + this._setEmitterShape(particleSystem, config.shape, emitterData.cumulativeScale, emitterData.matrix, options); + + // Load texture (ParticleSystem only needs texture, not material) + if (emitterData.materialId) { + const texture = this._materialFactory.createTexture(emitterData.materialId); + if (texture) { + particleSystem.particleTexture = texture; + // Get blend mode from material + const { jsonData } = this._context; + const material = jsonData.materials?.find((m: any) => m.uuid === emitterData.materialId); + if (material?.blending !== undefined) { + if (material.blending === 2) { + // Additive blending (Three.js AdditiveBlending) + particleSystem.blendMode = Constants.ALPHA_ADD; + } else if (material.blending === 1) { + // Normal blending (Three.js NormalBlending) + particleSystem.blendMode = Constants.ALPHA_COMBINE; + } else if (material.blending === 0) { + // No blending (Three.js NoBlending) + particleSystem.blendMode = Constants.ALPHA_DISABLE; + } + } + } + } + + // Handle emission bursts + if (config.emissionBursts && Array.isArray(config.emissionBursts) && config.emissionBursts.length > 0) { + this._applyEmissionBursts(particleSystem, config.emissionBursts, emissionRate, duration, options); + } + + // Apply behaviors + if (config.behaviors && Array.isArray(config.behaviors) && config.behaviors.length > 0) { + this._applyBehaviorsToPS(particleSystem, config.behaviors); + } + + // Set world space + if (config.worldSpace !== undefined) { + particleSystem.isLocal = !config.worldSpace; + this._logger.log(` World space: ${config.worldSpace}`, options); + } + + // Set looping + if (config.looping !== undefined) { + particleSystem.targetStopDuration = config.looping ? 0 : duration; + this._logger.log(` Looping: ${config.looping}`, options); + } + + // Set render mode + if (config.renderMode !== undefined) { + if (config.renderMode === 0) { + particleSystem.isBillboardBased = true; + this._logger.log(` Render mode: Billboard`, options); + } else if (config.renderMode === 1) { + particleSystem.billboardMode = ParticleSystem.BILLBOARDMODE_STRETCHED; + this._logger.log(` Render mode: Stretched Billboard`, options); + } + } + + // Set soft particles and auto destroy + if (config.softParticles !== undefined) { + this._logger.log(` Soft particles: ${config.softParticles} (not fully supported)`, options); + } + if (config.autoDestroy !== undefined) { + particleSystem.disposeOnStop = config.autoDestroy; + this._logger.log(` Auto destroy: ${config.autoDestroy}`, options); + } + + this._logger.log(`ParticleSystem created: ${name}`, options); + return particleSystem; + } + + /** + * Create a SolidParticleSystem (mesh-based particles) + */ + private _createSolidParticleSystem(emitterData: VFXEmitterData): Nullable { + const { name, config } = emitterData; + const { scene, options } = this._context; + + this._logger.log(`Creating SolidParticleSystem: ${name}`, options); + + // Calculate capacity based on emission rate and particle lifetime + // duration = particle lifetime (how long each particle lives) + // startLife = when particle becomes "alive" (for behaviors that depend on age) + // emissionOverTime = particles per second (e.g., 2.5 means 2.5 particles per second) + const emissionRate = config.emissionOverTime !== undefined ? this._valueParser.parseConstantValue(config.emissionOverTime) : 10; // particles per second + const particleLifetime = config.duration || 5; // duration is the particle lifetime + const isLooping = config.looping !== false; + + let capacity: number; + if (isLooping) { + // For looping systems: capacity = emissionRate * particleLifetime + // This gives the steady-state number of particles needed for perfect looping + // Example: emissionRate=2.5 particles/sec, particleLifetime=5 sec + // -> capacity = 2.5 * 5 = 12.5 -> 13 particles + // This ensures we have enough particles to cover the lifetime at the emission rate + capacity = Math.ceil(emissionRate * particleLifetime); + // Ensure minimum capacity of at least 1 + capacity = Math.max(capacity, 1); + this._logger.log(` Looping system: Emission rate: ${emissionRate} particles/sec, Particle lifetime: ${particleLifetime} sec, Capacity: ${capacity}`, options); + } else { + // For non-looping: capacity = emissionRate * particleLifetime * 2 (buffer for particles still alive) + capacity = Math.ceil(emissionRate * particleLifetime * 2); + this._logger.log(` Non-looping system: Emission rate: ${emissionRate} particles/sec, Particle lifetime: ${particleLifetime} sec, Capacity: ${capacity}`, options); + } + + // Get VFX transform from emitter data (stored during conversion) + // This is the clean way - transform is already in left-handed coordinate system + let vfxTransform: { position: Vector3; rotation: Quaternion; scale: Vector3 } | null = null; + const vfxEmitter = emitterData.vfxEmitter; + if (vfxEmitter && vfxEmitter.transform) { + vfxTransform = vfxEmitter.transform; + } + + const sps = new VFXSolidParticleSystem(name, scene, config, this._valueParser, { + updatable: true, + isPickable: false, + enableDepthSort: false, + particleIntersection: false, + useModelMaterial: true, + parentGroup: emitterData.parentGroup, + vfxTransform: vfxTransform, + logger: this._logger, + loaderOptions: options, + }); + + // Load geometry for particle shape + let particleMesh: Nullable = null; + if (config.instancingGeometry) { + this._logger.log(` Loading geometry: ${config.instancingGeometry}`, options); + particleMesh = this._geometryFactory.createMesh(config.instancingGeometry, emitterData.materialId, name + "_shape"); + if (!particleMesh) { + this._logger.warn(` Failed to load geometry ${config.instancingGeometry}, will create default plane`, options); + } + } + + // Default to plane if no geometry found + if (!particleMesh) { + this._logger.log(` Creating default plane geometry`, options); + particleMesh = CreatePlane(name + "_shape", { width: 1, height: 1 }, scene); + if (emitterData.materialId && particleMesh) { + const particleMaterial = this._materialFactory.createMaterial(emitterData.materialId, name); + if (particleMaterial) { + particleMesh.material = particleMaterial; + } + } + } else { + // Ensure material is applied + if (emitterData.materialId && particleMesh && !particleMesh.material) { + const particleMaterial = this._materialFactory.createMaterial(emitterData.materialId, name); + if (particleMaterial) { + particleMesh.material = particleMaterial; + } + } + } + + if (!particleMesh) { + this._logger.warn(` Cannot add shape to SPS: particleMesh is null`, options); + return null; + } + + this._logger.log(` Adding shape to SPS: mesh name=${particleMesh.name}, hasMaterial=${!!particleMesh.material}`, options); + sps.addShape(particleMesh, capacity); + + // Set billboard mode if needed + if (config.renderMode === 0 || config.renderMode === 1) { + sps.billboard = true; + } + + // Apply behaviors to SPS + if (config.behaviors && Array.isArray(config.behaviors) && config.behaviors.length > 0) { + this._applyBehaviorsToSPS(sps, config.behaviors); + this._logger.log(` Set SPS behaviors (${config.behaviors.length})`, options); + } + + // Cleanup temporary mesh + if (particleMesh) { + particleMesh.dispose(); + } + + this._logger.log(`SolidParticleSystem created: ${name}`, options); + return sps; + } + + /** + * Set the emitter shape based on Three.js shape configuration + * @param matrix Optional 4x4 matrix array from Three.js to extract rotation + */ + private _setEmitterShape(particleSystem: ParticleSystem, shape: any, cumulativeScale: Vector3, matrix?: number[], options?: VFXLoaderOptions): void { + if (!shape || !shape.type) { + particleSystem.createPointEmitter(Vector3.Zero(), Vector3.Zero()); + return; + } + + const scaleX = cumulativeScale.x; + const scaleY = cumulativeScale.y; + const scaleZ = cumulativeScale.z; + + // Extract rotation from matrix if provided + let rotationMatrix: Matrix | null = null; + if (matrix && matrix.length >= 16) { + // Three.js uses column-major order, Babylon.js uses row-major + const mat = Matrix.FromArray(matrix); + mat.transpose(); + + // Extract rotation matrix (remove scale and translation) + rotationMatrix = mat.getRotationMatrix(); + this._logger.log(` Extracted rotation from matrix`, options); + } + + // Helper function to apply rotation to default direction + const applyRotation = (defaultDir: Vector3): Vector3 => { + if (rotationMatrix) { + const rotatedDir = Vector3.Zero(); + Vector3.TransformNormalToRef(defaultDir, rotationMatrix, rotatedDir); + return rotatedDir; + } + return defaultDir; + }; + + switch (shape.type.toLowerCase()) { + case "cone": { + let radius = shape.radius || 1; + const angle = shape.angle !== undefined ? shape.angle : Math.PI / 4; + const coneScale = (scaleX + scaleZ) / 2; + radius = radius * coneScale; + + // Default direction for cone is up (0, 1, 0) + const defaultDir = new Vector3(0, 1, 0); + const rotatedDir = applyRotation(defaultDir); + + if (rotationMatrix) { + // Use directed emitter with rotated direction + particleSystem.createDirectedConeEmitter(radius, angle, rotatedDir, rotatedDir); + this._logger.log( + ` Created directed cone emitter with rotated direction: (${rotatedDir.x.toFixed(2)}, ${rotatedDir.y.toFixed(2)}, ${rotatedDir.z.toFixed(2)})`, + options + ); + } else { + particleSystem.createConeEmitter(radius, angle); + } + break; + } + + case "sphere": { + let sphereRadius = shape.radius || 1; + const sphereScale = (scaleX + scaleY + scaleZ) / 3; + sphereRadius = sphereRadius * sphereScale; + + // Default direction for sphere is up (0, 1, 0) + const defaultDir = new Vector3(0, 1, 0); + const rotatedDir = applyRotation(defaultDir); + + if (rotationMatrix) { + particleSystem.createDirectedSphereEmitter(sphereRadius, rotatedDir, rotatedDir); + this._logger.log( + ` Created directed sphere emitter with rotated direction: (${rotatedDir.x.toFixed(2)}, ${rotatedDir.y.toFixed(2)}, ${rotatedDir.z.toFixed(2)})`, + options + ); + } else { + particleSystem.createSphereEmitter(sphereRadius); + } + break; + } + + case "point": { + const defaultDir = new Vector3(0, 1, 0); + const rotatedDir = applyRotation(defaultDir); + + if (rotationMatrix) { + particleSystem.createPointEmitter(rotatedDir, rotatedDir); + this._logger.log( + ` Created point emitter with rotated direction: (${rotatedDir.x.toFixed(2)}, ${rotatedDir.y.toFixed(2)}, ${rotatedDir.z.toFixed(2)})`, + options + ); + } else { + particleSystem.createPointEmitter(Vector3.Zero(), Vector3.Zero()); + } + break; + } + + case "box": { + let boxSize = shape.size || [1, 1, 1]; + boxSize = [boxSize[0] * scaleX, boxSize[1] * scaleY, boxSize[2] * scaleZ]; + const minBox = new Vector3(-boxSize[0] / 2, -boxSize[1] / 2, -boxSize[2] / 2); + const maxBox = new Vector3(boxSize[0] / 2, boxSize[1] / 2, boxSize[2] / 2); + + const defaultDir = new Vector3(0, 1, 0); + const rotatedDir = applyRotation(defaultDir); + + if (rotationMatrix) { + particleSystem.createBoxEmitter(rotatedDir, rotatedDir, minBox, maxBox); + this._logger.log(` Created box emitter with rotated direction: (${rotatedDir.x.toFixed(2)}, ${rotatedDir.y.toFixed(2)}, ${rotatedDir.z.toFixed(2)})`, options); + } else { + particleSystem.createBoxEmitter(Vector3.Zero(), Vector3.Zero(), minBox, maxBox); + } + break; + } + + case "hemisphere": { + let hemRadius = shape.radius || 1; + const hemScale = (scaleX + scaleY + scaleZ) / 3; + hemRadius = hemRadius * hemScale; + particleSystem.createHemisphericEmitter(hemRadius); + break; + } + + case "cylinder": { + let cylRadius = shape.radius || 1; + let height = shape.height || 1; + const cylRadiusScale = (scaleX + scaleZ) / 2; + cylRadius = cylRadius * cylRadiusScale; + height = height * scaleY; + + // Default direction for cylinder is up (0, 1, 0) + const defaultDir = new Vector3(0, 1, 0); + const rotatedDir = applyRotation(defaultDir); + + if (rotationMatrix) { + particleSystem.createDirectedCylinderEmitter(cylRadius, height, 1, rotatedDir, rotatedDir); + this._logger.log( + ` Created directed cylinder emitter with rotated direction: (${rotatedDir.x.toFixed(2)}, ${rotatedDir.y.toFixed(2)}, ${rotatedDir.z.toFixed(2)})`, + options + ); + } else { + particleSystem.createCylinderEmitter(cylRadius, height); + } + break; + } + + default: { + const defaultDir = new Vector3(0, 1, 0); + const rotatedDir = applyRotation(defaultDir); + + if (rotationMatrix) { + particleSystem.createPointEmitter(rotatedDir, rotatedDir); + } else { + particleSystem.createPointEmitter(Vector3.Zero(), Vector3.Zero()); + } + break; + } + } + } + + /** + * Apply emission bursts via emit rate gradients + */ + private _applyEmissionBursts( + particleSystem: ParticleSystem, + bursts: import("../types/emitterConfig").VFXEmissionBurst[], + baseEmitRate: number, + duration: number, + options?: VFXLoaderOptions + ): void { + for (const burst of bursts) { + if (burst.time !== undefined && burst.count !== undefined) { + const burstTime = this._valueParser.parseConstantValue(burst.time); + const burstCount = this._valueParser.parseConstantValue(burst.count); + const timeRatio = Math.min(Math.max(burstTime / duration, 0), 1); + + const windowSize = 0.02; + const burstEmitRate = burstCount / windowSize; + + const beforeTime = Math.max(0, timeRatio - windowSize); + const afterTime = Math.min(1, timeRatio + windowSize); + + particleSystem.addEmitRateGradient(beforeTime, baseEmitRate); + particleSystem.addEmitRateGradient(timeRatio, burstEmitRate); + particleSystem.addEmitRateGradient(afterTime, baseEmitRate); + } + } + } + + /** + * Apply behaviors to ParticleSystem + */ + private _applyBehaviorsToPS(particleSystem: ParticleSystem, behaviors: import("../types/behaviors").VFXBehavior[]): void { + const { options } = this._context; + const valueParser = this._valueParser; + + const vfxPS = particleSystem as any as VFXParticleSystem; + if (vfxPS && typeof vfxPS.setPerParticleBehaviors === "function") { + // Apply system-level behaviors (gradients, etc.) + for (const behavior of behaviors) { + if (!behavior.type) { + this._logger.warn(`Behavior missing type: ${JSON.stringify(behavior)}`, options); + continue; + } + + this._logger.log(` Processing behavior: ${behavior.type}`, options); + + switch (behavior.type) { + case "ColorOverLife": + applyColorOverLifePS(particleSystem, behavior as any); + break; + case "SizeOverLife": + applySizeOverLifePS(particleSystem, behavior as any); + break; + case "RotationOverLife": + case "Rotation3DOverLife": + applyRotationOverLifePS(particleSystem, behavior as any, valueParser); + break; + case "ForceOverLife": + case "ApplyForce": + applyForceOverLifePS(particleSystem, behavior as any, valueParser); + break; + case "GravityForce": + applyGravityForcePS(particleSystem, behavior as any, valueParser); + break; + case "SpeedOverLife": + applySpeedOverLifePS(particleSystem, behavior as any, valueParser); + break; + case "FrameOverLife": + applyFrameOverLifePS(particleSystem, behavior as any, valueParser); + break; + case "LimitSpeedOverLife": + applyLimitSpeedOverLifePS(particleSystem, behavior as any, valueParser); + break; + } + } + + // Create and set per-particle behavior functions + const perParticleFunctions = VFXBehaviorFunctionFactory.createPerParticleFunctionsPS(behaviors, valueParser, particleSystem); + vfxPS.setPerParticleBehaviors(perParticleFunctions); + } + } + + /** + * Apply behaviors to SolidParticleSystem + */ + private _applyBehaviorsToSPS(sps: SolidParticleSystem, behaviors: import("../types/behaviors").VFXBehavior[]): void { + const vfxSPS = sps as any as VFXSolidParticleSystem; + if (vfxSPS && typeof vfxSPS.setPerParticleBehaviors === "function") { + const perParticleFunctions = VFXBehaviorFunctionFactory.createPerParticleFunctionsSPS(behaviors, this._valueParser); + vfxSPS.setPerParticleBehaviors(perParticleFunctions); + } + } +} diff --git a/editor/src/editor/windows/fx-editor/VFX/factories/VFXGeometryFactory.ts b/editor/src/editor/windows/fx-editor/VFX/factories/VFXGeometryFactory.ts new file mode 100644 index 000000000..30cc63183 --- /dev/null +++ b/editor/src/editor/windows/fx-editor/VFX/factories/VFXGeometryFactory.ts @@ -0,0 +1,150 @@ +import type { Nullable } from "../../../types"; +import type { Scene } from "../../../scene"; +import { Mesh } from "../../../Meshes/mesh"; +import { VertexData } from "../../../Meshes/mesh.vertexData"; +import { CreatePlane } from "../../../Meshes/Builders/planeBuilder"; +import type { IVFXGeometryFactory } from "../types/factories"; +import type { VFXParseContext } from "../types/context"; +import type { VFXLoaderOptions } from "../types/loader"; +import { VFXLogger } from "../loggers/VFXLogger"; +import { VFXMaterialFactory } from "./VFXMaterialFactory"; + +/** + * Factory for creating meshes from Three.js geometry data + */ +export class VFXGeometryFactory implements IVFXGeometryFactory { + private _logger: VFXLogger; + private _context: VFXParseContext; + private _materialFactory: VFXMaterialFactory; + + constructor(context: VFXParseContext, materialFactory: VFXMaterialFactory) { + this._context = context; + this._logger = new VFXLogger("[VFXGeometryFactory]"); + this._materialFactory = materialFactory; + } + + /** + * Create a mesh from geometry ID with material applied + */ + public createMesh(geometryId: string, materialId: string | undefined, name: string): Nullable { + const { jsonData, scene, options } = this._context; + + this._logger.log(`Creating mesh from geometry ID: ${geometryId}, name: ${name}`, options); + if (!jsonData.geometries) { + this._logger.warn("No geometries data available", options); + return null; + } + + // Find geometry + const geometryData = jsonData.geometries.find((g) => g.uuid === geometryId); + if (!geometryData) { + this._logger.warn(`Geometry not found: ${geometryId}`, options); + return null; + } + + this._logger.log(`Found geometry: ${geometryData.name || geometryData.type || geometryId} (type: ${geometryData.type})`, options); + + // Create mesh from geometry + const mesh = this._createMeshFromGeometry(geometryData, scene, name, options); + if (!mesh) { + this._logger.warn(`Failed to create mesh from geometry ${geometryId}`, options); + return null; + } + + // Apply material if provided + if (materialId) { + const material = this._materialFactory.createMaterial(materialId, name); + if (material) { + mesh.material = material; + this._logger.log(`Applied material to mesh: ${name}`, options); + } + } + + return mesh; + } + + /** + * Create a mesh from Three.js geometry data + */ + private _createMeshFromGeometry( + geometryData: import("../types/quarksTypes").QuarksGeometry, + scene: Scene, + name: string = "ParticleMesh", + options?: VFXLoaderOptions + ): Nullable { + if (!geometryData) { + this._logger.warn(`createMeshFromGeometry: geometryData is null`, options); + return null; + } + + this._logger.log(`createMeshFromGeometry: type=${geometryData.type}, name=${name}`, options); + + // Handle PlaneGeometry + if (geometryData.type === "PlaneGeometry") { + const width = typeof geometryData.width === "number" ? geometryData.width : 1; + const height = typeof geometryData.height === "number" ? geometryData.height : 1; + this._logger.log(` Creating PlaneGeometry: width=${width}, height=${height}`, options); + const mesh = CreatePlane(name, { width, height }, scene); + if (mesh) { + this._logger.log(` PlaneGeometry created successfully`, options); + } else { + this._logger.warn(` Failed to create PlaneGeometry`, options); + } + return mesh; + } + + // Handle BufferGeometry + if (geometryData.type === "BufferGeometry" && geometryData.data && geometryData.data.attributes) { + const attrs = geometryData.data.attributes; + const positions = attrs.position; + const normals = attrs.normal; + const uvs = attrs.uv; + const colors = attrs.color; + const indices = geometryData.data.index; + + if (!positions || !positions.array) { + return null; + } + + const vertexData = new VertexData(); + vertexData.positions = Array.from(positions.array); + + if (normals && normals.array) { + vertexData.normals = Array.from(normals.array); + } + + if (uvs && uvs.array) { + vertexData.uvs = Array.from(uvs.array); + } + + if (colors && colors.array) { + vertexData.colors = Array.from(colors.array); + } + + if (indices && indices.array) { + vertexData.indices = Array.from(indices.array); + } else { + // Generate indices if not provided + const vertexCount = vertexData.positions.length / 3; + const generatedIndices: number[] = []; + for (let i = 0; i < vertexCount; i++) { + generatedIndices.push(i); + } + vertexData.indices = generatedIndices; + } + + const mesh = new Mesh(name, scene); + vertexData.applyToMesh(mesh); + + // Convert from Three.js (right-handed) to Babylon.js (left-handed) coordinate system + // This inverts Z coordinates, flips face winding, and negates normal Z + if (mesh.geometry) { + mesh.geometry.toLeftHanded(); + } + + return mesh; + } + + return null; + } +} diff --git a/editor/src/editor/windows/fx-editor/VFX/factories/VFXMaterialFactory.ts b/editor/src/editor/windows/fx-editor/VFX/factories/VFXMaterialFactory.ts new file mode 100644 index 000000000..ef2bc7d33 --- /dev/null +++ b/editor/src/editor/windows/fx-editor/VFX/factories/VFXMaterialFactory.ts @@ -0,0 +1,366 @@ +import type { Nullable } from "../../../types"; +import { Color3 } from "../../../Maths/math.color"; +import { Texture } from "../../../Materials/Textures/texture"; +import { PBRMaterial } from "../../../Materials/PBR/pbrMaterial"; +import { Material } from "../../../Materials/material"; +import { Constants } from "../../../Engines/constants"; +import { Tools } from "../../../Misc/tools"; +import type { IVFXMaterialFactory } from "../types/factories"; +import type { VFXParseContext } from "../types/context"; +import type { VFXLoaderOptions } from "../types/loader"; +import { VFXLogger } from "../loggers/VFXLogger"; +import type { QuarksTexture } from "../types/quarksTypes"; +import type { Scene } from "../../../scene"; + +/** + * Factory for creating materials and textures from Three.js JSON data + */ +export class VFXMaterialFactory implements IVFXMaterialFactory { + private _logger: VFXLogger; + private _context: VFXParseContext; + + constructor(context: VFXParseContext) { + this._context = context; + this._logger = new VFXLogger("[VFXMaterialFactory]"); + } + + /** + * Create a texture from material ID (for ParticleSystem - no material needed) + */ + public createTexture(materialId: string): Nullable { + const { jsonData, scene, rootUrl, options } = this._context; + + if (!jsonData.materials || !jsonData.textures || !jsonData.images) { + this._logger.warn(`Missing materials/textures/images data for material ${materialId}`, options); + return null; + } + + // Find material + const material = jsonData.materials.find((m) => m.uuid === materialId); + if (!material) { + this._logger.warn(`Material not found: ${materialId}`, options); + return null; + } + if (!material.map) { + this._logger.warn(`Material ${materialId} has no texture map`, options); + return null; + } + + // Find texture + const texture = jsonData.textures.find((t) => t.uuid === material.map); + if (!texture) { + this._logger.warn(`Texture not found: ${material.map}`, options); + return null; + } + if (!texture.image) { + this._logger.warn(`Texture ${material.map} has no image`, options); + return null; + } + + // Find image + const image = jsonData.images.find((img) => img.uuid === texture.image); + if (!image) { + this._logger.warn(`Image not found: ${texture.image}`, options); + return null; + } + + // Create texture URL from image data + let textureUrl: string; + if (image.url) { + textureUrl = Tools.GetAssetUrl(rootUrl + image.url); + } else if (image.data) { + // Base64 embedded texture + textureUrl = `data:image/${image.format || "png"};base64,${image.data}`; + } else { + this._logger.warn(`Image ${texture.image} has neither URL nor data`, options); + return null; + } + + // Create texture using helper method + return this._createTextureFromData(textureUrl, texture, scene, options); + } + + /** + * Helper method to create texture from texture data + */ + private _createTextureFromData(textureUrl: string, texture: QuarksTexture, scene: Scene, options?: VFXLoaderOptions): Texture { + // Determine sampling mode from texture filters + let samplingMode = Texture.TRILINEAR_SAMPLINGMODE; // Default + if (texture.minFilter !== undefined) { + if (texture.minFilter === 1008 || texture.minFilter === 1009) { + samplingMode = Texture.TRILINEAR_SAMPLINGMODE; + } else if (texture.minFilter === 1007 || texture.minFilter === 1006) { + samplingMode = Texture.BILINEAR_SAMPLINGMODE; + } else { + samplingMode = Texture.NEAREST_SAMPLINGMODE; + } + } else if (texture.magFilter !== undefined) { + if (texture.magFilter === 1006) { + samplingMode = Texture.BILINEAR_SAMPLINGMODE; + } else { + samplingMode = Texture.NEAREST_SAMPLINGMODE; + } + } + + // Create texture with proper settings + const babylonTexture = new Texture(textureUrl, scene, { + noMipmap: !texture.generateMipmaps, + invertY: texture.flipY !== false, // Three.js flipY defaults to true + samplingMode: samplingMode, + }); + + // Configure texture properties from Three.js JSON + if (texture.wrap && Array.isArray(texture.wrap)) { + const wrapU = texture.wrap[0] === 1000 ? Texture.WRAP_ADDRESSMODE : texture.wrap[0] === 1001 ? Texture.CLAMP_ADDRESSMODE : Texture.MIRROR_ADDRESSMODE; + const wrapV = texture.wrap[1] === 1000 ? Texture.WRAP_ADDRESSMODE : texture.wrap[1] === 1001 ? Texture.CLAMP_ADDRESSMODE : Texture.MIRROR_ADDRESSMODE; + babylonTexture.wrapU = wrapU; + babylonTexture.wrapV = wrapV; + } + + if (texture.repeat && Array.isArray(texture.repeat)) { + babylonTexture.uScale = texture.repeat[0] || 1; + babylonTexture.vScale = texture.repeat[1] || 1; + } + + if (texture.offset && Array.isArray(texture.offset)) { + babylonTexture.uOffset = texture.offset[0] || 0; + babylonTexture.vOffset = texture.offset[1] || 0; + } + + if (texture.channel !== undefined && typeof texture.channel === "number") { + babylonTexture.coordinatesIndex = texture.channel; + } + + if (texture.rotation !== undefined) { + babylonTexture.uAng = texture.rotation; + } + + return babylonTexture; + } + + /** + * Create a material with texture from material ID + */ + public createMaterial(materialId: string, name: string): Nullable { + const { jsonData, scene, rootUrl, options } = this._context; + + this._logger.log(`Creating material for ID: ${materialId}, name: ${name}`, options); + if (!jsonData.materials || !jsonData.textures || !jsonData.images) { + this._logger.warn(`Missing materials/textures/images data for material ${materialId}`, options); + return null; + } + + // Find material + const material = jsonData.materials.find((m) => m.uuid === materialId); + if (!material) { + this._logger.warn(`Material not found: ${materialId}`, options); + return null; + } + if (!material.map) { + this._logger.warn(`Material ${materialId} has no texture map`, options); + return null; + } + + const materialType = material.type || "MeshStandardMaterial"; + this._logger.log(`Found material: type=${materialType}, uuid=${material.uuid}, transparent=${material.transparent}, blending=${material.blending}`, options); + + // Find texture + const texture = jsonData.textures.find((t) => t.uuid === material.map); + if (!texture) { + this._logger.warn(`Texture not found: ${material.map}`, options); + return null; + } + if (!texture.image) { + this._logger.warn(`Texture ${material.map} has no image`, options); + return null; + } + + this._logger.log(`Found texture: ${JSON.stringify({ uuid: texture.uuid, image: texture.image })}`, options); + + // Find image + const image = jsonData.images.find((img) => img.uuid === texture.image); + if (!image) { + this._logger.warn(`Image not found: ${texture.image}`, options); + return null; + } + + const imageInfo = []; + if (image.url) { + const urlParts = image.url.split("/"); + let filename = urlParts[urlParts.length - 1] || image.url; + // If filename looks like base64 data (very long), truncate it + if (filename.length > 50) { + filename = filename.substring(0, 20) + "..."; + } + imageInfo.push(`file: ${filename}`); + } + if (image.data) { + imageInfo.push("embedded"); + } + if (image.format) { + imageInfo.push(`format: ${image.format}`); + } + this._logger.log(`Found image: ${imageInfo.join(", ") || "unknown"}`, options); + + // Create texture URL from image data + let textureUrl: string; + if (image.url) { + textureUrl = Tools.GetAssetUrl(rootUrl + image.url); + // Extract filename from URL for logging + const urlParts = image.url.split("/"); + let filename = urlParts[urlParts.length - 1] || image.url; + // If filename looks like base64 data (very long), truncate it + if (filename.length > 50) { + filename = filename.substring(0, 20) + "..."; + } + this._logger.log(`Using external texture: ${filename}`, options); + } else if (image.data) { + // Base64 embedded texture + textureUrl = `data:image/${image.format || "png"};base64,${image.data}`; + this._logger.log(`Using base64 embedded texture (format: ${image.format || "png"})`, options); + } else { + this._logger.warn(`Image ${texture.image} has neither URL nor data`, options); + return null; + } + + // Determine sampling mode from texture filters + let samplingMode = Texture.TRILINEAR_SAMPLINGMODE; // Default + if (texture.minFilter !== undefined) { + if (texture.minFilter === 1008 || texture.minFilter === 1009) { + samplingMode = Texture.TRILINEAR_SAMPLINGMODE; + } else if (texture.minFilter === 1007 || texture.minFilter === 1006) { + samplingMode = Texture.BILINEAR_SAMPLINGMODE; + } else { + samplingMode = Texture.NEAREST_SAMPLINGMODE; + } + } else if (texture.magFilter !== undefined) { + if (texture.magFilter === 1006) { + samplingMode = Texture.BILINEAR_SAMPLINGMODE; + } else { + samplingMode = Texture.NEAREST_SAMPLINGMODE; + } + } + + // Create texture with proper settings + const babylonTexture = new Texture(textureUrl, scene, { + noMipmap: !texture.generateMipmaps, + invertY: texture.flipY !== false, // Three.js flipY defaults to true + samplingMode: samplingMode, + }); + + // Configure texture properties from Three.js JSON + // wrap: [1001, 1001] = WRAP_ADDRESSMODE (repeat) + if (texture.wrap && Array.isArray(texture.wrap)) { + // Three.js wrap: 1000 = RepeatWrapping, 1001 = ClampToEdgeWrapping, 1002 = MirroredRepeatWrapping + // Babylon.js: WRAP_ADDRESSMODE = 0, CLAMP_ADDRESSMODE = 1, MIRROR_ADDRESSMODE = 2 + const wrapU = texture.wrap[0] === 1000 ? Texture.WRAP_ADDRESSMODE : texture.wrap[0] === 1001 ? Texture.CLAMP_ADDRESSMODE : Texture.MIRROR_ADDRESSMODE; + const wrapV = texture.wrap[1] === 1000 ? Texture.WRAP_ADDRESSMODE : texture.wrap[1] === 1001 ? Texture.CLAMP_ADDRESSMODE : Texture.MIRROR_ADDRESSMODE; + babylonTexture.wrapU = wrapU; + babylonTexture.wrapV = wrapV; + } + + // repeat: [1, 1] -> uScale, vScale + if (texture.repeat && Array.isArray(texture.repeat)) { + babylonTexture.uScale = texture.repeat[0] || 1; + babylonTexture.vScale = texture.repeat[1] || 1; + } + + // offset: [0, 0] -> uOffset, vOffset + if (texture.offset && Array.isArray(texture.offset)) { + babylonTexture.uOffset = texture.offset[0] || 0; + babylonTexture.vOffset = texture.offset[1] || 0; + } + + // channel: 0 -> coordinatesIndex + if (texture.channel !== undefined && typeof texture.channel === "number") { + babylonTexture.coordinatesIndex = texture.channel; + } + + // rotation: 0 -> uAng (rotation in radians) + if (texture.rotation !== undefined) { + babylonTexture.uAng = texture.rotation; + } + + // Parse color from Three.js material (default is white 0xffffff) + let materialColor = new Color3(1, 1, 1); + if (material.color !== undefined) { + // Three.js color is stored as hex number (e.g., 16777215 = 0xffffff) or hex string + let colorHex: number; + if (typeof material.color === "number") { + colorHex = material.color; + } else if (typeof material.color === "string") { + colorHex = parseInt((material.color as string).replace("#", ""), 16); + } else { + colorHex = 0xffffff; + } + const r = ((colorHex >> 16) & 0xff) / 255; + const g = ((colorHex >> 8) & 0xff) / 255; + const b = (colorHex & 0xff) / 255; + materialColor = new Color3(r, g, b); + this._logger.log(`Parsed material color: R=${r.toFixed(2)}, G=${g.toFixed(2)}, B=${b.toFixed(2)}`, options); + } + + // Handle different Three.js material types + if (materialType === "MeshBasicMaterial") { + // MeshBasicMaterial: Use PBRMaterial with unlit = true (equivalent to UnlitMaterial) + const unlitMaterial = new PBRMaterial(name + "_material", scene); + unlitMaterial.unlit = true; + unlitMaterial.albedoColor = materialColor; + unlitMaterial.albedoTexture = babylonTexture; + + // Transparency + if (material.transparent !== undefined && material.transparent) { + unlitMaterial.transparencyMode = Material.MATERIAL_ALPHABLEND; + unlitMaterial.needDepthPrePass = false; + babylonTexture.hasAlpha = true; + unlitMaterial.useAlphaFromAlbedoTexture = true; + this._logger.log(`Material is transparent (transparencyMode: ALPHABLEND, alphaMode: COMBINE)`, options); + } else { + unlitMaterial.transparencyMode = Material.MATERIAL_OPAQUE; + unlitMaterial.alpha = 1.0; + } + + // Depth write + if (material.depthWrite !== undefined) { + unlitMaterial.disableDepthWrite = !material.depthWrite; + this._logger.log(`Set disableDepthWrite: ${!material.depthWrite}`, options); + } else { + unlitMaterial.disableDepthWrite = true; // Default to false depthWrite = true disableDepthWrite + } + + // Double sided + unlitMaterial.backFaceCulling = false; + + // Side orientation + if (material.side !== undefined) { + // Three.js: 0 = FrontSide, 1 = BackSide, 2 = DoubleSide + // Babylon.js: 0 = Front, 1 = Back, 2 = Double + unlitMaterial.sideOrientation = material.side; + this._logger.log(`Set sideOrientation: ${material.side}`, options); + } + + // Blend mode + if (material.blending !== undefined) { + if (material.blending === 2) { + // Additive blending (Three.js AdditiveBlending) + unlitMaterial.alphaMode = Constants.ALPHA_ADD; + this._logger.log("Set blend mode: ADDITIVE", options); + } else if (material.blending === 1) { + // Normal blending (Three.js NormalBlending) + unlitMaterial.alphaMode = Constants.ALPHA_COMBINE; + this._logger.log("Set blend mode: NORMAL", options); + } else if (material.blending === 0) { + // No blending (Three.js NoBlending) + unlitMaterial.alphaMode = Constants.ALPHA_DISABLE; + this._logger.log("Set blend mode: NO_BLENDING", options); + } + } + + this._logger.log(`Using MeshBasicMaterial: PBRMaterial with unlit=true, albedoTexture`, options); + this._logger.log(`Material created successfully: ${name}_material`, options); + return unlitMaterial; + } else { + return new PBRMaterial(name + "_material", scene); + } + } +} diff --git a/editor/src/editor/windows/fx-editor/VFX/index.ts b/editor/src/editor/windows/fx-editor/VFX/index.ts new file mode 100644 index 000000000..0a58635b8 --- /dev/null +++ b/editor/src/editor/windows/fx-editor/VFX/index.ts @@ -0,0 +1,13 @@ +export * from "./types"; +export * from "./parsers/VFXParser"; +export * from "./parsers/VFXValueParser"; +export * from "./parsers/VFXDataConverter"; +export * from "./factories/VFXMaterialFactory"; +export * from "./factories/VFXGeometryFactory"; +export * from "./factories/VFXEmitterFactory"; +export * from "./factories/VFXBehaviorFunctionFactory"; +export * from "./processors/VFXHierarchyProcessor"; +export * from "./systems/VFXSolidParticleSystem"; +export * from "./systems/VFXParticleSystem"; +export * from "./loggers/VFXLogger"; +export * from "./VFXEffect"; diff --git a/editor/src/editor/windows/fx-editor/VFX/loggers/VFXLogger.ts b/editor/src/editor/windows/fx-editor/VFX/loggers/VFXLogger.ts new file mode 100644 index 000000000..0c05d5896 --- /dev/null +++ b/editor/src/editor/windows/fx-editor/VFX/loggers/VFXLogger.ts @@ -0,0 +1,38 @@ +import { Logger } from "../../../Misc/logger"; +import type { VFXLoaderOptions } from "../types"; + +/** + * Logger utility for VFX operations + */ +export class VFXLogger { + private _prefix: string; + + constructor(prefix: string = "[VFX]") { + this._prefix = prefix; + } + + /** + * Log a message if verbose mode is enabled + */ + public log(message: string, options?: VFXLoaderOptions): void { + if (options?.verbose) { + Logger.Log(`${this._prefix} ${message}`); + } + } + + /** + * Log a warning if verbose or validate mode is enabled + */ + public warn(message: string, options?: VFXLoaderOptions): void { + if (options?.verbose || options?.validate) { + Logger.Warn(`${this._prefix} ${message}`); + } + } + + /** + * Log an error + */ + public error(message: string, options?: VFXLoaderOptions): void { + Logger.Error(`${this._prefix} ${message}`); + } +} diff --git a/editor/src/editor/windows/fx-editor/VFX/parsers/VFXDataConverter.ts b/editor/src/editor/windows/fx-editor/VFX/parsers/VFXDataConverter.ts new file mode 100644 index 000000000..190e16ecc --- /dev/null +++ b/editor/src/editor/windows/fx-editor/VFX/parsers/VFXDataConverter.ts @@ -0,0 +1,570 @@ +import { Vector3, Matrix, Quaternion } from "../../../Maths/math.vector"; +import type { VFXLoaderOptions } from "../types/loader"; +import type { QuarksVFXJSON } from "../types/quarksTypes"; +import type { + QuarksObject, + QuarksParticleEmitterConfig, + QuarksBehavior, + QuarksValue, + QuarksColor, + QuarksRotation, + QuarksGradientKey, + QuarksShape, + QuarksColorOverLifeBehavior, + QuarksSizeOverLifeBehavior, + QuarksRotationOverLifeBehavior, + QuarksForceOverLifeBehavior, + QuarksGravityForceBehavior, + QuarksSpeedOverLifeBehavior, + QuarksFrameOverLifeBehavior, + QuarksLimitSpeedOverLifeBehavior, + QuarksColorBySpeedBehavior, + QuarksSizeBySpeedBehavior, + QuarksRotationBySpeedBehavior, + QuarksOrbitOverLifeBehavior, +} from "../types/quarksTypes"; +import type { VFXTransform, VFXGroup, VFXEmitter, VFXHierarchy } from "../types/hierarchy"; +import type { VFXParticleEmitterConfig } from "../types/emitterConfig"; +import type { + VFXBehavior, + VFXColorOverLifeBehavior, + VFXSizeOverLifeBehavior, + VFXForceOverLifeBehavior, + VFXSpeedOverLifeBehavior, + VFXLimitSpeedOverLifeBehavior, + VFXColorBySpeedBehavior, + VFXSizeBySpeedBehavior, +} from "../types/behaviors"; +import type { VFXValue } from "../types/values"; +import type { VFXColor } from "../types/colors"; +import type { VFXRotation } from "../types/rotations"; +import type { VFXGradientKey } from "../types/gradients"; +import type { VFXShape } from "../types/shapes"; +import { VFXLogger } from "../loggers/VFXLogger"; + +/** + * Converts Quarks/Three.js VFX JSON (right-handed) to Babylon.js VFX format (left-handed) + * All coordinate system conversions happen here, once + */ +export class VFXDataConverter { + private _logger: VFXLogger; + private _options?: VFXLoaderOptions; + + constructor(options?: VFXLoaderOptions) { + this._logger = new VFXLogger("[VFXDataConverter]"); + this._options = options; + } + + /** + * Convert Quarks/Three.js VFX JSON to Babylon.js VFX format + */ + public convert(quarksVFXData: QuarksVFXJSON): VFXHierarchy { + this._logger.log("=== Converting Quarks VFX to Babylon.js VFX format ===", this._options); + + const groups = new Map(); + const emitters = new Map(); + + let root: VFXGroup | VFXEmitter | null = null; + + if (quarksVFXData.object) { + root = this._convertObject(quarksVFXData.object, null, groups, emitters, 0); + } + + this._logger.log(`=== Conversion complete. Groups: ${groups.size}, Emitters: ${emitters.size} ===`, this._options); + + return { + root, + groups, + emitters, + }; + } + + /** + * Convert a Quarks/Three.js object to Babylon.js VFX format + */ + private _convertObject( + obj: QuarksObject, + parentUuid: string | null, + groups: Map, + emitters: Map, + depth: number + ): VFXGroup | VFXEmitter | null { + const indent = " ".repeat(depth); + const options = this._options; + + if (!obj || typeof obj !== "object") { + return null; + } + + this._logger.log(`${indent}Converting object: ${obj.type || "unknown"} (name: ${obj.name || "unnamed"})`, options); + + // Convert transform from right-handed to left-handed + const transform = this._convertTransform(obj.matrix, obj.position, obj.rotation, obj.scale, options); + + if (obj.type === "Group") { + const group: VFXGroup = { + uuid: obj.uuid || `group_${groups.size}`, + name: obj.name || "Group", + transform, + children: [], + }; + + // Convert children + if (obj.children && Array.isArray(obj.children)) { + for (const child of obj.children) { + const convertedChild = this._convertObject(child, group.uuid, groups, emitters, depth + 1); + if (convertedChild) { + if ("config" in convertedChild) { + // It's an emitter + group.children.push(convertedChild as VFXEmitter); + } else { + // It's a group + group.children.push(convertedChild as VFXGroup); + } + } + } + } + + groups.set(group.uuid, group); + this._logger.log(`${indent}Converted Group: ${group.name} (uuid: ${group.uuid})`, options); + return group; + } else if (obj.type === "ParticleEmitter" && obj.ps) { + // Convert emitter config from Quarks to VFX format + const vfxConfig = this._convertEmitterConfig(obj.ps); + + const emitter: VFXEmitter = { + uuid: obj.uuid || `emitter_${emitters.size}`, + name: obj.name || "ParticleEmitter", + transform, + config: vfxConfig, + materialId: obj.ps.material, + parentUuid: parentUuid || undefined, + }; + + emitters.set(emitter.uuid, emitter); + this._logger.log(`${indent}Converted Emitter: ${emitter.name} (uuid: ${emitter.uuid})`, options); + return emitter; + } + + return null; + } + + /** + * Convert transform from Quarks/Three.js (right-handed) to Babylon.js VFX (left-handed) + * This is the ONLY place where handedness conversion happens + */ + private _convertTransform(matrixArray?: number[], positionArray?: number[], rotationArray?: number[], scaleArray?: number[], options?: VFXLoaderOptions): VFXTransform { + const position = Vector3.Zero(); + const rotation = Quaternion.Identity(); + const scale = Vector3.One(); + + if (matrixArray && Array.isArray(matrixArray) && matrixArray.length >= 16) { + // Use matrix (most accurate) + const matrix = Matrix.FromArray(matrixArray); + const tempPos = Vector3.Zero(); + const tempRot = Quaternion.Zero(); + const tempScale = Vector3.Zero(); + matrix.decompose(tempScale, tempRot, tempPos); + + // Convert from right-handed to left-handed + position.copyFrom(tempPos); + position.z = -position.z; // Negate Z position + + rotation.copyFrom(tempRot); + // Convert rotation quaternion: invert X component for proper X-axis rotation conversion + // This handles the case where X=-90° in RH looks like X=0° in LH + rotation.x *= -1; + + scale.copyFrom(tempScale); + } else { + // Use individual components + if (positionArray && Array.isArray(positionArray)) { + position.set(positionArray[0] || 0, positionArray[1] || 0, positionArray[2] || 0); + position.z = -position.z; // Convert to left-handed + } + + if (rotationArray && Array.isArray(rotationArray)) { + // If rotation is Euler angles, convert to quaternion + const eulerX = rotationArray[0] || 0; + const eulerY = rotationArray[1] || 0; + const eulerZ = rotationArray[2] || 0; + Quaternion.RotationYawPitchRollToRef(eulerY, eulerX, -eulerZ, rotation); // Negate Z for handedness + rotation.x *= -1; // Adjust X rotation component + } + + if (scaleArray && Array.isArray(scaleArray)) { + scale.set(scaleArray[0] || 1, scaleArray[1] || 1, scaleArray[2] || 1); + } + } + + return { + position, + rotation, + scale, + }; + } + + /** + * Convert emitter config from Quarks to VFX format + */ + private _convertEmitterConfig(quarksConfig: QuarksParticleEmitterConfig): VFXParticleEmitterConfig { + const vfxConfig: VFXParticleEmitterConfig = { + version: quarksConfig.version, + autoDestroy: quarksConfig.autoDestroy, + looping: quarksConfig.looping, + prewarm: quarksConfig.prewarm, + duration: quarksConfig.duration, + onlyUsedByOther: quarksConfig.onlyUsedByOther, + instancingGeometry: quarksConfig.instancingGeometry, + renderOrder: quarksConfig.renderOrder, + renderMode: quarksConfig.renderMode, + rendererEmitterSettings: quarksConfig.rendererEmitterSettings, + material: quarksConfig.material, + layers: quarksConfig.layers, + uTileCount: quarksConfig.uTileCount, + vTileCount: quarksConfig.vTileCount, + blendTiles: quarksConfig.blendTiles, + softParticles: quarksConfig.softParticles, + softFarFade: quarksConfig.softFarFade, + softNearFade: quarksConfig.softNearFade, + worldSpace: quarksConfig.worldSpace, + }; + + // Convert values + if (quarksConfig.startLife !== undefined) { + vfxConfig.startLife = this._convertValue(quarksConfig.startLife); + } + if (quarksConfig.startSpeed !== undefined) { + vfxConfig.startSpeed = this._convertValue(quarksConfig.startSpeed); + } + if (quarksConfig.startRotation !== undefined) { + vfxConfig.startRotation = this._convertRotation(quarksConfig.startRotation); + } + if (quarksConfig.startSize !== undefined) { + vfxConfig.startSize = this._convertValue(quarksConfig.startSize); + } + if (quarksConfig.startColor !== undefined) { + vfxConfig.startColor = this._convertColor(quarksConfig.startColor); + } + if (quarksConfig.emissionOverTime !== undefined) { + vfxConfig.emissionOverTime = this._convertValue(quarksConfig.emissionOverTime); + } + if (quarksConfig.emissionOverDistance !== undefined) { + vfxConfig.emissionOverDistance = this._convertValue(quarksConfig.emissionOverDistance); + } + if (quarksConfig.startTileIndex !== undefined) { + vfxConfig.startTileIndex = this._convertValue(quarksConfig.startTileIndex); + } + + // Convert shape + if (quarksConfig.shape !== undefined) { + vfxConfig.shape = this._convertShape(quarksConfig.shape); + } + + // Convert emission bursts + if (quarksConfig.emissionBursts !== undefined && Array.isArray(quarksConfig.emissionBursts)) { + vfxConfig.emissionBursts = quarksConfig.emissionBursts.map((burst) => ({ + time: this._convertValue(burst.time), + count: this._convertValue(burst.count), + })); + } + + // Convert behaviors + if (quarksConfig.behaviors !== undefined && Array.isArray(quarksConfig.behaviors)) { + vfxConfig.behaviors = quarksConfig.behaviors.map((behavior) => this._convertBehavior(behavior)); + } + + return vfxConfig; + } + + /** + * Convert Quarks value to VFX value + */ + private _convertValue(quarksValue: QuarksValue): VFXValue { + if (typeof quarksValue === "number") { + return quarksValue; + } + if (quarksValue.type === "ConstantValue") { + return { + type: "ConstantValue", + value: quarksValue.value, + }; + } + if (quarksValue.type === "IntervalValue") { + return { + type: "IntervalValue", + min: quarksValue.a ?? 0, + max: quarksValue.b ?? 0, + }; + } + if (quarksValue.type === "PiecewiseBezier") { + return { + type: "PiecewiseBezier", + functions: quarksValue.functions.map((f) => ({ + function: f.function, + start: f.start, + })), + }; + } + return quarksValue; + } + + /** + * Convert Quarks color to VFX color + */ + private _convertColor(quarksColor: QuarksColor): VFXColor { + if (typeof quarksColor === "string" || Array.isArray(quarksColor)) { + return quarksColor; + } + if (quarksColor.type === "ConstantColor") { + if (quarksColor.value && Array.isArray(quarksColor.value)) { + return { + type: "ConstantColor", + value: quarksColor.value, + }; + } else if (quarksColor.color) { + return { + type: "ConstantColor", + value: [quarksColor.color.r || 0, quarksColor.color.g || 0, quarksColor.color.b || 0, quarksColor.color.a !== undefined ? quarksColor.color.a : 1], + }; + } else { + // Fallback: return default color if neither value nor color is present + return { + type: "ConstantColor", + value: [1, 1, 1, 1], + }; + } + } + return quarksColor as VFXColor; + } + + /** + * Convert Quarks rotation to VFX rotation + */ + private _convertRotation(quarksRotation: QuarksRotation): VFXRotation { + if (typeof quarksRotation === "number" || (typeof quarksRotation === "object" && quarksRotation !== null && "type" in quarksRotation && quarksRotation.type !== "Euler")) { + return this._convertValue(quarksRotation as QuarksValue); + } + if (typeof quarksRotation === "object" && quarksRotation !== null && "type" in quarksRotation && quarksRotation.type === "Euler") { + return { + type: "Euler", + angleX: quarksRotation.angleX !== undefined ? this._convertValue(quarksRotation.angleX) : undefined, + angleY: quarksRotation.angleY !== undefined ? this._convertValue(quarksRotation.angleY) : undefined, + angleZ: quarksRotation.angleZ !== undefined ? this._convertValue(quarksRotation.angleZ) : undefined, + }; + } + return this._convertValue(quarksRotation as QuarksValue); + } + + /** + * Convert Quarks gradient key to VFX gradient key + */ + private _convertGradientKey(quarksKey: QuarksGradientKey): VFXGradientKey { + return { + time: quarksKey.time, + value: quarksKey.value, + pos: quarksKey.pos, + }; + } + + /** + * Convert Quarks shape to VFX shape + */ + private _convertShape(quarksShape: QuarksShape): VFXShape { + const vfxShape: VFXShape = { + type: quarksShape.type, + radius: quarksShape.radius, + arc: quarksShape.arc, + thickness: quarksShape.thickness, + angle: quarksShape.angle, + mode: quarksShape.mode, + spread: quarksShape.spread, + size: quarksShape.size, + height: quarksShape.height, + }; + if (quarksShape.speed !== undefined) { + vfxShape.speed = this._convertValue(quarksShape.speed); + } + return vfxShape; + } + + /** + * Convert Quarks behavior to VFX behavior + */ + private _convertBehavior(quarksBehavior: QuarksBehavior): VFXBehavior { + switch (quarksBehavior.type) { + case "ColorOverLife": { + const behavior = quarksBehavior as QuarksColorOverLifeBehavior; + if (behavior.color) { + const vfxColor: VFXColorOverLifeBehavior["color"] = {}; + if (behavior.color.color?.keys) { + vfxColor.color = { keys: behavior.color.color.keys.map((k) => this._convertGradientKey(k)) }; + } + if (behavior.color.alpha?.keys) { + vfxColor.alpha = { keys: behavior.color.alpha.keys.map((k) => this._convertGradientKey(k)) }; + } + if (behavior.color.keys) { + vfxColor.keys = behavior.color.keys.map((k) => this._convertGradientKey(k)); + } + return { type: "ColorOverLife", color: vfxColor }; + } + return { type: "ColorOverLife" }; + } + + case "SizeOverLife": { + const behavior = quarksBehavior as QuarksSizeOverLifeBehavior; + if (behavior.size) { + const vfxSize: VFXSizeOverLifeBehavior["size"] = {}; + if (behavior.size.keys) { + vfxSize.keys = behavior.size.keys.map((k: QuarksGradientKey) => this._convertGradientKey(k)); + } + if (behavior.size.functions) { + vfxSize.functions = behavior.size.functions; + } + return { type: "SizeOverLife", size: vfxSize }; + } + return { type: "SizeOverLife" }; + } + + case "RotationOverLife": + case "Rotation3DOverLife": { + const behavior = quarksBehavior as QuarksRotationOverLifeBehavior; + return { + type: behavior.type, + angularVelocity: behavior.angularVelocity !== undefined ? this._convertValue(behavior.angularVelocity) : undefined, + }; + } + + case "ForceOverLife": + case "ApplyForce": { + const behavior = quarksBehavior as QuarksForceOverLifeBehavior; + const vfxBehavior: VFXForceOverLifeBehavior = { type: behavior.type }; + if (behavior.force) { + vfxBehavior.force = { + x: behavior.force.x !== undefined ? this._convertValue(behavior.force.x) : undefined, + y: behavior.force.y !== undefined ? this._convertValue(behavior.force.y) : undefined, + z: behavior.force.z !== undefined ? this._convertValue(behavior.force.z) : undefined, + }; + } + if (behavior.x !== undefined) vfxBehavior.x = this._convertValue(behavior.x); + if (behavior.y !== undefined) vfxBehavior.y = this._convertValue(behavior.y); + if (behavior.z !== undefined) vfxBehavior.z = this._convertValue(behavior.z); + return vfxBehavior; + } + + case "GravityForce": { + const behavior = quarksBehavior as QuarksGravityForceBehavior; + const vfxBehavior: { type: string; gravity?: VFXValue } = { + type: "GravityForce", + gravity: behavior.gravity !== undefined ? this._convertValue(behavior.gravity) : undefined, + }; + return vfxBehavior as VFXBehavior; + } + + case "SpeedOverLife": { + const behavior = quarksBehavior as QuarksSpeedOverLifeBehavior; + if (behavior.speed) { + if (typeof behavior.speed === "object" && behavior.speed !== null && "keys" in behavior.speed) { + const vfxSpeed: VFXSpeedOverLifeBehavior["speed"] = {}; + if (behavior.speed.keys) { + vfxSpeed.keys = behavior.speed.keys.map((k: QuarksGradientKey) => this._convertGradientKey(k)); + } + if (behavior.speed.functions) { + vfxSpeed.functions = behavior.speed.functions; + } + return { type: "SpeedOverLife", speed: vfxSpeed }; + } else if (typeof behavior.speed === "number" || (typeof behavior.speed === "object" && behavior.speed !== null && "type" in behavior.speed)) { + return { type: "SpeedOverLife", speed: this._convertValue(behavior.speed as QuarksValue) }; + } + } + return { type: "SpeedOverLife" }; + } + + case "FrameOverLife": { + const behavior = quarksBehavior as QuarksFrameOverLifeBehavior; + const vfxBehavior: { type: string; frame?: VFXValue | { keys?: VFXGradientKey[] } } = { type: "FrameOverLife" }; + if (behavior.frame) { + if (typeof behavior.frame === "object" && behavior.frame !== null && "keys" in behavior.frame) { + vfxBehavior.frame = { + keys: behavior.frame.keys?.map((k: QuarksGradientKey) => this._convertGradientKey(k)), + }; + } else if (typeof behavior.frame === "number" || (typeof behavior.frame === "object" && behavior.frame !== null && "type" in behavior.frame)) { + vfxBehavior.frame = this._convertValue(behavior.frame as QuarksValue); + } + } + return vfxBehavior as VFXBehavior; + } + + case "LimitSpeedOverLife": { + const behavior = quarksBehavior as QuarksLimitSpeedOverLifeBehavior; + const vfxBehavior: VFXLimitSpeedOverLifeBehavior = { type: "LimitSpeedOverLife" }; + if (behavior.maxSpeed !== undefined) { + vfxBehavior.maxSpeed = this._convertValue(behavior.maxSpeed); + } + if (behavior.speed !== undefined) { + if (typeof behavior.speed === "object" && behavior.speed !== null && "keys" in behavior.speed) { + vfxBehavior.speed = { keys: behavior.speed.keys?.map((k: QuarksGradientKey) => this._convertGradientKey(k)) }; + } else if (typeof behavior.speed === "number" || (typeof behavior.speed === "object" && behavior.speed !== null && "type" in behavior.speed)) { + vfxBehavior.speed = this._convertValue(behavior.speed as QuarksValue); + } + } + if (behavior.dampen !== undefined) { + vfxBehavior.dampen = this._convertValue(behavior.dampen); + } + return vfxBehavior; + } + + case "ColorBySpeed": { + const behavior = quarksBehavior as QuarksColorBySpeedBehavior; + const vfxBehavior: VFXColorBySpeedBehavior = { + type: "ColorBySpeed", + minSpeed: behavior.minSpeed !== undefined ? this._convertValue(behavior.minSpeed) : undefined, + maxSpeed: behavior.maxSpeed !== undefined ? this._convertValue(behavior.maxSpeed) : undefined, + }; + if (behavior.color?.keys) { + vfxBehavior.color = { keys: behavior.color.keys.map((k: QuarksGradientKey) => this._convertGradientKey(k)) }; + } + return vfxBehavior; + } + + case "SizeBySpeed": { + const behavior = quarksBehavior as QuarksSizeBySpeedBehavior; + const vfxBehavior: VFXSizeBySpeedBehavior = { + type: "SizeBySpeed", + minSpeed: behavior.minSpeed !== undefined ? this._convertValue(behavior.minSpeed) : undefined, + maxSpeed: behavior.maxSpeed !== undefined ? this._convertValue(behavior.maxSpeed) : undefined, + }; + if (behavior.size?.keys) { + vfxBehavior.size = { keys: behavior.size.keys.map((k: QuarksGradientKey) => this._convertGradientKey(k)) }; + } + return vfxBehavior; + } + + case "RotationBySpeed": { + const behavior = quarksBehavior as QuarksRotationBySpeedBehavior; + const vfxBehavior: { type: string; angularVelocity?: VFXValue; minSpeed?: VFXValue; maxSpeed?: VFXValue } = { + type: "RotationBySpeed", + angularVelocity: behavior.angularVelocity !== undefined ? this._convertValue(behavior.angularVelocity) : undefined, + minSpeed: behavior.minSpeed !== undefined ? this._convertValue(behavior.minSpeed) : undefined, + maxSpeed: behavior.maxSpeed !== undefined ? this._convertValue(behavior.maxSpeed) : undefined, + }; + return vfxBehavior as VFXBehavior; + } + + case "OrbitOverLife": { + const behavior = quarksBehavior as QuarksOrbitOverLifeBehavior; + const vfxBehavior: { type: string; center?: { x?: number; y?: number; z?: number }; radius?: VFXValue; speed?: VFXValue } = { + type: "OrbitOverLife", + center: behavior.center, + radius: behavior.radius !== undefined ? this._convertValue(behavior.radius) : undefined, + speed: behavior.speed !== undefined ? this._convertValue(behavior.speed) : undefined, + }; + return vfxBehavior as VFXBehavior; + } + + default: + // Fallback for unknown behaviors - copy as-is + return quarksBehavior as VFXBehavior; + } + } +} diff --git a/editor/src/editor/windows/fx-editor/VFX/parsers/VFXParser.ts b/editor/src/editor/windows/fx-editor/VFX/parsers/VFXParser.ts new file mode 100644 index 000000000..9732054a4 --- /dev/null +++ b/editor/src/editor/windows/fx-editor/VFX/parsers/VFXParser.ts @@ -0,0 +1,123 @@ +import type { Scene } from "../../../scene"; +import type { QuarksVFXJSON } from "../types/quarksTypes"; +import type { VFXLoaderOptions } from "../types/loader"; +import type { VFXParseContext } from "../types/context"; +import { VFXLogger } from "../loggers/VFXLogger"; +import { VFXValueParser } from "./VFXValueParser"; +import { VFXMaterialFactory } from "../factories/VFXMaterialFactory"; +import { VFXGeometryFactory } from "../factories/VFXGeometryFactory"; +import { VFXEmitterFactory } from "../factories/VFXEmitterFactory"; +import { VFXHierarchyProcessor } from "../processors/VFXHierarchyProcessor"; +import { VFXDataConverter } from "./VFXDataConverter"; +import { TransformNode } from "../../../Meshes/transformNode"; +import { VFXParticleSystem } from "../systems/VFXParticleSystem"; +import { VFXSolidParticleSystem } from "../systems/VFXSolidParticleSystem"; + +/** + * Main parser for Three.js particle JSON files + * Orchestrates the parsing process using modular components + */ +export class VFXParser { + private _context: VFXParseContext; + private _logger: VFXLogger; + private _valueParser: VFXValueParser; + private _materialFactory: VFXMaterialFactory; + private _geometryFactory: VFXGeometryFactory; + private _emitterFactory: VFXEmitterFactory; + private _hierarchyProcessor: VFXHierarchyProcessor; + + constructor(scene: Scene, rootUrl: string, jsonData: QuarksVFXJSON, options?: VFXLoaderOptions) { + const opts = options || {}; + this._context = { + scene, + rootUrl, + jsonData, + options: opts, + groupNodesMap: new Map(), + }; + + this._logger = new VFXLogger("[VFXParser]"); + this._valueParser = new VFXValueParser(); + this._materialFactory = new VFXMaterialFactory(this._context); + this._geometryFactory = new VFXGeometryFactory(this._context, this._materialFactory); + this._emitterFactory = new VFXEmitterFactory(this._context, this._valueParser, this._materialFactory, this._geometryFactory); + this._hierarchyProcessor = new VFXHierarchyProcessor(this._context, this._emitterFactory); + } + + /** + * Parse the JSON data and create particle systems + */ + public parse(): (VFXParticleSystem | VFXSolidParticleSystem)[] { + const { jsonData, options } = this._context; + this._logger.log("=== Starting Particle System Parsing ===", options); + + if (options.validate) { + this._validateJSONStructure(jsonData, options); + } + + const dataConverter = new VFXDataConverter(options); + const vfxData = dataConverter.convert(jsonData); + this._context.vfxData = vfxData; + const particleSystems = this._hierarchyProcessor.processHierarchy(vfxData); + + this._logger.log(`=== Parsing complete. Created ${particleSystems.length} particle system(s) ===`, options); + return particleSystems; + } + + /** + * Validate JSON structure + */ + private _validateJSONStructure(jsonData: QuarksVFXJSON, options: VFXLoaderOptions): void { + this._logger.log("Validating JSON structure...", options); + + if (!jsonData.object) { + this._logger.warn("JSON missing 'object' property", options); + } + + if (!jsonData.materials || jsonData.materials.length === 0) { + this._logger.warn("JSON has no materials", options); + } + + if (!jsonData.textures || jsonData.textures.length === 0) { + this._logger.warn("JSON has no textures", options); + } + + if (!jsonData.images || jsonData.images.length === 0) { + this._logger.warn("JSON has no images", options); + } + + if (!jsonData.geometries || jsonData.geometries.length === 0) { + this._logger.warn("JSON has no geometries", options); + } + + this._logger.log("Validation complete", options); + } + + /** + * Get the parse context (for use by other components) + */ + public getContext(): VFXParseContext { + return this._context; + } + + /** + * Get the value parser + */ + public getValueParser(): VFXValueParser { + return this._valueParser; + } + + /** + * Get the material factory + */ + public getMaterialFactory(): VFXMaterialFactory { + return this._materialFactory; + } + + /** + * Get the geometry factory + */ + public getGeometryFactory(): VFXGeometryFactory { + return this._geometryFactory; + } +} diff --git a/editor/src/editor/windows/fx-editor/VFX/parsers/VFXValueParser.ts b/editor/src/editor/windows/fx-editor/VFX/parsers/VFXValueParser.ts new file mode 100644 index 000000000..bec68557c --- /dev/null +++ b/editor/src/editor/windows/fx-editor/VFX/parsers/VFXValueParser.ts @@ -0,0 +1,187 @@ +import { Color4 } from "../../../Maths/math.color"; +import { ColorGradient } from "../../../Misc/gradients"; +import type { IVFXValueParser } from "../types/factories"; +import type { VFXValue } from "../types/values"; +import type { VFXColor } from "../types/colors"; +import type { VFXGradientKey } from "../types/gradients"; +import type { VFXPiecewiseBezier } from "../types/values"; + +/** + * Parser for Three.js value types (ConstantValue, IntervalValue, Gradient, etc.) + */ +export class VFXValueParser implements IVFXValueParser { + /** + * Parse a constant value + */ + public parseConstantValue(value: VFXValue): number { + if (value && typeof value === "object" && value.type === "ConstantValue") { + return value.value || 0; + } + return typeof value === "number" ? value : 0; + } + + /** + * Parse an interval value (returns min and max) + */ + public parseIntervalValue(value: VFXValue): { min: number; max: number } { + if (value && typeof value === "object" && "type" in value && value.type === "IntervalValue") { + return { + min: value.min ?? 0, + max: value.max ?? 0, + }; + } + const constant = this.parseConstantValue(value); + return { min: constant, max: constant }; + } + + /** + * Parse a constant color + */ + public parseConstantColor(value: VFXColor): Color4 { + if (value && typeof value === "object" && !Array.isArray(value)) { + if ("type" in value && value.type === "ConstantColor") { + if (value.value && Array.isArray(value.value)) { + return new Color4(value.value[0] || 0, value.value[1] || 0, value.value[2] || 0, value.value[3] !== undefined ? value.value[3] : 1); + } + } else if (Array.isArray(value) && value.length >= 3) { + // Array format [r, g, b, a?] + return new Color4(value[0] || 0, value[1] || 0, value[2] || 0, value[3] !== undefined ? value[3] : 1); + } + } + return new Color4(1, 1, 1, 1); + } + + /** + * Parse gradient color keys + */ + public parseGradientColorKeys(keys: VFXGradientKey[]): ColorGradient[] { + const gradients: ColorGradient[] = []; + for (const key of keys) { + const pos = key.pos ?? key.time ?? 0; + if (key.value !== undefined && pos !== undefined) { + let color4: Color4; + if (typeof key.value === "number") { + // Single number - grayscale + color4 = new Color4(key.value, key.value, key.value, 1); + } else if (Array.isArray(key.value)) { + // Array format [r, g, b, a?] + color4 = new Color4(key.value[0] || 0, key.value[1] || 0, key.value[2] || 0, key.value[3] !== undefined ? key.value[3] : 1); + } else { + // Object format { r, g, b, a? } + color4 = new Color4(key.value.r || 0, key.value.g || 0, key.value.b || 0, key.value.a !== undefined ? key.value.a : 1); + } + gradients.push(new ColorGradient(pos, color4)); + } + } + return gradients; + } + + /** + * Parse gradient alpha keys + */ + public parseGradientAlphaKeys(keys: VFXGradientKey[]): { gradient: number; factor: number }[] { + const gradients: { gradient: number; factor: number }[] = []; + for (const key of keys) { + const pos = key.pos ?? key.time ?? 0; + if (key.value !== undefined && pos !== undefined) { + let factor: number; + if (typeof key.value === "number") { + factor = key.value; + } else if (Array.isArray(key.value)) { + factor = key.value[3] !== undefined ? key.value[3] : 1; + } else { + factor = key.value.a !== undefined ? key.value.a : 1; + } + gradients.push({ gradient: pos, factor }); + } + } + return gradients; + } + + /** + * Parse a value for particle spawn (returns a single value based on type) + * Handles ConstantValue, IntervalValue, PiecewiseBezier, and number + * @param value The value to parse + * @param normalizedTime Normalized time (0-1) for PiecewiseBezier, default is random for IntervalValue + */ + public parseValue(value: VFXValue, normalizedTime?: number): number { + if (!value || typeof value === "number") { + return typeof value === "number" ? value : 0; + } + + if (value.type === "ConstantValue") { + return value.value || 0; + } + + if (value.type === "IntervalValue") { + const min = value.min ?? 0; + const max = value.max ?? 0; + return min + Math.random() * (max - min); + } + + if (value.type === "PiecewiseBezier") { + // Use provided normalizedTime or random for spawn + const t = normalizedTime !== undefined ? normalizedTime : Math.random(); + return this._evaluatePiecewiseBezier(value, t); + } + + // Fallback + return 0; + } + + /** + * Evaluate PiecewiseBezier at normalized time t (0-1) + */ + private _evaluatePiecewiseBezier(bezier: VFXPiecewiseBezier, t: number): number { + if (!bezier.functions || bezier.functions.length === 0) { + return 0; + } + + // Clamp t to [0, 1] + const clampedT = Math.max(0, Math.min(1, t)); + + // Find which function segment contains t + let segmentIndex = -1; + for (let i = 0; i < bezier.functions.length; i++) { + const func = bezier.functions[i]; + const start = func.start; + const end = i < bezier.functions.length - 1 ? bezier.functions[i + 1].start : 1; + + if (clampedT >= start && clampedT < end) { + segmentIndex = i; + break; + } + } + + // If t is at the end (1.0), use last segment + if (segmentIndex === -1 && clampedT >= 1) { + segmentIndex = bezier.functions.length - 1; + } + + // If still not found, use first segment + if (segmentIndex === -1) { + segmentIndex = 0; + } + + const func = bezier.functions[segmentIndex]; + const start = func.start; + const end = segmentIndex < bezier.functions.length - 1 ? bezier.functions[segmentIndex + 1].start : 1; + + // Normalize t within this segment + const segmentT = end > start ? (clampedT - start) / (end - start) : 0; + + // Evaluate cubic Bezier: B(t) = (1-t)³P₀ + 3(1-t)²tP₁ + 3(1-t)t²P₂ + t³P₃ + const p0 = func.function.p0; + const p1 = func.function.p1; + const p2 = func.function.p2; + const p3 = func.function.p3; + + const t2 = segmentT * segmentT; + const t3 = t2 * segmentT; + const mt = 1 - segmentT; + const mt2 = mt * mt; + const mt3 = mt2 * mt; + + return mt3 * p0 + 3 * mt2 * segmentT * p1 + 3 * mt * t2 * p2 + t3 * p3; + } +} diff --git a/editor/src/editor/windows/fx-editor/VFX/processors/VFXHierarchyProcessor.ts b/editor/src/editor/windows/fx-editor/VFX/processors/VFXHierarchyProcessor.ts new file mode 100644 index 000000000..a896ed098 --- /dev/null +++ b/editor/src/editor/windows/fx-editor/VFX/processors/VFXHierarchyProcessor.ts @@ -0,0 +1,294 @@ +import type { Nullable } from "../../../types"; +import { Vector3, Quaternion } from "../../../Maths/math.vector"; +import { TransformNode } from "../../../Meshes/transformNode"; +import { VFXParticleSystem } from "../systems/VFXParticleSystem"; +import { VFXSolidParticleSystem } from "../systems/VFXSolidParticleSystem"; +import type { VFXParseContext } from "../types/context"; +import type { VFXEmitterData } from "../types/emitter"; +import type { VFXLoaderOptions } from "../types/loader"; +import type { VFXHierarchy, VFXGroup, VFXEmitter, VFXTransform } from "../types/hierarchy"; +import { VFXLogger } from "../loggers/VFXLogger"; +import type { IVFXEmitterFactory } from "../types/factories"; + +/** + * Processor for Three.js object hierarchy (Groups and ParticleEmitters) + */ +export class VFXHierarchyProcessor { + private _logger: VFXLogger; + private _context: VFXParseContext; + private _emitterFactory: IVFXEmitterFactory; + + constructor(context: VFXParseContext, emitterFactory: IVFXEmitterFactory) { + this._context = context; + this._logger = new VFXLogger("[VFXHierarchyProcessor]"); + this._emitterFactory = emitterFactory; + } + + /** + * Process the VFX hierarchy and create particle systems + * Uses pre-converted data (already in left-handed coordinate system) + */ + public processHierarchy(vfxData: VFXHierarchy): (VFXParticleSystem | VFXSolidParticleSystem)[] { + const { options } = this._context; + const particleSystems: (VFXParticleSystem | VFXSolidParticleSystem)[] = []; + + if (!vfxData.root) { + this._logger.warn("No root object found in VFX data", options); + return particleSystems; + } + + this._logger.log("Phase 1: Creating nodes and building hierarchy", options); + // Phase 1: Create all nodes without transformations, build hierarchy + this._processVFXObject(vfxData.root, null, 0, particleSystems, false, vfxData); + + this._logger.log("Phase 2: Applying transformations", options); + // Phase 2: Apply transformations after hierarchy is established + this._processVFXObject(vfxData.root, null, 0, particleSystems, true, vfxData); + + return particleSystems; + } + + /** + * Recursively process VFX object hierarchy + * @param applyTransformations If true, applies transformations. If false, creates nodes and builds hierarchy. + */ + private _processVFXObject( + vfxObj: VFXGroup | VFXEmitter, + parentGroup: Nullable, + depth: number, + particleSystems: (VFXParticleSystem | VFXSolidParticleSystem)[], + applyTransformations: boolean, + vfxData: VFXHierarchy + ): void { + const { options } = this._context; + const indent = " ".repeat(depth); + + if (!applyTransformations) { + this._logger.log(`${indent}Creating object: ${vfxObj.name}`, options); + } else { + this._logger.log(`${indent}Applying transformations to: ${vfxObj.name}`, options); + } + + let currentGroup: Nullable = parentGroup; + + // Handle Group objects + if ("children" in vfxObj) { + const vfxGroup = vfxObj as VFXGroup; + if (!applyTransformations) { + // Phase 1: Create group without transformations, set parent + currentGroup = this._createGroupFromVFX(vfxGroup, parentGroup, depth, options); + } else { + // Phase 2: Apply transformations to group + currentGroup = this._context.groupNodesMap.get(vfxGroup.uuid) || null; + if (currentGroup) { + this._applyVFXTransformToGroup(currentGroup, vfxGroup.transform, depth, options); + } + } + + // Process children recursively + if (vfxGroup.children && vfxGroup.children.length > 0) { + if (!applyTransformations) { + this._logger.log(`${indent}Processing ${vfxGroup.children.length} children`, options); + } + for (const child of vfxGroup.children) { + this._processVFXObject(child, currentGroup, depth + 1, particleSystems, applyTransformations, vfxData); + } + } + } else { + // Handle Emitter objects + const vfxEmitter = vfxObj as VFXEmitter; + if (!applyTransformations) { + // Phase 1: Create particle system without transformations + const particleSystem = this._processVFXEmitter(vfxEmitter, currentGroup, depth, options, false); + if (particleSystem) { + particleSystems.push(particleSystem); + } + } else { + // Phase 2: Apply transformations to particle system + this._applyVFXTransformToEmitter(vfxEmitter, currentGroup, depth, options); + } + } + } + + /** + * Create a Group (TransformNode) from VFX Group data + * Phase 1: Creates node without transformations, sets parent + */ + private _createGroupFromVFX(vfxGroup: VFXGroup, parentGroup: Nullable, depth: number, options?: VFXLoaderOptions): TransformNode { + const { scene } = this._context; + const indent = " ".repeat(depth); + + this._logger.log(`${indent}Creating Group: ${vfxGroup.name} (without transformations)`, options); + const groupNode = new TransformNode(vfxGroup.name, scene); + + // Initialize with identity transform (will be applied in phase 2) + groupNode.position.setAll(0); + if (!groupNode.rotationQuaternion) { + groupNode.rotationQuaternion = Quaternion.Identity(); + } else { + groupNode.rotationQuaternion.set(0, 0, 0, 1); + } + groupNode.scaling.setAll(1); + + // Set visibility + groupNode.isVisible = false; + + // Set parent FIRST (before applying transformations) + if (parentGroup) { + groupNode.setParent(parentGroup); + this._logger.log(`${indent}Group parent set: ${parentGroup.name}`, options); + } + + // Store in map for reference (needed for phase 2) + this._context.groupNodesMap.set(vfxGroup.uuid, groupNode); + this._logger.log(`${indent}Group stored in map: ${vfxGroup.uuid}`, options); + + return groupNode; + } + + /** + * Apply VFX transform to a Group node + * Phase 2: Applies pre-converted transformations (already in left-handed system) + */ + private _applyVFXTransformToGroup(groupNode: TransformNode, transform: VFXTransform, depth: number, options?: VFXLoaderOptions): void { + const indent = " ".repeat(depth); + this._logger.log(`${indent}Applying converted transform to group: ${groupNode.name}`, options); + + // Transform is already converted to left-handed, apply directly + groupNode.position.copyFrom(transform.position); + groupNode.rotationQuaternion = transform.rotation.clone(); + groupNode.scaling.copyFrom(transform.scale); + + const pos = groupNode.position; + const rot = groupNode.rotationQuaternion; + const scl = groupNode.scaling; + this._logger.log(`${indent}Group position: (${pos.x.toFixed(2)}, ${pos.y.toFixed(2)}, ${pos.z.toFixed(2)})`, options); + if (rot) { + this._logger.log(`${indent}Group rotation quaternion: (${rot.x.toFixed(4)}, ${rot.y.toFixed(4)}, ${rot.z.toFixed(4)}, ${rot.w.toFixed(4)})`, options); + } + this._logger.log(`${indent}Group scale: (${scl.x.toFixed(2)}, ${scl.y.toFixed(2)}, ${scl.z.toFixed(2)})`, options); + } + + /** + * Process a VFX ParticleEmitter + * @param applyTransformations If false, creates system without transformations. If true, applies transformations. + */ + private _processVFXEmitter( + vfxEmitter: VFXEmitter, + currentGroup: Nullable, + depth: number, + options?: VFXLoaderOptions, + applyTransformations: boolean = false + ): Nullable { + const indent = " ".repeat(depth); + const emitterName = vfxEmitter.name; + + this._logger.log(`${indent}=== Processing ParticleEmitter: ${emitterName} ===`, options); + this._logger.log(`${indent}Current parent group: ${currentGroup ? currentGroup.name : "none"}`, options); + + // Log emitter configuration + if (options?.verbose) { + this._logger.log( + `${indent}Emitter config: ${JSON.stringify( + { + renderMode: vfxEmitter.config.renderMode, + duration: vfxEmitter.config.duration, + looping: vfxEmitter.config.looping, + prewarm: vfxEmitter.config.prewarm, + emissionOverTime: vfxEmitter.config.emissionOverTime, + startLife: vfxEmitter.config.startLife, + startSpeed: vfxEmitter.config.startSpeed, + startSize: vfxEmitter.config.startSize, + behaviorsCount: vfxEmitter.config.behaviors?.length || 0, + worldSpace: vfxEmitter.config.worldSpace, + }, + null, + 2 + )}`, + options + ); + } + + // Calculate cumulative scale from parent groups + const cumulativeScale = this._calculateCumulativeScale(currentGroup); + + const emitterData: VFXEmitterData = { + name: emitterName, + config: vfxEmitter.config, + materialId: vfxEmitter.materialId, + // Transform is already converted, will be passed through emitterData + matrix: undefined, + position: undefined, + parentGroup: currentGroup, + cumulativeScale, + }; + + // Store VFX emitter data (including transform) in emitterData for use in factory + emitterData.vfxEmitter = vfxEmitter; + + if (options?.verbose && (cumulativeScale.x !== 1 || cumulativeScale.y !== 1 || cumulativeScale.z !== 1)) { + this._logger.log( + `${indent}Cumulative scale from parent groups: (${cumulativeScale.x.toFixed(2)}, ${cumulativeScale.y.toFixed(2)}, ${cumulativeScale.z.toFixed(2)})`, + options + ); + } + + if (!applyTransformations) { + // Phase 1: Create particle system without transformations + const particleSystem = this._emitterFactory.createEmitter(emitterData); + + if (particleSystem) { + this._logger.log(`${indent}Particle system created successfully (without transformations)`, options); + + // VFX emitter data is already stored in emitterData, no need to store in particle system + + // Handle prewarm + if (vfxEmitter.config.prewarm) { + particleSystem.start(); + } + + return particleSystem as VFXParticleSystem; + } else { + this._logger.warn(`${indent}Failed to create particle system for ${emitterName}`, options); + return null; + } + } else { + // Phase 2: Apply transformations (this will be handled separately) + return null; + } + } + + /** + * Apply VFX transform to emitter (Phase 2) + * For SPS, transformations are applied in initParticles (after buildMesh) + * For ParticleSystem, we need to find and update the emitter mesh + */ + private _applyVFXTransformToEmitter(vfxEmitter: VFXEmitter, currentGroup: Nullable, depth: number, options?: VFXLoaderOptions): void { + const indent = " ".repeat(depth); + const emitterName = vfxEmitter.name; + + // For SPS: transformations are applied in initParticles (called after buildMesh) + // Transform is already stored in _vfxEmitter and will be applied there + // For ParticleSystem: emitter is set during creation, but we need to apply transform if it's a mesh + // Note: ParticleSystem emitter transformations are handled during creation phase + // because emitter needs to be set before particle system starts + this._logger.log(`${indent}Transformations for emitter ${emitterName} (will be applied in initParticles for SPS)`, options); + } + + /** + * Calculate cumulative scale from parent groups + */ + private _calculateCumulativeScale(parent: Nullable): Vector3 { + const cumulativeScale = new Vector3(1, 1, 1); + let current = parent; + + while (current) { + cumulativeScale.x *= current.scaling.x; + cumulativeScale.y *= current.scaling.y; + cumulativeScale.z *= current.scaling.z; + current = current.parent as TransformNode; + } + + return cumulativeScale; + } +} diff --git a/editor/src/editor/windows/fx-editor/VFX/systems/VFXParticleSystem.ts b/editor/src/editor/windows/fx-editor/VFX/systems/VFXParticleSystem.ts new file mode 100644 index 000000000..8fbb88bb4 --- /dev/null +++ b/editor/src/editor/windows/fx-editor/VFX/systems/VFXParticleSystem.ts @@ -0,0 +1,21 @@ +import { Color4 } from "@babylonjs/core/Maths/math.color"; +import { ParticleSystem } from "@babylonjs/core/Particles/particleSystem"; +import type { Scene } from "@babylonjs/core/scene"; +import type { VFXValueParser } from "../parsers/VFXValueParser"; +import type { VFXPerParticleBehaviorFunction } from "../types/VFXBehaviorFunction"; + +/** + * Extended ParticleSystem with VFX behaviors support + * (logic intentionally minimal, behaviors handled elsewhere) + */ +export class VFXParticleSystem extends ParticleSystem { + constructor(name: string, capacity: number, scene: Scene, valueParser: VFXValueParser, avgStartSpeed: number, avgStartSize: number, startColor: Color4) { + super(name, capacity, scene); + // behavior wiring omitted by design (see VFXEmitterFactory) + } + + public setPerParticleBehaviors(functions: VFXPerParticleBehaviorFunction[]): void { + // intentionally no-op (kept for API parity) + } +} + diff --git a/editor/src/editor/windows/fx-editor/VFX/systems/VFXSolidParticleSystem.ts b/editor/src/editor/windows/fx-editor/VFX/systems/VFXSolidParticleSystem.ts new file mode 100644 index 000000000..54148092c --- /dev/null +++ b/editor/src/editor/windows/fx-editor/VFX/systems/VFXSolidParticleSystem.ts @@ -0,0 +1,538 @@ +import { Vector3, Quaternion, Matrix } from "../../../Maths/math.vector"; +import { Color4 } from "../../../Maths/math.color"; +import { SolidParticleSystem } from "../../solidParticleSystem"; +import { SolidParticle } from "../../solidParticle"; +import type { TransformNode } from "../../../Meshes/transformNode"; +import type { VFXParticleEmitterConfig, VFXEmissionBurst } from "../types/emitterConfig"; +import type { VFXValueParser } from "../parsers/VFXValueParser"; +import { VFXLogger } from "../loggers/VFXLogger"; +import type { VFXLoaderOptions } from "../types/loader"; +import type { VFXPerSolidParticleBehaviorFunction, VFXPerParticleContext } from "../types/VFXBehaviorFunction"; + +/** + * Emission state matching three.quarks EmissionState structure + */ +interface EmissionState { + time: number; + waitEmiting: number; + travelDistance: number; + previousWorldPos?: Vector3; + burstIndex: number; + burstWaveIndex: number; + burstParticleIndex: number; + burstParticleCount: number; + isBursting: boolean; +} + +/** + * Extended SolidParticleSystem implementing three.quarks Mesh renderMode (renderMode = 2) logic + * This class replicates the exact behavior of three.quarks ParticleSystem with renderMode = Mesh + */ +export class VFXSolidParticleSystem extends SolidParticleSystem { + private _emissionState: EmissionState; + private _config: VFXParticleEmitterConfig; + private _valueParser: VFXValueParser; + private _perParticleBehaviors: VFXPerSolidParticleBehaviorFunction[]; + private _parentGroup: TransformNode | null; + private _vfxTransform: { position: Vector3; rotation: Quaternion; scale: Vector3 } | null; + private _logger: VFXLogger | null; + private _options: VFXLoaderOptions | undefined; + private _name: string; + private _duration: number; + private _looping: boolean; + private _emitEnded: boolean; + private _normalMatrix: Matrix; + private _tempVec: Vector3; + private _tempVec2: Vector3; + private _tempQuat: Quaternion; + + constructor( + name: string, + scene: any, + config: VFXParticleEmitterConfig, + valueParser: VFXValueParser, + options?: { + updatable?: boolean; + isPickable?: boolean; + enableDepthSort?: boolean; + particleIntersection?: boolean; + useModelMaterial?: boolean; + parentGroup?: TransformNode | null; + vfxTransform?: { position: Vector3; rotation: Quaternion; scale: Vector3 } | null; + logger?: VFXLogger | null; + loaderOptions?: VFXLoaderOptions; + } + ) { + super(name, scene, options); + + this._name = name; + this._config = config; + this._valueParser = valueParser; + this._perParticleBehaviors = []; + this._parentGroup = options?.parentGroup ?? null; + this._vfxTransform = options?.vfxTransform ?? null; + this._logger = options?.logger ?? null; + this._options = options?.loaderOptions; + this._duration = config.duration || 5; + this._looping = config.looping !== false; + this._emitEnded = false; + this._normalMatrix = new Matrix(); + this._tempVec = Vector3.Zero(); + this._tempVec2 = Vector3.Zero(); + this._tempQuat = Quaternion.Identity(); + + this.updateParticle = this._updateParticle.bind(this); + + this._emissionState = { + time: 0, + waitEmiting: 0, + travelDistance: 0, + burstIndex: 0, + burstWaveIndex: 0, + burstParticleIndex: 0, + burstParticleCount: 0, + isBursting: false, + }; + } + + private _findDeadParticle(): SolidParticle | null { + for (let j = 0; j < this.nbParticles; j++) { + if (!this.particles[j].alive) { + return this.particles[j]; + } + } + return null; + } + + private _resetParticle(particle: SolidParticle): void { + particle.age = 0; + particle.alive = true; + particle.isVisible = true; + particle.position.setAll(0); + particle.velocity.setAll(0); + particle.rotation.setAll(0); + particle.scaling.setAll(1); + if (particle.color) { + particle.color.set(1, 1, 1, 1); + } else { + particle.color = new Color4(1, 1, 1, 1); + } + + if (!particle.props) { + particle.props = {}; + } + particle.props.speedModifier = 1.0; + } + + private _initializeParticleColor(particle: SolidParticle): void { + const config = this._config; + const valueParser = this._valueParser; + + if (!particle.color) { + particle.color = new Color4(1, 1, 1, 1); + } + + if (config.startColor !== undefined) { + const startColor = valueParser.parseConstantColor(config.startColor); + particle.props!.startColor = startColor.clone(); + particle.color.copyFrom(startColor); + } else { + const defaultColor = new Color4(1, 1, 1, 1); + particle.props!.startColor = defaultColor.clone(); + particle.color.copyFrom(defaultColor); + } + } + + private _initializeParticleSpeed(particle: SolidParticle): void { + const config = this._config; + const valueParser = this._valueParser; + + if (config.startSpeed !== undefined) { + const normalizedTime = this._emissionState.time / this._duration; + particle.props!.startSpeed = valueParser.parseValue(config.startSpeed, normalizedTime); + } else { + particle.props!.startSpeed = 0; + } + } + + private _initializeParticleLife(particle: SolidParticle): void { + const config = this._config; + const valueParser = this._valueParser; + + if (config.startLife !== undefined) { + const normalizedTime = this._emissionState.time / this._duration; + particle.lifeTime = valueParser.parseValue(config.startLife, normalizedTime); + } else { + particle.lifeTime = 1; + } + } + + private _initializeParticleSize(particle: SolidParticle): void { + const config = this._config; + const valueParser = this._valueParser; + + if (config.startSize !== undefined) { + const normalizedTime = this._emissionState.time / this._duration; + const sizeValue = valueParser.parseValue(config.startSize, normalizedTime); + particle.props!.startSize = sizeValue; + particle.scaling.setAll(sizeValue); + } else { + particle.props!.startSize = 1; + particle.scaling.setAll(1); + } + } + + private _spawn(count: number): void { + const emissionState = this._emissionState; + + const emitterMatrix = this._getEmitterMatrix(); + const translation = this._tempVec; + const quaternion = this._tempQuat; + const scale = this._tempVec2; + emitterMatrix.decompose(scale, quaternion, translation); + emitterMatrix.toNormalMatrix(this._normalMatrix); + + for (let i = 0; i < count; i++) { + emissionState.burstParticleIndex = i; + + const particle = this._findDeadParticle(); + if (!particle) { + continue; + } + + this._resetParticle(particle); + this._initializeParticleColor(particle); + this._initializeParticleSpeed(particle); + this._initializeParticleLife(particle); + this._initializeParticleSize(particle); + + this._initializeEmitterShape(particle, emissionState); + } + } + + private _initializeSphereShape(particle: SolidParticle, radius: number, arc: number, thickness: number, startSpeed: number): void { + const u = Math.random(); + const v = Math.random(); + const rand = 1 - thickness + Math.random() * thickness; + const theta = u * arc; + const phi = Math.acos(2.0 * v - 1.0); + const sinTheta = Math.sin(theta); + const cosTheta = Math.cos(theta); + const sinPhi = Math.sin(phi); + const cosPhi = Math.cos(phi); + + particle.position.set(sinPhi * cosTheta, sinPhi * sinTheta, cosPhi); + particle.velocity.copyFrom(particle.position); + particle.velocity.scaleInPlace(startSpeed); + particle.position.scaleInPlace(radius * rand); + } + + private _initializeConeShape(particle: SolidParticle, radius: number, arc: number, thickness: number, angle: number, startSpeed: number): void { + const u = Math.random(); + const rand = 1 - thickness + Math.random() * thickness; + const theta = u * arc; + const r = Math.sqrt(rand); + const sinTheta = Math.sin(theta); + const cosTheta = Math.cos(theta); + + particle.position.set(r * cosTheta, r * sinTheta, 0); + const coneAngle = angle * r; + particle.velocity.set(0, 0, Math.cos(coneAngle)); + particle.velocity.addInPlace(particle.position.scale(Math.sin(coneAngle))); + particle.velocity.scaleInPlace(startSpeed); + particle.position.scaleInPlace(radius); + } + + private _initializePointShape(particle: SolidParticle, startSpeed: number): void { + const theta = Math.random() * Math.PI * 2; + const phi = Math.acos(2.0 * Math.random() - 1.0); + const direction = new Vector3(Math.sin(phi) * Math.cos(theta), Math.sin(phi) * Math.sin(theta), Math.cos(phi)); + particle.position.setAll(0); + particle.velocity.copyFrom(direction); + particle.velocity.scaleInPlace(startSpeed); + } + + private _initializeDefaultShape(particle: SolidParticle, startSpeed: number): void { + particle.position.setAll(0); + particle.velocity.set(0, 1, 0); + particle.velocity.scaleInPlace(startSpeed); + } + + private _initializeEmitterShape(particle: SolidParticle, emissionState: EmissionState): void { + const config = this._config; + const startSpeed = particle.props?.startSpeed ?? 0; + + if (!config.shape) { + this._initializeDefaultShape(particle, startSpeed); + return; + } + + const shapeType = config.shape.type?.toLowerCase(); + const radius = config.shape.radius ?? 1; + const arc = config.shape.arc ?? Math.PI * 2; + const thickness = config.shape.thickness ?? 1; + const angle = config.shape.angle ?? Math.PI / 6; + + if (shapeType === "sphere") { + this._initializeSphereShape(particle, radius, arc, thickness, startSpeed); + } else if (shapeType === "cone") { + this._initializeConeShape(particle, radius, arc, thickness, angle, startSpeed); + } else if (shapeType === "point") { + this._initializePointShape(particle, startSpeed); + } else { + this._initializeDefaultShape(particle, startSpeed); + } + } + + private _getEmitterMatrix(): Matrix { + const matrix = Matrix.Identity(); + if (this.mesh) { + this.mesh.computeWorldMatrix(true); + matrix.copyFrom(this.mesh.getWorldMatrix()); + } + return matrix; + } + + private _handleEmissionLooping(): void { + const emissionState = this._emissionState; + + if (emissionState.time > this._duration) { + if (this._looping) { + emissionState.time -= this._duration; + emissionState.burstIndex = 0; + } else if (!this._emitEnded) { + this._emitEnded = true; + } + } + } + + private _spawnFromWaitEmiting(): void { + const emissionState = this._emissionState; + const totalSpawn = Math.ceil(emissionState.waitEmiting); + if (totalSpawn > 0) { + this._spawn(totalSpawn); + emissionState.waitEmiting -= totalSpawn; + } + } + + private _spawnBursts(): void { + const emissionState = this._emissionState; + const config = this._config; + const valueParser = this._valueParser; + + if (!config.emissionBursts || !Array.isArray(config.emissionBursts)) { + return; + } + + while (emissionState.burstIndex < config.emissionBursts.length && this._getBurstTime(config.emissionBursts[emissionState.burstIndex]) <= emissionState.time) { + const burst = config.emissionBursts[emissionState.burstIndex]; + const burstCount = valueParser.parseConstantValue(burst.count); + emissionState.isBursting = true; + emissionState.burstParticleCount = burstCount; + this._spawn(burstCount); + emissionState.isBursting = false; + emissionState.burstIndex++; + } + } + + private _accumulateEmission(delta: number): void { + const emissionState = this._emissionState; + const config = this._config; + const valueParser = this._valueParser; + + if (this._emitEnded) { + return; + } + + const emissionRate = config.emissionOverTime !== undefined ? valueParser.parseConstantValue(config.emissionOverTime) : 10; + emissionState.waitEmiting += delta * emissionRate; + + if (config.emissionOverDistance !== undefined && this.mesh && this.mesh.position) { + const emitPerMeter = valueParser.parseConstantValue(config.emissionOverDistance); + if (emitPerMeter > 0 && emissionState.previousWorldPos) { + const distance = Vector3.Distance(emissionState.previousWorldPos, this.mesh.position); + emissionState.travelDistance += distance; + if (emissionState.travelDistance * emitPerMeter > 0) { + const count = Math.floor(emissionState.travelDistance * emitPerMeter); + emissionState.travelDistance -= count / emitPerMeter; + emissionState.waitEmiting += count; + } + } + if (!emissionState.previousWorldPos) { + emissionState.previousWorldPos = Vector3.Zero(); + } + emissionState.previousWorldPos.copyFrom(this.mesh.position); + } + } + + private _emit(delta: number): void { + this._handleEmissionLooping(); + this._spawnFromWaitEmiting(); + this._spawnBursts(); + this._accumulateEmission(delta); + + this._emissionState.time += delta; + } + + private _getBurstTime(burst: VFXEmissionBurst): number { + return this._valueParser.parseConstantValue(burst.time); + } + + private _setupMeshProperties(): void { + const config = this._config; + + if (!this.mesh) { + if (this._logger) { + this._logger.warn(` SPS mesh is null in initParticles!`, this._options); + } + return; + } + + if (this._logger) { + this._logger.log(` initParticles called for SPS: ${this._name}`, this._options); + this._logger.log(` SPS mesh exists: ${this.mesh.name}`, this._options); + } + + if (config.renderOrder !== undefined) { + this.mesh.renderingGroupId = config.renderOrder; + if (this._logger) { + this._logger.log(` Set SPS mesh renderingGroupId: ${config.renderOrder}`, this._options); + } + } + + if (config.layers !== undefined) { + this.mesh.layerMask = config.layers; + if (this._logger) { + this._logger.log(` Set SPS mesh layerMask: ${config.layers}`, this._options); + } + } + + if (this._parentGroup) { + this.mesh.setParent(this._parentGroup, false, true); + if (this._logger) { + this._logger.log(` Set SPS mesh parent to: ${this._parentGroup.name}`, this._options); + } + } else if (this._logger) { + this._logger.log(` No parent group to set for SPS mesh`, this._options); + } + + if (this._vfxTransform) { + this.mesh.position.copyFrom(this._vfxTransform.position); + this.mesh.rotationQuaternion = this._vfxTransform.rotation.clone(); + this.mesh.scaling.copyFrom(this._vfxTransform.scale); + + if (this._logger) { + const rot = this.mesh.rotationQuaternion; + this._logger.log( + ` Applied VFX transform to SPS mesh: pos=(${this._vfxTransform.position.x.toFixed(2)}, ${this._vfxTransform.position.y.toFixed(2)}, ${this._vfxTransform.position.z.toFixed(2)}), rot=(${rot ? rot.x.toFixed(4) : 0}, ${rot ? rot.y.toFixed(4) : 0}, ${rot ? rot.z.toFixed(4) : 0}, ${rot ? rot.w.toFixed(4) : 1}), scale=(${this._vfxTransform.scale.x.toFixed(2)}, ${this._vfxTransform.scale.y.toFixed(2)}, ${this._vfxTransform.scale.z.toFixed(2)})`, + this._options + ); + } + } else if (this._logger) { + this._logger.log(` No VFX transform to apply to SPS mesh`, this._options); + } + } + + private _initializeDeadParticles(): void { + for (let i = 0; i < this.nbParticles; i++) { + const particle = this.particles[i]; + particle.alive = false; + particle.isVisible = false; + particle.age = 0; + particle.lifeTime = Infinity; + particle.position.setAll(0); + particle.velocity.setAll(0); + particle.rotation.setAll(0); + particle.scaling.setAll(1); + if (particle.color) { + particle.color.set(1, 1, 1, 1); + } else { + particle.color = new Color4(1, 1, 1, 1); + } + } + } + + private _resetEmissionState(): void { + this._emissionState.time = 0; + this._emissionState.waitEmiting = 0; + this._emissionState.travelDistance = 0; + this._emissionState.burstIndex = 0; + this._emissionState.burstWaveIndex = 0; + this._emissionState.burstParticleIndex = 0; + this._emissionState.burstParticleCount = 0; + this._emissionState.isBursting = false; + if (this.mesh && this.mesh.position) { + this._emissionState.previousWorldPos = this.mesh.position.clone(); + } + this._emitEnded = false; + } + + public override initParticles(): void { + this._setupMeshProperties(); + this._initializeDeadParticles(); + this._resetEmissionState(); + } + + public setPerParticleBehaviors(functions: VFXPerSolidParticleBehaviorFunction[]): void { + this._perParticleBehaviors = functions; + } + + public override beforeUpdateParticles(start?: number, stop?: number, update?: boolean): void { + super.beforeUpdateParticles(start, stop, update); + + if (!this._started || this._stopped) { + return; + } + + const deltaTime = this._scaledUpdateSpeed || 0.016; + + this._emit(deltaTime); + this._emissionState.time += deltaTime; + } + + private _updateParticle(particle: SolidParticle): SolidParticle { + if (!particle.alive) { + particle.isVisible = false; + + if (this._positions32 && particle._model) { + const shape = particle._model._shape; + const startIdx = particle._pos; + for (let pt = 0; pt < shape.length; pt++) { + const idx = startIdx + pt * 3; + this._positions32[idx] = 0; + this._positions32[idx + 1] = 0; + this._positions32[idx + 2] = 0; + } + } + + return particle; + } + + if (particle.age < 0) { + return particle; + } + + const lifeRatio = particle.age / particle.lifeTime; + const startSpeed = particle.props?.startSpeed ?? 0; + const startSize = particle.props?.startSize ?? 1; + const startColor = particle.props?.startColor ?? new Color4(1, 1, 1, 1); + + const context: VFXPerParticleContext = { + lifeRatio, + startSpeed, + startSize, + startColor: { r: startColor.r, g: startColor.g, b: startColor.b, a: startColor.a }, + updateSpeed: this.updateSpeed, + valueParser: this._valueParser, + }; + + for (const behaviorFn of this._perParticleBehaviors) { + behaviorFn(particle, context); + } + + const speedModifier = particle.props?.speedModifier ?? 1.0; + particle.position.addInPlace(particle.velocity.scale(this.updateSpeed * speedModifier)); + + return particle; + } +} diff --git a/editor/src/editor/windows/fx-editor/VFX/treejs3dobject.particle.json b/editor/src/editor/windows/fx-editor/VFX/treejs3dobject.particle.json new file mode 100644 index 000000000..f8fb4d962 --- /dev/null +++ b/editor/src/editor/windows/fx-editor/VFX/treejs3dobject.particle.json @@ -0,0 +1,870 @@ +{ + "metadata": { "version": 4.6, "type": "Object", "generator": "Object3D.toJSON" }, + "geometries": [ + { "uuid": "780917d8-bd1b-4d63-8aca-f79e3211f964", "type": "PlaneGeometry", "name": "PlaneGeometry", "width": 1, "height": 1, "widthSegments": 1, "heightSegments": 1 }, + { + "uuid": "f40b6ee0-aa01-46e0-b05a-d938b54eec83", + "type": "BufferGeometry", + "name": "GlowCircleEmitter_geometry", + "data": { + "attributes": { + "position": { + "itemSize": 3, + "type": "Float32Array", + "array": [ + 0.41758671402931213, 0.08306316286325455, 0.10689251124858856, 0.42576777935028076, -1.2535783966427516e-8, 0.10689251124858856, 0.3199999928474426, 0, + 0, 0.3199999928474426, 0, 0, 0.3138512670993805, 0.062428902834653854, 0, 0.41758671402931213, 0.08306316286325455, 0.10689251124858856, + 0.39335811138153076, 0.16293425858020782, 0.10689251124858856, 0.41758671402931213, 0.08306316286325455, 0.10689251124858856, 0.3138512670993805, + 0.062428902834653854, 0, 0.3138512670993805, 0.062428902834653854, 0, 0.2956414520740509, 0.12245870381593704, 0, 0.39335811138153076, + 0.16293425858020782, 0.10689251124858856, 0.35401293635368347, 0.23654387891292572, 0.10689251124858856, 0.39335811138153076, 0.16293425858020782, + 0.10689251124858856, 0.2956414520740509, 0.12245870381593704, 0, 0.2956414520740509, 0.12245870381593704, 0, 0.26607027649879456, 0.17778247594833374, + 0, 0.35401293635368347, 0.23654387891292572, 0.10689251124858856, 0.3010632395744324, 0.30106326937675476, 0.10689251124858856, 0.35401293635368347, + 0.23654387891292572, 0.10689251124858856, 0.26607027649879456, 0.17778247594833374, 0, 0.26607027649879456, 0.17778247594833374, 0, 0.22627416253089905, + 0.22627416253089905, 0, 0.3010632395744324, 0.30106326937675476, 0.10689251124858856, 0.23654384911060333, 0.35401299595832825, 0.10689251124858856, + 0.3010632395744324, 0.30106326937675476, 0.10689251124858856, 0.22627416253089905, 0.22627416253089905, 0, 0.22627416253089905, 0.22627416253089905, 0, + 0.17778246104717255, 0.26607027649879456, 0, 0.23654384911060333, 0.35401299595832825, 0.10689251124858856, 0.16293422877788544, 0.39335811138153076, + 0.10689251124858856, 0.23654384911060333, 0.35401299595832825, 0.10689251124858856, 0.17778246104717255, 0.26607027649879456, 0, 0.17778246104717255, + 0.26607027649879456, 0, 0.12245869636535645, 0.2956414520740509, 0, 0.16293422877788544, 0.39335811138153076, 0.10689251124858856, 0.08306317031383514, + 0.41758671402931213, 0.10689251124858856, 0.16293422877788544, 0.39335811138153076, 0.10689251124858856, 0.12245869636535645, 0.2956414520740509, 0, + 0.12245869636535645, 0.2956414520740509, 0, 0.06242891401052475, 0.3138512670993805, 0, 0.08306317031383514, 0.41758671402931213, 0.10689251124858856, + 2.0868840877596995e-8, 0.42576777935028076, 0.10689251124858856, 0.08306317031383514, 0.41758671402931213, 0.10689251124858856, 0.06242891401052475, + 0.3138512670993805, 0, 0.06242891401052475, 0.3138512670993805, 0, 2.415932875976523e-8, 0.3199999928474426, 0, 2.0868840877596995e-8, + 0.42576777935028076, 0.10689251124858856, -0.08306313306093216, 0.4175867438316345, 0.10689251124858856, 2.0868840877596995e-8, 0.42576777935028076, + 0.10689251124858856, 2.415932875976523e-8, 0.3199999928474426, 0, 2.415932875976523e-8, 0.3199999928474426, 0, -0.06242886558175087, 0.3138512969017029, + 0, -0.08306313306093216, 0.4175867438316345, 0.10689251124858856, -0.16293422877788544, 0.39335814118385315, 0.10689251124858856, -0.08306313306093216, + 0.4175867438316345, 0.10689251124858856, -0.06242886558175087, 0.3138512969017029, 0, -0.06242886558175087, 0.3138512969017029, 0, -0.12245865166187286, + 0.2956414520740509, 0, -0.16293422877788544, 0.39335814118385315, 0.10689251124858856, -0.23654387891292572, 0.35401299595832825, 0.10689251124858856, + -0.16293422877788544, 0.39335814118385315, 0.10689251124858856, -0.12245865166187286, 0.2956414520740509, 0, -0.12245865166187286, 0.2956414520740509, + 0, -0.17778246104717255, 0.26607027649879456, 0, -0.23654387891292572, 0.35401299595832825, 0.10689251124858856, -0.30106329917907715, + 0.30106326937675476, 0.10689251124858856, -0.23654387891292572, 0.35401299595832825, 0.10689251124858856, -0.17778246104717255, 0.26607027649879456, 0, + -0.17778246104717255, 0.26607027649879456, 0, -0.22627416253089905, 0.22627416253089905, 0, -0.30106329917907715, 0.30106326937675476, + 0.10689251124858856, -0.35401299595832825, 0.23654386401176453, 0.10689251124858856, -0.30106329917907715, 0.30106326937675476, 0.10689251124858856, + -0.22627416253089905, 0.22627416253089905, 0, -0.22627416253089905, 0.22627416253089905, 0, -0.26607027649879456, 0.17778246104717255, 0, + -0.35401299595832825, 0.23654386401176453, 0.10689251124858856, -0.39335814118385315, 0.16293418407440186, 0.10689251124858856, -0.35401299595832825, + 0.23654386401176453, 0.10689251124858856, -0.26607027649879456, 0.17778246104717255, 0, -0.26607027649879456, 0.17778246104717255, 0, + -0.2956414818763733, 0.12245865166187286, 0, -0.39335814118385315, 0.16293418407440186, 0.10689251124858856, -0.4175867438316345, 0.08306305855512619, + 0.10689251124858856, -0.39335814118385315, 0.16293418407440186, 0.10689251124858856, -0.2956414818763733, 0.12245865166187286, 0, -0.2956414818763733, + 0.12245865166187286, 0, -0.3138512969017029, 0.062428828328847885, 0, -0.4175867438316345, 0.08306305855512619, 0.10689251124858856, + -0.42576777935028076, -1.5126852304092608e-7, 0.10689251124858856, -0.4175867438316345, 0.08306305855512619, 0.10689251124858856, -0.3138512969017029, + 0.062428828328847885, 0, -0.3138512969017029, 0.062428828328847885, 0, -0.3199999928474426, -1.0426924035300544e-7, 0, -0.42576777935028076, + -1.5126852304092608e-7, 0.10689251124858856, -0.41758671402931213, -0.08306335657835007, 0.10689251124858856, -0.42576777935028076, + -1.5126852304092608e-7, 0.10689251124858856, -0.3199999928474426, -1.0426924035300544e-7, 0, -0.3199999928474426, -1.0426924035300544e-7, 0, + -0.3138512670993805, -0.0624290332198143, 0, -0.41758671402931213, -0.08306335657835007, 0.10689251124858856, -0.393358051776886, -0.16293445229530334, + 0.10689251124858856, -0.41758671402931213, -0.08306335657835007, 0.10689251124858856, -0.3138512670993805, -0.0624290332198143, 0, -0.3138512670993805, + -0.0624290332198143, 0, -0.29564139246940613, -0.12245883792638779, 0, -0.393358051776886, -0.16293445229530334, 0.10689251124858856, + -0.35401278734207153, -0.23654408752918243, 0.10689251124858856, -0.393358051776886, -0.16293445229530334, 0.10689251124858856, -0.29564139246940613, + -0.12245883792638779, 0, -0.29564139246940613, -0.12245883792638779, 0, -0.2660701870918274, -0.17778262495994568, 0, -0.35401278734207153, + -0.23654408752918243, 0.10689251124858856, -0.30106309056282043, -0.3010634481906891, 0.10689251124858856, -0.35401278734207153, -0.23654408752918243, + 0.10689251124858856, -0.2660701870918274, -0.17778262495994568, 0, -0.2660701870918274, -0.17778262495994568, 0, -0.2262740284204483, + -0.226274311542511, 0, -0.30106309056282043, -0.3010634481906891, 0.10689251124858856, -0.2365436553955078, -0.3540131449699402, 0.10689251124858856, + -0.30106309056282043, -0.3010634481906891, 0.10689251124858856, -0.2262740284204483, -0.226274311542511, 0, -0.2262740284204483, -0.226274311542511, 0, + -0.17778228223323822, -0.2660703957080841, 0, -0.2365436553955078, -0.3540131449699402, 0.10689251124858856, -0.16293397545814514, -0.3933582603931427, + 0.10689251124858856, -0.2365436553955078, -0.3540131449699402, 0.10689251124858856, -0.17778228223323822, -0.2660703957080841, 0, -0.17778228223323822, + -0.2660703957080841, 0, -0.12245845794677734, -0.29564154148101807, 0, -0.16293397545814514, -0.3933582603931427, 0.10689251124858856, + -0.08306281268596649, -0.4175868332386017, 0.10689251124858856, -0.16293397545814514, -0.3933582603931427, 0.10689251124858856, -0.12245845794677734, + -0.29564154148101807, 0, -0.12245845794677734, -0.29564154148101807, 0, -0.06242862716317177, -0.31385132670402527, 0, -0.08306281268596649, + -0.4175868332386017, 0.10689251124858856, 3.9984527688829985e-7, -0.42576777935028076, 0.10689251124858856, -0.08306281268596649, -0.4175868332386017, + 0.10689251124858856, -0.06242862716317177, -0.31385132670402527, 0, -0.06242862716317177, -0.31385132670402527, 0, 3.0899172998033464e-7, + -0.3199999928474426, 0, 3.9984527688829985e-7, -0.42576777935028076, 0.10689251124858856, 0.08306359499692917, -0.41758668422698975, + 0.10689251124858856, 3.9984527688829985e-7, -0.42576777935028076, 0.10689251124858856, 3.0899172998033464e-7, -0.3199999928474426, 0, + 3.0899172998033464e-7, -0.3199999928474426, 0, 0.06242923438549042, -0.3138512372970581, 0, 0.08306359499692917, -0.41758668422698975, + 0.10689251124858856, 0.16293466091156006, -0.39335793256759644, 0.10689251124858856, 0.08306359499692917, -0.41758668422698975, 0.10689251124858856, + 0.06242923438549042, -0.3138512372970581, 0, 0.06242923438549042, -0.3138512372970581, 0, 0.1224590316414833, -0.29564130306243896, 0, + 0.16293466091156006, -0.39335793256759644, 0.10689251124858856, 0.23654431104660034, -0.3540126383304596, 0.10689251124858856, 0.16293466091156006, + -0.39335793256759644, 0.10689251124858856, 0.1224590316414833, -0.29564130306243896, 0, 0.1224590316414833, -0.29564130306243896, 0, 0.17778280377388, + -0.26607006788253784, 0, 0.23654431104660034, -0.3540126383304596, 0.10689251124858856, 0.3010636270046234, -0.3010628819465637, 0.10689251124858856, + 0.23654431104660034, -0.3540126383304596, 0.10689251124858856, 0.17778280377388, -0.26607006788253784, 0, 0.17778280377388, -0.26607006788253784, 0, + 0.22627444565296173, -0.22627387940883636, 0, 0.3010636270046234, -0.3010628819465637, 0.10689251124858856, 0.3540132939815521, -0.23654340207576752, + 0.10689251124858856, 0.3010636270046234, -0.3010628819465637, 0.10689251124858856, 0.22627444565296173, -0.22627387940883636, 0, 0.22627444565296173, + -0.22627387940883636, 0, 0.26607051491737366, -0.1777821183204651, 0, 0.3540132939815521, -0.23654340207576752, 0.10689251124858856, + 0.39335834980010986, -0.16293370723724365, 0.10689251124858856, 0.3540132939815521, -0.23654340207576752, 0.10689251124858856, 0.26607051491737366, + -0.1777821183204651, 0, 0.26607051491737366, -0.1777821183204651, 0, 0.29564163088798523, -0.12245826423168182, 0, 0.39335834980010986, + -0.16293370723724365, 0.10689251124858856, 0.4175868630409241, -0.083062544465065, 0.10689251124858856, 0.39335834980010986, -0.16293370723724365, + 0.10689251124858856, 0.29564163088798523, -0.12245826423168182, 0, 0.29564163088798523, -0.12245826423168182, 0, 0.31385138630867004, + -0.06242842227220535, 0, 0.4175868630409241, -0.083062544465065, 0.10689251124858856, 0.42576777935028076, -1.2535783966427516e-8, 0.10689251124858856, + 0.4175868630409241, -0.083062544465065, 0.10689251124858856, 0.31385138630867004, -0.06242842227220535, 0, 0.31385138630867004, -0.06242842227220535, 0, + 0.3199999928474426, 0, 0, 0.42576777935028076, -1.2535783966427516e-8, 0.10689251124858856, 0.4175865948200226, 0.08306316286325455, + 0.10689251124858856, 0.31385117769241333, 0.062428902834653854, 0, 0.3199998736381531, 0, 0, 0.3199998736381531, 0, 0, 0.4257676303386688, + -1.2535783966427516e-8, 0.10689251124858856, 0.4175865948200226, 0.08306316286325455, 0.10689251124858856, 0.3933579623699188, 0.16293425858020782, + 0.10689251124858856, 0.29564133286476135, 0.12245870381593704, 0, 0.31385117769241333, 0.062428902834653854, 0, 0.31385117769241333, + 0.062428902834653854, 0, 0.4175865948200226, 0.08306316286325455, 0.10689251124858856, 0.3933579623699188, 0.16293425858020782, 0.10689251124858856, + 0.35401275753974915, 0.23654387891292572, 0.10689251124858856, 0.2660701274871826, 0.17778247594833374, 0, 0.29564133286476135, 0.12245870381593704, 0, + 0.29564133286476135, 0.12245870381593704, 0, 0.3933579623699188, 0.16293425858020782, 0.10689251124858856, 0.35401275753974915, 0.23654387891292572, + 0.10689251124858856, 0.30106306076049805, 0.30106326937675476, 0.10689251124858856, 0.2262740284204483, 0.22627416253089905, 0, 0.2660701274871826, + 0.17778247594833374, 0, 0.2660701274871826, 0.17778247594833374, 0, 0.35401275753974915, 0.23654387891292572, 0.10689251124858856, 0.30106306076049805, + 0.30106326937675476, 0.10689251124858856, 0.2365437150001526, 0.35401299595832825, 0.10689251124858856, 0.177782341837883, 0.26607027649879456, 0, + 0.2262740284204483, 0.22627416253089905, 0, 0.2262740284204483, 0.22627416253089905, 0, 0.30106306076049805, 0.30106326937675476, 0.10689251124858856, + 0.2365437150001526, 0.35401299595832825, 0.10689251124858856, 0.1629340499639511, 0.39335811138153076, 0.10689251124858856, 0.1224585697054863, + 0.2956414520740509, 0, 0.177782341837883, 0.26607027649879456, 0, 0.177782341837883, 0.26607027649879456, 0, 0.2365437150001526, 0.35401299595832825, + 0.10689251124858856, 0.1629340499639511, 0.39335811138153076, 0.10689251124858856, 0.08306301385164261, 0.41758671402931213, 0.10689251124858856, + 0.0624287948012352, 0.3138512670993805, 0, 0.1224585697054863, 0.2956414520740509, 0, 0.1224585697054863, 0.2956414520740509, 0, 0.1629340499639511, + 0.39335811138153076, 0.10689251124858856, 0.08306301385164261, 0.41758671402931213, 0.10689251124858856, -1.3816443811265344e-7, 0.42576777935028076, + 0.10689251124858856, -9.536743306171047e-8, 0.3199999928474426, 0, 0.0624287948012352, 0.3138512670993805, 0, 0.0624287948012352, 0.3138512670993805, 0, + 0.08306301385164261, 0.41758671402931213, 0.10689251124858856, -1.3816443811265344e-7, 0.42576777935028076, 0.10689251124858856, -0.0830632895231247, + 0.4175867438316345, 0.10689251124858856, -0.06242898479104042, 0.3138512969017029, 0, -9.536743306171047e-8, 0.3199999928474426, 0, + -9.536743306171047e-8, 0.3199999928474426, 0, -1.3816443811265344e-7, 0.42576777935028076, 0.10689251124858856, -0.0830632895231247, 0.4175867438316345, + 0.10689251124858856, -0.16293437778949738, 0.39335814118385315, 0.10689251124858856, -0.12245876342058182, 0.2956414520740509, 0, -0.06242898479104042, + 0.3138512969017029, 0, -0.06242898479104042, 0.3138512969017029, 0, -0.0830632895231247, 0.4175867438316345, 0.10689251124858856, -0.16293437778949738, + 0.39335814118385315, 0.10689251124858856, -0.23654407262802124, 0.35401299595832825, 0.10689251124858856, -0.1777825951576233, 0.26607027649879456, 0, + -0.12245876342058182, 0.2956414520740509, 0, -0.12245876342058182, 0.2956414520740509, 0, -0.16293437778949738, 0.39335814118385315, + 0.10689251124858856, -0.23654407262802124, 0.35401299595832825, 0.10689251124858856, -0.3010634481906891, 0.30106326937675476, 0.10689251124858856, + -0.2262742966413498, 0.22627416253089905, 0, -0.1777825951576233, 0.26607027649879456, 0, -0.1777825951576233, 0.26607027649879456, 0, + -0.23654407262802124, 0.35401299595832825, 0.10689251124858856, -0.3010634481906891, 0.30106326937675476, 0.10689251124858856, -0.3540131449699402, + 0.23654386401176453, 0.10689251124858856, -0.2660703957080841, 0.17778246104717255, 0, -0.2262742966413498, 0.22627416253089905, 0, -0.2262742966413498, + 0.22627416253089905, 0, -0.3010634481906891, 0.30106326937675476, 0.10689251124858856, -0.3540131449699402, 0.23654386401176453, 0.10689251124858856, + -0.3933583199977875, 0.16293418407440186, 0.10689251124858856, -0.29564160108566284, 0.12245865166187286, 0, -0.2660703957080841, 0.17778246104717255, + 0, -0.2660703957080841, 0.17778246104717255, 0, -0.3540131449699402, 0.23654386401176453, 0.10689251124858856, -0.3933583199977875, 0.16293418407440186, + 0.10689251124858856, -0.41758692264556885, 0.08306305855512619, 0.10689251124858856, -0.3138514459133148, 0.062428828328847885, 0, -0.29564160108566284, + 0.12245865166187286, 0, -0.29564160108566284, 0.12245865166187286, 0, -0.3933583199977875, 0.16293418407440186, 0.10689251124858856, + -0.41758692264556885, 0.08306305855512619, 0.10689251124858856, -0.4257678985595703, -1.5126852304092608e-7, 0.10689251124858856, -0.3200001120567322, + -1.0426924035300544e-7, 0, -0.3138514459133148, 0.062428828328847885, 0, -0.3138514459133148, 0.062428828328847885, 0, -0.41758692264556885, + 0.08306305855512619, 0.10689251124858856, -0.4257678985595703, -1.5126852304092608e-7, 0.10689251124858856, -0.41758689284324646, -0.08306335657835007, + 0.10689251124858856, -0.31385138630867004, -0.0624290332198143, 0, -0.3200001120567322, -1.0426924035300544e-7, 0, -0.3200001120567322, + -1.0426924035300544e-7, 0, -0.4257678985595703, -1.5126852304092608e-7, 0.10689251124858856, -0.41758689284324646, -0.08306335657835007, + 0.10689251124858856, -0.3933582305908203, -0.16293445229530334, 0.10689251124858856, -0.2956415116786957, -0.12245883792638779, 0, -0.31385138630867004, + -0.0624290332198143, 0, -0.31385138630867004, -0.0624290332198143, 0, -0.41758689284324646, -0.08306335657835007, 0.10689251124858856, + -0.3933582305908203, -0.16293445229530334, 0.10689251124858856, -0.35401299595832825, -0.23654408752918243, 0.10689251124858856, -0.26607027649879456, + -0.17778262495994568, 0, -0.2956415116786957, -0.12245883792638779, 0, -0.2956415116786957, -0.12245883792638779, 0, -0.3933582305908203, + -0.16293445229530334, 0.10689251124858856, -0.35401299595832825, -0.23654408752918243, 0.10689251124858856, -0.3010632395744324, -0.3010634481906891, + 0.10689251124858856, -0.22627414762973785, -0.226274311542511, 0, -0.26607027649879456, -0.17778262495994568, 0, -0.26607027649879456, + -0.17778262495994568, 0, -0.35401299595832825, -0.23654408752918243, 0.10689251124858856, -0.3010632395744324, -0.3010634481906891, 0.10689251124858856, + -0.23654380440711975, -0.3540131449699402, 0.10689251124858856, -0.17778240144252777, -0.2660703957080841, 0, -0.22627414762973785, -0.226274311542511, + 0, -0.22627414762973785, -0.226274311542511, 0, -0.3010632395744324, -0.3010634481906891, 0.10689251124858856, -0.23654380440711975, + -0.3540131449699402, 0.10689251124858856, -0.16293412446975708, -0.3933582603931427, 0.10689251124858856, -0.1224585697054863, -0.29564154148101807, 0, + -0.17778240144252777, -0.2660703957080841, 0, -0.17778240144252777, -0.2660703957080841, 0, -0.23654380440711975, -0.3540131449699402, + 0.10689251124858856, -0.16293412446975708, -0.3933582603931427, 0.10689251124858856, -0.08306297659873962, -0.4175868332386017, 0.10689251124858856, + -0.06242874637246132, -0.31385132670402527, 0, -0.1224585697054863, -0.29564154148101807, 0, -0.1224585697054863, -0.29564154148101807, 0, + -0.16293412446975708, -0.3933582603931427, 0.10689251124858856, -0.08306297659873962, -0.4175868332386017, 0.10689251124858856, 2.4250164187833434e-7, + -0.42576777935028076, 0.10689251124858856, 1.9073486612342094e-7, -0.3199999928474426, 0, -0.06242874637246132, -0.31385132670402527, 0, + -0.06242874637246132, -0.31385132670402527, 0, -0.08306297659873962, -0.4175868332386017, 0.10689251124858856, 2.4250164187833434e-7, + -0.42576777935028076, 0.10689251124858856, 0.08306345343589783, -0.41758668422698975, 0.10689251124858856, 0.062429118901491165, -0.3138512372970581, 0, + 1.9073486612342094e-7, -0.3199999928474426, 0, 1.9073486612342094e-7, -0.3199999928474426, 0, 2.4250164187833434e-7, -0.42576777935028076, + 0.10689251124858856, 0.08306345343589783, -0.41758668422698975, 0.10689251124858856, 0.16293452680110931, -0.39335793256759644, 0.10689251124858856, + 0.12245891243219376, -0.29564130306243896, 0, 0.062429118901491165, -0.3138512372970581, 0, 0.062429118901491165, -0.3138512372970581, 0, + 0.08306345343589783, -0.41758668422698975, 0.10689251124858856, 0.16293452680110931, -0.39335793256759644, 0.10689251124858856, 0.2365441471338272, + -0.3540126383304596, 0.10689251124858856, 0.17778268456459045, -0.26607006788253784, 0, 0.12245891243219376, -0.29564130306243896, 0, + 0.12245891243219376, -0.29564130306243896, 0, 0.16293452680110931, -0.39335793256759644, 0.10689251124858856, 0.2365441471338272, -0.3540126383304596, + 0.10689251124858856, 0.3010634779930115, -0.3010628819465637, 0.10689251124858856, 0.22627434134483337, -0.22627387940883636, 0, 0.17778268456459045, + -0.26607006788253784, 0, 0.17778268456459045, -0.26607006788253784, 0, 0.2365441471338272, -0.3540126383304596, 0.10689251124858856, 0.3010634779930115, + -0.3010628819465637, 0.10689251124858856, 0.3540131449699402, -0.23654340207576752, 0.10689251124858856, 0.2660703957080841, -0.1777821183204651, 0, + 0.22627434134483337, -0.22627387940883636, 0, 0.22627434134483337, -0.22627387940883636, 0, 0.3010634779930115, -0.3010628819465637, + 0.10689251124858856, 0.3540131449699402, -0.23654340207576752, 0.10689251124858856, 0.3933582305908203, -0.16293370723724365, 0.10689251124858856, + 0.2956415116786957, -0.12245826423168182, 0, 0.2660703957080841, -0.1777821183204651, 0, 0.2660703957080841, -0.1777821183204651, 0, 0.3540131449699402, + -0.23654340207576752, 0.10689251124858856, 0.3933582305908203, -0.16293370723724365, 0.10689251124858856, 0.41758668422698975, -0.083062544465065, + 0.10689251124858856, 0.3138512372970581, -0.06242842227220535, 0, 0.2956415116786957, -0.12245826423168182, 0, 0.2956415116786957, -0.12245826423168182, + 0, 0.3933582305908203, -0.16293370723724365, 0.10689251124858856, 0.41758668422698975, -0.083062544465065, 0.10689251124858856, 0.4257676303386688, + -1.2535783966427516e-8, 0.10689251124858856, 0.3199998736381531, 0, 0, 0.3138512372970581, -0.06242842227220535, 0, 0.3138512372970581, + -0.06242842227220535, 0, 0.41758668422698975, -0.083062544465065, 0.10689251124858856, 0.4257676303386688, -1.2535783966427516e-8, 0.10689251124858856 + ], + "normalized": false + }, + "normal": { + "itemSize": 3, + "type": "Float32Array", + "array": [ + 0.6971781849861145, 0.1386774480342865, -0.7033570408821106, 0.7108367085456848, 3.257474361362256e-7, -0.7033571004867554, 0.71083664894104, + 3.6210147413839877e-7, -0.7033571600914001, 0.71083664894104, 3.6210147413839877e-7, -0.7033571600914001, 0.6971781849861145, 0.1386774480342865, + -0.7033570408821106, 0.6971781849861145, 0.1386774480342865, -0.7033570408821106, 0.6567274928092957, 0.27202534675598145, -0.7033570408821106, + 0.6971781849861145, 0.1386774480342865, -0.7033570408821106, 0.6971781849861145, 0.1386774480342865, -0.7033570408821106, 0.6971781849861145, + 0.1386774480342865, -0.7033570408821106, 0.6567275524139404, 0.27202534675598145, -0.7033570408821106, 0.6567274928092957, 0.27202534675598145, + -0.7033570408821106, 0.5910391211509705, 0.394919753074646, -0.7033570408821106, 0.6567274928092957, 0.27202534675598145, -0.7033570408821106, + 0.6567275524139404, 0.27202534675598145, -0.7033570408821106, 0.6567275524139404, 0.27202534675598145, -0.7033570408821106, 0.5910391211509705, + 0.3949197828769684, -0.7033570408821106, 0.5910391211509705, 0.394919753074646, -0.7033570408821106, 0.5026374459266663, 0.5026374459266663, + -0.7033571004867554, 0.5910391211509705, 0.394919753074646, -0.7033570408821106, 0.5910391211509705, 0.3949197828769684, -0.7033570408821106, + 0.5910391211509705, 0.3949197828769684, -0.7033570408821106, 0.5026374459266663, 0.5026374459266663, -0.7033570408821106, 0.5026374459266663, + 0.5026374459266663, -0.7033571004867554, 0.39491966366767883, 0.5910391807556152, -0.7033571004867554, 0.5026374459266663, 0.5026374459266663, + -0.7033571004867554, 0.5026374459266663, 0.5026374459266663, -0.7033570408821106, 0.5026374459266663, 0.5026374459266663, -0.7033570408821106, + 0.39491966366767883, 0.5910391211509705, -0.7033571004867554, 0.39491966366767883, 0.5910391807556152, -0.7033571004867554, 0.27202531695365906, + 0.6567276120185852, -0.7033570408821106, 0.39491966366767883, 0.5910391807556152, -0.7033571004867554, 0.39491966366767883, 0.5910391211509705, + -0.7033571004867554, 0.39491966366767883, 0.5910391211509705, -0.7033571004867554, 0.27202528715133667, 0.6567275524139404, -0.7033570408821106, + 0.27202531695365906, 0.6567276120185852, -0.7033570408821106, 0.13867750763893127, 0.6971781849861145, -0.7033570408821106, 0.27202531695365906, + 0.6567276120185852, -0.7033570408821106, 0.27202528715133667, 0.6567275524139404, -0.7033570408821106, 0.27202528715133667, 0.6567275524139404, + -0.7033570408821106, 0.13867750763893127, 0.6971781849861145, -0.7033570408821106, 0.13867750763893127, 0.6971781849861145, -0.7033570408821106, + 1.3676421417585516e-7, 0.71083664894104, -0.7033571004867554, 0.13867750763893127, 0.6971781849861145, -0.7033570408821106, 0.13867750763893127, + 0.6971781849861145, -0.7033570408821106, 0.13867750763893127, 0.6971781849861145, -0.7033570408821106, 1.525836097471256e-7, 0.7108367085456848, + -0.7033571004867554, 1.3676421417585516e-7, 0.71083664894104, -0.7033571004867554, -0.13867735862731934, 0.6971781253814697, -0.7033571004867554, + 1.3676421417585516e-7, 0.71083664894104, -0.7033571004867554, 1.525836097471256e-7, 0.7108367085456848, -0.7033571004867554, 1.525836097471256e-7, + 0.7108367085456848, -0.7033571004867554, -0.13867734372615814, 0.6971781253814697, -0.7033571004867554, -0.13867735862731934, 0.6971781253814697, + -0.7033571004867554, -0.2720252573490143, 0.6567274332046509, -0.7033571600914001, -0.13867735862731934, 0.6971781253814697, -0.7033571004867554, + -0.13867734372615814, 0.6971781253814697, -0.7033571004867554, -0.13867734372615814, 0.6971781253814697, -0.7033571004867554, -0.2720252573490143, + 0.6567274928092957, -0.7033571600914001, -0.2720252573490143, 0.6567274332046509, -0.7033571600914001, -0.39491966366767883, 0.5910390019416809, + -0.7033572196960449, -0.2720252573490143, 0.6567274332046509, -0.7033571600914001, -0.2720252573490143, 0.6567274928092957, -0.7033571600914001, + -0.2720252573490143, 0.6567274928092957, -0.7033571600914001, -0.39491963386535645, 0.5910390615463257, -0.7033571600914001, -0.39491966366767883, + 0.5910390019416809, -0.7033572196960449, -0.502637505531311, 0.5026373863220215, -0.7033571004867554, -0.39491966366767883, 0.5910390019416809, + -0.7033572196960449, -0.39491963386535645, 0.5910390615463257, -0.7033571600914001, -0.39491963386535645, 0.5910390615463257, -0.7033571600914001, + -0.5026374459266663, 0.5026373863220215, -0.7033571600914001, -0.502637505531311, 0.5026373863220215, -0.7033571004867554, -0.5910391211509705, + 0.39491963386535645, -0.7033571600914001, -0.502637505531311, 0.5026373863220215, -0.7033571004867554, -0.5026374459266663, 0.5026373863220215, + -0.7033571600914001, -0.5026374459266663, 0.5026373863220215, -0.7033571600914001, -0.5910391211509705, 0.39491963386535645, -0.7033571600914001, + -0.5910391211509705, 0.39491963386535645, -0.7033571600914001, -0.6567275524139404, 0.2720252275466919, -0.7033571004867554, -0.5910391211509705, + 0.39491963386535645, -0.7033571600914001, -0.5910391211509705, 0.39491963386535645, -0.7033571600914001, -0.5910391211509705, 0.39491963386535645, + -0.7033571600914001, -0.6567275524139404, 0.2720251977443695, -0.7033571004867554, -0.6567275524139404, 0.2720252275466919, -0.7033571004867554, + -0.6971781849861145, 0.1386772245168686, -0.7033570408821106, -0.6567275524139404, 0.2720252275466919, -0.7033571004867554, -0.6567275524139404, + 0.2720251977443695, -0.7033571004867554, -0.6567275524139404, 0.2720251977443695, -0.7033571004867554, -0.6971781849861145, 0.1386772245168686, + -0.7033570408821106, -0.6971781849861145, 0.1386772245168686, -0.7033570408821106, -0.71083664894104, -1.9644313908884214e-7, -0.7033571004867554, + -0.6971781849861145, 0.1386772245168686, -0.7033570408821106, -0.6971781849861145, 0.1386772245168686, -0.7033570408821106, -0.6971781849861145, + 0.1386772245168686, -0.7033570408821106, -0.71083664894104, -1.8446674232563964e-7, -0.7033571004867554, -0.71083664894104, -1.9644313908884214e-7, + -0.7033571004867554, -0.697178065776825, -0.13867776095867157, -0.7033571004867554, -0.71083664894104, -1.9644313908884214e-7, -0.7033571004867554, + -0.71083664894104, -1.8446674232563964e-7, -0.7033571004867554, -0.71083664894104, -1.8446674232563964e-7, -0.7033571004867554, -0.697178065776825, + -0.13867774605751038, -0.7033571004867554, -0.697178065776825, -0.13867776095867157, -0.7033571004867554, -0.6567273139953613, -0.27202582359313965, + -0.7033571004867554, -0.697178065776825, -0.13867776095867157, -0.7033571004867554, -0.697178065776825, -0.13867774605751038, -0.7033571004867554, + -0.697178065776825, -0.13867774605751038, -0.7033571004867554, -0.6567272543907166, -0.27202582359313965, -0.7033571004867554, -0.6567273139953613, + -0.27202582359313965, -0.7033571004867554, -0.5910389423370361, -0.3949200212955475, -0.7033571004867554, -0.6567273139953613, -0.27202582359313965, + -0.7033571004867554, -0.6567272543907166, -0.27202582359313965, -0.7033571004867554, -0.6567272543907166, -0.27202582359313965, -0.7033571004867554, + -0.5910389423370361, -0.3949199914932251, -0.7033570408821106, -0.5910389423370361, -0.3949200212955475, -0.7033571004867554, -0.5026372671127319, + -0.5026376843452454, -0.7033571004867554, -0.5910389423370361, -0.3949200212955475, -0.7033571004867554, -0.5910389423370361, -0.3949199914932251, + -0.7033570408821106, -0.5910389423370361, -0.3949199914932251, -0.7033570408821106, -0.5026372075080872, -0.5026376843452454, -0.7033571004867554, + -0.5026372671127319, -0.5026376843452454, -0.7033571004867554, -0.3949193060398102, -0.5910392999649048, -0.7033571600914001, -0.5026372671127319, + -0.5026376843452454, -0.7033571004867554, -0.5026372075080872, -0.5026376843452454, -0.7033571004867554, -0.5026372075080872, -0.5026376843452454, + -0.7033571004867554, -0.39491933584213257, -0.5910392999649048, -0.7033571600914001, -0.3949193060398102, -0.5910392999649048, -0.7033571600914001, + -0.27202484011650085, -0.6567276120185852, -0.7033571600914001, -0.3949193060398102, -0.5910392999649048, -0.7033571600914001, -0.39491933584213257, + -0.5910392999649048, -0.7033571600914001, -0.39491933584213257, -0.5910392999649048, -0.7033571600914001, -0.27202484011650085, -0.65672767162323, + -0.7033571600914001, -0.27202484011650085, -0.6567276120185852, -0.7033571600914001, -0.1386767476797104, -0.697178304195404, -0.7033571004867554, + -0.27202484011650085, -0.6567276120185852, -0.7033571600914001, -0.27202484011650085, -0.65672767162323, -0.7033571600914001, -0.27202484011650085, + -0.65672767162323, -0.7033571600914001, -0.1386767327785492, -0.6971782445907593, -0.7033571004867554, -0.1386767476797104, -0.697178304195404, + -0.7033571004867554, 6.514948722724512e-7, -0.7108367085456848, -0.7033571004867554, -0.1386767476797104, -0.697178304195404, -0.7033571004867554, + -0.1386767327785492, -0.6971782445907593, -0.7033571004867554, -0.1386767327785492, -0.6971782445907593, -0.7033571004867554, 6.536043883897946e-7, + -0.71083664894104, -0.7033571004867554, 6.514948722724512e-7, -0.7108367085456848, -0.7033571004867554, 0.1386781930923462, -0.6971779465675354, + -0.7033571004867554, 6.514948722724512e-7, -0.7108367085456848, -0.7033571004867554, 6.536043883897946e-7, -0.71083664894104, -0.7033571004867554, + 6.536043883897946e-7, -0.71083664894104, -0.7033571004867554, 0.1386781930923462, -0.6971779465675354, -0.7033571004867554, 0.1386781930923462, + -0.6971779465675354, -0.7033571004867554, 0.2720262110233307, -0.6567271947860718, -0.7033570408821106, 0.1386781930923462, -0.6971779465675354, + -0.7033571004867554, 0.1386781930923462, -0.6971779465675354, -0.7033571004867554, 0.1386781930923462, -0.6971779465675354, -0.7033571004867554, + 0.2720262408256531, -0.6567271947860718, -0.7033570408821106, 0.2720262110233307, -0.6567271947860718, -0.7033570408821106, 0.3949204683303833, + -0.591038703918457, -0.7033569812774658, 0.2720262110233307, -0.6567271947860718, -0.7033570408821106, 0.2720262408256531, -0.6567271947860718, + -0.7033570408821106, 0.2720262408256531, -0.6567271947860718, -0.7033570408821106, 0.3949204683303833, -0.591038703918457, -0.7033569812774658, + 0.3949204683303833, -0.591038703918457, -0.7033569812774658, 0.5026381015777588, -0.5026369094848633, -0.7033569812774658, 0.3949204683303833, + -0.591038703918457, -0.7033569812774658, 0.3949204683303833, -0.591038703918457, -0.7033569812774658, 0.3949204683303833, -0.591038703918457, + -0.7033569812774658, 0.502638041973114, -0.5026369094848633, -0.7033570408821106, 0.5026381015777588, -0.5026369094848633, -0.7033569812774658, + 0.5910396575927734, -0.39491894841194153, -0.7033571004867554, 0.5026381015777588, -0.5026369094848633, -0.7033569812774658, 0.502638041973114, + -0.5026369094848633, -0.7033570408821106, 0.502638041973114, -0.5026369094848633, -0.7033570408821106, 0.5910395979881287, -0.3949189782142639, + -0.7033571004867554, 0.5910396575927734, -0.39491894841194153, -0.7033571004867554, 0.6567279100418091, -0.2720244228839874, -0.7033571004867554, + 0.5910396575927734, -0.39491894841194153, -0.7033571004867554, 0.5910395979881287, -0.3949189782142639, -0.7033571004867554, 0.5910395979881287, + -0.3949189782142639, -0.7033571004867554, 0.6567278504371643, -0.27202436327934265, -0.7033571004867554, 0.6567279100418091, -0.2720244228839874, + -0.7033571004867554, 0.697178304195404, -0.13867662847042084, -0.7033571004867554, 0.6567279100418091, -0.2720244228839874, -0.7033571004867554, + 0.6567278504371643, -0.27202436327934265, -0.7033571004867554, 0.6567278504371643, -0.27202436327934265, -0.7033571004867554, 0.697178304195404, + -0.13867664337158203, -0.7033571004867554, 0.697178304195404, -0.13867662847042084, -0.7033571004867554, 0.7108367085456848, 3.257474361362256e-7, + -0.7033571004867554, 0.697178304195404, -0.13867662847042084, -0.7033571004867554, 0.697178304195404, -0.13867664337158203, -0.7033571004867554, + 0.697178304195404, -0.13867664337158203, -0.7033571004867554, 0.71083664894104, 3.6210147413839877e-7, -0.7033571600914001, 0.7108367085456848, + 3.257474361362256e-7, -0.7033571004867554, -0.6971782445907593, -0.1386774629354477, 0.7033569812774658, -0.6971781849861145, -0.1386774629354477, + 0.7033570408821106, -0.7108367085456848, -1.1159099955193597e-7, 0.7033570408821106, -0.7108367085456848, -1.1159099955193597e-7, 0.7033570408821106, + -0.7108367085456848, -9.200498851669181e-8, 0.7033570408821106, -0.6971782445907593, -0.1386774629354477, 0.7033569812774658, -0.6567275524139404, + -0.27202561497688293, 0.7033569812774658, -0.6567274928092957, -0.27202558517456055, 0.7033569812774658, -0.6971781849861145, -0.1386774629354477, + 0.7033570408821106, -0.6971781849861145, -0.1386774629354477, 0.7033570408821106, -0.6971782445907593, -0.1386774629354477, 0.7033569812774658, + -0.6567275524139404, -0.27202561497688293, 0.7033569812774658, -0.5910391807556152, -0.3949199616909027, 0.703356921672821, -0.5910391211509705, + -0.3949199616909027, 0.703356921672821, -0.6567274928092957, -0.27202558517456055, 0.7033569812774658, -0.6567274928092957, -0.27202558517456055, + 0.7033569812774658, -0.6567275524139404, -0.27202561497688293, 0.7033569812774658, -0.5910391807556152, -0.3949199616909027, 0.703356921672821, + -0.5026376247406006, -0.502637505531311, 0.703356921672821, -0.5026376247406006, -0.5026374459266663, 0.703356921672821, -0.5910391211509705, + -0.3949199616909027, 0.703356921672821, -0.5910391211509705, -0.3949199616909027, 0.703356921672821, -0.5910391807556152, -0.3949199616909027, + 0.703356921672821, -0.5026376247406006, -0.502637505531311, 0.703356921672821, -0.3949197232723236, -0.5910391807556152, 0.7033570408821106, + -0.394919753074646, -0.5910391807556152, 0.7033569812774658, -0.5026376247406006, -0.5026374459266663, 0.703356921672821, -0.5026376247406006, + -0.5026374459266663, 0.703356921672821, -0.5026376247406006, -0.502637505531311, 0.703356921672821, -0.3949197232723236, -0.5910391807556152, + 0.7033570408821106, -0.27202528715133667, -0.65672767162323, 0.7033569812774658, -0.27202528715133667, -0.65672767162323, 0.703356921672821, + -0.394919753074646, -0.5910391807556152, 0.7033569812774658, -0.394919753074646, -0.5910391807556152, 0.7033569812774658, -0.3949197232723236, + -0.5910391807556152, 0.7033570408821106, -0.27202528715133667, -0.65672767162323, 0.7033569812774658, -0.13867752254009247, -0.6971781849861145, + 0.7033569812774658, -0.13867753744125366, -0.6971782445907593, 0.7033569812774658, -0.27202528715133667, -0.65672767162323, 0.703356921672821, + -0.27202528715133667, -0.65672767162323, 0.703356921672821, -0.27202528715133667, -0.65672767162323, 0.7033569812774658, -0.13867752254009247, + -0.6971781849861145, 0.7033569812774658, -1.2184446518404002e-7, -0.7108367085456848, 0.7033571004867554, -1.594157055251344e-7, -0.71083664894104, + 0.7033571004867554, -0.13867753744125366, -0.6971782445907593, 0.7033569812774658, -0.13867753744125366, -0.6971782445907593, 0.7033569812774658, + -0.13867752254009247, -0.6971781849861145, 0.7033569812774658, -1.2184446518404002e-7, -0.7108367085456848, 0.7033571004867554, 0.13867734372615814, + -0.6971781253814697, 0.7033571600914001, 0.13867731392383575, -0.6971781253814697, 0.7033571004867554, -1.594157055251344e-7, -0.71083664894104, + 0.7033571004867554, -1.594157055251344e-7, -0.71083664894104, 0.7033571004867554, -1.2184446518404002e-7, -0.7108367085456848, 0.7033571004867554, + 0.13867734372615814, -0.6971781253814697, 0.7033571600914001, 0.2720251977443695, -0.6567274332046509, 0.7033571600914001, 0.27202513813972473, + -0.6567274928092957, 0.7033571600914001, 0.13867731392383575, -0.6971781253814697, 0.7033571004867554, 0.13867731392383575, -0.6971781253814697, + 0.7033571004867554, 0.13867734372615814, -0.6971781253814697, 0.7033571600914001, 0.2720251977443695, -0.6567274332046509, 0.7033571600914001, + 0.3949195146560669, -0.5910390615463257, 0.7033572793006897, 0.3949195146560669, -0.5910390615463257, 0.7033572793006897, 0.27202513813972473, + -0.6567274928092957, 0.7033571600914001, 0.27202513813972473, -0.6567274928092957, 0.7033571600914001, 0.2720251977443695, -0.6567274332046509, + 0.7033571600914001, 0.3949195146560669, -0.5910390615463257, 0.7033572793006897, 0.5026373863220215, -0.5026372671127319, 0.7033572196960449, + 0.5026374459266663, -0.5026372671127319, 0.7033572196960449, 0.3949195146560669, -0.5910390615463257, 0.7033572793006897, 0.3949195146560669, + -0.5910390615463257, 0.7033572793006897, 0.3949195146560669, -0.5910390615463257, 0.7033572793006897, 0.5026373863220215, -0.5026372671127319, + 0.7033572196960449, 0.5910391211509705, -0.3949195444583893, 0.7033572196960449, 0.5910390615463257, -0.39491957426071167, 0.7033572196960449, + 0.5026374459266663, -0.5026372671127319, 0.7033572196960449, 0.5026374459266663, -0.5026372671127319, 0.7033572196960449, 0.5026373863220215, + -0.5026372671127319, 0.7033572196960449, 0.5910391211509705, -0.3949195444583893, 0.7033572196960449, 0.6567274332046509, -0.27202528715133667, + 0.7033571600914001, 0.6567274928092957, -0.27202534675598145, 0.7033571600914001, 0.5910390615463257, -0.39491957426071167, 0.7033572196960449, + 0.5910390615463257, -0.39491957426071167, 0.7033572196960449, 0.5910391211509705, -0.3949195444583893, 0.7033572196960449, 0.6567274332046509, + -0.27202528715133667, 0.7033571600914001, 0.6971781253814697, -0.13867710530757904, 0.7033572196960449, 0.6971781253814697, -0.13867712020874023, + 0.7033572196960449, 0.6567274928092957, -0.27202534675598145, 0.7033571600914001, 0.6567274928092957, -0.27202534675598145, 0.7033571600914001, + 0.6567274332046509, -0.27202528715133667, 0.7033571600914001, 0.6971781253814697, -0.13867710530757904, 0.7033572196960449, 0.7108365893363953, + 1.9644303961285914e-7, 0.7033571600914001, 0.7108365893363953, 1.8674414548058849e-7, 0.7033572196960449, 0.6971781253814697, -0.13867712020874023, + 0.7033572196960449, 0.6971781253814697, -0.13867712020874023, 0.7033572196960449, 0.6971781253814697, -0.13867710530757904, 0.7033572196960449, + 0.7108365893363953, 1.9644303961285914e-7, 0.7033571600914001, 0.6971780061721802, 0.1386774778366089, 0.7033572793006897, 0.6971780061721802, + 0.13867749273777008, 0.7033572793006897, 0.7108365893363953, 1.8674414548058849e-7, 0.7033572196960449, 0.7108365893363953, 1.8674414548058849e-7, + 0.7033572196960449, 0.7108365893363953, 1.9644303961285914e-7, 0.7033571600914001, 0.6971780061721802, 0.1386774778366089, 0.7033572793006897, + 0.656727135181427, 0.2720257043838501, 0.7033573389053345, 0.6567271947860718, 0.2720257341861725, 0.7033572793006897, 0.6971780061721802, + 0.13867749273777008, 0.7033572793006897, 0.6971780061721802, 0.13867749273777008, 0.7033572793006897, 0.6971780061721802, 0.1386774778366089, + 0.7033572793006897, 0.656727135181427, 0.2720257043838501, 0.7033573389053345, 0.5910387635231018, 0.3949199616909027, 0.7033572196960449, + 0.5910387635231018, 0.3949199616909027, 0.7033572793006897, 0.6567271947860718, 0.2720257341861725, 0.7033572793006897, 0.6567271947860718, + 0.2720257341861725, 0.7033572793006897, 0.656727135181427, 0.2720257043838501, 0.7033573389053345, 0.5910387635231018, 0.3949199616909027, + 0.7033572196960449, 0.5026370286941528, 0.5026376247406006, 0.7033572793006897, 0.5026370882987976, 0.5026376843452454, 0.7033572196960449, + 0.5910387635231018, 0.3949199616909027, 0.7033572793006897, 0.5910387635231018, 0.3949199616909027, 0.7033572793006897, 0.5910387635231018, + 0.3949199616909027, 0.7033572196960449, 0.5026370286941528, 0.5026376247406006, 0.7033572793006897, 0.39491918683052063, 0.59103924036026, + 0.7033572196960449, 0.394919216632843, 0.5910392999649048, 0.7033572196960449, 0.5026370882987976, 0.5026376843452454, 0.7033572196960449, + 0.5026370882987976, 0.5026376843452454, 0.7033572196960449, 0.5026370286941528, 0.5026376247406006, 0.7033572793006897, 0.39491918683052063, + 0.59103924036026, 0.7033572196960449, 0.27202481031417847, 0.6567276120185852, 0.7033572196960449, 0.2720247805118561, 0.6567276120185852, + 0.7033572196960449, 0.394919216632843, 0.5910392999649048, 0.7033572196960449, 0.394919216632843, 0.5910392999649048, 0.7033572196960449, + 0.39491918683052063, 0.59103924036026, 0.7033572196960449, 0.27202481031417847, 0.6567276120185852, 0.7033572196960449, 0.1386767327785492, + 0.6971782445907593, 0.7033571600914001, 0.138676717877388, 0.6971781849861145, 0.7033571600914001, 0.2720247805118561, 0.6567276120185852, + 0.7033572196960449, 0.2720247805118561, 0.6567276120185852, 0.7033572196960449, 0.27202481031417847, 0.6567276120185852, 0.7033572196960449, + 0.1386767327785492, 0.6971782445907593, 0.7033571600914001, -6.365750095937983e-7, 0.7108365893363953, 0.7033571600914001, -6.627138304793334e-7, + 0.71083664894104, 0.7033571600914001, 0.138676717877388, 0.6971781849861145, 0.7033571600914001, 0.138676717877388, 0.6971781849861145, + 0.7033571600914001, 0.1386767327785492, 0.6971782445907593, 0.7033571600914001, -6.365750095937983e-7, 0.7108365893363953, 0.7033571600914001, + -0.138678178191185, 0.697178065776825, 0.7033570408821106, -0.13867820799350739, 0.6971779465675354, 0.7033571004867554, -6.627138304793334e-7, + 0.71083664894104, 0.7033571600914001, -6.627138304793334e-7, 0.71083664894104, 0.7033571600914001, -6.365750095937983e-7, 0.7108365893363953, + 0.7033571600914001, -0.138678178191185, 0.697178065776825, 0.7033570408821106, -0.27202627062797546, 0.6567271947860718, 0.7033569812774658, + -0.27202630043029785, 0.6567271947860718, 0.7033570408821106, -0.13867820799350739, 0.6971779465675354, 0.7033571004867554, -0.13867820799350739, + 0.6971779465675354, 0.7033571004867554, -0.138678178191185, 0.697178065776825, 0.7033570408821106, -0.27202627062797546, 0.6567271947860718, + 0.7033569812774658, -0.3949204385280609, 0.5910387635231018, 0.703356921672821, -0.3949204683303833, 0.591038703918457, 0.7033569812774658, + -0.27202630043029785, 0.6567271947860718, 0.7033570408821106, -0.27202630043029785, 0.6567271947860718, 0.7033570408821106, -0.27202627062797546, + 0.6567271947860718, 0.7033569812774658, -0.3949204385280609, 0.5910387635231018, 0.703356921672821, -0.5026381015777588, 0.5026369094848633, + 0.7033569812774658, -0.5026381015777588, 0.5026369094848633, 0.7033569812774658, -0.3949204683303833, 0.591038703918457, 0.7033569812774658, + -0.3949204683303833, 0.591038703918457, 0.7033569812774658, -0.3949204385280609, 0.5910387635231018, 0.703356921672821, -0.5026381015777588, + 0.5026369094848633, 0.7033569812774658, -0.5910396575927734, 0.3949190378189087, 0.7033569812774658, -0.5910396575927734, 0.3949190676212311, + 0.7033569812774658, -0.5026381015777588, 0.5026369094848633, 0.7033569812774658, -0.5026381015777588, 0.5026369094848633, 0.7033569812774658, + -0.5026381015777588, 0.5026369094848633, 0.7033569812774658, -0.5910396575927734, 0.3949190378189087, 0.7033569812774658, -0.6567279696464539, + 0.27202433347702026, 0.7033570408821106, -0.6567279696464539, 0.2720243036746979, 0.7033570408821106, -0.5910396575927734, 0.3949190676212311, + 0.7033569812774658, -0.5910396575927734, 0.3949190676212311, 0.7033569812774658, -0.5910396575927734, 0.3949190378189087, 0.7033569812774658, + -0.6567279696464539, 0.27202433347702026, 0.7033570408821106, -0.6971784234046936, 0.13867659866809845, 0.7033569812774658, -0.6971784234046936, + 0.13867662847042084, 0.703356921672821, -0.6567279696464539, 0.2720243036746979, 0.7033570408821106, -0.6567279696464539, 0.2720243036746979, + 0.7033570408821106, -0.6567279696464539, 0.27202433347702026, 0.7033570408821106, -0.6971784234046936, 0.13867659866809845, 0.7033569812774658, + -0.7108367085456848, -9.200498851669181e-8, 0.7033570408821106, -0.7108367085456848, -1.1159099955193597e-7, 0.7033570408821106, -0.6971784234046936, + 0.13867662847042084, 0.703356921672821, -0.6971784234046936, 0.13867662847042084, 0.703356921672821, -0.6971784234046936, 0.13867659866809845, + 0.7033569812774658, -0.7108367085456848, -9.200498851669181e-8, 0.7033570408821106 + ], + "normalized": false + }, + "uv": { + "itemSize": 2, + "type": "Float32Array", + "array": [ + 0.8906737565994263, 0.9400254487991333, 0.4999995231628418, 0.9400254487991333, 0.4999995231628418, 0.0599745512008667, 0.4999995231628418, + 0.0599745512008667, 0.8906737565994263, 0.0599745512008667, 0.8906737565994263, 0.9400254487991333, 1.2663346529006958, 0.9400254487991333, + 0.8906737565994263, 0.9400254487991333, 0.8906737565994263, 0.0599745512008667, 0.8906737565994263, 0.0599745512008667, 1.2663346529006958, + 0.0599745512008667, 1.2663346529006958, 0.9400254487991333, 1.6125456094741821, 0.9400254487991333, 1.2663346529006958, 0.9400254487991333, + 1.2663346529006958, 0.0599745512008667, 1.2663346529006958, 0.0599745512008667, 1.6125456094741821, 0.0599745512008667, 1.6125456094741821, + 0.9400254487991333, 1.9160020351409912, 0.9400254487991333, 1.6125456094741821, 0.9400254487991333, 1.6125456094741821, 0.0599745512008667, + 1.6125456094741821, 0.0599745512008667, 1.9160020351409912, 0.0599745512008667, 1.9160020351409912, 0.9400254487991333, -0.6125462055206299, + 0.9400254487991333, -0.9160027503967285, 0.9400254487991333, -0.9160027503967285, 0.0599745512008667, -0.9160027503967285, 0.0599745512008667, + -0.6125462055206299, 0.0599745512008667, -0.6125462055206299, 0.9400254487991333, -0.26633548736572266, 0.9400254487991333, -0.6125462055206299, + 0.9400254487991333, -0.6125462055206299, 0.0599745512008667, -0.6125462055206299, 0.0599745512008667, -0.26633548736572266, 0.0599745512008667, + -0.26633548736572266, 0.9400254487991333, 0.10932528972625732, 0.9400254487991333, -0.26633548736572266, 0.9400254487991333, -0.26633548736572266, + 0.0599745512008667, -0.26633548736572266, 0.0599745512008667, 0.10932528972625732, 0.0599745512008667, 0.10932528972625732, 0.9400254487991333, + 0.49999934434890747, 0.9400254487991333, 0.10932528972625732, 0.9400254487991333, 0.10932528972625732, 0.0599745512008667, 0.10932528972625732, + 0.0599745512008667, 0.49999934434890747, 0.0599745512008667, 0.49999934434890747, 0.9400254487991333, 0.8906735181808472, 0.9400254487991333, + 0.49999934434890747, 0.9400254487991333, 0.49999934434890747, 0.0599745512008667, 0.49999934434890747, 0.0599745512008667, 0.8906735181808472, + 0.0599745512008667, 0.8906735181808472, 0.9400254487991333, 1.2663342952728271, 0.9400254487991333, 0.8906735181808472, 0.9400254487991333, + 0.8906735181808472, 0.0599745512008667, 0.8906735181808472, 0.0599745512008667, 1.2663342952728271, 0.0599745512008667, 1.2663342952728271, + 0.9400254487991333, 1.6125454902648926, 0.9400254487991333, 1.2663342952728271, 0.9400254487991333, 1.2663342952728271, 0.0599745512008667, + 1.2663342952728271, 0.0599745512008667, 1.6125454902648926, 0.0599745512008667, 1.6125454902648926, 0.9400254487991333, 1.9160020351409912, + 0.9400254487991333, 1.6125454902648926, 0.9400254487991333, 1.6125454902648926, 0.0599745512008667, 1.6125454902648926, 0.0599745512008667, + 1.9160020351409912, 0.0599745512008667, 1.9160020351409912, 0.9400254487991333, -0.6125462055206299, 0.9400254487991333, -0.9160027503967285, + 0.9400254487991333, -0.9160027503967285, 0.0599745512008667, -0.9160027503967285, 0.0599745512008667, -0.6125462055206299, 0.0599745512008667, + -0.6125462055206299, 0.9400254487991333, -0.266335129737854, 0.9400254487991333, -0.6125462055206299, 0.9400254487991333, -0.6125462055206299, + 0.0599745512008667, -0.6125462055206299, 0.0599745512008667, -0.266335129737854, 0.0599745512008667, -0.266335129737854, 0.9400254487991333, + 0.10932576656341553, 0.9400254487991333, -0.266335129737854, 0.9400254487991333, -0.266335129737854, 0.0599745512008667, -0.266335129737854, + 0.0599745512008667, 0.10932576656341553, 0.0599745512008667, 0.10932576656341553, 0.9400254487991333, 0.5000001788139343, 0.9400254487991333, + 0.10932576656341553, 0.9400254487991333, 0.10932576656341553, 0.0599745512008667, 0.10932576656341553, 0.0599745512008667, 0.5000001788139343, + 0.0599745512008667, 0.5000001788139343, 0.9400254487991333, 0.8906745314598083, 0.9400254487991333, 0.5000001788139343, 0.9400254487991333, + 0.5000001788139343, 0.0599745512008667, 0.5000001788139343, 0.0599745512008667, 0.8906745314598083, 0.0599745512008667, 0.8906745314598083, + 0.9400254487991333, 1.2663354873657227, 0.9400254487991333, 0.8906745314598083, 0.9400254487991333, 0.8906745314598083, 0.0599745512008667, + 0.8906745314598083, 0.0599745512008667, 1.2663354873657227, 0.0599745512008667, 1.2663354873657227, 0.9400254487991333, 1.6125465631484985, + 0.9400254487991333, 1.2663354873657227, 0.9400254487991333, 1.2663354873657227, 0.0599745512008667, 1.2663354873657227, 0.0599745512008667, + 1.6125465631484985, 0.0599745512008667, 1.6125465631484985, 0.9400254487991333, 1.9160029888153076, 0.9400254487991333, 1.6125465631484985, + 0.9400254487991333, 1.6125465631484985, 0.0599745512008667, 1.6125465631484985, 0.0599745512008667, 1.9160029888153076, 0.0599745512008667, + 1.9160029888153076, 0.9400254487991333, -0.6125451326370239, 0.9400254487991333, -0.9160019159317017, 0.9400254487991333, -0.9160019159317017, + 0.0599745512008667, -0.9160019159317017, 0.0599745512008667, -0.6125451326370239, 0.0599745512008667, -0.6125451326370239, 0.9400254487991333, + -0.2663339376449585, 0.9400254487991333, -0.6125451326370239, 0.9400254487991333, -0.6125451326370239, 0.0599745512008667, -0.6125451326370239, + 0.0599745512008667, -0.2663339376449585, 0.0599745512008667, -0.2663339376449585, 0.9400254487991333, 0.1093270480632782, 0.9400254487991333, + -0.2663339376449585, 0.9400254487991333, -0.2663339376449585, 0.0599745512008667, -0.2663339376449585, 0.0599745512008667, 0.1093270480632782, + 0.0599745512008667, 0.1093270480632782, 0.9400254487991333, 0.5000014305114746, 0.9400254487991333, 0.1093270480632782, 0.9400254487991333, + 0.1093270480632782, 0.0599745512008667, 0.1093270480632782, 0.0599745512008667, 0.5000014305114746, 0.0599745512008667, 0.5000014305114746, + 0.9400254487991333, 0.8906757831573486, 0.9400254487991333, 0.5000014305114746, 0.9400254487991333, 0.5000014305114746, 0.0599745512008667, + 0.5000014305114746, 0.0599745512008667, 0.8906757831573486, 0.0599745512008667, 0.8906757831573486, 0.9400254487991333, 1.2663366794586182, + 0.9400254487991333, 0.8906757831573486, 0.9400254487991333, 0.8906757831573486, 0.0599745512008667, 0.8906757831573486, 0.0599745512008667, + 1.2663366794586182, 0.0599745512008667, 1.2663366794586182, 0.9400254487991333, 1.6125476360321045, 0.9400254487991333, 1.2663366794586182, + 0.9400254487991333, 1.2663366794586182, 0.0599745512008667, 1.2663366794586182, 0.0599745512008667, 1.6125476360321045, 0.0599745512008667, + 1.6125476360321045, 0.9400254487991333, 1.9160038232803345, 0.9400254487991333, 1.6125476360321045, 0.9400254487991333, 1.6125476360321045, + 0.0599745512008667, 1.6125476360321045, 0.0599745512008667, 1.9160038232803345, 0.0599745512008667, 1.9160038232803345, 0.9400254487991333, + -0.612544059753418, 0.9400254487991333, -0.9160009622573853, 0.9400254487991333, -0.9160009622573853, 0.0599745512008667, -0.9160009622573853, + 0.0599745512008667, -0.612544059753418, 0.0599745512008667, -0.612544059753418, 0.9400254487991333, -0.266332745552063, 0.9400254487991333, + -0.612544059753418, 0.9400254487991333, -0.612544059753418, 0.0599745512008667, -0.612544059753418, 0.0599745512008667, -0.266332745552063, + 0.0599745512008667, -0.266332745552063, 0.9400254487991333, 0.10932832956314087, 0.9400254487991333, -0.266332745552063, 0.9400254487991333, + -0.266332745552063, 0.0599745512008667, -0.266332745552063, 0.0599745512008667, 0.10932832956314087, 0.0599745512008667, 0.10932832956314087, + 0.9400254487991333, 0.4999995231628418, 0.9400254487991333, 0.10932832956314087, 0.9400254487991333, 0.10932832956314087, 0.0599745512008667, + 0.10932832956314087, 0.0599745512008667, 0.4999995231628418, 0.0599745512008667, 0.4999995231628418, 0.9400254487991333, 0.8906737565994263, + 0.9400254487991333, 0.8906737565994263, 0.0599745512008667, 0.4999995231628418, 0.0599745512008667, 0.4999995231628418, 0.0599745512008667, + 0.4999995231628418, 0.9400254487991333, 0.8906737565994263, 0.9400254487991333, 1.2663346529006958, 0.9400254487991333, 1.2663346529006958, + 0.0599745512008667, 0.8906737565994263, 0.0599745512008667, 0.8906737565994263, 0.0599745512008667, 0.8906737565994263, 0.9400254487991333, + 1.2663346529006958, 0.9400254487991333, 1.6125456094741821, 0.9400254487991333, 1.6125456094741821, 0.0599745512008667, 1.2663346529006958, + 0.0599745512008667, 1.2663346529006958, 0.0599745512008667, 1.2663346529006958, 0.9400254487991333, 1.6125456094741821, 0.9400254487991333, + 1.9160020351409912, 0.9400254487991333, 1.9160020351409912, 0.0599745512008667, 1.6125456094741821, 0.0599745512008667, 1.6125456094741821, + 0.0599745512008667, 1.6125456094741821, 0.9400254487991333, 1.9160020351409912, 0.9400254487991333, -0.6125462055206299, 0.9400254487991333, + -0.6125462055206299, 0.0599745512008667, -0.9160027503967285, 0.0599745512008667, -0.9160027503967285, 0.0599745512008667, -0.9160027503967285, + 0.9400254487991333, -0.6125462055206299, 0.9400254487991333, -0.26633548736572266, 0.9400254487991333, -0.26633548736572266, 0.0599745512008667, + -0.6125462055206299, 0.0599745512008667, -0.6125462055206299, 0.0599745512008667, -0.6125462055206299, 0.9400254487991333, -0.26633548736572266, + 0.9400254487991333, 0.10932528972625732, 0.9400254487991333, 0.10932528972625732, 0.0599745512008667, -0.26633548736572266, 0.0599745512008667, + -0.26633548736572266, 0.0599745512008667, -0.26633548736572266, 0.9400254487991333, 0.10932528972625732, 0.9400254487991333, 0.49999934434890747, + 0.9400254487991333, 0.49999934434890747, 0.0599745512008667, 0.10932528972625732, 0.0599745512008667, 0.10932528972625732, 0.0599745512008667, + 0.10932528972625732, 0.9400254487991333, 0.49999934434890747, 0.9400254487991333, 0.8906735181808472, 0.9400254487991333, 0.8906735181808472, + 0.0599745512008667, 0.49999934434890747, 0.0599745512008667, 0.49999934434890747, 0.0599745512008667, 0.49999934434890747, 0.9400254487991333, + 0.8906735181808472, 0.9400254487991333, 1.2663342952728271, 0.9400254487991333, 1.2663342952728271, 0.0599745512008667, 0.8906735181808472, + 0.0599745512008667, 0.8906735181808472, 0.0599745512008667, 0.8906735181808472, 0.9400254487991333, 1.2663342952728271, 0.9400254487991333, + 1.6125454902648926, 0.9400254487991333, 1.6125454902648926, 0.0599745512008667, 1.2663342952728271, 0.0599745512008667, 1.2663342952728271, + 0.0599745512008667, 1.2663342952728271, 0.9400254487991333, 1.6125454902648926, 0.9400254487991333, 1.9160020351409912, 0.9400254487991333, + 1.9160020351409912, 0.0599745512008667, 1.6125454902648926, 0.0599745512008667, 1.6125454902648926, 0.0599745512008667, 1.6125454902648926, + 0.9400254487991333, 1.9160020351409912, 0.9400254487991333, -0.6125462055206299, 0.9400254487991333, -0.6125462055206299, 0.0599745512008667, + -0.9160027503967285, 0.0599745512008667, -0.9160027503967285, 0.0599745512008667, -0.9160027503967285, 0.9400254487991333, -0.6125462055206299, + 0.9400254487991333, -0.266335129737854, 0.9400254487991333, -0.266335129737854, 0.0599745512008667, -0.6125462055206299, 0.0599745512008667, + -0.6125462055206299, 0.0599745512008667, -0.6125462055206299, 0.9400254487991333, -0.266335129737854, 0.9400254487991333, 0.10932576656341553, + 0.9400254487991333, 0.10932576656341553, 0.0599745512008667, -0.266335129737854, 0.0599745512008667, -0.266335129737854, 0.0599745512008667, + -0.266335129737854, 0.9400254487991333, 0.10932576656341553, 0.9400254487991333, 0.5000001788139343, 0.9400254487991333, 0.5000001788139343, + 0.0599745512008667, 0.10932576656341553, 0.0599745512008667, 0.10932576656341553, 0.0599745512008667, 0.10932576656341553, 0.9400254487991333, + 0.5000001788139343, 0.9400254487991333, 0.8906745314598083, 0.9400254487991333, 0.8906745314598083, 0.0599745512008667, 0.5000001788139343, + 0.0599745512008667, 0.5000001788139343, 0.0599745512008667, 0.5000001788139343, 0.9400254487991333, 0.8906745314598083, 0.9400254487991333, + 1.2663354873657227, 0.9400254487991333, 1.2663354873657227, 0.0599745512008667, 0.8906745314598083, 0.0599745512008667, 0.8906745314598083, + 0.0599745512008667, 0.8906745314598083, 0.9400254487991333, 1.2663354873657227, 0.9400254487991333, 1.6125465631484985, 0.9400254487991333, + 1.6125465631484985, 0.0599745512008667, 1.2663354873657227, 0.0599745512008667, 1.2663354873657227, 0.0599745512008667, 1.2663354873657227, + 0.9400254487991333, 1.6125465631484985, 0.9400254487991333, 1.9160029888153076, 0.9400254487991333, 1.9160029888153076, 0.0599745512008667, + 1.6125465631484985, 0.0599745512008667, 1.6125465631484985, 0.0599745512008667, 1.6125465631484985, 0.9400254487991333, 1.9160029888153076, + 0.9400254487991333, -0.6125451326370239, 0.9400254487991333, -0.6125451326370239, 0.0599745512008667, -0.9160019159317017, 0.0599745512008667, + -0.9160019159317017, 0.0599745512008667, -0.9160019159317017, 0.9400254487991333, -0.6125451326370239, 0.9400254487991333, -0.2663339376449585, + 0.9400254487991333, -0.2663339376449585, 0.0599745512008667, -0.6125451326370239, 0.0599745512008667, -0.6125451326370239, 0.0599745512008667, + -0.6125451326370239, 0.9400254487991333, -0.2663339376449585, 0.9400254487991333, 0.1093270480632782, 0.9400254487991333, 0.1093270480632782, + 0.0599745512008667, -0.2663339376449585, 0.0599745512008667, -0.2663339376449585, 0.0599745512008667, -0.2663339376449585, 0.9400254487991333, + 0.1093270480632782, 0.9400254487991333, 0.5000014305114746, 0.9400254487991333, 0.5000014305114746, 0.0599745512008667, 0.1093270480632782, + 0.0599745512008667, 0.1093270480632782, 0.0599745512008667, 0.1093270480632782, 0.9400254487991333, 0.5000014305114746, 0.9400254487991333, + 0.8906757831573486, 0.9400254487991333, 0.8906757831573486, 0.0599745512008667, 0.5000014305114746, 0.0599745512008667, 0.5000014305114746, + 0.0599745512008667, 0.5000014305114746, 0.9400254487991333, 0.8906757831573486, 0.9400254487991333, 1.2663366794586182, 0.9400254487991333, + 1.2663366794586182, 0.0599745512008667, 0.8906757831573486, 0.0599745512008667, 0.8906757831573486, 0.0599745512008667, 0.8906757831573486, + 0.9400254487991333, 1.2663366794586182, 0.9400254487991333, 1.6125476360321045, 0.9400254487991333, 1.6125476360321045, 0.0599745512008667, + 1.2663366794586182, 0.0599745512008667, 1.2663366794586182, 0.0599745512008667, 1.2663366794586182, 0.9400254487991333, 1.6125476360321045, + 0.9400254487991333, 1.9160038232803345, 0.9400254487991333, 1.9160038232803345, 0.0599745512008667, 1.6125476360321045, 0.0599745512008667, + 1.6125476360321045, 0.0599745512008667, 1.6125476360321045, 0.9400254487991333, 1.9160038232803345, 0.9400254487991333, -0.612544059753418, + 0.9400254487991333, -0.612544059753418, 0.0599745512008667, -0.9160009622573853, 0.0599745512008667, -0.9160009622573853, 0.0599745512008667, + -0.9160009622573853, 0.9400254487991333, -0.612544059753418, 0.9400254487991333, -0.266332745552063, 0.9400254487991333, -0.266332745552063, + 0.0599745512008667, -0.612544059753418, 0.0599745512008667, -0.612544059753418, 0.0599745512008667, -0.612544059753418, 0.9400254487991333, + -0.266332745552063, 0.9400254487991333, 0.10932832956314087, 0.9400254487991333, 0.10932832956314087, 0.0599745512008667, -0.266332745552063, + 0.0599745512008667, -0.266332745552063, 0.0599745512008667, -0.266332745552063, 0.9400254487991333, 0.10932832956314087, 0.9400254487991333, + 0.4999995231628418, 0.9400254487991333, 0.4999995231628418, 0.0599745512008667, 0.10932832956314087, 0.0599745512008667, 0.10932832956314087, + 0.0599745512008667, 0.10932832956314087, 0.9400254487991333, 0.4999995231628418, 0.9400254487991333 + ], + "normalized": false + } + } + } + } + ], + "materials": [ + { + "uuid": "769df3ee-4567-40b7-8da4-473fb149f350", + "type": "MeshBasicMaterial", + "color": 16777215, + "map": "3874a02e-6d61-4cbb-8379-9c1436361bb4", + "envMapRotation": [0, 0, 0, "XYZ"], + "reflectivity": 1, + "refractionRatio": 0.98, + "blending": 2, + "side": 2, + "transparent": true, + "blendColor": 0, + "depthWrite": false + }, + { + "uuid": "6d9283b7-81c2-4063-84cc-f696054ce6f6", + "type": "MeshBasicMaterial", + "color": 16777215, + "map": "93d77365-4fc6-43a7-b19a-e2fb18ab38d0", + "envMapRotation": [0, 0, 0, "XYZ"], + "reflectivity": 1, + "refractionRatio": 0.98, + "blending": 2, + "side": 2, + "transparent": true, + "blendColor": 0, + "depthWrite": false + }, + { + "uuid": "7442c205-fb42-4fb9-baec-82a192b81351", + "type": "MeshBasicMaterial", + "color": 16777215, + "map": "65e3d0e9-bf33-48b4-a8a1-5bc966d46e4e", + "envMapRotation": [0, 0, 0, "XYZ"], + "reflectivity": 1, + "refractionRatio": 0.98, + "blending": 2, + "side": 2, + "transparent": true, + "blendColor": 0, + "depthWrite": false + } + ], + "textures": [ + { + "uuid": "3874a02e-6d61-4cbb-8379-9c1436361bb4", + "name": "GroundGlowEmitter_texture", + "image": "396bc86c-4059-45f7-b34f-f6228436b397", + "mapping": 300, + "channel": 0, + "repeat": [1, 1], + "offset": [0, 0], + "center": [0, 0], + "rotation": 0, + "wrap": [1001, 1001], + "format": 1023, + "internalFormat": null, + "type": 1009, + "colorSpace": "", + "minFilter": 1008, + "magFilter": 1006, + "anisotropy": 1, + "flipY": true, + "generateMipmaps": true, + "premultiplyAlpha": false, + "unpackAlignment": 4 + }, + { + "uuid": "93d77365-4fc6-43a7-b19a-e2fb18ab38d0", + "name": "GlowCircleEmitter_texture", + "image": "55a3bc1f-853b-4d4a-bbe1-77abe1ccb390", + "mapping": 300, + "channel": 0, + "repeat": [1, 1], + "offset": [0, 0], + "center": [0, 0], + "rotation": 0, + "wrap": [1001, 1001], + "format": 1023, + "internalFormat": null, + "type": 1009, + "colorSpace": "", + "minFilter": 1008, + "magFilter": 1006, + "anisotropy": 1, + "flipY": true, + "generateMipmaps": true, + "premultiplyAlpha": false, + "unpackAlignment": 4 + }, + { + "uuid": "65e3d0e9-bf33-48b4-a8a1-5bc966d46e4e", + "name": "BasicZoneBlueEmitter_texture", + "image": "a44aaf69-213b-4f68-96fc-304a19e9cdae", + "mapping": 300, + "channel": 0, + "repeat": [1, 1], + "offset": [0, 0], + "center": [0, 0], + "rotation": 0, + "wrap": [1001, 1001], + "format": 1023, + "internalFormat": null, + "type": 1009, + "colorSpace": "", + "minFilter": 1008, + "magFilter": 1006, + "anisotropy": 1, + "flipY": true, + "generateMipmaps": true, + "premultiplyAlpha": false, + "unpackAlignment": 4 + } + ], + "images": [ + { + "uuid": "396bc86c-4059-45f7-b34f-f6228436b397", + "url": "data:image/jpeg;base64,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" + }, + { + "uuid": "55a3bc1f-853b-4d4a-bbe1-77abe1ccb390", + "url": "data:image/jpeg;base64,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" + }, + { + "uuid": "a44aaf69-213b-4f68-96fc-304a19e9cdae", + "url": "data:image/jpeg;base64,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" + } + ], + "object": { + "uuid": "da66c047-c0da-4a53-90dd-589c4e53e868", + "type": "Group", + "name": "MagicZoneGreen", + "visible": true, + "layers": 1, + "matrix": [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1], + "up": [0, 1, 0], + "children": [ + { + "uuid": "1921b779-fac1-42ec-b40a-a1af729bf6fa", + "type": "Group", + "name": "PortalDust", + "layers": 1, + "matrix": [ + -1.0000003044148624, 7.619931978698374e-8, 1.2979021821838232e-8, 0, -1.2979033855407715e-8, -1.8384778810981241e-7, -1.0000001522074553, 0, + -7.619930580271816e-8, -1.0000001522073967, 1.8384778922002538e-7, 0, 0, 0, 0, 1 + ], + "up": [0, 1, 0], + "children": [ + { + "uuid": "9ec4a1d9-3f3d-49a6-85fc-577cef6084a2", + "type": "ParticleEmitter", + "name": "PortalDustEmitter", + "layers": 1, + "matrix": [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1], + "up": [0, 1, 0], + "ps": { + "version": "3.0", + "autoDestroy": false, + "looping": true, + "prewarm": false, + "duration": 5, + "shape": { + "type": "cone", + "radius": 1.21, + "arc": 6.283185307179586, + "thickness": 0, + "angle": 0, + "mode": 0, + "spread": 0, + "speed": { "type": "ConstantValue", "value": 1 } + }, + "startLife": { "type": "IntervalValue", "a": 1, "b": 1.5 }, + "startSpeed": { "type": "ConstantValue", "value": -1 }, + "startRotation": { "type": "IntervalValue", "a": 0, "b": 6.283185 }, + "startSize": { "type": "IntervalValue", "a": 0.15, "b": 0.2 }, + "startColor": { "type": "ConstantColor", "color": { "r": 1, "g": 1, "b": 1, "a": 1 } }, + "emissionOverTime": { "type": "ConstantValue", "value": 25 }, + "emissionOverDistance": { "type": "ConstantValue", "value": 0 }, + "emissionBursts": [], + "onlyUsedByOther": false, + "instancingGeometry": "780917d8-bd1b-4d63-8aca-f79e3211f964", + "renderOrder": 0, + "renderMode": 0, + "rendererEmitterSettings": {}, + "material": "769df3ee-4567-40b7-8da4-473fb149f350", + "layers": 1, + "startTileIndex": { "type": "ConstantValue", "value": 0 }, + "uTileCount": 1, + "vTileCount": 1, + "blendTiles": false, + "softParticles": false, + "softFarFade": 0, + "softNearFade": 0, + "behaviors": [ + { + "type": "ForceOverLife", + "x": { "type": "ConstantValue", "value": 0 }, + "y": { "type": "ConstantValue", "value": 0 }, + "z": { "type": "ConstantValue", "value": 0 } + }, + { + "type": "SizeOverLife", + "size": { + "type": "PiecewiseBezier", + "functions": [ + { "function": { "p0": 0.8495575, "p1": 0.8495575, "p2": 1, "p3": 1 }, "start": 0 }, + { "function": { "p0": 1, "p1": 1, "p2": 0, "p3": 0 }, "start": 0.49871457 } + ] + } + }, + { "type": "RotationOverLife", "angularVelocity": { "type": "IntervalValue", "a": -3.1415925, "b": 3.1415925 } }, + { + "type": "ColorOverLife", + "color": { + "type": "Gradient", + "color": { + "type": "CLinearFunction", + "subType": "Color", + "keys": [ + { "value": { "r": 1, "g": 1, "b": 1 }, "pos": 0 }, + { "value": { "r": 0.59607846, "g": 1, "b": 0.050980393 }, "pos": 0.4587167162584878 }, + { "value": { "r": 0, "g": 1, "b": 0.047058824 }, "pos": 0.9518272678721293 } + ] + }, + "alpha": { + "type": "CLinearFunction", + "subType": "Number", + "keys": [ + { "value": 0, "pos": 0 }, + { "value": 1, "pos": 0.41690699626153965 }, + { "value": 1, "pos": 0.7580224307621881 }, + { "value": 0, "pos": 1 } + ] + } + } + } + ], + "worldSpace": true + } + } + ] + }, + { + "uuid": "cfe42db9-925f-4bd2-bc92-3d15a4e2b795", + "type": "Group", + "name": "GlowCircle", + "layers": 1, + "matrix": [1, 0, 0, 0, 0, -5.321248014494817e-8, -1.0000000532124802, 0, 0, 1.0000000532124802, -5.321248014494817e-8, 0, 0, 0, 0, 1], + "up": [0, 1, 0], + "children": [ + { + "uuid": "94cac5fe-52a9-431d-ba8a-19fe44a2cdc1", + "type": "ParticleEmitter", + "name": "GlowCircleEmitter", + "layers": 1, + "matrix": [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1], + "up": [0, 1, 0], + "ps": { + "version": "3.0", + "autoDestroy": false, + "looping": true, + "prewarm": false, + "duration": 2, + "shape": { + "type": "cone", + "radius": 0.01, + "arc": 6.283185307179586, + "thickness": 1, + "angle": 0.06981317007977318, + "mode": 0, + "spread": 0, + "speed": { "type": "ConstantValue", "value": 1 } + }, + "startLife": { "type": "ConstantValue", "value": 2 }, + "startSpeed": { "type": "ConstantValue", "value": 0 }, + "startRotation": { + "type": "Euler", + "angleX": { "type": "IntervalValue", "a": 0, "b": 0 }, + "angleY": { "type": "IntervalValue", "a": 0, "b": 0 }, + "angleZ": { "type": "IntervalValue", "a": 0, "b": 6.283185 }, + "eulerOrder": "XYZ" + }, + "startSize": { "type": "ConstantValue", "value": 4.1 }, + "startColor": { "type": "ConstantColor", "color": { "r": 0.45882353, "g": 1, "b": 0.28627452, "a": 0.4509804 } }, + "emissionOverTime": { "type": "ConstantValue", "value": 1 }, + "emissionOverDistance": { "type": "ConstantValue", "value": 0 }, + "emissionBursts": [], + "onlyUsedByOther": false, + "instancingGeometry": "f40b6ee0-aa01-46e0-b05a-d938b54eec83", + "renderOrder": 0, + "renderMode": 2, + "rendererEmitterSettings": {}, + "material": "6d9283b7-81c2-4063-84cc-f696054ce6f6", + "layers": 1, + "startTileIndex": { "type": "ConstantValue", "value": 0 }, + "uTileCount": 1, + "vTileCount": 1, + "blendTiles": false, + "softParticles": false, + "softFarFade": 0, + "softNearFade": 0, + "behaviors": [ + { + "type": "ForceOverLife", + "x": { "type": "ConstantValue", "value": 0 }, + "y": { "type": "ConstantValue", "value": 0 }, + "z": { "type": "ConstantValue", "value": 0 } + }, + { + "type": "ColorOverLife", + "color": { + "type": "Gradient", + "color": { + "type": "CLinearFunction", + "subType": "Color", + "keys": [ + { "value": { "r": 1, "g": 1, "b": 1 }, "pos": 0 }, + { "value": { "r": 1, "g": 1, "b": 1 }, "pos": 1 } + ] + }, + "alpha": { + "type": "CLinearFunction", + "subType": "Number", + "keys": [ + { "value": 0, "pos": 0 }, + { "value": 1, "pos": 0.5014572365911345 }, + { "value": 0, "pos": 1 } + ] + } + } + } + ], + "worldSpace": true + } + } + ] + }, + { + "uuid": "c86a5eb7-2571-4e87-b5fd-e68a0f965b0a", + "type": "ParticleEmitter", + "name": "MagicZoneGreenEmitter", + "layers": 1, + "matrix": [1, 0, 0, 0, 0, -2.220446049250313e-16, -1, 0, 0, 1, -2.220446049250313e-16, 0, 0, 0, 0, 1], + "up": [0, 1, 0], + "ps": { + "version": "3.0", + "autoDestroy": false, + "looping": true, + "prewarm": false, + "duration": 5, + "shape": { "type": "point" }, + "startLife": { "type": "ConstantValue", "value": 1 }, + "startSpeed": { "type": "ConstantValue", "value": 0 }, + "startRotation": { + "type": "Euler", + "angleX": { "type": "IntervalValue", "a": 1.5707963, "b": 1.5707963 }, + "angleY": { "type": "IntervalValue", "a": 0, "b": 6.283185 }, + "angleZ": { "type": "IntervalValue", "a": 0, "b": 0 }, + "eulerOrder": "XYZ" + }, + "startSize": { "type": "ConstantValue", "value": 2.8 }, + "startColor": { "type": "ConstantColor", "color": { "r": 1, "g": 1, "b": 1, "a": 1 } }, + "emissionOverTime": { "type": "ConstantValue", "value": 2.5 }, + "emissionOverDistance": { "type": "ConstantValue", "value": 0 }, + "emissionBursts": [], + "onlyUsedByOther": false, + "instancingGeometry": "780917d8-bd1b-4d63-8aca-f79e3211f964", + "renderOrder": 0, + "renderMode": 2, + "rendererEmitterSettings": {}, + "material": "7442c205-fb42-4fb9-baec-82a192b81351", + "layers": 1, + "startTileIndex": { "type": "ConstantValue", "value": 0 }, + "uTileCount": 1, + "vTileCount": 1, + "blendTiles": false, + "softParticles": false, + "softFarFade": 0, + "softNearFade": 0, + "behaviors": [ + { + "type": "ForceOverLife", + "x": { "type": "ConstantValue", "value": 0 }, + "y": { "type": "ConstantValue", "value": 0 }, + "z": { "type": "ConstantValue", "value": 0 } + }, + { + "type": "ColorOverLife", + "color": { + "type": "Gradient", + "color": { + "type": "CLinearFunction", + "subType": "Color", + "keys": [ + { "value": { "r": 0.6156863, "g": 1, "b": 0 }, "pos": 0 }, + { "value": { "r": 0.101960786, "g": 1, "b": 0.10980392 }, "pos": 1 } + ] + }, + "alpha": { + "type": "CLinearFunction", + "subType": "Number", + "keys": [ + { "value": 0, "pos": 0.004592965590905623 }, + { "value": 1, "pos": 0.5014572365911345 }, + { "value": 0, "pos": 1 } + ] + } + } + } + ], + "worldSpace": true + } + } + ] + } +} diff --git a/editor/src/editor/windows/fx-editor/VFX/types/VFXBehaviorFunction.ts b/editor/src/editor/windows/fx-editor/VFX/types/VFXBehaviorFunction.ts new file mode 100644 index 000000000..e7115e73b --- /dev/null +++ b/editor/src/editor/windows/fx-editor/VFX/types/VFXBehaviorFunction.ts @@ -0,0 +1,32 @@ +import type { Particle } from "../../particle"; +import type { SolidParticle } from "../../solidParticle"; +import type { ParticleSystem } from "../../particleSystem"; +import type { VFXValueParser } from "../parsers/VFXValueParser"; + +/** + * Context for per-particle behavior functions + */ +export interface VFXPerParticleContext { + lifeRatio: number; + startSpeed: number; + startSize: number; + startColor: { r: number; g: number; b: number; a: number }; + updateSpeed: number; + valueParser: VFXValueParser; +} + +/** + * Per-particle behavior function for ParticleSystem + */ +export type VFXPerParticleBehaviorFunction = (particle: Particle, context: VFXPerParticleContext) => void; + +/** + * Per-particle behavior function for SolidParticleSystem + */ +export type VFXPerSolidParticleBehaviorFunction = (particle: SolidParticle, context: VFXPerParticleContext) => void; + +/** + * System-level behavior function (applied once during initialization) + */ +export type VFXSystemBehaviorFunction = (particleSystem: ParticleSystem, valueParser: VFXValueParser) => void; + diff --git a/editor/src/editor/windows/fx-editor/VFX/types/behaviors.ts b/editor/src/editor/windows/fx-editor/VFX/types/behaviors.ts new file mode 100644 index 000000000..227d08b90 --- /dev/null +++ b/editor/src/editor/windows/fx-editor/VFX/types/behaviors.ts @@ -0,0 +1,138 @@ +import type { VFXValue } from "./values"; +import type { VFXGradientKey } from "./gradients"; + +/** + * VFX behavior types (converted from Quarks) + */ +export interface VFXColorOverLifeBehavior { + type: "ColorOverLife"; + color?: { + color?: { + keys: VFXGradientKey[]; + }; + alpha?: { + keys: VFXGradientKey[]; + }; + keys?: VFXGradientKey[]; + }; +} + +export interface VFXSizeOverLifeBehavior { + type: "SizeOverLife"; + size?: { + keys?: VFXGradientKey[]; + functions?: Array<{ + start: number; + function: { + p0?: number; + p3?: number; + }; + }>; + }; +} + +export interface VFXRotationOverLifeBehavior { + type: "RotationOverLife" | "Rotation3DOverLife"; + angularVelocity?: VFXValue; +} + +export interface VFXForceOverLifeBehavior { + type: "ForceOverLife" | "ApplyForce"; + force?: { + x?: VFXValue; + y?: VFXValue; + z?: VFXValue; + }; + x?: VFXValue; + y?: VFXValue; + z?: VFXValue; +} + +export interface VFXGravityForceBehavior { + type: "GravityForce"; + gravity?: VFXValue; +} + +export interface VFXSpeedOverLifeBehavior { + type: "SpeedOverLife"; + speed?: + | { + keys?: VFXGradientKey[]; + functions?: Array<{ + start: number; + function: { + p0?: number; + p3?: number; + }; + }>; + } + | VFXValue; +} + +export interface VFXFrameOverLifeBehavior { + type: "FrameOverLife"; + frame?: + | { + keys?: VFXGradientKey[]; + } + | VFXValue; +} + +export interface VFXLimitSpeedOverLifeBehavior { + type: "LimitSpeedOverLife"; + maxSpeed?: VFXValue; + speed?: VFXValue | { keys?: VFXGradientKey[] }; + dampen?: VFXValue; +} + +export interface VFXColorBySpeedBehavior { + type: "ColorBySpeed"; + color?: { + keys: VFXGradientKey[]; + }; + minSpeed?: VFXValue; + maxSpeed?: VFXValue; +} + +export interface VFXSizeBySpeedBehavior { + type: "SizeBySpeed"; + size?: { + keys: VFXGradientKey[]; + }; + minSpeed?: VFXValue; + maxSpeed?: VFXValue; +} + +export interface VFXRotationBySpeedBehavior { + type: "RotationBySpeed"; + angularVelocity?: VFXValue; + minSpeed?: VFXValue; + maxSpeed?: VFXValue; +} + +export interface VFXOrbitOverLifeBehavior { + type: "OrbitOverLife"; + center?: { + x?: number; + y?: number; + z?: number; + }; + radius?: VFXValue | { keys?: VFXGradientKey[] }; + speed?: VFXValue; +} + +export type VFXBehavior = + | VFXColorOverLifeBehavior + | VFXSizeOverLifeBehavior + | VFXRotationOverLifeBehavior + | VFXForceOverLifeBehavior + | VFXGravityForceBehavior + | VFXSpeedOverLifeBehavior + | VFXFrameOverLifeBehavior + | VFXLimitSpeedOverLifeBehavior + | VFXColorBySpeedBehavior + | VFXSizeBySpeedBehavior + | VFXRotationBySpeedBehavior + | VFXOrbitOverLifeBehavior + | { type: string; [key: string]: unknown }; // Fallback for unknown behaviors + diff --git a/editor/src/editor/windows/fx-editor/VFX/types/colors.ts b/editor/src/editor/windows/fx-editor/VFX/types/colors.ts new file mode 100644 index 000000000..4ff7aae20 --- /dev/null +++ b/editor/src/editor/windows/fx-editor/VFX/types/colors.ts @@ -0,0 +1,10 @@ +/** + * VFX color types (converted from Quarks) + */ +export interface VFXConstantColor { + type: "ConstantColor"; + value: [number, number, number, number]; // RGBA +} + +export type VFXColor = VFXConstantColor | [number, number, number, number] | string; + diff --git a/editor/src/editor/windows/fx-editor/VFX/types/context.ts b/editor/src/editor/windows/fx-editor/VFX/types/context.ts new file mode 100644 index 000000000..24142cbda --- /dev/null +++ b/editor/src/editor/windows/fx-editor/VFX/types/context.ts @@ -0,0 +1,17 @@ +import type { Scene } from "../../../scene"; +import type { TransformNode } from "../../../Meshes/transformNode"; +import type { QuarksVFXJSON } from "./quarksTypes"; +import type { VFXHierarchy } from "./hierarchy"; +import type { VFXLoaderOptions } from "./loader"; + +/** + * Context for VFX parsing operations + */ +export interface VFXParseContext { + scene: Scene; + rootUrl: string; + jsonData: QuarksVFXJSON; + options: VFXLoaderOptions; + groupNodesMap: Map; + vfxData?: VFXHierarchy; +} diff --git a/editor/src/editor/windows/fx-editor/VFX/types/emitter.ts b/editor/src/editor/windows/fx-editor/VFX/types/emitter.ts new file mode 100644 index 000000000..931883e21 --- /dev/null +++ b/editor/src/editor/windows/fx-editor/VFX/types/emitter.ts @@ -0,0 +1,20 @@ +import type { Nullable } from "../../../types"; +import type { TransformNode } from "../../../Meshes/transformNode"; +import type { Vector3 } from "../../../Maths/math.vector"; +import type { VFXParticleEmitterConfig } from "./emitterConfig"; +import type { VFXEmitter } from "./hierarchy"; + +/** + * Data structure for emitter creation + */ +export interface VFXEmitterData { + name: string; + config: VFXParticleEmitterConfig; + materialId?: string; + matrix?: number[]; + position?: number[]; + parentGroup: Nullable; + cumulativeScale: Vector3; + vfxEmitter?: VFXEmitter; +} + diff --git a/editor/src/editor/windows/fx-editor/VFX/types/emitterConfig.ts b/editor/src/editor/windows/fx-editor/VFX/types/emitterConfig.ts new file mode 100644 index 000000000..67609db8e --- /dev/null +++ b/editor/src/editor/windows/fx-editor/VFX/types/emitterConfig.ts @@ -0,0 +1,50 @@ +import type { VFXValue } from "./values"; +import type { VFXColor } from "./colors"; +import type { VFXRotation } from "./rotations"; +import type { VFXShape } from "./shapes"; +import type { VFXBehavior } from "./behaviors"; + +/** + * VFX emission burst (converted from Quarks) + */ +export interface VFXEmissionBurst { + time: VFXValue; + count: VFXValue; +} + +/** + * VFX particle emitter configuration (converted from Quarks) + */ +export interface VFXParticleEmitterConfig { + version?: string; + autoDestroy?: boolean; + looping?: boolean; + prewarm?: boolean; + duration?: number; + shape?: VFXShape; + startLife?: VFXValue; + startSpeed?: VFXValue; + startRotation?: VFXRotation; + startSize?: VFXValue; + startColor?: VFXColor; + emissionOverTime?: VFXValue; + emissionOverDistance?: VFXValue; + emissionBursts?: VFXEmissionBurst[]; + onlyUsedByOther?: boolean; + instancingGeometry?: string; + renderOrder?: number; + renderMode?: number; + rendererEmitterSettings?: Record; + material?: string; + layers?: number; + startTileIndex?: VFXValue; + uTileCount?: number; + vTileCount?: number; + blendTiles?: boolean; + softParticles?: boolean; + softFarFade?: number; + softNearFade?: number; + behaviors?: VFXBehavior[]; + worldSpace?: boolean; +} + diff --git a/editor/src/editor/windows/fx-editor/VFX/types/factories.ts b/editor/src/editor/windows/fx-editor/VFX/types/factories.ts new file mode 100644 index 000000000..c5ad41366 --- /dev/null +++ b/editor/src/editor/windows/fx-editor/VFX/types/factories.ts @@ -0,0 +1,37 @@ +import type { Nullable } from "../../../types"; +import type { Mesh } from "../../../Meshes/mesh"; +import type { ParticleSystem } from "../../particleSystem"; +import type { SolidParticleSystem } from "../../solidParticleSystem"; +import { PBRMaterial } from "../../../Materials/PBR/pbrMaterial"; +import type { Color4 } from "../../../Maths/math.color"; +import type { Texture } from "../../../Materials/Textures/texture"; +import type { ColorGradient } from "../../../Misc/gradients"; +import type { VFXValue } from "./values"; +import type { VFXColor } from "./colors"; +import type { VFXGradientKey } from "./gradients"; +import type { VFXEmitterData } from "./emitter"; + +/** + * Factory interfaces for dependency injection + */ +export interface IVFXMaterialFactory { + createMaterial(materialId: string, name: string): Nullable; + createTexture(materialId: string): Nullable; +} + +export interface IVFXGeometryFactory { + createMesh(geometryId: string, materialId: string | undefined, name: string): Nullable; +} + +export interface IVFXEmitterFactory { + createEmitter(emitterData: VFXEmitterData): Nullable; +} + +export interface IVFXValueParser { + parseConstantValue(value: VFXValue): number; + parseIntervalValue(value: VFXValue): { min: number; max: number }; + parseConstantColor(value: VFXColor): Color4; + parseGradientColorKeys(keys: VFXGradientKey[]): ColorGradient[]; + parseGradientAlphaKeys(keys: VFXGradientKey[]): { gradient: number; factor: number }[]; +} + diff --git a/editor/src/editor/windows/fx-editor/VFX/types/gradients.ts b/editor/src/editor/windows/fx-editor/VFX/types/gradients.ts new file mode 100644 index 000000000..77066d797 --- /dev/null +++ b/editor/src/editor/windows/fx-editor/VFX/types/gradients.ts @@ -0,0 +1,9 @@ +/** + * VFX gradient key (converted from Quarks) + */ +export interface VFXGradientKey { + time?: number; + value: number | [number, number, number, number] | { r: number; g: number; b: number; a?: number }; + pos?: number; +} + diff --git a/editor/src/editor/windows/fx-editor/VFX/types/hierarchy.ts b/editor/src/editor/windows/fx-editor/VFX/types/hierarchy.ts new file mode 100644 index 000000000..6dd0b9a63 --- /dev/null +++ b/editor/src/editor/windows/fx-editor/VFX/types/hierarchy.ts @@ -0,0 +1,43 @@ +import { Vector3, Quaternion } from "../../../Maths/math.vector"; +import type { VFXParticleEmitterConfig } from "./emitterConfig"; + +/** + * VFX transform (converted from Quarks, left-handed coordinate system) + */ +export interface VFXTransform { + position: Vector3; + rotation: Quaternion; + scale: Vector3; +} + +/** + * VFX group (converted from Quarks) + */ +export interface VFXGroup { + uuid: string; + name: string; + transform: VFXTransform; + children: (VFXGroup | VFXEmitter)[]; +} + +/** + * VFX emitter (converted from Quarks) + */ +export interface VFXEmitter { + uuid: string; + name: string; + transform: VFXTransform; + config: VFXParticleEmitterConfig; + materialId?: string; + parentUuid?: string; +} + +/** + * VFX hierarchy (converted from Quarks) + */ +export interface VFXHierarchy { + root: VFXGroup | VFXEmitter | null; + groups: Map; + emitters: Map; +} + diff --git a/editor/src/editor/windows/fx-editor/VFX/types/index.ts b/editor/src/editor/windows/fx-editor/VFX/types/index.ts new file mode 100644 index 000000000..26f12a8cc --- /dev/null +++ b/editor/src/editor/windows/fx-editor/VFX/types/index.ts @@ -0,0 +1,42 @@ +/** + * VFX Types - Centralized type definitions + * + * This module exports all VFX-related types organized by category. + * Import types directly from their specific modules for better tree-shaking. + */ + +// Loader and context types +export type { VFXLoaderOptions } from "./loader"; +export type { VFXParseContext } from "./context"; + +// Emitter types +export type { VFXEmitterData } from "./emitter"; + +// Factory interfaces +export type { IVFXMaterialFactory, IVFXGeometryFactory, IVFXEmitterFactory, IVFXValueParser } from "./factories"; + +// Core VFX types +export type { VFXConstantValue, VFXIntervalValue, VFXValue } from "./values"; +export type { VFXConstantColor, VFXColor } from "./colors"; +export type { VFXEulerRotation, VFXRotation } from "./rotations"; +export type { VFXGradientKey } from "./gradients"; +export type { VFXShape } from "./shapes"; +export type { + VFXColorOverLifeBehavior, + VFXSizeOverLifeBehavior, + VFXRotationOverLifeBehavior, + VFXForceOverLifeBehavior, + VFXGravityForceBehavior, + VFXSpeedOverLifeBehavior, + VFXFrameOverLifeBehavior, + VFXLimitSpeedOverLifeBehavior, + VFXColorBySpeedBehavior, + VFXSizeBySpeedBehavior, + VFXRotationBySpeedBehavior, + VFXOrbitOverLifeBehavior, + VFXBehavior, +} from "./behaviors"; +export type { VFXEmissionBurst, VFXParticleEmitterConfig } from "./emitterConfig"; +export type { VFXTransform, VFXGroup, VFXEmitter, VFXHierarchy } from "./hierarchy"; +export type { QuarksVFXJSON } from "./quarksTypes"; +export type { VFXPerParticleContext, VFXPerParticleBehaviorFunction, VFXPerSolidParticleBehaviorFunction, VFXSystemBehaviorFunction } from "./VFXBehaviorFunction"; diff --git a/editor/src/editor/windows/fx-editor/VFX/types/loader.ts b/editor/src/editor/windows/fx-editor/VFX/types/loader.ts new file mode 100644 index 000000000..bff7133b5 --- /dev/null +++ b/editor/src/editor/windows/fx-editor/VFX/types/loader.ts @@ -0,0 +1,14 @@ +/** + * Options for parsing Quarks/Three.js particle JSON + */ +export interface VFXLoaderOptions { + /** + * Enable verbose logging for debugging + */ + verbose?: boolean; + /** + * Validate parsed data and log warnings + */ + validate?: boolean; +} + diff --git a/editor/src/editor/windows/fx-editor/VFX/types/quarksTypes.ts b/editor/src/editor/windows/fx-editor/VFX/types/quarksTypes.ts new file mode 100644 index 000000000..73b69c151 --- /dev/null +++ b/editor/src/editor/windows/fx-editor/VFX/types/quarksTypes.ts @@ -0,0 +1,379 @@ +/** + * Type definitions for Quarks/Three.js VFX JSON structures + * These represent the incoming format from Quarks/Three.js + */ + +/** + * Quarks/Three.js value types + */ +export interface QuarksConstantValue { + type: "ConstantValue"; + value: number; +} + +export interface QuarksIntervalValue { + type: "IntervalValue"; + a: number; // min + b: number; // max +} + +export interface QuarksPiecewiseBezier { + type: "PiecewiseBezier"; + functions: Array<{ + function: { + p0: number; + p1: number; + p2: number; + p3: number; + }; + start: number; + }>; +} + +export type QuarksValue = QuarksConstantValue | QuarksIntervalValue | QuarksPiecewiseBezier | number; + +/** + * Quarks/Three.js color types + */ +export interface QuarksConstantColor { + type: "ConstantColor"; + color?: { + r: number; + g: number; + b: number; + a?: number; + }; + value?: [number, number, number, number]; // RGBA array alternative +} + +export type QuarksColor = QuarksConstantColor | [number, number, number, number] | string; + +/** + * Quarks/Three.js rotation types + */ +export interface QuarksEulerRotation { + type: "Euler"; + angleX?: QuarksValue; + angleY?: QuarksValue; + angleZ?: QuarksValue; +} + +export type QuarksRotation = QuarksEulerRotation | QuarksValue; + +/** + * Quarks/Three.js gradient key + */ +export interface QuarksGradientKey { + time?: number; + value: number | [number, number, number, number] | { r: number; g: number; b: number; a?: number }; + pos?: number; +} + +/** + * Quarks/Three.js shape configuration + */ +export interface QuarksShape { + type: string; + radius?: number; + arc?: number; + thickness?: number; + angle?: number; + mode?: number; + spread?: number; + speed?: QuarksValue; + size?: number[]; + height?: number; +} + +/** + * Quarks/Three.js emission burst + */ +export interface QuarksEmissionBurst { + time: QuarksValue; + count: QuarksValue; +} + +/** + * Quarks/Three.js behavior types + */ +export interface QuarksColorOverLifeBehavior { + type: "ColorOverLife"; + color?: { + color?: { + keys: QuarksGradientKey[]; + }; + alpha?: { + keys: QuarksGradientKey[]; + }; + keys?: QuarksGradientKey[]; + }; +} + +export interface QuarksSizeOverLifeBehavior { + type: "SizeOverLife"; + size?: { + keys?: QuarksGradientKey[]; + functions?: Array<{ + start: number; + function: { + p0?: number; + p3?: number; + }; + }>; + }; +} + +export interface QuarksRotationOverLifeBehavior { + type: "RotationOverLife" | "Rotation3DOverLife"; + angularVelocity?: QuarksValue; +} + +export interface QuarksForceOverLifeBehavior { + type: "ForceOverLife" | "ApplyForce"; + force?: { + x?: QuarksValue; + y?: QuarksValue; + z?: QuarksValue; + }; + x?: QuarksValue; + y?: QuarksValue; + z?: QuarksValue; +} + +export interface QuarksGravityForceBehavior { + type: "GravityForce"; + gravity?: QuarksValue; +} + +export interface QuarksSpeedOverLifeBehavior { + type: "SpeedOverLife"; + speed?: + | { + keys?: QuarksGradientKey[]; + functions?: Array<{ + start: number; + function: { + p0?: number; + p3?: number; + }; + }>; + } + | QuarksValue; +} + +export interface QuarksFrameOverLifeBehavior { + type: "FrameOverLife"; + frame?: + | { + keys?: QuarksGradientKey[]; + } + | QuarksValue; +} + +export interface QuarksLimitSpeedOverLifeBehavior { + type: "LimitSpeedOverLife"; + maxSpeed?: QuarksValue; + speed?: QuarksValue | { keys?: QuarksGradientKey[] }; + dampen?: QuarksValue; +} + +export interface QuarksColorBySpeedBehavior { + type: "ColorBySpeed"; + color?: { + keys: QuarksGradientKey[]; + }; + minSpeed?: QuarksValue; + maxSpeed?: QuarksValue; +} + +export interface QuarksSizeBySpeedBehavior { + type: "SizeBySpeed"; + size?: { + keys: QuarksGradientKey[]; + }; + minSpeed?: QuarksValue; + maxSpeed?: QuarksValue; +} + +export interface QuarksRotationBySpeedBehavior { + type: "RotationBySpeed"; + angularVelocity?: QuarksValue; + minSpeed?: QuarksValue; + maxSpeed?: QuarksValue; +} + +export interface QuarksOrbitOverLifeBehavior { + type: "OrbitOverLife"; + center?: { + x?: number; + y?: number; + z?: number; + }; + radius?: QuarksValue; + speed?: QuarksValue; +} + +export type QuarksBehavior = + | QuarksColorOverLifeBehavior + | QuarksSizeOverLifeBehavior + | QuarksRotationOverLifeBehavior + | QuarksForceOverLifeBehavior + | QuarksGravityForceBehavior + | QuarksSpeedOverLifeBehavior + | QuarksFrameOverLifeBehavior + | QuarksLimitSpeedOverLifeBehavior + | QuarksColorBySpeedBehavior + | QuarksSizeBySpeedBehavior + | QuarksRotationBySpeedBehavior + | QuarksOrbitOverLifeBehavior + | { type: string; [key: string]: unknown }; // Fallback for unknown behaviors + +/** + * Quarks/Three.js particle emitter configuration + */ +export interface QuarksParticleEmitterConfig { + version?: string; + autoDestroy?: boolean; + looping?: boolean; + prewarm?: boolean; + duration?: number; + shape?: QuarksShape; + startLife?: QuarksValue; + startSpeed?: QuarksValue; + startRotation?: QuarksRotation; + startSize?: QuarksValue; + startColor?: QuarksColor; + emissionOverTime?: QuarksValue; + emissionOverDistance?: QuarksValue; + emissionBursts?: QuarksEmissionBurst[]; + onlyUsedByOther?: boolean; + instancingGeometry?: string; + renderOrder?: number; + renderMode?: number; + rendererEmitterSettings?: Record; + material?: string; + layers?: number; + startTileIndex?: QuarksValue; + uTileCount?: number; + vTileCount?: number; + blendTiles?: boolean; + softParticles?: boolean; + softFarFade?: number; + softNearFade?: number; + behaviors?: QuarksBehavior[]; + worldSpace?: boolean; +} + +/** + * Quarks/Three.js object types + */ +export interface QuarksGroup { + uuid: string; + type: "Group"; + name: string; + matrix?: number[]; + position?: number[]; + rotation?: number[]; + scale?: number[]; + children?: QuarksObject[]; +} + +export interface QuarksParticleEmitter { + uuid: string; + type: "ParticleEmitter"; + name: string; + matrix?: number[]; + position?: number[]; + rotation?: number[]; + scale?: number[]; + ps: QuarksParticleEmitterConfig; + children?: QuarksObject[]; +} + +export type QuarksObject = QuarksGroup | QuarksParticleEmitter; + +/** + * Quarks/Three.js material + */ +export interface QuarksMaterial { + uuid: string; + type: string; + name?: string; + color?: number; + map?: string; + blending?: number; + side?: number; + transparent?: boolean; + depthWrite?: boolean; + [key: string]: unknown; +} + +/** + * Quarks/Three.js texture + */ +export interface QuarksTexture { + uuid: string; + name?: string; + image?: string; + mapping?: number; + wrap?: number[]; + repeat?: number[]; + offset?: number[]; + rotation?: number; + minFilter?: number; + magFilter?: number; + flipY?: boolean; + generateMipmaps?: boolean; + format?: number; + [key: string]: unknown; +} + +/** + * Quarks/Three.js image + */ +export interface QuarksImage { + uuid: string; + url?: string; + data?: string; + [key: string]: unknown; +} + +/** + * Quarks/Three.js geometry + */ +export interface QuarksGeometry { + uuid: string; + type: string; + data?: { + attributes?: Record< + string, + { + itemSize: number; + type: string; + array: number[]; + } + >; + index?: { + type: string; + array: number[]; + }; + }; + [key: string]: unknown; +} + +/** + * Quarks/Three.js JSON structure + */ +export interface QuarksVFXJSON { + metadata?: { + version?: number; + type?: string; + generator?: string; + [key: string]: unknown; + }; + geometries?: QuarksGeometry[]; + materials?: QuarksMaterial[]; + textures?: QuarksTexture[]; + images?: QuarksImage[]; + object?: QuarksObject; +} diff --git a/editor/src/editor/windows/fx-editor/VFX/types/rotations.ts b/editor/src/editor/windows/fx-editor/VFX/types/rotations.ts new file mode 100644 index 000000000..b9ff54210 --- /dev/null +++ b/editor/src/editor/windows/fx-editor/VFX/types/rotations.ts @@ -0,0 +1,14 @@ +import type { VFXValue } from "./values"; + +/** + * VFX rotation types (converted from Quarks) + */ +export interface VFXEulerRotation { + type: "Euler"; + angleX?: VFXValue; + angleY?: VFXValue; + angleZ?: VFXValue; +} + +export type VFXRotation = VFXEulerRotation | VFXValue; + diff --git a/editor/src/editor/windows/fx-editor/VFX/types/shapes.ts b/editor/src/editor/windows/fx-editor/VFX/types/shapes.ts new file mode 100644 index 000000000..f280206cb --- /dev/null +++ b/editor/src/editor/windows/fx-editor/VFX/types/shapes.ts @@ -0,0 +1,18 @@ +import type { VFXValue } from "./values"; + +/** + * VFX shape configuration (converted from Quarks) + */ +export interface VFXShape { + type: string; + radius?: number; + arc?: number; + thickness?: number; + angle?: number; + mode?: number; + spread?: number; + speed?: VFXValue; + size?: number[]; + height?: number; +} + diff --git a/editor/src/editor/windows/fx-editor/VFX/types/values.ts b/editor/src/editor/windows/fx-editor/VFX/types/values.ts new file mode 100644 index 000000000..b9d9a8414 --- /dev/null +++ b/editor/src/editor/windows/fx-editor/VFX/types/values.ts @@ -0,0 +1,27 @@ +/** + * VFX value types (converted from Quarks) + */ +export interface VFXConstantValue { + type: "ConstantValue"; + value: number; +} + +export interface VFXIntervalValue { + type: "IntervalValue"; + min: number; + max: number; +} + +export interface VFXPiecewiseBezier { + type: "PiecewiseBezier"; + functions: Array<{ + function: { + p0: number; + p1: number; + p2: number; + p3: number; + }; + start: number; + }>; +} +export type VFXValue = VFXConstantValue | VFXIntervalValue | VFXPiecewiseBezier | number; diff --git a/editor/src/editor/windows/fx-editor/animation.tsx b/editor/src/editor/windows/fx-editor/animation.tsx index 7b839ef92..dfd62986b 100644 --- a/editor/src/editor/windows/fx-editor/animation.tsx +++ b/editor/src/editor/windows/fx-editor/animation.tsx @@ -1,7 +1,9 @@ import { Component, ReactNode } from "react"; +import { IFXEditor } from "."; export interface IFXEditorAnimationProps { filePath: string | null; + editor: IFXEditor; } export class FXEditorAnimation extends Component { diff --git a/editor/src/editor/windows/fx-editor/graph.tsx b/editor/src/editor/windows/fx-editor/graph.tsx index 1c01188de..3cec0b2ee 100644 --- a/editor/src/editor/windows/fx-editor/graph.tsx +++ b/editor/src/editor/windows/fx-editor/graph.tsx @@ -1,9 +1,9 @@ import { Component, ReactNode } from "react"; import { Tree, TreeNodeInfo } from "@blueprintjs/core"; -import { Scene, AbstractMesh, ParticleSystem, SolidParticleSystem, Vector3, Color4, StandardMaterial, PBRMaterial, Texture, Color3 } from "babylonjs"; -import { createParticleSystemFromData, createGroupMesh } from "./particle-generator"; +import { Scene, AbstractMesh, ParticleSystem, SolidParticleSystem, Vector3, Color4, ParticleSystemSet } from "babylonjs"; import { IFXParticleData, IFXGroupData, IFXNodeData, isGroupData, isParticleData } from "./properties/types"; -import { IConvertedNode, convertThreeJSJSONToFXEditor, IConvertedData } from "./loader"; +import { IConvertedNode, convertThreeJSJSONToFXEditor } from "./loader"; +import { ThreeJSParticleLoader } from "./threeJSParticleLoader"; import { AiOutlinePlus, AiOutlineClose } from "react-icons/ai"; import { IoSparklesSharp } from "react-icons/io5"; @@ -19,18 +19,19 @@ import { ContextMenuSubTrigger, ContextMenuSubContent, } from "../../../ui/shadcn/ui/context-menu"; +import { FXEditorPreview } from "./preview"; +import { IFXEditor } from "."; // Maps to track created particle systems and meshes const createdParticleSystemsMap = new Map(); const createdMeshesMap = new Map(); -const createdMaterialsMap = new Map(); -const createdTexturesMap = new Map(); +const particleSystemSetsMap = new Map(); export interface IFXEditorGraphProps { filePath: string | null; onNodeSelected?: (nodeId: string | number | null) => void; onResourcesLoaded?: (resources: IConvertedNode[]) => void; - scene?: Scene; + editor: IFXEditor; } export interface IFXEditorGraphState { @@ -247,164 +248,67 @@ export class FXEditorGraph extends Component { try { - const convertedData = await convertThreeJSJSONToFXEditor(filePath); - const { nodes, resources, materials, textures } = convertedData; - - // Create materials and textures if scene is available - if (this.props.scene) { - await this._createMaterialsAndTextures(materials, textures, filePath); - } - - // Filter out resource nodes (texture, geometry) - they will be shown in Resources tab - const particleNodes = nodes.filter((n) => n.type === "particle" || n.type === "group"); - const treeNodes = this._convertToTreeNodeInfo(particleNodes, null); - this.setState({ nodes: treeNodes }, () => { - this._syncParticlesWithScene(); - }); - // Notify parent about loaded resources - if (this.props.onResourcesLoaded) { - this.props.onResourcesLoaded(resources); + if (!this.props.editor.preview?.scene) { + console.error("Scene is not available"); + return; } - } catch (error) { - console.error("Failed to load FX file:", error); - } - } - /** - * Creates materials and textures from converted data - */ - private async _createMaterialsAndTextures( - materials: IConvertedData["materials"], - textures: IConvertedData["textures"], - filePath: string - ): Promise { - if (!this.props.scene) { - return; - } - - const scene = this.props.scene; - const dirname = require("path").dirname(filePath); - - // Clear existing materials and textures - createdMaterialsMap.forEach((mat) => mat.dispose()); - createdMaterialsMap.clear(); - createdTexturesMap.forEach((tex) => tex.dispose()); - createdTexturesMap.clear(); - - // Create textures first - for (const textureData of textures) { - if (!textureData.imageUrl) { - continue; - } - - try { - // Resolve texture path relative to JSON file - const texturePath = require("path").isAbsolute(textureData.imageUrl) - ? textureData.imageUrl - : require("path").join(dirname, textureData.imageUrl); - - const texture = new Texture(texturePath, scene); - texture.name = textureData.name || textureData.uuid; - createdTexturesMap.set(textureData.uuid, texture); - } catch (error) { - console.warn(`Failed to load texture ${textureData.uuid}:`, error); - } - } - - // Create materials - for (const materialData of materials) { - try { - let material: StandardMaterial | PBRMaterial; - - if (materialData.type === "MeshBasicMaterial") { - material = new StandardMaterial(`Material_${materialData.uuid}`, scene); - } else { - // MeshStandardMaterial -> PBRMaterial - material = new PBRMaterial(`Material_${materialData.uuid}`, scene); - } - - // Convert color from hex number (0xRRGGBB) to Color3 - if (materialData.color !== undefined) { - const r = ((materialData.color >> 16) & 0xff) / 255; - const g = ((materialData.color >> 8) & 0xff) / 255; - const b = (materialData.color & 0xff) / 255; - - if (material instanceof StandardMaterial) { - material.diffuseColor = new Color3(r, g, b); - } else if (material instanceof PBRMaterial) { - material.albedoColor = new Color3(r, g, b); - } - } - - // Apply texture - if (materialData.map) { - const texture = createdTexturesMap.get(materialData.map); - if (texture) { - if (material instanceof StandardMaterial) { - material.diffuseTexture = texture; - } else if (material instanceof PBRMaterial) { - material.albedoTexture = texture; - } + // Use ThreeJSParticleLoader to load and create particle systems + const dirname = require("path").dirname(filePath); + const particleSystemSet = await ThreeJSParticleLoader.LoadAsync(filePath, this.props.editor.preview!.scene, dirname + "/"); + + // Store particle system set + const setId = `set-${Date.now()}`; + particleSystemSetsMap.set(setId, particleSystemSet); + + // Map particle systems to our tracking map and START them + particleSystemSet.systems.forEach((ps, index) => { + if (ps instanceof ParticleSystem) { + const nodeId = `particle-${setId}-${index}`; + createdParticleSystemsMap.set(nodeId, ps); + // Start the particle system + if (!ps.isStarted()) { + ps.start(); } + } else if (ps instanceof SolidParticleSystem) { + const nodeId = `particle-${setId}-${index}`; + createdParticleSystemsMap.set(nodeId, ps); + // For SPS, call setParticles to update them + ps.setParticles(); } + }); - // Apply transparency - if (materialData.transparent !== undefined) { - material.alpha = materialData.transparent ? (materialData.opacity ?? 1.0) : 1.0; - } else if (materialData.opacity !== undefined) { - material.alpha = materialData.opacity; - } - - // Apply side (0 = Front, 1 = Back, 2 = Double) - if (materialData.side !== undefined) { - if (materialData.side === 0) { - material.sideOrientation = 1; // Front - } else if (materialData.side === 1) { - material.sideOrientation = 2; // Back - } else { - material.sideOrientation = 0; // Double - } - } - - // Apply depth write - if (materialData.depthWrite !== undefined) { - material.disableDepthWrite = !materialData.depthWrite; - } + // // Also convert to our node structure for tree view + // const convertedData = await convertThreeJSJSONToFXEditor(filePath); + // const { nodes, resources } = convertedData; - // Apply blending (0 = No, 1 = Normal, 2 = Additive, 3 = Multiply, etc.) - if (materialData.blending !== undefined) { - // Three.js blending modes: 0 = No, 1 = Normal, 2 = Additive, 3 = Multiply - // Babylon.js: use alpha blending for additive, multiply for multiply - if (materialData.blending === 2) { - // Additive blending - material.alphaMode = 2; // ALPHA_ADD - } else if (materialData.blending === 3) { - // Multiply blending - material.alphaMode = 3; // ALPHA_MULTIPLY - } - } + // // Filter out resource nodes (texture, geometry) - they will be shown in Resources tab + // const particleNodes = nodes.filter((n) => n.type === "particle" || n.type === "group"); + // const treeNodes = this._convertToTreeNodeInfo(particleNodes, null); + // this.setState({ nodes: treeNodes }); - createdMaterialsMap.set(materialData.uuid, material); - } catch (error) { - console.warn(`Failed to create material ${materialData.uuid}:`, error); - } + // Notify parent about loaded resources + // if (this.props.onResourcesLoaded) { + // this.props.onResourcesLoaded(resources); + // } + } catch (error) { + console.error("Failed to load FX file:", error); } } + /** * Updates node names in the graph (called when name changes in properties) */ @@ -469,89 +373,6 @@ export class FXEditorGraph extends Component { - if (ps.dispose) { - ps.dispose(); - } - }); - createdParticleSystemsMap.clear(); - - createdMeshesMap.forEach((mesh) => { - mesh.dispose(); - }); - createdMeshesMap.clear(); - - // Create particle systems from graph nodes - this._createParticlesFromNodes(this.state.nodes, this.props.scene, null); - } - - /** - * Recursively creates particle systems from graph nodes - */ - private _createParticlesFromNodes(nodes: TreeNodeInfo[], scene: Scene, parentMesh: AbstractMesh | null): void { - for (const node of nodes) { - const nodeData = node.nodeData as IFXNodeData | undefined; - if (!nodeData) { - continue; - } - - if (isGroupData(nodeData)) { - // Create group mesh (empty mesh) - const groupMesh = createGroupMesh(scene, nodeData.name, nodeData.position, nodeData.rotation, nodeData.scale); - groupMesh.setEnabled(nodeData.visibility); - - if (parentMesh) { - groupMesh.parent = parentMesh; - } - - createdMeshesMap.set(nodeData.id, groupMesh); - - // Recursively create children - if (node.childNodes) { - this._createParticlesFromNodes(node.childNodes, scene, groupMesh); - } - } else if (isParticleData(nodeData)) { - // Apply material and texture to particle data if available - if (nodeData.particleRenderer.material?.uuid) { - const material = createdMaterialsMap.get(nodeData.particleRenderer.material.uuid); - if (material) { - // Material is already created, we can use it - // Note: ParticleSystem doesn't directly use materials, but we can apply texture - } - } - - // Apply texture if available - if (nodeData.particleRenderer.texture?.uuid) { - const texture = createdTexturesMap.get(nodeData.particleRenderer.texture.uuid); - if (texture) { - nodeData.particleRenderer.texture = texture; - } - } - - // Create particle system - const emitter = parentMesh || undefined; - const particleSystem = createParticleSystemFromData(scene, nodeData, emitter); - - if (particleSystem) { - createdParticleSystemsMap.set(nodeData.id, particleSystem); - } - - // Recursively create children (particles can have children too) - if (node.childNodes) { - this._createParticlesFromNodes(node.childNodes, scene, emitter || null); - } - } - } - } public render(): ReactNode { return ( @@ -706,24 +527,6 @@ export class FXEditorGraph extends Component { - this._syncParticlesWithScene(); - }); - } else { - // Add to root - this.setState( - { - nodes: [...this.state.nodes, newNode], - }, - () => { - this._syncParticlesWithScene(); - } - ); - } } private _handleAddGroup(parentId?: string | number): void { @@ -740,24 +543,6 @@ export class FXEditorGraph extends Component { - this._syncParticlesWithScene(); - }); - } else { - // Add to root - this.setState( - { - nodes: [...this.state.nodes, newNode], - }, - () => { - this._syncParticlesWithScene(); - } - ); - } } private _handleAddParticlesToNode(node: TreeNodeInfo): void { @@ -828,15 +613,6 @@ export class FXEditorGraph extends Component { - this._syncParticlesWithScene(); - } - ); if (this.state.selectedNodeId === deletedId) { this.props.onNodeSelected?.(null); diff --git a/editor/src/editor/windows/fx-editor/index.tsx b/editor/src/editor/windows/fx-editor/index.tsx index 5c1f6f107..ec883c596 100644 --- a/editor/src/editor/windows/fx-editor/index.tsx +++ b/editor/src/editor/windows/fx-editor/index.tsx @@ -11,6 +11,11 @@ import { FXEditorLayout } from "./layout"; import { FXEditorToolbar } from "./toolbar"; import { projectConfiguration, onProjectConfigurationChangedObservable, IProjectConfiguration } from "../../../project/configuration"; +import { FXEditorAnimation } from "./animation"; +import { FXEditorGraph } from "./graph"; +import { FXEditorPreview } from "./preview"; +import { FXEditorProperties } from "./properties"; +import { FXEditorResources } from "./resources"; export interface IFXEditorWindowProps { filePath?: string; @@ -21,8 +26,23 @@ export interface IFXEditorWindowState { filePath: string | null; } +export interface IFXEditor { + layout: FXEditorLayout | null; + preview: FXEditorPreview | null; + graph: FXEditorGraph | null; + animation: FXEditorAnimation | null; + properties: FXEditorProperties | null; + resources: FXEditorResources | null; +} export default class FXEditorWindow extends Component { - private layout: FXEditorLayout | null = null; + public editor: IFXEditor = { + layout: null, + preview: null, + graph: null, + animation: null, + properties: null, + resources: null, + } public constructor(props: IFXEditorWindowProps) { super(props); @@ -39,7 +59,7 @@ export default class FXEditorWindow extends Component
- (this.layout = r)} filePath={this.state.filePath || ""} /> + (this.editor.layout = r)} filePath={this.state.filePath || ""} editor={this.editor} />
@@ -92,8 +112,8 @@ export default class FXEditorWindow extends Component { try { // Get graph component reference from layout - if (this.layout && this.layout.graph) { - await this.layout.graph.loadFromFile(filePath); + if (this.editor.graph) { + await this.editor.graph.loadFromFile(filePath); toast.success("FX imported"); } else { toast.error("Failed to import FX: Graph not available"); diff --git a/editor/src/editor/windows/fx-editor/layout.tsx b/editor/src/editor/windows/fx-editor/layout.tsx index b786b7cff..9f290fe08 100644 --- a/editor/src/editor/windows/fx-editor/layout.tsx +++ b/editor/src/editor/windows/fx-editor/layout.tsx @@ -8,6 +8,7 @@ import { FXEditorGraph } from "./graph"; import { FXEditorAnimation } from "./animation"; import { FXEditorProperties } from "./properties"; import { FXEditorResources } from "./resources"; +import { IFXEditor } from "."; const layoutModel: IJsonModel = { global: { @@ -104,6 +105,7 @@ const layoutModel: IJsonModel = { export interface IFXEditorLayoutProps { filePath: string | null; + editor: IFXEditor; } export interface IFXEditorLayoutState { @@ -113,11 +115,6 @@ export interface IFXEditorLayoutState { } export class FXEditorLayout extends Component { - public preview: FXEditorPreview; - public graph: FXEditorGraph; - public animation: FXEditorAnimation; - public properties: FXEditorProperties; - public resources: FXEditorResources; private _model: Model = Model.fromJson(layoutModel as any); @@ -151,8 +148,8 @@ export class FXEditorLayout extends Component (this.preview = r!)} + ref={(r) => (this.props.editor.preview = r!)} filePath={this.props.filePath} onSceneReady={(scene) => { // Update graph when scene is ready - if (this.graph) { - this.graph.forceUpdate(); + if (this.props.editor.graph) { + this.props.editor.graph.forceUpdate(); } }} /> ), graph: ( (this.graph = r!)} + ref={(r) => (this.props.editor.graph = r!)} filePath={this.props.filePath} onNodeSelected={this._handleNodeSelected} - scene={this.preview?.scene || undefined} + editor={this.props.editor} onResourcesLoaded={(resources) => { this.setState({ resources }); }} @@ -187,27 +184,27 @@ export class FXEditorLayout extends Component (this.resources = r!)} + ref={(r) => (this.props.editor.resources = r!)} resources={this.state.resources} /> ), - animation: (this.animation = r!)} filePath={this.props.filePath} />, + animation: (this.props.editor.animation = r!)} filePath={this.props.filePath} editor={this.props.editor} />, properties: ( (this.properties = r!)} + ref={(r) => (this.props.editor.properties = r!)} filePath={this.props.filePath} selectedNodeId={this.state.selectedNodeId} - scene={this.preview?.scene || undefined} + editor={this.props.editor} onNameChanged={() => { // Update graph node names when name changes - if (this.graph) { - this.graph.updateNodeNames(); + if (this.props.editor.graph) { + this.props.editor.graph.updateNodeNames(); } }} - getOrCreateParticleData={(nodeId) => this.graph?.getOrCreateParticleData(nodeId)!} - getOrCreateGroupData={(nodeId) => this.graph?.getOrCreateGroupData(nodeId)!} - isGroupNode={(nodeId) => this.graph?.isGroupNode(nodeId) ?? false} + getOrCreateParticleData={(nodeId) => this.props.editor.graph?.getOrCreateParticleData(nodeId)!} + getOrCreateGroupData={(nodeId) => this.props.editor.graph?.getOrCreateGroupData(nodeId)!} + isGroupNode={(nodeId) => this.props.editor.graph?.isGroupNode(nodeId) ?? false} /> ), }; @@ -239,7 +236,7 @@ export class FXEditorLayout extends Component { - waitNextAnimationFrame().then(() => this.preview?.resize()); + waitNextAnimationFrame().then(() => this.props.editor.preview?.resize()); }); return component; diff --git a/editor/src/editor/windows/fx-editor/loader.ts b/editor/src/editor/windows/fx-editor/loader.ts index e9eeb9044..17a6209b6 100644 --- a/editor/src/editor/windows/fx-editor/loader.ts +++ b/editor/src/editor/windows/fx-editor/loader.ts @@ -1,4 +1,4 @@ -import { Vector3, Color4, Matrix, Quaternion } from "babylonjs"; +import { Vector3, Matrix, Quaternion, Color4 } from "babylonjs"; import { readJSON } from "fs-extra"; import { IFXParticleData, IFXGroupData } from "./properties/types"; @@ -188,22 +188,38 @@ export interface IConvertedData { image?: string; imageUrl?: string; }>; + geometries: Array<{ + uuid: string; + type: string; + name?: string; + data?: any; + [name: string]: any; + }>; + images: Array<{ + uuid: string; + url?: string; + name?: string; + data?: string; + format?: string; + }>; } /** - * Converts a Three.js JSON file (from quarks) to FX editor format + * Converts a Three.js JSON file to FX editor node structure for UI tree view + * Note: Actual particle systems are created by ThreeJSParticleLoader */ export async function convertThreeJSJSONToFXEditor(filePath: string): Promise { const json: IThreeJSJSON = await readJSON(filePath); if (!json.object) { - return { nodes: [], resources: [], materials: [], textures: [] }; + return { nodes: [], resources: [], materials: [], textures: [], geometries: [], images: [] }; } const convertedNodes: IConvertedNode[] = []; const usedResources = new Set(); // Track used texture and geometry UUIDs - _convertObject(json.object, null, convertedNodes, json, usedResources); + // Simple conversion - just create node structure for tree view + _convertObjectToNodes(json.object, null, convertedNodes, json, usedResources); // Add resource nodes for textures and geometries const resourceNodes: IConvertedNode[] = []; @@ -228,9 +244,24 @@ export async function convertThreeJSJSONToFXEditor(filePath: string): Promise { + imagesData.push({ + uuid: image.uuid, + url: image.url, + name: image.name, + data: image.data, + format: image.format, + }); + }); + } + + if (json.textures) { json.textures.forEach((texture) => { if (usedResources.has(texture.uuid)) { const image = json.images?.find((img) => img.uuid === texture.image); @@ -257,10 +288,19 @@ export async function convertThreeJSJSONToFXEditor(filePath: string): Promise { if (usedResources.has(geometry.uuid)) { + geometriesData.push({ + uuid: geometry.uuid, + type: geometry.type || "BufferGeometry", + name: geometry.name, + data: geometry.data || geometry, + ...geometry, + }); + resourceNodes.push({ id: `geometry-${geometry.uuid}`, name: geometry.name || `Geometry ${geometry.uuid.substring(0, 8)}`, @@ -274,7 +314,14 @@ export async function convertThreeJSJSONToFXEditor(filePath: string): Promise ): void { if (obj.type === "ParticleEmitter" && obj.ps) { - // Convert particle emitter + // Create simple particle node for tree view const nodeId = `particle-${obj.uuid || Date.now()}-${Math.random()}`; - const particleData = _convertParticleSystem(obj.ps, obj.name || "Particle", json); - particleData.id = nodeId; - particleData.name = obj.name || "Particle"; - - // Extract position, rotation, scale from matrix if available - if (obj.matrix && obj.matrix.length >= 16) { - const { position, rotation, scale } = _decomposeMatrix(obj.matrix); - particleData.position = position; - particleData.rotation = rotation; - particleData.scale = scale; - } - + const node: IConvertedNode = { id: nodeId, name: obj.name || "Particle", type: "particle", parentId: parentId || undefined, - particleData, + // Store minimal particle data - actual creation is done by ThreeJSParticleLoader + particleData: { + type: "particle", + id: nodeId, + name: obj.name || "Particle", + visibility: obj.visible !== false, + position: new Vector3(0, 0, 0), + rotation: new Vector3(0, 0, 0), + scale: new Vector3(1, 1, 1), + emitterShape: { shape: "Box" }, + particleRenderer: { + renderMode: "Billboard", + worldSpace: false, + material: null, + materialType: "MeshStandardMaterial", + transparent: true, + opacity: 1.0, + side: "Double", + blending: "Add", + color: new Color4(1, 1, 1, 1), + renderOrder: 0, + uvTile: { column: 1, row: 1, startTileIndex: 0, blendTiles: false }, + texture: null, + meshPath: null, + softParticles: false, + }, + emission: { looping: true, duration: 5, prewarm: false, onlyUsedByOtherSystem: false, emitOverTime: 10, emitOverDistance: 0 }, + bursts: [], + particleInitialization: { + startLife: { functionType: "IntervalValue", data: { min: 1, max: 2 } }, + startSize: { functionType: "IntervalValue", data: { min: 0.1, max: 0.2 } }, + startSpeed: { functionType: "IntervalValue", data: { min: 1, max: 2 } }, + startColor: { colorFunctionType: "ConstantColor", data: { color: { r: 1, g: 1, b: 1, a: 1 } } }, + startRotation: { functionType: "IntervalValue", data: { min: 0, max: 360 } }, + }, + behaviors: [], + }, }; - // Track used resources - if (particleData.particleRenderer.texture?.uuid) { - usedResources.add(particleData.particleRenderer.texture.uuid); + // Extract position, rotation, scale from matrix if available + if (obj.matrix && obj.matrix.length >= 16) { + const { position, rotation, scale } = _decomposeMatrix(obj.matrix); + if (node.particleData) { + node.particleData.position = position; + node.particleData.rotation = rotation; + node.particleData.scale = scale; + } } - if (particleData.particleRenderer.material?.uuid) { - usedResources.add(particleData.particleRenderer.material.uuid); - // Also track texture from material - if (json.materials) { - const material = json.materials.find((m) => m.uuid === particleData.particleRenderer.material?.uuid); + + // Track used resources + if (obj.ps) { + const ps = obj.ps; + if (ps.material && json.materials) { + usedResources.add(ps.material); + const material = json.materials.find((m) => m.uuid === ps.material); if (material?.map && json.textures) { usedResources.add(material.map); } } - } - if (particleData.particleRenderer.meshPath) { - usedResources.add(particleData.particleRenderer.meshPath); - } - if (particleData.emitterShape.meshPath) { - usedResources.add(particleData.emitterShape.meshPath); + if (ps.instancingGeometry) { + usedResources.add(ps.instancingGeometry); + } } // Process children if (obj.children) { node.children = []; obj.children.forEach((child) => { - _convertObject(child, nodeId, node.children!, json, usedResources); + _convertObjectToNodes(child, nodeId, node.children!, json, usedResources); }); } @@ -420,12 +496,11 @@ function _convertObject( // Convert group const nodeId = `group-${obj.uuid || Date.now()}-${Math.random()}`; - // Create group data with default values const groupData: IFXGroupData = { type: "group", id: nodeId, name: obj.name || "Group", - visibility: obj.visible !== false, // Three.js uses 'visible' property + visibility: obj.visible !== false, position: new Vector3(0, 0, 0), rotation: new Vector3(0, 0, 0), scale: new Vector3(1, 1, 1), @@ -433,7 +508,6 @@ function _convertObject( // Extract position, rotation, scale from matrix if available if (obj.matrix && obj.matrix.length >= 16) { - console.log("[_convertObject] Group matrix:", obj.name); const { position, rotation, scale } = _decomposeMatrix(obj.matrix); groupData.position = position; groupData.rotation = rotation; @@ -452,7 +526,7 @@ function _convertObject( // Process children if (obj.children) { obj.children.forEach((child) => { - _convertObject(child, nodeId, node.children!, json, usedResources); + _convertObjectToNodes(child, nodeId, node.children!, json, usedResources); }); } @@ -460,538 +534,10 @@ function _convertObject( } else if (obj.children) { // Process children of other object types obj.children.forEach((child) => { - _convertObject(child, parentId, convertedNodes, json, usedResources); + _convertObjectToNodes(child, parentId, convertedNodes, json, usedResources); }); } } -/** - * Converts a quarks particle system to FX editor particle data - */ -function _convertParticleSystem(ps: IQuarksParticleSystem, name: string, json: IThreeJSJSON): IFXParticleData { - // Convert emitter shape - const emitterShape = _convertEmitterShape(ps.shape); - - // Convert particle renderer - const particleRenderer = _convertParticleRenderer(ps, json); - - // Convert emission - const emission = { - looping: ps.looping ?? true, - duration: ps.duration ?? 5.0, - prewarm: ps.prewarm ?? false, - onlyUsedByOtherSystem: ps.onlyUsedByOther ?? false, - emitOverTime: _extractConstantValue(ps.emissionOverTime) ?? 10, - emitOverDistance: _extractConstantValue(ps.emissionOverDistance) ?? 0, - }; - - // Convert bursts - const bursts = (ps.emissionBursts || []).map((burst, index) => ({ - id: `burst-${Date.now()}-${index}`, - time: burst.time ?? 0, - count: _extractConstantValue(burst.count) ?? 1, - cycle: burst.cycle ?? 1, - interval: burst.interval ?? 0, - probability: burst.probability ?? 1.0, - })); - - // Convert particle initialization - const particleInitialization = { - startLife: _convertQuarksValueToFunction(ps.startLife) ?? { - functionType: "IntervalValue", - data: { min: 1.0, max: 2.0 }, - }, - startSize: _convertQuarksValueToFunction(ps.startSize) ?? { - functionType: "IntervalValue", - data: { min: 0.1, max: 0.2 }, - }, - startSpeed: _convertQuarksValueToFunction(ps.startSpeed) ?? { - functionType: "IntervalValue", - data: { min: 1.0, max: 2.0 }, - }, - startColor: _convertQuarksColorToColorFunction(ps.startColor) ?? { - colorFunctionType: "ConstantColor", - data: { color: new Color4(1, 1, 1, 1) }, - }, - startRotation: _convertQuarksValueToFunction(ps.startRotation) ?? { - functionType: "IntervalValue", - data: { min: 0, max: 360 }, - }, - }; - - // Convert behaviors - const behaviors = (ps.behaviors || []).map((behavior, index) => ({ - id: `behavior-${Date.now()}-${index}`, - type: _convertBehaviorType(behavior.type), - ..._convertBehavior(behavior), - })); - - return { - type: "particle", - id: "", - name, - visibility: true, - position: new Vector3(0, 0, 0), - rotation: new Vector3(0, 0, 0), - scale: new Vector3(1, 1, 1), - emitterShape, - particleRenderer, - emission, - bursts, - particleInitialization, - behaviors, - }; -} - -/** - * Converts quarks emitter shape to FX editor format - */ -function _convertEmitterShape(shape?: IQuarksShape): IFXParticleData["emitterShape"] { - if (!shape) { - return { shape: "Box" }; - } - - const shapeType = shape.type?.toLowerCase() || "box"; - - switch (shapeType) { - case "cone": - return { - shape: "Cone", - radius: shape.radius ?? 1.0, - angle: shape.angle ?? 0.785398, - radiusRange: shape.thickness ?? 0.0, - heightRange: 0.0, - emitFromSpawnPointOnly: shape.mode === 1, - }; - - case "box": - return { - shape: "Box", - direction1: new Vector3(0, 1, 0), - direction2: new Vector3(0, 1, 0), - minEmitBox: new Vector3( - -(shape.boxWidth ?? shape.width ?? 1.0) / 2, - -(shape.boxHeight ?? shape.height ?? 1.0) / 2, - -(shape.boxDepth ?? shape.depth ?? 1.0) / 2 - ), - maxEmitBox: new Vector3( - (shape.boxWidth ?? shape.width ?? 1.0) / 2, - (shape.boxHeight ?? shape.height ?? 1.0) / 2, - (shape.boxDepth ?? shape.depth ?? 1.0) / 2 - ), - }; - - case "sphere": - return { - shape: "Sphere", - radius: shape.radius ?? 1.0, - }; - - case "hemisphere": - case "hemispheric": - return { - shape: "Hemispheric", - radius: shape.radius ?? 1.0, - }; - - case "cylinder": - return { - shape: "Cylinder", - radius: shape.radius ?? 1.0, - height: shape.height ?? 1.0, - directionRandomizer: 0.0, - }; - - case "point": - return { - shape: "Point", - }; - - default: - return { shape: "Box" }; - } -} - -/** - * Converts quarks particle renderer to FX editor format - */ -function _convertParticleRenderer(ps: IQuarksParticleSystem, json: IThreeJSJSON): IFXParticleData["particleRenderer"] { - // Convert render mode - const renderModeMap: Record = { - 0: "Billboard", - 1: "Stretched Billboard", - 2: "Mesh", - 3: "Trail", - }; - const renderMode = renderModeMap[ps.renderMode ?? 0] || "Billboard"; - - // Extract texture from material if available - let texture: any = null; - if (ps.material && json.materials) { - const material = json.materials.find((m) => m.uuid === ps.material); - if (material && material.map && json.textures) { - const textureData = json.textures.find((t) => t.uuid === material.map); - if (textureData && textureData.image && json.images) { - const image = json.images.find((img) => img.uuid === textureData.image); - // Store image path or data for later processing - texture = { - uuid: textureData.uuid, - image: image, - }; - } - } - } - - // Extract start tile index - const startTileIndex = _extractConstantValue(ps.startTileIndex) ?? 0; - - // Extract material type from material if available - // In quarks, materials can be MeshBasicMaterial or MeshStandardMaterial - let materialType = "MeshStandardMaterial"; // Default - if (ps.material && json.materials) { - const material = json.materials.find((m) => m.uuid === ps.material); - if (material && material.type) { - // Map quarks material types to our format - const materialTypeMap: Record = { - MeshBasicMaterial: "MeshBasicMaterial", - MeshStandardMaterial: "MeshStandardMaterial", - // Fallback for other material types - "MeshLambertMaterial": "MeshStandardMaterial", - "MeshPhongMaterial": "MeshStandardMaterial", - }; - materialType = materialTypeMap[material.type] || "MeshStandardMaterial"; - } - } - - return { - renderMode, - worldSpace: ps.worldSpace ?? false, - material: ps.material ? { uuid: ps.material } : null, - materialType, - transparent: true, - opacity: 1.0, - side: "Double", - blending: "Add", - color: new Color4(1, 1, 1, 1), - renderOrder: ps.renderOrder ?? 0, - uvTile: { - column: ps.uTileCount ?? 1, - row: ps.vTileCount ?? 1, - startTileIndex, - blendTiles: ps.blendTiles ?? false, - }, - texture, - meshPath: ps.instancingGeometry || null, // Store geometry UUID for now - softParticles: ps.softParticles ?? false, - }; -} - -/** - * Converts quarks behavior to FX editor format - */ -function _convertBehavior(behavior: IQuarksBehavior): any { - const converted: any = {}; - - switch (behavior.type) { - case "ForceOverLife": - const x = _convertQuarksValueToFunction(behavior.x) || { - functionType: "ConstantValue", - data: { value: 0 }, - }; - const y = _convertQuarksValueToFunction(behavior.y) || { - functionType: "ConstantValue", - data: { value: 0 }, - }; - const z = _convertQuarksValueToFunction(behavior.z) || { - functionType: "ConstantValue", - data: { value: 0 }, - }; - return { - force: { - functionType: "Vector3Function", - data: { - x, - y, - z, - }, - }, - }; - - case "SizeOverLife": - // Convert size value using _convertQuarksValueToFunction - const sizeFunction = _convertQuarksValueToFunction(behavior.size) || { - functionType: "ConstantValue", - data: { value: 1 }, - }; - return { - size: sizeFunction, - }; - - case "RotationOverLife": - const angularVelocity = _convertQuarksValueToFunction(behavior.angularVelocity) || { - functionType: "IntervalValue", - data: { min: 0, max: 0 }, - }; - return { - angularVelocity, - }; - - case "ColorOverLife": - // Convert color function from quarks format to our format - const colorFunction = _convertQuarksColorToColorFunction(behavior.color) || { - colorFunctionType: "ConstantColor", - data: { color: new Color4(1, 1, 1, 1) }, - }; - return { - color: colorFunction, - }; - - case "ColorBySpeed": - // Convert color function from quarks format to our format - const colorBySpeedFunction = _convertQuarksColorToColorFunction(behavior.color) || { - colorFunctionType: "ConstantColor", - data: { color: new Color4(1, 1, 1, 1) }, - }; - return { - color: colorBySpeedFunction, - }; - - case "ApplyForce": - const magnitude = _convertQuarksValueToFunction(behavior.magnitude) || { - functionType: "ConstantValue", - data: { value: 1 }, - }; - return { - magnitude, - direction: behavior.direction - ? new Vector3(behavior.direction.x || 0, behavior.direction.y || 0, behavior.direction.z || 0) - : new Vector3(0, 1, 0), - }; - - case "Noise": - const frequency = _convertQuarksValueToFunction(behavior.frequency) || { - functionType: "ConstantValue", - data: { value: 1 }, - }; - const power = _convertQuarksValueToFunction(behavior.power) || { - functionType: "ConstantValue", - data: { value: 1 }, - }; - const positionAmount = _convertQuarksValueToFunction(behavior.positionAmount) || { - functionType: "ConstantValue", - data: { value: 1 }, - }; - const rotationAmount = _convertQuarksValueToFunction(behavior.rotationAmount) || { - functionType: "ConstantValue", - data: { value: 1 }, - }; - return { - frequency, - power, - positionAmount, - rotationAmount, - }; - - case "GravityForce": - const gravity = _convertQuarksValueToFunction(behavior.gravity) || { - functionType: "ConstantValue", - data: { value: -9.81 }, - }; - return { - gravity, - }; - - case "TurbulenceField": - const strength = _convertQuarksValueToFunction(behavior.strength) || { - functionType: "ConstantValue", - data: { value: 1 }, - }; - const size = _convertQuarksValueToFunction(behavior.size) || { - functionType: "ConstantValue", - data: { value: 1 }, - }; - return { - strength, - size, - }; - - default: - // Copy all properties as-is for unknown behaviors - Object.keys(behavior).forEach((key) => { - if (key !== "type") { - converted[key] = behavior[key]; - } - }); - return converted; - } -} - -/** - * Converts quarks behavior type name to FX editor format - */ -function _convertBehaviorType(quarksType: string): string { - const typeMap: Record = { - ForceOverLife: "ForceOverLife", - SizeOverLife: "SizeOverLife", - RotationOverLife: "RotationOverLife", - ColorOverLife: "ColorOverLife", - ColorBySpeed: "ColorBySpeed", - ApplyForce: "ApplyForce", - Noise: "Noise", - GravityForce: "GravityForce", - TurbulenceField: "TurbulenceField", - }; - - return typeMap[quarksType] || quarksType; -} - -/** - * Converts quarks value to function format - */ -function _convertQuarksValueToFunction(value?: IQuarksValue): any | null { - if (!value) { - return null; - } - - if (value.type === "ConstantValue" && value.value !== undefined) { - return { - functionType: "ConstantValue", - data: { value: value.value }, - }; - } - - if (value.type === "IntervalValue" && value.a !== undefined && value.b !== undefined) { - return { - functionType: "IntervalValue", - data: { min: value.a, max: value.b }, - }; - } - - if (value.type === "PiecewiseBezier" && value.functions && value.functions.length > 0) { - // Use the first function segment - const firstSegment = value.functions[0]; - return { - functionType: "PiecewiseBezier", - data: { - function: firstSegment.function || { p0: 0, p1: 1.0 / 3, p2: (1.0 / 3) * 2, p3: 1 }, - }, - }; - } - - return null; -} - -/** - * Converts quarks color to color function format - */ -function _convertQuarksColorToColorFunction(color?: IQuarksColor): any | null { - if (!color) { - return null; - } - - if (color.type === "ConstantColor" && color.color && typeof color.color === "object" && "r" in color.color) { - const colorObj = color.color as { r: number; g: number; b: number; a?: number }; - return { - colorFunctionType: "ConstantColor", - data: { - color: new Color4(colorObj.r ?? 1, colorObj.g ?? 1, colorObj.b ?? 1, colorObj.a ?? 1), - }, - }; - } - - if (color.type === "ColorRange" && color.color1 && color.color2) { - return { - colorFunctionType: "ColorRange", - data: { - colorA: new Color4(color.color1.r ?? 0, color.color1.g ?? 0, color.color1.b ?? 0, color.color1.a ?? 1), - colorB: new Color4(color.color2.r ?? 1, color.color2.g ?? 1, color.color2.b ?? 1, color.color2.a ?? 1), - }, - }; - } - - if (color.type === "Gradient") { - // Convert quarks Gradient to our Gradient format - // Gradient has color and alpha as CLinearFunction objects with keys - const colorKeys: any[] = []; - const alphaKeys: any[] = []; - - // Extract color keys from color.color.keys (CLinearFunction) - if (color.color && typeof color.color === "object" && "keys" in color.color && Array.isArray(color.color.keys)) { - colorKeys.push( - ...color.color.keys.map((key: any) => { - if (typeof key.value === "object" && key.value.r !== undefined) { - return { - color: new Vector3(key.value.r ?? 0, key.value.g ?? 0, key.value.b ?? 0), - position: key.pos ?? 0, - }; - } - return { - color: new Vector3(0, 0, 0), - position: key.pos ?? 0, - }; - }) - ); - } - - // Extract alpha keys from color.alpha.keys (CLinearFunction) - if (color.alpha && typeof color.alpha === "object" && "keys" in color.alpha && Array.isArray(color.alpha.keys)) { - alphaKeys.push( - ...color.alpha.keys.map((key: any) => ({ - value: typeof key.value === "number" ? key.value : 1, - position: key.pos ?? 0, - })) - ); - } - - // Fallback to default if no keys found - if (colorKeys.length === 0) { - colorKeys.push( - { color: new Vector3(0, 0, 0), position: 0 }, - { color: new Vector3(1, 1, 1), position: 1 } - ); - } - if (alphaKeys.length === 0) { - alphaKeys.push( - { value: 1, position: 0 }, - { value: 1, position: 1 } - ); - } - - const convertedGradient: any = { - colorFunctionType: "Gradient", - data: { - colorKeys, - alphaKeys, - }, - }; - return convertedGradient; - } - - // RandomColor - similar to ColorRange but selects random from range - if (color.type === "RandomColor" && color.color1 && color.color2) { - return { - colorFunctionType: "RandomColor", - data: { - colorA: new Color4(color.color1.r ?? 0, color.color1.g ?? 0, color.color1.b ?? 0, color.color1.a ?? 1), - colorB: new Color4(color.color2.r ?? 1, color.color2.g ?? 1, color.color2.b ?? 1, color.color2.a ?? 1), - }, - }; - } - - return null; -} - -/** - * Extracts constant value from quarks value - */ -function _extractConstantValue(value?: IQuarksValue): number | null { - if (!value) { - return null; - } - if (value.type === "ConstantValue" && value.value !== undefined) { - return value.value; - } - return null; -} diff --git a/editor/src/editor/windows/fx-editor/particle-generator.ts b/editor/src/editor/windows/fx-editor/particle-generator.ts deleted file mode 100644 index aedff6df1..000000000 --- a/editor/src/editor/windows/fx-editor/particle-generator.ts +++ /dev/null @@ -1,191 +0,0 @@ -import { - Scene, - ParticleSystem, - SolidParticleSystem, - AbstractMesh, - MeshBuilder, - Vector3, - Color4, - Color3, - IParticleEmitterType, - BoxParticleEmitter, - ConeParticleEmitter, - CylinderParticleEmitter, - SphereParticleEmitter, - HemisphericParticleEmitter, - PointParticleEmitter, - MeshParticleEmitter, - Tools, -} from "babylonjs"; -import { IFXParticleData } from "./properties/types"; - -/** - * Creates a particle system or solid particle system from FX particle data - */ -export function createParticleSystemFromData(scene: Scene, particleData: IFXParticleData, emitter?: AbstractMesh): ParticleSystem | SolidParticleSystem | null { - if (!emitter) { - // Create default emitter if none provided - emitter = MeshBuilder.CreateBox("emitter", { size: 0.1 }, scene); - emitter.isVisible = false; - } - - // Apply transform - emitter.position = particleData.position.clone(); - emitter.rotation = particleData.rotation.clone(); - emitter.scaling = particleData.scale.clone(); - emitter.setEnabled(particleData.visibility); - - // Check if we need SolidParticleSystem (for Mesh render mode) - if (particleData.particleRenderer.renderMode === "Mesh") { - return createSolidParticleSystemFromData(scene, particleData, emitter); - } - - // Create regular ParticleSystem - const capacity = 1000; // Default capacity - const particleSystem = new ParticleSystem(particleData.name || "Particle System", capacity, scene); - particleSystem.id = particleData.id || Tools.RandomId(); - particleSystem.emitter = emitter; - - // Configure emitter shape - configureEmitterShape(particleSystem, particleData.emitterShape); - - // Configure emission - particleSystem.targetStopDuration = particleData.emission.duration; - particleSystem.manualEmitCount = particleData.emission.emitOverTime; - particleSystem.emitRate = particleData.emission.emitOverTime; - particleSystem.minEmitPower = particleData.particleInitialization.startSpeed.min; - particleSystem.maxEmitPower = particleData.particleInitialization.startSpeed.max; - - // Configure particle initialization - particleSystem.minLifeTime = particleData.particleInitialization.startLife.min; - particleSystem.maxLifeTime = particleData.particleInitialization.startLife.max; - particleSystem.minSize = particleData.particleInitialization.startSize.min; - particleSystem.maxSize = particleData.particleInitialization.startSize.max; - particleSystem.minEmitBox = particleData.emitterShape.minEmitBox?.clone() || new Vector3(-0.5, -0.5, -0.5); - particleSystem.maxEmitBox = particleData.emitterShape.maxEmitBox?.clone() || new Vector3(0.5, 0.5, 0.5); - - // Configure color - const startColor = particleData.particleInitialization.startColor; - particleSystem.color1 = new Color4(startColor.r, startColor.g, startColor.b, startColor.a); - particleSystem.color2 = new Color4(startColor.r, startColor.g, startColor.b, startColor.a); - particleSystem.colorDead = new Color4(startColor.r, startColor.g, startColor.b, 0); - - // Configure renderer - if (particleData.particleRenderer.texture) { - particleSystem.particleTexture = particleData.particleRenderer.texture; - } - - // Configure looping - particleSystem.targetStopDuration = particleData.emission.looping ? undefined : particleData.emission.duration; - - // TODO: Apply behaviors - // TODO: Apply bursts - - return particleSystem; -} - -/** - * Creates a SolidParticleSystem for Mesh render mode - */ -function createSolidParticleSystemFromData(scene: Scene, particleData: IFXParticleData, emitter: AbstractMesh): SolidParticleSystem | null { - // For SolidParticleSystem, we need a mesh to use as the particle shape - let particleMesh: AbstractMesh | null = null; - - if (particleData.particleRenderer.meshPath) { - // TODO: Load mesh from path - // For now, create a default box - particleMesh = MeshBuilder.CreateBox("particleMesh", { size: 0.1 }, scene); - } else { - // Default particle mesh - particleMesh = MeshBuilder.CreateBox("particleMesh", { size: 0.1 }, scene); - } - - if (!particleMesh) { - return null; - } - - const sps = new SolidParticleSystem("SolidParticleSystem", scene); - sps.addShape(particleMesh, 1000); // Add shape with capacity - const mesh = sps.buildMesh(); - - // Set emitter - mesh.position = particleData.position.clone(); - mesh.rotation = particleData.rotation.clone(); - mesh.scaling = particleData.scale.clone(); - mesh.setEnabled(particleData.visibility); - - // TODO: Configure SPS properties based on particleData - // TODO: Apply behaviors - // TODO: Apply emission settings - - return sps; -} - -/** - * Configures the emitter shape for a particle system - */ -function configureEmitterShape(particleSystem: ParticleSystem, emitterShape: IFXParticleData["emitterShape"]): void { - let emitter: IParticleEmitterType; - - switch (emitterShape.shape) { - case "Box": - emitter = new BoxParticleEmitter(); - if (emitterShape.minEmitBox && emitterShape.maxEmitBox) { - (emitter as BoxParticleEmitter).minEmitBox = emitterShape.minEmitBox.clone(); - (emitter as BoxParticleEmitter).maxEmitBox = emitterShape.maxEmitBox.clone(); - } - if (emitterShape.direction1 && emitterShape.direction2) { - particleSystem.direction1 = emitterShape.direction1.clone(); - particleSystem.direction2 = emitterShape.direction2.clone(); - } - break; - - case "Cone": - emitter = new ConeParticleEmitter(emitterShape.radius || 1.0, emitterShape.angle || 0.785398); - // TODO: Configure cone-specific properties - break; - - case "Cylinder": - emitter = new CylinderParticleEmitter(emitterShape.radius || 1.0, emitterShape.height || 1.0); - // TODO: Configure cylinder-specific properties - break; - - case "Sphere": - emitter = new SphereParticleEmitter(emitterShape.radius || 1.0); - // TODO: Configure sphere-specific properties - break; - - case "Hemispheric": - emitter = new HemisphericParticleEmitter(emitterShape.radius || 1.0); - // TODO: Configure hemispheric-specific properties - break; - - case "Point": - emitter = new PointParticleEmitter(); - break; - - case "Mesh": - // TODO: Load mesh and create MeshParticleEmitter - emitter = new PointParticleEmitter(); // Fallback - break; - - default: - emitter = new BoxParticleEmitter(); - break; - } - - particleSystem.particleEmitterType = emitter; -} - -/** - * Creates an empty mesh for grouping particles - */ -export function createGroupMesh(scene: Scene, name: string, position: Vector3 = Vector3.Zero(), rotation: Vector3 = Vector3.Zero(), scale: Vector3 = new Vector3(1, 1, 1)): AbstractMesh { - const mesh = MeshBuilder.CreateBox(name, { size: 0.01 }, scene); - mesh.isVisible = false; - mesh.position = position; - mesh.rotation = rotation; - mesh.scaling = scale; - return mesh; -} - diff --git a/editor/src/editor/windows/fx-editor/properties.tsx b/editor/src/editor/windows/fx-editor/properties.tsx index 7551a889a..254344d40 100644 --- a/editor/src/editor/windows/fx-editor/properties.tsx +++ b/editor/src/editor/windows/fx-editor/properties.tsx @@ -10,11 +10,12 @@ import { FXEditorEmissionProperties } from "./properties/emission"; import { FXEditorParticleInitializationProperties } from "./properties/particle-initialization"; import { FXEditorBehaviorsProperties } from "./properties/behaviors"; import { IFXParticleData, IFXGroupData } from "./properties/types"; +import { IFXEditor } from "."; export interface IFXEditorPropertiesProps { filePath: string | null; selectedNodeId: string | number | null; - scene?: Scene; + editor: IFXEditor; onNameChanged?: () => void; getOrCreateParticleData: (nodeId: string | number) => IFXParticleData; getOrCreateGroupData: (nodeId: string | number) => IFXGroupData; @@ -108,7 +109,7 @@ export class FXEditorProperties extends Component - this.forceUpdate()} /> + this.forceUpdate()} /> diff --git a/editor/src/editor/windows/fx-editor/properties/particle-renderer.tsx b/editor/src/editor/windows/fx-editor/properties/particle-renderer.tsx index 76446fb58..fe759fe04 100644 --- a/editor/src/editor/windows/fx-editor/properties/particle-renderer.tsx +++ b/editor/src/editor/windows/fx-editor/properties/particle-renderer.tsx @@ -14,10 +14,11 @@ import { AiOutlineClose } from "react-icons/ai"; import { getProjectAssetsRootUrl } from "../../../../project/configuration"; import { IFXParticleData } from "./types"; +import { IFXEditor } from ".."; export interface IFXEditorParticleRendererPropertiesProps { particleData: IFXParticleData; - scene?: Scene; + editor: IFXEditor; onChange: () => void; } @@ -109,13 +110,13 @@ export class FXEditorParticleRendererProperties extends Component this.props.onChange()} />; + return this.props.onChange()} />; } private _getRenderModeSpecificProperties(renderMode: string): ReactNode { diff --git a/editor/src/editor/windows/fx-editor/t-318 (1).json b/editor/src/editor/windows/fx-editor/t-318 (1).json deleted file mode 100644 index dfcb49719..000000000 --- a/editor/src/editor/windows/fx-editor/t-318 (1).json +++ /dev/null @@ -1 +0,0 @@ -{"metadata":{"version":4.6,"type":"Object","generator":"Object3D.toJSON"},"geometries":[{"uuid":"780917d8-bd1b-4d63-8aca-f79e3211f964","type":"PlaneGeometry","name":"PlaneGeometry","width":1,"height":1,"widthSegments":1,"heightSegments":1},{"uuid":"f40b6ee0-aa01-46e0-b05a-d938b54eec83","type":"BufferGeometry","name":"GlowCircleEmitter_geometry","data":{"attributes":{"position":{"itemSize":3,"type":"Float32Array","array":[0.41758671402931213,0.08306316286325455,0.10689251124858856,0.42576777935028076,-1.2535783966427516e-8,0.10689251124858856,0.3199999928474426,0,0,0.3199999928474426,0,0,0.3138512670993805,0.062428902834653854,0,0.41758671402931213,0.08306316286325455,0.10689251124858856,0.39335811138153076,0.16293425858020782,0.10689251124858856,0.41758671402931213,0.08306316286325455,0.10689251124858856,0.3138512670993805,0.062428902834653854,0,0.3138512670993805,0.062428902834653854,0,0.2956414520740509,0.12245870381593704,0,0.39335811138153076,0.16293425858020782,0.10689251124858856,0.35401293635368347,0.23654387891292572,0.10689251124858856,0.39335811138153076,0.16293425858020782,0.10689251124858856,0.2956414520740509,0.12245870381593704,0,0.2956414520740509,0.12245870381593704,0,0.26607027649879456,0.17778247594833374,0,0.35401293635368347,0.23654387891292572,0.10689251124858856,0.3010632395744324,0.30106326937675476,0.10689251124858856,0.35401293635368347,0.23654387891292572,0.10689251124858856,0.26607027649879456,0.17778247594833374,0,0.26607027649879456,0.17778247594833374,0,0.22627416253089905,0.22627416253089905,0,0.3010632395744324,0.30106326937675476,0.10689251124858856,0.23654384911060333,0.35401299595832825,0.10689251124858856,0.3010632395744324,0.30106326937675476,0.10689251124858856,0.22627416253089905,0.22627416253089905,0,0.22627416253089905,0.22627416253089905,0,0.17778246104717255,0.26607027649879456,0,0.23654384911060333,0.35401299595832825,0.10689251124858856,0.16293422877788544,0.39335811138153076,0.10689251124858856,0.23654384911060333,0.35401299595832825,0.10689251124858856,0.17778246104717255,0.26607027649879456,0,0.17778246104717255,0.26607027649879456,0,0.12245869636535645,0.2956414520740509,0,0.16293422877788544,0.39335811138153076,0.10689251124858856,0.08306317031383514,0.41758671402931213,0.10689251124858856,0.16293422877788544,0.39335811138153076,0.10689251124858856,0.12245869636535645,0.2956414520740509,0,0.12245869636535645,0.2956414520740509,0,0.06242891401052475,0.3138512670993805,0,0.08306317031383514,0.41758671402931213,0.10689251124858856,2.0868840877596995e-8,0.42576777935028076,0.10689251124858856,0.08306317031383514,0.41758671402931213,0.10689251124858856,0.06242891401052475,0.3138512670993805,0,0.06242891401052475,0.3138512670993805,0,2.415932875976523e-8,0.3199999928474426,0,2.0868840877596995e-8,0.42576777935028076,0.10689251124858856,-0.08306313306093216,0.4175867438316345,0.10689251124858856,2.0868840877596995e-8,0.42576777935028076,0.10689251124858856,2.415932875976523e-8,0.3199999928474426,0,2.415932875976523e-8,0.3199999928474426,0,-0.06242886558175087,0.3138512969017029,0,-0.08306313306093216,0.4175867438316345,0.10689251124858856,-0.16293422877788544,0.39335814118385315,0.10689251124858856,-0.08306313306093216,0.4175867438316345,0.10689251124858856,-0.06242886558175087,0.3138512969017029,0,-0.06242886558175087,0.3138512969017029,0,-0.12245865166187286,0.2956414520740509,0,-0.16293422877788544,0.39335814118385315,0.10689251124858856,-0.23654387891292572,0.35401299595832825,0.10689251124858856,-0.16293422877788544,0.39335814118385315,0.10689251124858856,-0.12245865166187286,0.2956414520740509,0,-0.12245865166187286,0.2956414520740509,0,-0.17778246104717255,0.26607027649879456,0,-0.23654387891292572,0.35401299595832825,0.10689251124858856,-0.30106329917907715,0.30106326937675476,0.10689251124858856,-0.23654387891292572,0.35401299595832825,0.10689251124858856,-0.17778246104717255,0.26607027649879456,0,-0.17778246104717255,0.26607027649879456,0,-0.22627416253089905,0.22627416253089905,0,-0.30106329917907715,0.30106326937675476,0.10689251124858856,-0.35401299595832825,0.23654386401176453,0.10689251124858856,-0.30106329917907715,0.30106326937675476,0.10689251124858856,-0.22627416253089905,0.22627416253089905,0,-0.22627416253089905,0.22627416253089905,0,-0.26607027649879456,0.17778246104717255,0,-0.35401299595832825,0.23654386401176453,0.10689251124858856,-0.39335814118385315,0.16293418407440186,0.10689251124858856,-0.35401299595832825,0.23654386401176453,0.10689251124858856,-0.26607027649879456,0.17778246104717255,0,-0.26607027649879456,0.17778246104717255,0,-0.2956414818763733,0.12245865166187286,0,-0.39335814118385315,0.16293418407440186,0.10689251124858856,-0.4175867438316345,0.08306305855512619,0.10689251124858856,-0.39335814118385315,0.16293418407440186,0.10689251124858856,-0.2956414818763733,0.12245865166187286,0,-0.2956414818763733,0.12245865166187286,0,-0.3138512969017029,0.062428828328847885,0,-0.4175867438316345,0.08306305855512619,0.10689251124858856,-0.42576777935028076,-1.5126852304092608e-7,0.10689251124858856,-0.4175867438316345,0.08306305855512619,0.10689251124858856,-0.3138512969017029,0.062428828328847885,0,-0.3138512969017029,0.062428828328847885,0,-0.3199999928474426,-1.0426924035300544e-7,0,-0.42576777935028076,-1.5126852304092608e-7,0.10689251124858856,-0.41758671402931213,-0.08306335657835007,0.10689251124858856,-0.42576777935028076,-1.5126852304092608e-7,0.10689251124858856,-0.3199999928474426,-1.0426924035300544e-7,0,-0.3199999928474426,-1.0426924035300544e-7,0,-0.3138512670993805,-0.0624290332198143,0,-0.41758671402931213,-0.08306335657835007,0.10689251124858856,-0.393358051776886,-0.16293445229530334,0.10689251124858856,-0.41758671402931213,-0.08306335657835007,0.10689251124858856,-0.3138512670993805,-0.0624290332198143,0,-0.3138512670993805,-0.0624290332198143,0,-0.29564139246940613,-0.12245883792638779,0,-0.393358051776886,-0.16293445229530334,0.10689251124858856,-0.35401278734207153,-0.23654408752918243,0.10689251124858856,-0.393358051776886,-0.16293445229530334,0.10689251124858856,-0.29564139246940613,-0.12245883792638779,0,-0.29564139246940613,-0.12245883792638779,0,-0.2660701870918274,-0.17778262495994568,0,-0.35401278734207153,-0.23654408752918243,0.10689251124858856,-0.30106309056282043,-0.3010634481906891,0.10689251124858856,-0.35401278734207153,-0.23654408752918243,0.10689251124858856,-0.2660701870918274,-0.17778262495994568,0,-0.2660701870918274,-0.17778262495994568,0,-0.2262740284204483,-0.226274311542511,0,-0.30106309056282043,-0.3010634481906891,0.10689251124858856,-0.2365436553955078,-0.3540131449699402,0.10689251124858856,-0.30106309056282043,-0.3010634481906891,0.10689251124858856,-0.2262740284204483,-0.226274311542511,0,-0.2262740284204483,-0.226274311542511,0,-0.17778228223323822,-0.2660703957080841,0,-0.2365436553955078,-0.3540131449699402,0.10689251124858856,-0.16293397545814514,-0.3933582603931427,0.10689251124858856,-0.2365436553955078,-0.3540131449699402,0.10689251124858856,-0.17778228223323822,-0.2660703957080841,0,-0.17778228223323822,-0.2660703957080841,0,-0.12245845794677734,-0.29564154148101807,0,-0.16293397545814514,-0.3933582603931427,0.10689251124858856,-0.08306281268596649,-0.4175868332386017,0.10689251124858856,-0.16293397545814514,-0.3933582603931427,0.10689251124858856,-0.12245845794677734,-0.29564154148101807,0,-0.12245845794677734,-0.29564154148101807,0,-0.06242862716317177,-0.31385132670402527,0,-0.08306281268596649,-0.4175868332386017,0.10689251124858856,3.9984527688829985e-7,-0.42576777935028076,0.10689251124858856,-0.08306281268596649,-0.4175868332386017,0.10689251124858856,-0.06242862716317177,-0.31385132670402527,0,-0.06242862716317177,-0.31385132670402527,0,3.0899172998033464e-7,-0.3199999928474426,0,3.9984527688829985e-7,-0.42576777935028076,0.10689251124858856,0.08306359499692917,-0.41758668422698975,0.10689251124858856,3.9984527688829985e-7,-0.42576777935028076,0.10689251124858856,3.0899172998033464e-7,-0.3199999928474426,0,3.0899172998033464e-7,-0.3199999928474426,0,0.06242923438549042,-0.3138512372970581,0,0.08306359499692917,-0.41758668422698975,0.10689251124858856,0.16293466091156006,-0.39335793256759644,0.10689251124858856,0.08306359499692917,-0.41758668422698975,0.10689251124858856,0.06242923438549042,-0.3138512372970581,0,0.06242923438549042,-0.3138512372970581,0,0.1224590316414833,-0.29564130306243896,0,0.16293466091156006,-0.39335793256759644,0.10689251124858856,0.23654431104660034,-0.3540126383304596,0.10689251124858856,0.16293466091156006,-0.39335793256759644,0.10689251124858856,0.1224590316414833,-0.29564130306243896,0,0.1224590316414833,-0.29564130306243896,0,0.17778280377388,-0.26607006788253784,0,0.23654431104660034,-0.3540126383304596,0.10689251124858856,0.3010636270046234,-0.3010628819465637,0.10689251124858856,0.23654431104660034,-0.3540126383304596,0.10689251124858856,0.17778280377388,-0.26607006788253784,0,0.17778280377388,-0.26607006788253784,0,0.22627444565296173,-0.22627387940883636,0,0.3010636270046234,-0.3010628819465637,0.10689251124858856,0.3540132939815521,-0.23654340207576752,0.10689251124858856,0.3010636270046234,-0.3010628819465637,0.10689251124858856,0.22627444565296173,-0.22627387940883636,0,0.22627444565296173,-0.22627387940883636,0,0.26607051491737366,-0.1777821183204651,0,0.3540132939815521,-0.23654340207576752,0.10689251124858856,0.39335834980010986,-0.16293370723724365,0.10689251124858856,0.3540132939815521,-0.23654340207576752,0.10689251124858856,0.26607051491737366,-0.1777821183204651,0,0.26607051491737366,-0.1777821183204651,0,0.29564163088798523,-0.12245826423168182,0,0.39335834980010986,-0.16293370723724365,0.10689251124858856,0.4175868630409241,-0.083062544465065,0.10689251124858856,0.39335834980010986,-0.16293370723724365,0.10689251124858856,0.29564163088798523,-0.12245826423168182,0,0.29564163088798523,-0.12245826423168182,0,0.31385138630867004,-0.06242842227220535,0,0.4175868630409241,-0.083062544465065,0.10689251124858856,0.42576777935028076,-1.2535783966427516e-8,0.10689251124858856,0.4175868630409241,-0.083062544465065,0.10689251124858856,0.31385138630867004,-0.06242842227220535,0,0.31385138630867004,-0.06242842227220535,0,0.3199999928474426,0,0,0.42576777935028076,-1.2535783966427516e-8,0.10689251124858856,0.4175865948200226,0.08306316286325455,0.10689251124858856,0.31385117769241333,0.062428902834653854,0,0.3199998736381531,0,0,0.3199998736381531,0,0,0.4257676303386688,-1.2535783966427516e-8,0.10689251124858856,0.4175865948200226,0.08306316286325455,0.10689251124858856,0.3933579623699188,0.16293425858020782,0.10689251124858856,0.29564133286476135,0.12245870381593704,0,0.31385117769241333,0.062428902834653854,0,0.31385117769241333,0.062428902834653854,0,0.4175865948200226,0.08306316286325455,0.10689251124858856,0.3933579623699188,0.16293425858020782,0.10689251124858856,0.35401275753974915,0.23654387891292572,0.10689251124858856,0.2660701274871826,0.17778247594833374,0,0.29564133286476135,0.12245870381593704,0,0.29564133286476135,0.12245870381593704,0,0.3933579623699188,0.16293425858020782,0.10689251124858856,0.35401275753974915,0.23654387891292572,0.10689251124858856,0.30106306076049805,0.30106326937675476,0.10689251124858856,0.2262740284204483,0.22627416253089905,0,0.2660701274871826,0.17778247594833374,0,0.2660701274871826,0.17778247594833374,0,0.35401275753974915,0.23654387891292572,0.10689251124858856,0.30106306076049805,0.30106326937675476,0.10689251124858856,0.2365437150001526,0.35401299595832825,0.10689251124858856,0.177782341837883,0.26607027649879456,0,0.2262740284204483,0.22627416253089905,0,0.2262740284204483,0.22627416253089905,0,0.30106306076049805,0.30106326937675476,0.10689251124858856,0.2365437150001526,0.35401299595832825,0.10689251124858856,0.1629340499639511,0.39335811138153076,0.10689251124858856,0.1224585697054863,0.2956414520740509,0,0.177782341837883,0.26607027649879456,0,0.177782341837883,0.26607027649879456,0,0.2365437150001526,0.35401299595832825,0.10689251124858856,0.1629340499639511,0.39335811138153076,0.10689251124858856,0.08306301385164261,0.41758671402931213,0.10689251124858856,0.0624287948012352,0.3138512670993805,0,0.1224585697054863,0.2956414520740509,0,0.1224585697054863,0.2956414520740509,0,0.1629340499639511,0.39335811138153076,0.10689251124858856,0.08306301385164261,0.41758671402931213,0.10689251124858856,-1.3816443811265344e-7,0.42576777935028076,0.10689251124858856,-9.536743306171047e-8,0.3199999928474426,0,0.0624287948012352,0.3138512670993805,0,0.0624287948012352,0.3138512670993805,0,0.08306301385164261,0.41758671402931213,0.10689251124858856,-1.3816443811265344e-7,0.42576777935028076,0.10689251124858856,-0.0830632895231247,0.4175867438316345,0.10689251124858856,-0.06242898479104042,0.3138512969017029,0,-9.536743306171047e-8,0.3199999928474426,0,-9.536743306171047e-8,0.3199999928474426,0,-1.3816443811265344e-7,0.42576777935028076,0.10689251124858856,-0.0830632895231247,0.4175867438316345,0.10689251124858856,-0.16293437778949738,0.39335814118385315,0.10689251124858856,-0.12245876342058182,0.2956414520740509,0,-0.06242898479104042,0.3138512969017029,0,-0.06242898479104042,0.3138512969017029,0,-0.0830632895231247,0.4175867438316345,0.10689251124858856,-0.16293437778949738,0.39335814118385315,0.10689251124858856,-0.23654407262802124,0.35401299595832825,0.10689251124858856,-0.1777825951576233,0.26607027649879456,0,-0.12245876342058182,0.2956414520740509,0,-0.12245876342058182,0.2956414520740509,0,-0.16293437778949738,0.39335814118385315,0.10689251124858856,-0.23654407262802124,0.35401299595832825,0.10689251124858856,-0.3010634481906891,0.30106326937675476,0.10689251124858856,-0.2262742966413498,0.22627416253089905,0,-0.1777825951576233,0.26607027649879456,0,-0.1777825951576233,0.26607027649879456,0,-0.23654407262802124,0.35401299595832825,0.10689251124858856,-0.3010634481906891,0.30106326937675476,0.10689251124858856,-0.3540131449699402,0.23654386401176453,0.10689251124858856,-0.2660703957080841,0.17778246104717255,0,-0.2262742966413498,0.22627416253089905,0,-0.2262742966413498,0.22627416253089905,0,-0.3010634481906891,0.30106326937675476,0.10689251124858856,-0.3540131449699402,0.23654386401176453,0.10689251124858856,-0.3933583199977875,0.16293418407440186,0.10689251124858856,-0.29564160108566284,0.12245865166187286,0,-0.2660703957080841,0.17778246104717255,0,-0.2660703957080841,0.17778246104717255,0,-0.3540131449699402,0.23654386401176453,0.10689251124858856,-0.3933583199977875,0.16293418407440186,0.10689251124858856,-0.41758692264556885,0.08306305855512619,0.10689251124858856,-0.3138514459133148,0.062428828328847885,0,-0.29564160108566284,0.12245865166187286,0,-0.29564160108566284,0.12245865166187286,0,-0.3933583199977875,0.16293418407440186,0.10689251124858856,-0.41758692264556885,0.08306305855512619,0.10689251124858856,-0.4257678985595703,-1.5126852304092608e-7,0.10689251124858856,-0.3200001120567322,-1.0426924035300544e-7,0,-0.3138514459133148,0.062428828328847885,0,-0.3138514459133148,0.062428828328847885,0,-0.41758692264556885,0.08306305855512619,0.10689251124858856,-0.4257678985595703,-1.5126852304092608e-7,0.10689251124858856,-0.41758689284324646,-0.08306335657835007,0.10689251124858856,-0.31385138630867004,-0.0624290332198143,0,-0.3200001120567322,-1.0426924035300544e-7,0,-0.3200001120567322,-1.0426924035300544e-7,0,-0.4257678985595703,-1.5126852304092608e-7,0.10689251124858856,-0.41758689284324646,-0.08306335657835007,0.10689251124858856,-0.3933582305908203,-0.16293445229530334,0.10689251124858856,-0.2956415116786957,-0.12245883792638779,0,-0.31385138630867004,-0.0624290332198143,0,-0.31385138630867004,-0.0624290332198143,0,-0.41758689284324646,-0.08306335657835007,0.10689251124858856,-0.3933582305908203,-0.16293445229530334,0.10689251124858856,-0.35401299595832825,-0.23654408752918243,0.10689251124858856,-0.26607027649879456,-0.17778262495994568,0,-0.2956415116786957,-0.12245883792638779,0,-0.2956415116786957,-0.12245883792638779,0,-0.3933582305908203,-0.16293445229530334,0.10689251124858856,-0.35401299595832825,-0.23654408752918243,0.10689251124858856,-0.3010632395744324,-0.3010634481906891,0.10689251124858856,-0.22627414762973785,-0.226274311542511,0,-0.26607027649879456,-0.17778262495994568,0,-0.26607027649879456,-0.17778262495994568,0,-0.35401299595832825,-0.23654408752918243,0.10689251124858856,-0.3010632395744324,-0.3010634481906891,0.10689251124858856,-0.23654380440711975,-0.3540131449699402,0.10689251124858856,-0.17778240144252777,-0.2660703957080841,0,-0.22627414762973785,-0.226274311542511,0,-0.22627414762973785,-0.226274311542511,0,-0.3010632395744324,-0.3010634481906891,0.10689251124858856,-0.23654380440711975,-0.3540131449699402,0.10689251124858856,-0.16293412446975708,-0.3933582603931427,0.10689251124858856,-0.1224585697054863,-0.29564154148101807,0,-0.17778240144252777,-0.2660703957080841,0,-0.17778240144252777,-0.2660703957080841,0,-0.23654380440711975,-0.3540131449699402,0.10689251124858856,-0.16293412446975708,-0.3933582603931427,0.10689251124858856,-0.08306297659873962,-0.4175868332386017,0.10689251124858856,-0.06242874637246132,-0.31385132670402527,0,-0.1224585697054863,-0.29564154148101807,0,-0.1224585697054863,-0.29564154148101807,0,-0.16293412446975708,-0.3933582603931427,0.10689251124858856,-0.08306297659873962,-0.4175868332386017,0.10689251124858856,2.4250164187833434e-7,-0.42576777935028076,0.10689251124858856,1.9073486612342094e-7,-0.3199999928474426,0,-0.06242874637246132,-0.31385132670402527,0,-0.06242874637246132,-0.31385132670402527,0,-0.08306297659873962,-0.4175868332386017,0.10689251124858856,2.4250164187833434e-7,-0.42576777935028076,0.10689251124858856,0.08306345343589783,-0.41758668422698975,0.10689251124858856,0.062429118901491165,-0.3138512372970581,0,1.9073486612342094e-7,-0.3199999928474426,0,1.9073486612342094e-7,-0.3199999928474426,0,2.4250164187833434e-7,-0.42576777935028076,0.10689251124858856,0.08306345343589783,-0.41758668422698975,0.10689251124858856,0.16293452680110931,-0.39335793256759644,0.10689251124858856,0.12245891243219376,-0.29564130306243896,0,0.062429118901491165,-0.3138512372970581,0,0.062429118901491165,-0.3138512372970581,0,0.08306345343589783,-0.41758668422698975,0.10689251124858856,0.16293452680110931,-0.39335793256759644,0.10689251124858856,0.2365441471338272,-0.3540126383304596,0.10689251124858856,0.17778268456459045,-0.26607006788253784,0,0.12245891243219376,-0.29564130306243896,0,0.12245891243219376,-0.29564130306243896,0,0.16293452680110931,-0.39335793256759644,0.10689251124858856,0.2365441471338272,-0.3540126383304596,0.10689251124858856,0.3010634779930115,-0.3010628819465637,0.10689251124858856,0.22627434134483337,-0.22627387940883636,0,0.17778268456459045,-0.26607006788253784,0,0.17778268456459045,-0.26607006788253784,0,0.2365441471338272,-0.3540126383304596,0.10689251124858856,0.3010634779930115,-0.3010628819465637,0.10689251124858856,0.3540131449699402,-0.23654340207576752,0.10689251124858856,0.2660703957080841,-0.1777821183204651,0,0.22627434134483337,-0.22627387940883636,0,0.22627434134483337,-0.22627387940883636,0,0.3010634779930115,-0.3010628819465637,0.10689251124858856,0.3540131449699402,-0.23654340207576752,0.10689251124858856,0.3933582305908203,-0.16293370723724365,0.10689251124858856,0.2956415116786957,-0.12245826423168182,0,0.2660703957080841,-0.1777821183204651,0,0.2660703957080841,-0.1777821183204651,0,0.3540131449699402,-0.23654340207576752,0.10689251124858856,0.3933582305908203,-0.16293370723724365,0.10689251124858856,0.41758668422698975,-0.083062544465065,0.10689251124858856,0.3138512372970581,-0.06242842227220535,0,0.2956415116786957,-0.12245826423168182,0,0.2956415116786957,-0.12245826423168182,0,0.3933582305908203,-0.16293370723724365,0.10689251124858856,0.41758668422698975,-0.083062544465065,0.10689251124858856,0.4257676303386688,-1.2535783966427516e-8,0.10689251124858856,0.3199998736381531,0,0,0.3138512372970581,-0.06242842227220535,0,0.3138512372970581,-0.06242842227220535,0,0.41758668422698975,-0.083062544465065,0.10689251124858856,0.4257676303386688,-1.2535783966427516e-8,0.10689251124858856],"normalized":false},"normal":{"itemSize":3,"type":"Float32Array","array":[0.6971781849861145,0.1386774480342865,-0.7033570408821106,0.7108367085456848,3.257474361362256e-7,-0.7033571004867554,0.71083664894104,3.6210147413839877e-7,-0.7033571600914001,0.71083664894104,3.6210147413839877e-7,-0.7033571600914001,0.6971781849861145,0.1386774480342865,-0.7033570408821106,0.6971781849861145,0.1386774480342865,-0.7033570408821106,0.6567274928092957,0.27202534675598145,-0.7033570408821106,0.6971781849861145,0.1386774480342865,-0.7033570408821106,0.6971781849861145,0.1386774480342865,-0.7033570408821106,0.6971781849861145,0.1386774480342865,-0.7033570408821106,0.6567275524139404,0.27202534675598145,-0.7033570408821106,0.6567274928092957,0.27202534675598145,-0.7033570408821106,0.5910391211509705,0.394919753074646,-0.7033570408821106,0.6567274928092957,0.27202534675598145,-0.7033570408821106,0.6567275524139404,0.27202534675598145,-0.7033570408821106,0.6567275524139404,0.27202534675598145,-0.7033570408821106,0.5910391211509705,0.3949197828769684,-0.7033570408821106,0.5910391211509705,0.394919753074646,-0.7033570408821106,0.5026374459266663,0.5026374459266663,-0.7033571004867554,0.5910391211509705,0.394919753074646,-0.7033570408821106,0.5910391211509705,0.3949197828769684,-0.7033570408821106,0.5910391211509705,0.3949197828769684,-0.7033570408821106,0.5026374459266663,0.5026374459266663,-0.7033570408821106,0.5026374459266663,0.5026374459266663,-0.7033571004867554,0.39491966366767883,0.5910391807556152,-0.7033571004867554,0.5026374459266663,0.5026374459266663,-0.7033571004867554,0.5026374459266663,0.5026374459266663,-0.7033570408821106,0.5026374459266663,0.5026374459266663,-0.7033570408821106,0.39491966366767883,0.5910391211509705,-0.7033571004867554,0.39491966366767883,0.5910391807556152,-0.7033571004867554,0.27202531695365906,0.6567276120185852,-0.7033570408821106,0.39491966366767883,0.5910391807556152,-0.7033571004867554,0.39491966366767883,0.5910391211509705,-0.7033571004867554,0.39491966366767883,0.5910391211509705,-0.7033571004867554,0.27202528715133667,0.6567275524139404,-0.7033570408821106,0.27202531695365906,0.6567276120185852,-0.7033570408821106,0.13867750763893127,0.6971781849861145,-0.7033570408821106,0.27202531695365906,0.6567276120185852,-0.7033570408821106,0.27202528715133667,0.6567275524139404,-0.7033570408821106,0.27202528715133667,0.6567275524139404,-0.7033570408821106,0.13867750763893127,0.6971781849861145,-0.7033570408821106,0.13867750763893127,0.6971781849861145,-0.7033570408821106,1.3676421417585516e-7,0.71083664894104,-0.7033571004867554,0.13867750763893127,0.6971781849861145,-0.7033570408821106,0.13867750763893127,0.6971781849861145,-0.7033570408821106,0.13867750763893127,0.6971781849861145,-0.7033570408821106,1.525836097471256e-7,0.7108367085456848,-0.7033571004867554,1.3676421417585516e-7,0.71083664894104,-0.7033571004867554,-0.13867735862731934,0.6971781253814697,-0.7033571004867554,1.3676421417585516e-7,0.71083664894104,-0.7033571004867554,1.525836097471256e-7,0.7108367085456848,-0.7033571004867554,1.525836097471256e-7,0.7108367085456848,-0.7033571004867554,-0.13867734372615814,0.6971781253814697,-0.7033571004867554,-0.13867735862731934,0.6971781253814697,-0.7033571004867554,-0.2720252573490143,0.6567274332046509,-0.7033571600914001,-0.13867735862731934,0.6971781253814697,-0.7033571004867554,-0.13867734372615814,0.6971781253814697,-0.7033571004867554,-0.13867734372615814,0.6971781253814697,-0.7033571004867554,-0.2720252573490143,0.6567274928092957,-0.7033571600914001,-0.2720252573490143,0.6567274332046509,-0.7033571600914001,-0.39491966366767883,0.5910390019416809,-0.7033572196960449,-0.2720252573490143,0.6567274332046509,-0.7033571600914001,-0.2720252573490143,0.6567274928092957,-0.7033571600914001,-0.2720252573490143,0.6567274928092957,-0.7033571600914001,-0.39491963386535645,0.5910390615463257,-0.7033571600914001,-0.39491966366767883,0.5910390019416809,-0.7033572196960449,-0.502637505531311,0.5026373863220215,-0.7033571004867554,-0.39491966366767883,0.5910390019416809,-0.7033572196960449,-0.39491963386535645,0.5910390615463257,-0.7033571600914001,-0.39491963386535645,0.5910390615463257,-0.7033571600914001,-0.5026374459266663,0.5026373863220215,-0.7033571600914001,-0.502637505531311,0.5026373863220215,-0.7033571004867554,-0.5910391211509705,0.39491963386535645,-0.7033571600914001,-0.502637505531311,0.5026373863220215,-0.7033571004867554,-0.5026374459266663,0.5026373863220215,-0.7033571600914001,-0.5026374459266663,0.5026373863220215,-0.7033571600914001,-0.5910391211509705,0.39491963386535645,-0.7033571600914001,-0.5910391211509705,0.39491963386535645,-0.7033571600914001,-0.6567275524139404,0.2720252275466919,-0.7033571004867554,-0.5910391211509705,0.39491963386535645,-0.7033571600914001,-0.5910391211509705,0.39491963386535645,-0.7033571600914001,-0.5910391211509705,0.39491963386535645,-0.7033571600914001,-0.6567275524139404,0.2720251977443695,-0.7033571004867554,-0.6567275524139404,0.2720252275466919,-0.7033571004867554,-0.6971781849861145,0.1386772245168686,-0.7033570408821106,-0.6567275524139404,0.2720252275466919,-0.7033571004867554,-0.6567275524139404,0.2720251977443695,-0.7033571004867554,-0.6567275524139404,0.2720251977443695,-0.7033571004867554,-0.6971781849861145,0.1386772245168686,-0.7033570408821106,-0.6971781849861145,0.1386772245168686,-0.7033570408821106,-0.71083664894104,-1.9644313908884214e-7,-0.7033571004867554,-0.6971781849861145,0.1386772245168686,-0.7033570408821106,-0.6971781849861145,0.1386772245168686,-0.7033570408821106,-0.6971781849861145,0.1386772245168686,-0.7033570408821106,-0.71083664894104,-1.8446674232563964e-7,-0.7033571004867554,-0.71083664894104,-1.9644313908884214e-7,-0.7033571004867554,-0.697178065776825,-0.13867776095867157,-0.7033571004867554,-0.71083664894104,-1.9644313908884214e-7,-0.7033571004867554,-0.71083664894104,-1.8446674232563964e-7,-0.7033571004867554,-0.71083664894104,-1.8446674232563964e-7,-0.7033571004867554,-0.697178065776825,-0.13867774605751038,-0.7033571004867554,-0.697178065776825,-0.13867776095867157,-0.7033571004867554,-0.6567273139953613,-0.27202582359313965,-0.7033571004867554,-0.697178065776825,-0.13867776095867157,-0.7033571004867554,-0.697178065776825,-0.13867774605751038,-0.7033571004867554,-0.697178065776825,-0.13867774605751038,-0.7033571004867554,-0.6567272543907166,-0.27202582359313965,-0.7033571004867554,-0.6567273139953613,-0.27202582359313965,-0.7033571004867554,-0.5910389423370361,-0.3949200212955475,-0.7033571004867554,-0.6567273139953613,-0.27202582359313965,-0.7033571004867554,-0.6567272543907166,-0.27202582359313965,-0.7033571004867554,-0.6567272543907166,-0.27202582359313965,-0.7033571004867554,-0.5910389423370361,-0.3949199914932251,-0.7033570408821106,-0.5910389423370361,-0.3949200212955475,-0.7033571004867554,-0.5026372671127319,-0.5026376843452454,-0.7033571004867554,-0.5910389423370361,-0.3949200212955475,-0.7033571004867554,-0.5910389423370361,-0.3949199914932251,-0.7033570408821106,-0.5910389423370361,-0.3949199914932251,-0.7033570408821106,-0.5026372075080872,-0.5026376843452454,-0.7033571004867554,-0.5026372671127319,-0.5026376843452454,-0.7033571004867554,-0.3949193060398102,-0.5910392999649048,-0.7033571600914001,-0.5026372671127319,-0.5026376843452454,-0.7033571004867554,-0.5026372075080872,-0.5026376843452454,-0.7033571004867554,-0.5026372075080872,-0.5026376843452454,-0.7033571004867554,-0.39491933584213257,-0.5910392999649048,-0.7033571600914001,-0.3949193060398102,-0.5910392999649048,-0.7033571600914001,-0.27202484011650085,-0.6567276120185852,-0.7033571600914001,-0.3949193060398102,-0.5910392999649048,-0.7033571600914001,-0.39491933584213257,-0.5910392999649048,-0.7033571600914001,-0.39491933584213257,-0.5910392999649048,-0.7033571600914001,-0.27202484011650085,-0.65672767162323,-0.7033571600914001,-0.27202484011650085,-0.6567276120185852,-0.7033571600914001,-0.1386767476797104,-0.697178304195404,-0.7033571004867554,-0.27202484011650085,-0.6567276120185852,-0.7033571600914001,-0.27202484011650085,-0.65672767162323,-0.7033571600914001,-0.27202484011650085,-0.65672767162323,-0.7033571600914001,-0.1386767327785492,-0.6971782445907593,-0.7033571004867554,-0.1386767476797104,-0.697178304195404,-0.7033571004867554,6.514948722724512e-7,-0.7108367085456848,-0.7033571004867554,-0.1386767476797104,-0.697178304195404,-0.7033571004867554,-0.1386767327785492,-0.6971782445907593,-0.7033571004867554,-0.1386767327785492,-0.6971782445907593,-0.7033571004867554,6.536043883897946e-7,-0.71083664894104,-0.7033571004867554,6.514948722724512e-7,-0.7108367085456848,-0.7033571004867554,0.1386781930923462,-0.6971779465675354,-0.7033571004867554,6.514948722724512e-7,-0.7108367085456848,-0.7033571004867554,6.536043883897946e-7,-0.71083664894104,-0.7033571004867554,6.536043883897946e-7,-0.71083664894104,-0.7033571004867554,0.1386781930923462,-0.6971779465675354,-0.7033571004867554,0.1386781930923462,-0.6971779465675354,-0.7033571004867554,0.2720262110233307,-0.6567271947860718,-0.7033570408821106,0.1386781930923462,-0.6971779465675354,-0.7033571004867554,0.1386781930923462,-0.6971779465675354,-0.7033571004867554,0.1386781930923462,-0.6971779465675354,-0.7033571004867554,0.2720262408256531,-0.6567271947860718,-0.7033570408821106,0.2720262110233307,-0.6567271947860718,-0.7033570408821106,0.3949204683303833,-0.591038703918457,-0.7033569812774658,0.2720262110233307,-0.6567271947860718,-0.7033570408821106,0.2720262408256531,-0.6567271947860718,-0.7033570408821106,0.2720262408256531,-0.6567271947860718,-0.7033570408821106,0.3949204683303833,-0.591038703918457,-0.7033569812774658,0.3949204683303833,-0.591038703918457,-0.7033569812774658,0.5026381015777588,-0.5026369094848633,-0.7033569812774658,0.3949204683303833,-0.591038703918457,-0.7033569812774658,0.3949204683303833,-0.591038703918457,-0.7033569812774658,0.3949204683303833,-0.591038703918457,-0.7033569812774658,0.502638041973114,-0.5026369094848633,-0.7033570408821106,0.5026381015777588,-0.5026369094848633,-0.7033569812774658,0.5910396575927734,-0.39491894841194153,-0.7033571004867554,0.5026381015777588,-0.5026369094848633,-0.7033569812774658,0.502638041973114,-0.5026369094848633,-0.7033570408821106,0.502638041973114,-0.5026369094848633,-0.7033570408821106,0.5910395979881287,-0.3949189782142639,-0.7033571004867554,0.5910396575927734,-0.39491894841194153,-0.7033571004867554,0.6567279100418091,-0.2720244228839874,-0.7033571004867554,0.5910396575927734,-0.39491894841194153,-0.7033571004867554,0.5910395979881287,-0.3949189782142639,-0.7033571004867554,0.5910395979881287,-0.3949189782142639,-0.7033571004867554,0.6567278504371643,-0.27202436327934265,-0.7033571004867554,0.6567279100418091,-0.2720244228839874,-0.7033571004867554,0.697178304195404,-0.13867662847042084,-0.7033571004867554,0.6567279100418091,-0.2720244228839874,-0.7033571004867554,0.6567278504371643,-0.27202436327934265,-0.7033571004867554,0.6567278504371643,-0.27202436327934265,-0.7033571004867554,0.697178304195404,-0.13867664337158203,-0.7033571004867554,0.697178304195404,-0.13867662847042084,-0.7033571004867554,0.7108367085456848,3.257474361362256e-7,-0.7033571004867554,0.697178304195404,-0.13867662847042084,-0.7033571004867554,0.697178304195404,-0.13867664337158203,-0.7033571004867554,0.697178304195404,-0.13867664337158203,-0.7033571004867554,0.71083664894104,3.6210147413839877e-7,-0.7033571600914001,0.7108367085456848,3.257474361362256e-7,-0.7033571004867554,-0.6971782445907593,-0.1386774629354477,0.7033569812774658,-0.6971781849861145,-0.1386774629354477,0.7033570408821106,-0.7108367085456848,-1.1159099955193597e-7,0.7033570408821106,-0.7108367085456848,-1.1159099955193597e-7,0.7033570408821106,-0.7108367085456848,-9.200498851669181e-8,0.7033570408821106,-0.6971782445907593,-0.1386774629354477,0.7033569812774658,-0.6567275524139404,-0.27202561497688293,0.7033569812774658,-0.6567274928092957,-0.27202558517456055,0.7033569812774658,-0.6971781849861145,-0.1386774629354477,0.7033570408821106,-0.6971781849861145,-0.1386774629354477,0.7033570408821106,-0.6971782445907593,-0.1386774629354477,0.7033569812774658,-0.6567275524139404,-0.27202561497688293,0.7033569812774658,-0.5910391807556152,-0.3949199616909027,0.703356921672821,-0.5910391211509705,-0.3949199616909027,0.703356921672821,-0.6567274928092957,-0.27202558517456055,0.7033569812774658,-0.6567274928092957,-0.27202558517456055,0.7033569812774658,-0.6567275524139404,-0.27202561497688293,0.7033569812774658,-0.5910391807556152,-0.3949199616909027,0.703356921672821,-0.5026376247406006,-0.502637505531311,0.703356921672821,-0.5026376247406006,-0.5026374459266663,0.703356921672821,-0.5910391211509705,-0.3949199616909027,0.703356921672821,-0.5910391211509705,-0.3949199616909027,0.703356921672821,-0.5910391807556152,-0.3949199616909027,0.703356921672821,-0.5026376247406006,-0.502637505531311,0.703356921672821,-0.3949197232723236,-0.5910391807556152,0.7033570408821106,-0.394919753074646,-0.5910391807556152,0.7033569812774658,-0.5026376247406006,-0.5026374459266663,0.703356921672821,-0.5026376247406006,-0.5026374459266663,0.703356921672821,-0.5026376247406006,-0.502637505531311,0.703356921672821,-0.3949197232723236,-0.5910391807556152,0.7033570408821106,-0.27202528715133667,-0.65672767162323,0.7033569812774658,-0.27202528715133667,-0.65672767162323,0.703356921672821,-0.394919753074646,-0.5910391807556152,0.7033569812774658,-0.394919753074646,-0.5910391807556152,0.7033569812774658,-0.3949197232723236,-0.5910391807556152,0.7033570408821106,-0.27202528715133667,-0.65672767162323,0.7033569812774658,-0.13867752254009247,-0.6971781849861145,0.7033569812774658,-0.13867753744125366,-0.6971782445907593,0.7033569812774658,-0.27202528715133667,-0.65672767162323,0.703356921672821,-0.27202528715133667,-0.65672767162323,0.703356921672821,-0.27202528715133667,-0.65672767162323,0.7033569812774658,-0.13867752254009247,-0.6971781849861145,0.7033569812774658,-1.2184446518404002e-7,-0.7108367085456848,0.7033571004867554,-1.594157055251344e-7,-0.71083664894104,0.7033571004867554,-0.13867753744125366,-0.6971782445907593,0.7033569812774658,-0.13867753744125366,-0.6971782445907593,0.7033569812774658,-0.13867752254009247,-0.6971781849861145,0.7033569812774658,-1.2184446518404002e-7,-0.7108367085456848,0.7033571004867554,0.13867734372615814,-0.6971781253814697,0.7033571600914001,0.13867731392383575,-0.6971781253814697,0.7033571004867554,-1.594157055251344e-7,-0.71083664894104,0.7033571004867554,-1.594157055251344e-7,-0.71083664894104,0.7033571004867554,-1.2184446518404002e-7,-0.7108367085456848,0.7033571004867554,0.13867734372615814,-0.6971781253814697,0.7033571600914001,0.2720251977443695,-0.6567274332046509,0.7033571600914001,0.27202513813972473,-0.6567274928092957,0.7033571600914001,0.13867731392383575,-0.6971781253814697,0.7033571004867554,0.13867731392383575,-0.6971781253814697,0.7033571004867554,0.13867734372615814,-0.6971781253814697,0.7033571600914001,0.2720251977443695,-0.6567274332046509,0.7033571600914001,0.3949195146560669,-0.5910390615463257,0.7033572793006897,0.3949195146560669,-0.5910390615463257,0.7033572793006897,0.27202513813972473,-0.6567274928092957,0.7033571600914001,0.27202513813972473,-0.6567274928092957,0.7033571600914001,0.2720251977443695,-0.6567274332046509,0.7033571600914001,0.3949195146560669,-0.5910390615463257,0.7033572793006897,0.5026373863220215,-0.5026372671127319,0.7033572196960449,0.5026374459266663,-0.5026372671127319,0.7033572196960449,0.3949195146560669,-0.5910390615463257,0.7033572793006897,0.3949195146560669,-0.5910390615463257,0.7033572793006897,0.3949195146560669,-0.5910390615463257,0.7033572793006897,0.5026373863220215,-0.5026372671127319,0.7033572196960449,0.5910391211509705,-0.3949195444583893,0.7033572196960449,0.5910390615463257,-0.39491957426071167,0.7033572196960449,0.5026374459266663,-0.5026372671127319,0.7033572196960449,0.5026374459266663,-0.5026372671127319,0.7033572196960449,0.5026373863220215,-0.5026372671127319,0.7033572196960449,0.5910391211509705,-0.3949195444583893,0.7033572196960449,0.6567274332046509,-0.27202528715133667,0.7033571600914001,0.6567274928092957,-0.27202534675598145,0.7033571600914001,0.5910390615463257,-0.39491957426071167,0.7033572196960449,0.5910390615463257,-0.39491957426071167,0.7033572196960449,0.5910391211509705,-0.3949195444583893,0.7033572196960449,0.6567274332046509,-0.27202528715133667,0.7033571600914001,0.6971781253814697,-0.13867710530757904,0.7033572196960449,0.6971781253814697,-0.13867712020874023,0.7033572196960449,0.6567274928092957,-0.27202534675598145,0.7033571600914001,0.6567274928092957,-0.27202534675598145,0.7033571600914001,0.6567274332046509,-0.27202528715133667,0.7033571600914001,0.6971781253814697,-0.13867710530757904,0.7033572196960449,0.7108365893363953,1.9644303961285914e-7,0.7033571600914001,0.7108365893363953,1.8674414548058849e-7,0.7033572196960449,0.6971781253814697,-0.13867712020874023,0.7033572196960449,0.6971781253814697,-0.13867712020874023,0.7033572196960449,0.6971781253814697,-0.13867710530757904,0.7033572196960449,0.7108365893363953,1.9644303961285914e-7,0.7033571600914001,0.6971780061721802,0.1386774778366089,0.7033572793006897,0.6971780061721802,0.13867749273777008,0.7033572793006897,0.7108365893363953,1.8674414548058849e-7,0.7033572196960449,0.7108365893363953,1.8674414548058849e-7,0.7033572196960449,0.7108365893363953,1.9644303961285914e-7,0.7033571600914001,0.6971780061721802,0.1386774778366089,0.7033572793006897,0.656727135181427,0.2720257043838501,0.7033573389053345,0.6567271947860718,0.2720257341861725,0.7033572793006897,0.6971780061721802,0.13867749273777008,0.7033572793006897,0.6971780061721802,0.13867749273777008,0.7033572793006897,0.6971780061721802,0.1386774778366089,0.7033572793006897,0.656727135181427,0.2720257043838501,0.7033573389053345,0.5910387635231018,0.3949199616909027,0.7033572196960449,0.5910387635231018,0.3949199616909027,0.7033572793006897,0.6567271947860718,0.2720257341861725,0.7033572793006897,0.6567271947860718,0.2720257341861725,0.7033572793006897,0.656727135181427,0.2720257043838501,0.7033573389053345,0.5910387635231018,0.3949199616909027,0.7033572196960449,0.5026370286941528,0.5026376247406006,0.7033572793006897,0.5026370882987976,0.5026376843452454,0.7033572196960449,0.5910387635231018,0.3949199616909027,0.7033572793006897,0.5910387635231018,0.3949199616909027,0.7033572793006897,0.5910387635231018,0.3949199616909027,0.7033572196960449,0.5026370286941528,0.5026376247406006,0.7033572793006897,0.39491918683052063,0.59103924036026,0.7033572196960449,0.394919216632843,0.5910392999649048,0.7033572196960449,0.5026370882987976,0.5026376843452454,0.7033572196960449,0.5026370882987976,0.5026376843452454,0.7033572196960449,0.5026370286941528,0.5026376247406006,0.7033572793006897,0.39491918683052063,0.59103924036026,0.7033572196960449,0.27202481031417847,0.6567276120185852,0.7033572196960449,0.2720247805118561,0.6567276120185852,0.7033572196960449,0.394919216632843,0.5910392999649048,0.7033572196960449,0.394919216632843,0.5910392999649048,0.7033572196960449,0.39491918683052063,0.59103924036026,0.7033572196960449,0.27202481031417847,0.6567276120185852,0.7033572196960449,0.1386767327785492,0.6971782445907593,0.7033571600914001,0.138676717877388,0.6971781849861145,0.7033571600914001,0.2720247805118561,0.6567276120185852,0.7033572196960449,0.2720247805118561,0.6567276120185852,0.7033572196960449,0.27202481031417847,0.6567276120185852,0.7033572196960449,0.1386767327785492,0.6971782445907593,0.7033571600914001,-6.365750095937983e-7,0.7108365893363953,0.7033571600914001,-6.627138304793334e-7,0.71083664894104,0.7033571600914001,0.138676717877388,0.6971781849861145,0.7033571600914001,0.138676717877388,0.6971781849861145,0.7033571600914001,0.1386767327785492,0.6971782445907593,0.7033571600914001,-6.365750095937983e-7,0.7108365893363953,0.7033571600914001,-0.138678178191185,0.697178065776825,0.7033570408821106,-0.13867820799350739,0.6971779465675354,0.7033571004867554,-6.627138304793334e-7,0.71083664894104,0.7033571600914001,-6.627138304793334e-7,0.71083664894104,0.7033571600914001,-6.365750095937983e-7,0.7108365893363953,0.7033571600914001,-0.138678178191185,0.697178065776825,0.7033570408821106,-0.27202627062797546,0.6567271947860718,0.7033569812774658,-0.27202630043029785,0.6567271947860718,0.7033570408821106,-0.13867820799350739,0.6971779465675354,0.7033571004867554,-0.13867820799350739,0.6971779465675354,0.7033571004867554,-0.138678178191185,0.697178065776825,0.7033570408821106,-0.27202627062797546,0.6567271947860718,0.7033569812774658,-0.3949204385280609,0.5910387635231018,0.703356921672821,-0.3949204683303833,0.591038703918457,0.7033569812774658,-0.27202630043029785,0.6567271947860718,0.7033570408821106,-0.27202630043029785,0.6567271947860718,0.7033570408821106,-0.27202627062797546,0.6567271947860718,0.7033569812774658,-0.3949204385280609,0.5910387635231018,0.703356921672821,-0.5026381015777588,0.5026369094848633,0.7033569812774658,-0.5026381015777588,0.5026369094848633,0.7033569812774658,-0.3949204683303833,0.591038703918457,0.7033569812774658,-0.3949204683303833,0.591038703918457,0.7033569812774658,-0.3949204385280609,0.5910387635231018,0.703356921672821,-0.5026381015777588,0.5026369094848633,0.7033569812774658,-0.5910396575927734,0.3949190378189087,0.7033569812774658,-0.5910396575927734,0.3949190676212311,0.7033569812774658,-0.5026381015777588,0.5026369094848633,0.7033569812774658,-0.5026381015777588,0.5026369094848633,0.7033569812774658,-0.5026381015777588,0.5026369094848633,0.7033569812774658,-0.5910396575927734,0.3949190378189087,0.7033569812774658,-0.6567279696464539,0.27202433347702026,0.7033570408821106,-0.6567279696464539,0.2720243036746979,0.7033570408821106,-0.5910396575927734,0.3949190676212311,0.7033569812774658,-0.5910396575927734,0.3949190676212311,0.7033569812774658,-0.5910396575927734,0.3949190378189087,0.7033569812774658,-0.6567279696464539,0.27202433347702026,0.7033570408821106,-0.6971784234046936,0.13867659866809845,0.7033569812774658,-0.6971784234046936,0.13867662847042084,0.703356921672821,-0.6567279696464539,0.2720243036746979,0.7033570408821106,-0.6567279696464539,0.2720243036746979,0.7033570408821106,-0.6567279696464539,0.27202433347702026,0.7033570408821106,-0.6971784234046936,0.13867659866809845,0.7033569812774658,-0.7108367085456848,-9.200498851669181e-8,0.7033570408821106,-0.7108367085456848,-1.1159099955193597e-7,0.7033570408821106,-0.6971784234046936,0.13867662847042084,0.703356921672821,-0.6971784234046936,0.13867662847042084,0.703356921672821,-0.6971784234046936,0.13867659866809845,0.7033569812774658,-0.7108367085456848,-9.200498851669181e-8,0.7033570408821106],"normalized":false},"uv":{"itemSize":2,"type":"Float32Array","array":[0.8906737565994263,0.9400254487991333,0.4999995231628418,0.9400254487991333,0.4999995231628418,0.0599745512008667,0.4999995231628418,0.0599745512008667,0.8906737565994263,0.0599745512008667,0.8906737565994263,0.9400254487991333,1.2663346529006958,0.9400254487991333,0.8906737565994263,0.9400254487991333,0.8906737565994263,0.0599745512008667,0.8906737565994263,0.0599745512008667,1.2663346529006958,0.0599745512008667,1.2663346529006958,0.9400254487991333,1.6125456094741821,0.9400254487991333,1.2663346529006958,0.9400254487991333,1.2663346529006958,0.0599745512008667,1.2663346529006958,0.0599745512008667,1.6125456094741821,0.0599745512008667,1.6125456094741821,0.9400254487991333,1.9160020351409912,0.9400254487991333,1.6125456094741821,0.9400254487991333,1.6125456094741821,0.0599745512008667,1.6125456094741821,0.0599745512008667,1.9160020351409912,0.0599745512008667,1.9160020351409912,0.9400254487991333,-0.6125462055206299,0.9400254487991333,-0.9160027503967285,0.9400254487991333,-0.9160027503967285,0.0599745512008667,-0.9160027503967285,0.0599745512008667,-0.6125462055206299,0.0599745512008667,-0.6125462055206299,0.9400254487991333,-0.26633548736572266,0.9400254487991333,-0.6125462055206299,0.9400254487991333,-0.6125462055206299,0.0599745512008667,-0.6125462055206299,0.0599745512008667,-0.26633548736572266,0.0599745512008667,-0.26633548736572266,0.9400254487991333,0.10932528972625732,0.9400254487991333,-0.26633548736572266,0.9400254487991333,-0.26633548736572266,0.0599745512008667,-0.26633548736572266,0.0599745512008667,0.10932528972625732,0.0599745512008667,0.10932528972625732,0.9400254487991333,0.49999934434890747,0.9400254487991333,0.10932528972625732,0.9400254487991333,0.10932528972625732,0.0599745512008667,0.10932528972625732,0.0599745512008667,0.49999934434890747,0.0599745512008667,0.49999934434890747,0.9400254487991333,0.8906735181808472,0.9400254487991333,0.49999934434890747,0.9400254487991333,0.49999934434890747,0.0599745512008667,0.49999934434890747,0.0599745512008667,0.8906735181808472,0.0599745512008667,0.8906735181808472,0.9400254487991333,1.2663342952728271,0.9400254487991333,0.8906735181808472,0.9400254487991333,0.8906735181808472,0.0599745512008667,0.8906735181808472,0.0599745512008667,1.2663342952728271,0.0599745512008667,1.2663342952728271,0.9400254487991333,1.6125454902648926,0.9400254487991333,1.2663342952728271,0.9400254487991333,1.2663342952728271,0.0599745512008667,1.2663342952728271,0.0599745512008667,1.6125454902648926,0.0599745512008667,1.6125454902648926,0.9400254487991333,1.9160020351409912,0.9400254487991333,1.6125454902648926,0.9400254487991333,1.6125454902648926,0.0599745512008667,1.6125454902648926,0.0599745512008667,1.9160020351409912,0.0599745512008667,1.9160020351409912,0.9400254487991333,-0.6125462055206299,0.9400254487991333,-0.9160027503967285,0.9400254487991333,-0.9160027503967285,0.0599745512008667,-0.9160027503967285,0.0599745512008667,-0.6125462055206299,0.0599745512008667,-0.6125462055206299,0.9400254487991333,-0.266335129737854,0.9400254487991333,-0.6125462055206299,0.9400254487991333,-0.6125462055206299,0.0599745512008667,-0.6125462055206299,0.0599745512008667,-0.266335129737854,0.0599745512008667,-0.266335129737854,0.9400254487991333,0.10932576656341553,0.9400254487991333,-0.266335129737854,0.9400254487991333,-0.266335129737854,0.0599745512008667,-0.266335129737854,0.0599745512008667,0.10932576656341553,0.0599745512008667,0.10932576656341553,0.9400254487991333,0.5000001788139343,0.9400254487991333,0.10932576656341553,0.9400254487991333,0.10932576656341553,0.0599745512008667,0.10932576656341553,0.0599745512008667,0.5000001788139343,0.0599745512008667,0.5000001788139343,0.9400254487991333,0.8906745314598083,0.9400254487991333,0.5000001788139343,0.9400254487991333,0.5000001788139343,0.0599745512008667,0.5000001788139343,0.0599745512008667,0.8906745314598083,0.0599745512008667,0.8906745314598083,0.9400254487991333,1.2663354873657227,0.9400254487991333,0.8906745314598083,0.9400254487991333,0.8906745314598083,0.0599745512008667,0.8906745314598083,0.0599745512008667,1.2663354873657227,0.0599745512008667,1.2663354873657227,0.9400254487991333,1.6125465631484985,0.9400254487991333,1.2663354873657227,0.9400254487991333,1.2663354873657227,0.0599745512008667,1.2663354873657227,0.0599745512008667,1.6125465631484985,0.0599745512008667,1.6125465631484985,0.9400254487991333,1.9160029888153076,0.9400254487991333,1.6125465631484985,0.9400254487991333,1.6125465631484985,0.0599745512008667,1.6125465631484985,0.0599745512008667,1.9160029888153076,0.0599745512008667,1.9160029888153076,0.9400254487991333,-0.6125451326370239,0.9400254487991333,-0.9160019159317017,0.9400254487991333,-0.9160019159317017,0.0599745512008667,-0.9160019159317017,0.0599745512008667,-0.6125451326370239,0.0599745512008667,-0.6125451326370239,0.9400254487991333,-0.2663339376449585,0.9400254487991333,-0.6125451326370239,0.9400254487991333,-0.6125451326370239,0.0599745512008667,-0.6125451326370239,0.0599745512008667,-0.2663339376449585,0.0599745512008667,-0.2663339376449585,0.9400254487991333,0.1093270480632782,0.9400254487991333,-0.2663339376449585,0.9400254487991333,-0.2663339376449585,0.0599745512008667,-0.2663339376449585,0.0599745512008667,0.1093270480632782,0.0599745512008667,0.1093270480632782,0.9400254487991333,0.5000014305114746,0.9400254487991333,0.1093270480632782,0.9400254487991333,0.1093270480632782,0.0599745512008667,0.1093270480632782,0.0599745512008667,0.5000014305114746,0.0599745512008667,0.5000014305114746,0.9400254487991333,0.8906757831573486,0.9400254487991333,0.5000014305114746,0.9400254487991333,0.5000014305114746,0.0599745512008667,0.5000014305114746,0.0599745512008667,0.8906757831573486,0.0599745512008667,0.8906757831573486,0.9400254487991333,1.2663366794586182,0.9400254487991333,0.8906757831573486,0.9400254487991333,0.8906757831573486,0.0599745512008667,0.8906757831573486,0.0599745512008667,1.2663366794586182,0.0599745512008667,1.2663366794586182,0.9400254487991333,1.6125476360321045,0.9400254487991333,1.2663366794586182,0.9400254487991333,1.2663366794586182,0.0599745512008667,1.2663366794586182,0.0599745512008667,1.6125476360321045,0.0599745512008667,1.6125476360321045,0.9400254487991333,1.9160038232803345,0.9400254487991333,1.6125476360321045,0.9400254487991333,1.6125476360321045,0.0599745512008667,1.6125476360321045,0.0599745512008667,1.9160038232803345,0.0599745512008667,1.9160038232803345,0.9400254487991333,-0.612544059753418,0.9400254487991333,-0.9160009622573853,0.9400254487991333,-0.9160009622573853,0.0599745512008667,-0.9160009622573853,0.0599745512008667,-0.612544059753418,0.0599745512008667,-0.612544059753418,0.9400254487991333,-0.266332745552063,0.9400254487991333,-0.612544059753418,0.9400254487991333,-0.612544059753418,0.0599745512008667,-0.612544059753418,0.0599745512008667,-0.266332745552063,0.0599745512008667,-0.266332745552063,0.9400254487991333,0.10932832956314087,0.9400254487991333,-0.266332745552063,0.9400254487991333,-0.266332745552063,0.0599745512008667,-0.266332745552063,0.0599745512008667,0.10932832956314087,0.0599745512008667,0.10932832956314087,0.9400254487991333,0.4999995231628418,0.9400254487991333,0.10932832956314087,0.9400254487991333,0.10932832956314087,0.0599745512008667,0.10932832956314087,0.0599745512008667,0.4999995231628418,0.0599745512008667,0.4999995231628418,0.9400254487991333,0.8906737565994263,0.9400254487991333,0.8906737565994263,0.0599745512008667,0.4999995231628418,0.0599745512008667,0.4999995231628418,0.0599745512008667,0.4999995231628418,0.9400254487991333,0.8906737565994263,0.9400254487991333,1.2663346529006958,0.9400254487991333,1.2663346529006958,0.0599745512008667,0.8906737565994263,0.0599745512008667,0.8906737565994263,0.0599745512008667,0.8906737565994263,0.9400254487991333,1.2663346529006958,0.9400254487991333,1.6125456094741821,0.9400254487991333,1.6125456094741821,0.0599745512008667,1.2663346529006958,0.0599745512008667,1.2663346529006958,0.0599745512008667,1.2663346529006958,0.9400254487991333,1.6125456094741821,0.9400254487991333,1.9160020351409912,0.9400254487991333,1.9160020351409912,0.0599745512008667,1.6125456094741821,0.0599745512008667,1.6125456094741821,0.0599745512008667,1.6125456094741821,0.9400254487991333,1.9160020351409912,0.9400254487991333,-0.6125462055206299,0.9400254487991333,-0.6125462055206299,0.0599745512008667,-0.9160027503967285,0.0599745512008667,-0.9160027503967285,0.0599745512008667,-0.9160027503967285,0.9400254487991333,-0.6125462055206299,0.9400254487991333,-0.26633548736572266,0.9400254487991333,-0.26633548736572266,0.0599745512008667,-0.6125462055206299,0.0599745512008667,-0.6125462055206299,0.0599745512008667,-0.6125462055206299,0.9400254487991333,-0.26633548736572266,0.9400254487991333,0.10932528972625732,0.9400254487991333,0.10932528972625732,0.0599745512008667,-0.26633548736572266,0.0599745512008667,-0.26633548736572266,0.0599745512008667,-0.26633548736572266,0.9400254487991333,0.10932528972625732,0.9400254487991333,0.49999934434890747,0.9400254487991333,0.49999934434890747,0.0599745512008667,0.10932528972625732,0.0599745512008667,0.10932528972625732,0.0599745512008667,0.10932528972625732,0.9400254487991333,0.49999934434890747,0.9400254487991333,0.8906735181808472,0.9400254487991333,0.8906735181808472,0.0599745512008667,0.49999934434890747,0.0599745512008667,0.49999934434890747,0.0599745512008667,0.49999934434890747,0.9400254487991333,0.8906735181808472,0.9400254487991333,1.2663342952728271,0.9400254487991333,1.2663342952728271,0.0599745512008667,0.8906735181808472,0.0599745512008667,0.8906735181808472,0.0599745512008667,0.8906735181808472,0.9400254487991333,1.2663342952728271,0.9400254487991333,1.6125454902648926,0.9400254487991333,1.6125454902648926,0.0599745512008667,1.2663342952728271,0.0599745512008667,1.2663342952728271,0.0599745512008667,1.2663342952728271,0.9400254487991333,1.6125454902648926,0.9400254487991333,1.9160020351409912,0.9400254487991333,1.9160020351409912,0.0599745512008667,1.6125454902648926,0.0599745512008667,1.6125454902648926,0.0599745512008667,1.6125454902648926,0.9400254487991333,1.9160020351409912,0.9400254487991333,-0.6125462055206299,0.9400254487991333,-0.6125462055206299,0.0599745512008667,-0.9160027503967285,0.0599745512008667,-0.9160027503967285,0.0599745512008667,-0.9160027503967285,0.9400254487991333,-0.6125462055206299,0.9400254487991333,-0.266335129737854,0.9400254487991333,-0.266335129737854,0.0599745512008667,-0.6125462055206299,0.0599745512008667,-0.6125462055206299,0.0599745512008667,-0.6125462055206299,0.9400254487991333,-0.266335129737854,0.9400254487991333,0.10932576656341553,0.9400254487991333,0.10932576656341553,0.0599745512008667,-0.266335129737854,0.0599745512008667,-0.266335129737854,0.0599745512008667,-0.266335129737854,0.9400254487991333,0.10932576656341553,0.9400254487991333,0.5000001788139343,0.9400254487991333,0.5000001788139343,0.0599745512008667,0.10932576656341553,0.0599745512008667,0.10932576656341553,0.0599745512008667,0.10932576656341553,0.9400254487991333,0.5000001788139343,0.9400254487991333,0.8906745314598083,0.9400254487991333,0.8906745314598083,0.0599745512008667,0.5000001788139343,0.0599745512008667,0.5000001788139343,0.0599745512008667,0.5000001788139343,0.9400254487991333,0.8906745314598083,0.9400254487991333,1.2663354873657227,0.9400254487991333,1.2663354873657227,0.0599745512008667,0.8906745314598083,0.0599745512008667,0.8906745314598083,0.0599745512008667,0.8906745314598083,0.9400254487991333,1.2663354873657227,0.9400254487991333,1.6125465631484985,0.9400254487991333,1.6125465631484985,0.0599745512008667,1.2663354873657227,0.0599745512008667,1.2663354873657227,0.0599745512008667,1.2663354873657227,0.9400254487991333,1.6125465631484985,0.9400254487991333,1.9160029888153076,0.9400254487991333,1.9160029888153076,0.0599745512008667,1.6125465631484985,0.0599745512008667,1.6125465631484985,0.0599745512008667,1.6125465631484985,0.9400254487991333,1.9160029888153076,0.9400254487991333,-0.6125451326370239,0.9400254487991333,-0.6125451326370239,0.0599745512008667,-0.9160019159317017,0.0599745512008667,-0.9160019159317017,0.0599745512008667,-0.9160019159317017,0.9400254487991333,-0.6125451326370239,0.9400254487991333,-0.2663339376449585,0.9400254487991333,-0.2663339376449585,0.0599745512008667,-0.6125451326370239,0.0599745512008667,-0.6125451326370239,0.0599745512008667,-0.6125451326370239,0.9400254487991333,-0.2663339376449585,0.9400254487991333,0.1093270480632782,0.9400254487991333,0.1093270480632782,0.0599745512008667,-0.2663339376449585,0.0599745512008667,-0.2663339376449585,0.0599745512008667,-0.2663339376449585,0.9400254487991333,0.1093270480632782,0.9400254487991333,0.5000014305114746,0.9400254487991333,0.5000014305114746,0.0599745512008667,0.1093270480632782,0.0599745512008667,0.1093270480632782,0.0599745512008667,0.1093270480632782,0.9400254487991333,0.5000014305114746,0.9400254487991333,0.8906757831573486,0.9400254487991333,0.8906757831573486,0.0599745512008667,0.5000014305114746,0.0599745512008667,0.5000014305114746,0.0599745512008667,0.5000014305114746,0.9400254487991333,0.8906757831573486,0.9400254487991333,1.2663366794586182,0.9400254487991333,1.2663366794586182,0.0599745512008667,0.8906757831573486,0.0599745512008667,0.8906757831573486,0.0599745512008667,0.8906757831573486,0.9400254487991333,1.2663366794586182,0.9400254487991333,1.6125476360321045,0.9400254487991333,1.6125476360321045,0.0599745512008667,1.2663366794586182,0.0599745512008667,1.2663366794586182,0.0599745512008667,1.2663366794586182,0.9400254487991333,1.6125476360321045,0.9400254487991333,1.9160038232803345,0.9400254487991333,1.9160038232803345,0.0599745512008667,1.6125476360321045,0.0599745512008667,1.6125476360321045,0.0599745512008667,1.6125476360321045,0.9400254487991333,1.9160038232803345,0.9400254487991333,-0.612544059753418,0.9400254487991333,-0.612544059753418,0.0599745512008667,-0.9160009622573853,0.0599745512008667,-0.9160009622573853,0.0599745512008667,-0.9160009622573853,0.9400254487991333,-0.612544059753418,0.9400254487991333,-0.266332745552063,0.9400254487991333,-0.266332745552063,0.0599745512008667,-0.612544059753418,0.0599745512008667,-0.612544059753418,0.0599745512008667,-0.612544059753418,0.9400254487991333,-0.266332745552063,0.9400254487991333,0.10932832956314087,0.9400254487991333,0.10932832956314087,0.0599745512008667,-0.266332745552063,0.0599745512008667,-0.266332745552063,0.0599745512008667,-0.266332745552063,0.9400254487991333,0.10932832956314087,0.9400254487991333,0.4999995231628418,0.9400254487991333,0.4999995231628418,0.0599745512008667,0.10932832956314087,0.0599745512008667,0.10932832956314087,0.0599745512008667,0.10932832956314087,0.9400254487991333,0.4999995231628418,0.9400254487991333],"normalized":false}}}}],"materials":[{"uuid":"769df3ee-4567-40b7-8da4-473fb149f350","type":"MeshBasicMaterial","color":16777215,"map":"3874a02e-6d61-4cbb-8379-9c1436361bb4","envMapRotation":[0,0,0,"XYZ"],"reflectivity":1,"refractionRatio":0.98,"blending":2,"side":2,"transparent":true,"blendColor":0,"depthWrite":false},{"uuid":"6d9283b7-81c2-4063-84cc-f696054ce6f6","type":"MeshBasicMaterial","color":16777215,"map":"93d77365-4fc6-43a7-b19a-e2fb18ab38d0","envMapRotation":[0,0,0,"XYZ"],"reflectivity":1,"refractionRatio":0.98,"blending":2,"side":2,"transparent":true,"blendColor":0,"depthWrite":false},{"uuid":"7442c205-fb42-4fb9-baec-82a192b81351","type":"MeshBasicMaterial","color":16777215,"map":"65e3d0e9-bf33-48b4-a8a1-5bc966d46e4e","envMapRotation":[0,0,0,"XYZ"],"reflectivity":1,"refractionRatio":0.98,"blending":2,"side":2,"transparent":true,"blendColor":0,"depthWrite":false}],"textures":[{"uuid":"3874a02e-6d61-4cbb-8379-9c1436361bb4","name":"GroundGlowEmitter_texture","image":"396bc86c-4059-45f7-b34f-f6228436b397","mapping":300,"channel":0,"repeat":[1,1],"offset":[0,0],"center":[0,0],"rotation":0,"wrap":[1001,1001],"format":1023,"internalFormat":null,"type":1009,"colorSpace":"","minFilter":1008,"magFilter":1006,"anisotropy":1,"flipY":true,"generateMipmaps":true,"premultiplyAlpha":false,"unpackAlignment":4},{"uuid":"93d77365-4fc6-43a7-b19a-e2fb18ab38d0","name":"GlowCircleEmitter_texture","image":"55a3bc1f-853b-4d4a-bbe1-77abe1ccb390","mapping":300,"channel":0,"repeat":[1,1],"offset":[0,0],"center":[0,0],"rotation":0,"wrap":[1001,1001],"format":1023,"internalFormat":null,"type":1009,"colorSpace":"","minFilter":1008,"magFilter":1006,"anisotropy":1,"flipY":true,"generateMipmaps":true,"premultiplyAlpha":false,"unpackAlignment":4},{"uuid":"65e3d0e9-bf33-48b4-a8a1-5bc966d46e4e","name":"BasicZoneBlueEmitter_texture","image":"a44aaf69-213b-4f68-96fc-304a19e9cdae","mapping":300,"channel":0,"repeat":[1,1],"offset":[0,0],"center":[0,0],"rotation":0,"wrap":[1001,1001],"format":1023,"internalFormat":null,"type":1009,"colorSpace":"","minFilter":1008,"magFilter":1006,"anisotropy":1,"flipY":true,"generateMipmaps":true,"premultiplyAlpha":false,"unpackAlignment":4}],"images":[{"uuid":"396bc86c-4059-45f7-b34f-f6228436b397","url":"data:image/jpeg;base64,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"},{"uuid":"55a3bc1f-853b-4d4a-bbe1-77abe1ccb390","url":"data:image/jpeg;base64,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"},{"uuid":"a44aaf69-213b-4f68-96fc-304a19e9cdae","url":"data:image/jpeg;base64,iVBORw0KGgoAAAANSUhEUgAAAgAAAAIACAYAAAD0eNT6AAAACXBIWXMAAAsTAAALEwEAmpwYAAAKT2lDQ1BQaG90b3Nob3AgSUNDIHByb2ZpbGUAAHjanVNnVFPpFj333vRCS4iAlEtvUhUIIFJCi4AUkSYqIQkQSoghodkVUcERRUUEG8igiAOOjoCMFVEsDIoK2AfkIaKOg6OIisr74Xuja9a89+bN/rXXPues852zzwfACAyWSDNRNYAMqUIeEeCDx8TG4eQuQIEKJHAAEAizZCFz/SMBAPh+PDwrIsAHvgABeNMLCADATZvAMByH/w/qQplcAYCEAcB0kThLCIAUAEB6jkKmAEBGAYCdmCZTAKAEAGDLY2LjAFAtAGAnf+bTAICd+Jl7AQBblCEVAaCRACATZYhEAGg7AKzPVopFAFgwABRmS8Q5ANgtADBJV2ZIALC3AMDOEAuyAAgMADBRiIUpAAR7AGDIIyN4AISZABRG8lc88SuuEOcqAAB4mbI8uSQ5RYFbCC1xB1dXLh4ozkkXKxQ2YQJhmkAuwnmZGTKBNA/g88wAAKCRFRHgg/P9eM4Ors7ONo62Dl8t6r8G/yJiYuP+5c+rcEAAAOF0ftH+LC+zGoA7BoBt/qIl7gRoXgugdfeLZrIPQLUAoOnaV/Nw+H48PEWhkLnZ2eXk5NhKxEJbYcpXff5nwl/AV/1s+X48/Pf14L7iJIEyXYFHBPjgwsz0TKUcz5IJhGLc5o9H/LcL//wd0yLESWK5WCoU41EScY5EmozzMqUiiUKSKcUl0v9k4t8s+wM+3zUAsGo+AXuRLahdYwP2SycQWHTA4vcAAPK7b8HUKAgDgGiD4c93/+8//UegJQCAZkmScQAAXkQkLlTKsz/HCAAARKCBKrBBG/TBGCzABhzBBdzBC/xgNoRCJMTCQhBCCmSAHHJgKayCQiiGzbAdKmAv1EAdNMBRaIaTcA4uwlW4Dj1wD/phCJ7BKLyBCQRByAgTYSHaiAFiilgjjggXmYX4IcFIBBKLJCDJiBRRIkuRNUgxUopUIFVIHfI9cgI5h1xGupE7yAAygvyGvEcxlIGyUT3UDLVDuag3GoRGogvQZHQxmo8WoJvQcrQaPYw2oefQq2gP2o8+Q8cwwOgYBzPEbDAuxsNCsTgsCZNjy7EirAyrxhqwVqwDu4n1Y8+xdwQSgUXACTYEd0IgYR5BSFhMWE7YSKggHCQ0EdoJNwkDhFHCJyKTqEu0JroR+cQYYjIxh1hILCPWEo8TLxB7iEPENyQSiUMyJ7mQAkmxpFTSEtJG0m5SI+ksqZs0SBojk8naZGuyBzmULCAryIXkneTD5DPkG+Qh8lsKnWJAcaT4U+IoUspqShnlEOU05QZlmDJBVaOaUt2ooVQRNY9aQq2htlKvUYeoEzR1mjnNgxZJS6WtopXTGmgXaPdpr+h0uhHdlR5Ol9BX0svpR+iX6AP0dwwNhhWDx4hnKBmbGAcYZxl3GK+YTKYZ04sZx1QwNzHrmOeZD5lvVVgqtip8FZHKCpVKlSaVGyovVKmqpqreqgtV81XLVI+pXlN9rkZVM1PjqQnUlqtVqp1Q61MbU2epO6iHqmeob1Q/pH5Z/YkGWcNMw09DpFGgsV/jvMYgC2MZs3gsIWsNq4Z1gTXEJrHN2Xx2KruY/R27iz2qqaE5QzNKM1ezUvOUZj8H45hx+Jx0TgnnKKeX836K3hTvKeIpG6Y0TLkxZVxrqpaXllirSKtRq0frvTau7aedpr1Fu1n7gQ5Bx0onXCdHZ4/OBZ3nU9lT3acKpxZNPTr1ri6qa6UbobtEd79up+6Ynr5egJ5Mb6feeb3n+hx9L/1U/W36p/VHDFgGswwkBtsMzhg8xTVxbzwdL8fb8VFDXcNAQ6VhlWGX4YSRudE8o9VGjUYPjGnGXOMk423GbcajJgYmISZLTepN7ppSTbmmKaY7TDtMx83MzaLN1pk1mz0x1zLnm+eb15vft2BaeFostqi2uGVJsuRaplnutrxuhVo5WaVYVVpds0atna0l1rutu6cRp7lOk06rntZnw7Dxtsm2qbcZsOXYBtuutm22fWFnYhdnt8Wuw+6TvZN9un2N/T0HDYfZDqsdWh1+c7RyFDpWOt6azpzuP33F9JbpL2dYzxDP2DPjthPLKcRpnVOb00dnF2e5c4PziIuJS4LLLpc+Lpsbxt3IveRKdPVxXeF60vWdm7Obwu2o26/uNu5p7ofcn8w0nymeWTNz0MPIQ+BR5dE/C5+VMGvfrH5PQ0+BZ7XnIy9jL5FXrdewt6V3qvdh7xc+9j5yn+M+4zw33jLeWV/MN8C3yLfLT8Nvnl+F30N/I/9k/3r/0QCngCUBZwOJgUGBWwL7+Hp8Ib+OPzrbZfay2e1BjKC5QRVBj4KtguXBrSFoyOyQrSH355jOkc5pDoVQfujW0Adh5mGLw34MJ4WHhVeGP45wiFga0TGXNXfR3ENz30T6RJZE3ptnMU85ry1KNSo+qi5qPNo3ujS6P8YuZlnM1VidWElsSxw5LiquNm5svt/87fOH4p3iC+N7F5gvyF1weaHOwvSFpxapLhIsOpZATIhOOJTwQRAqqBaMJfITdyWOCnnCHcJnIi/RNtGI2ENcKh5O8kgqTXqS7JG8NXkkxTOlLOW5hCepkLxMDUzdmzqeFpp2IG0yPTq9MYOSkZBxQqohTZO2Z+pn5mZ2y6xlhbL+xW6Lty8elQfJa7OQrAVZLQq2QqboVFoo1yoHsmdlV2a/zYnKOZarnivN7cyzytuQN5zvn//tEsIS4ZK2pYZLVy0dWOa9rGo5sjxxedsK4xUFK4ZWBqw8uIq2Km3VT6vtV5eufr0mek1rgV7ByoLBtQFr6wtVCuWFfevc1+1dT1gvWd+1YfqGnRs+FYmKrhTbF5cVf9go3HjlG4dvyr+Z3JS0qavEuWTPZtJm6ebeLZ5bDpaql+aXDm4N2dq0Dd9WtO319kXbL5fNKNu7g7ZDuaO/PLi8ZafJzs07P1SkVPRU+lQ27tLdtWHX+G7R7ht7vPY07NXbW7z3/T7JvttVAVVN1WbVZftJ+7P3P66Jqun4lvttXa1ObXHtxwPSA/0HIw6217nU1R3SPVRSj9Yr60cOxx++/p3vdy0NNg1VjZzG4iNwRHnk6fcJ3/ceDTradox7rOEH0x92HWcdL2pCmvKaRptTmvtbYlu6T8w+0dbq3nr8R9sfD5w0PFl5SvNUyWna6YLTk2fyz4ydlZ19fi753GDborZ752PO32oPb++6EHTh0kX/i+c7vDvOXPK4dPKy2+UTV7hXmq86X23qdOo8/pPTT8e7nLuarrlca7nuer21e2b36RueN87d9L158Rb/1tWeOT3dvfN6b/fF9/XfFt1+cif9zsu72Xcn7q28T7xf9EDtQdlD3YfVP1v+3Njv3H9qwHeg89HcR/cGhYPP/pH1jw9DBY+Zj8uGDYbrnjg+OTniP3L96fynQ89kzyaeF/6i/suuFxYvfvjV69fO0ZjRoZfyl5O/bXyl/erA6xmv28bCxh6+yXgzMV70VvvtwXfcdx3vo98PT+R8IH8o/2j5sfVT0Kf7kxmTk/8EA5jz/GMzLdsAAAAgY0hSTQAAeiUAAICDAAD5/wAAgOkAAHUwAADqYAAAOpgAABdvkl/FRgAAs09JREFUeNrsvWeXJMexJXgtsqo1Go1uNLQGIUmCEJRPzNuZ3X+9Z2dndnZ2Ht+jeCRBEgQIrbvRjW6gdVWG74dwr/T0dB3uITLNzslTqSorKzPC77Vr18xJCAEODg4ODg6O3YqGPwIODg4ODg4mABwcHBwcHBxMADg4ODg4ODiYAHBwcHBwcHAwAeDg4ODg4OBgAsDBwcHBwcHBBICDg4ODg4ODCQAHBwcHBwcHEwAODg4ODg6O8WIv9onL5ZI/LQ6O8YK0S2P8NK+ri/675k8yXl/ICyw/9cfVpbVcb43ncFSOxWLBHwJHfQLAwcFRFdgb47Lw3LdwXNRj6vVMsDeJg0kAWgvQKyBXjy3l9aXj0mo/9YvtPiYKHBxMADg4tjp04LWB+J687Gs/97X7bRfzOfsWMkAW5UC/kJHdLy3grGf5OtAfADiUlwPjtu2iP0d/ros8qL/JwcExVQLAEhQHRxfL5dIEehtg7wM4Zvw8Lq+fkNePa/cdM57vuuivvzBAXv20EQQ4AN6U9lsN+NXlnudiPn5Xu9zR7jswnn/gIBRHxGCxWDAx4OBgBYCDYxSgJwPoFwbIK1A+LkHddTmp/TwJ4JS8nNQuOik4IcHwOICWiFr5t29KgASA7ySQHmpv+UhmJ6Lv5eO22BdCnNVumz6CYwAeVNwfwGkASyGEIht35edwxwD729rllryo23e0n67LXZ1YLJfLA0M9UMSAywkcHAlBsdsBu0yArABw7Ehmr4P9vpG5HzdA/ZR2OSOBUl3OyPtPG5cFgH0iWkqguwPgKgBBRArUdXOesAC8zbznCvM5FLNeOH6q6/r9xySZIADn5WdzTAixkNn9UhIX/XILwA3jvhsaabhlkIW7hpJwoJMCVgo4OJgAcHDkZPdmZq+ycDNbN8H9DICzAO4DcL+8fr987p7M2lsJZFeI6JYEOtN057oN2N35PuDPzYwpgghQ4H7y3D4thDgF4IL8TBupJhxKsL8uFY3rAL6X129YSIKpKtzVSIGuFLBKwMHBBICDYw3wVYa/Z2T3ekZ/WoK6upw1AP5+AOfkZV8C/W0A3xLRFZmxtth01SMT/GOyfpFJBCiCCFDEzxgSoG43xvUTQogLAB4AcFISgwMA1+TlukEQvpMkQV1uGoqBrhIcys+/ZULAwQSACQDHboG+CfjHLGCvA74C9wfQydnqckZm9YcArhHRtwC+hbudLnRxgb9JAnz39wX/VBIQUw4IkYDYSwPgASHEAwDOCSH2JJjfQFcuUZdvNZKgEwKTFNzTCQGXDDiYADAB4NjeLF+v3yvAV9K9ntWrTF4B/YPyckzW6K8Q0dcSSFxDcIQn4++b/U9ZASipAujXbWRA/TwphHgYwAXpMbgH4Bt5UaRAVw6UWqBKCYoQ6D4CVgc4mAAwAeCYcZav6vjKla8y/DNGZn8eXR36ggT6i/K+PSK6DeCSBPxDuKfgCfiH6aRk/KkKgIsc5Gb/MUQgxRAYSwBiVQDXbXV9TxKCh4QQJ+X3dhXAZUkKrsjLVUMpuKEpBKrrYAlgyeoABxMAJgAc8wD9Paxkfb1+f9YA/IsAHgLwMIALRNQA+E6C/TVLdu/6qQ+tqUEAhAfUQwSgbxZLGQQgFfBTCQAcwO/7eU6SgrNCiFYSgK8BXJLEQCcESiFQJQNlKjxkMsDBBIAJAMc0QV8Z92yAf14C/kUAj8jLCSK6B+BTadQzx9T6fiKgBPhAv3XcX4oAIOKxFPBPLQHEAD484J6qBJikwPezQddtcAHAk0KIY+jk/6/k5bK8XHUQgjtMBjiYADAB4Jgm6N9vZPgPy8ujAB4kIqCT9L+Qi7lIBH4fAfApAK0F0FMIADJJgO/3fNm+C/hjwR8ZBMC2b0GsAhBLAMyRyISu4+AxdCUDoCsTfCkVgq8NheA6kwEOJgBMADjGA/1jBuifQ+fQfxCdpP8ogMcAPA7gFBEdENEncgFvLaBvA3iXzO8De58K0DoAvrWAuIi4zwbmsdJ/LAFwPUYRpMHnA4AB9EggAC4fgI0A2EDfRQT0+84LIZ4SQuyjKwF8DuALqRBcwspceM0gA/eYDHAwAeDgKAP6+kAeZeLTQf+8BvqPSMB/EsBpIvqeiD7AyqlvA/xlBvDnmP90soDAYzFkAGL9xM0B++g1IuYxktJKJOgDbsnf91iqGTCWCCwchEB1GDwnhLhPAv2nkgx86SEDykSoDITcTcDBBICDIzHbV2N2TdBX0v6jGuifkaD/IVaT9ZYOsPcRABFQBEKgH9P6BxdRkMDuk/r79v2HTniKvN83F8BZGjCIgk36txGA2JZA286HqSUBGylQ109pZOCGJAOfY1UuuGwhA2pcMasCHEwAODg8oK+G8xzDql1P1fSVW/9xAE9I0L+fiG4S0ftysW0DlxQCYGb7sQQAWG3MEwPy+s/WAfpA2gyAWhmnT+63Zfiuxxrb4xHkYOF4PEQAQoRg4XiO73JaCPG8EOI0Ol/ApwA+k4RA9w1cx6q9UJUIeOgQBxMADo7lcqlvsKPq+mexquk/rGX5TwO4SER3JOjfwPoe8qkEwOYHcBEAHdjbENjLVjNbxu+r+cPxuE8NqA36MWTAm/UbP5vA7zU2RUC2aIZIQWPc5yMAPk9ADAFQykAD4IwkAyck8H+sqQNfoysTfIuum0D5BQ6kKrDkVYCDCQDHLoG+yrqUi/8kVg7+C1gZ+Z4A8AyApyUAfEhElxNB35XtLwOAH1vnby0Zvc/0ZxIIX1kAHvCHgwiEIrcE4HquL+u3Pdf3O43j+TZioBSDmJKAyxOg379AvC/ASQaEEBcBPCsJ4McaGVCeAaUKfI/VxkXKOMheAQ4mABxbC/yNI9tXdX1l5HtKAv8FIroq6/p3DFBPJQA+UiAct62mP7m4x4B968jUW6TX+UNu/1jwKEUAfLK/SwkIKQWNQ1mI6RSAoRI0HkKwcNx2dQWkqAHqckII8aycN/ANgI8AfCJVATVv4KpDFeDyAAcTAI6tAX7TyX8GnaFPSfyPSdB/Wmb7APCBHM6zdAB+iADYsnzzvmD2b0j5Lqe/DexNcgDY2/9sIJ463a8PIUjJ9mNA3nc95AuwqQoN7PMEfF0CjaYSNJEqgEsJaCLVgIXj9gJdW+Hzcq39RCMDX2BVIriGrqR11EHA5QEOJgAccwd+3dSnavsX0Un8T8pM/zkAD8hs/wOZCZkg7yIAInA9yfmvAb5LCYgB+1CfP+Df4hcOohCTzdfwBVBAIfCpBk0kIXCRAxsxiCUFG+UBDyHwGQIXDjJgXncRAnV9X3YRXJDZ/weSDKgSwWWsvAKmaZDLAxxMADgmD/pqQVT1fSXzX8DK0PeUBP3niGifiP4uF0Qz218mZPxLB+i3cLv6W3n8m+Y+E+CXiKvxu8DdB/qtB8Rzav19nhMj/8c+J9Yb0ATIQGiqoI8ULByE4MgsKNWmBu6uAVd74CJDEdBVgQeEEC8IIQ4kEfgAqxLB11hNHVR7ERwwEeBgAsAxZeDXjX2qhe8Cutq+MvT9AJ3Mf4eI/oZO8nQB/zKQ8fuIgKv+b2b5JuinAn7sbn4hyT+mb3+DLAj/CVxDAVh/YNW6ZwN13+/6sn4fMYjZPCiGEGyUBAx1wOYDCAG/SxFYeH6eFEK8JDsIPgbwvqYKqPLAdax8AmwY5GACwDFJ4D+hAb8u8z8L4AUAjxHRN7KF7zAS+GPlf6fpT4Kkbyc/gc12vhbx0/xitvKNyfaFA9RT+vxrAwMlPEYOskAZqkAMCQDc/gCTECzg7xRotC4DnykwtgwQIgJ70idwUaoA7wH4EOvlATVTgIkABxMAjskB/zkJ/MrU97wE/oeI6FMi+lQD8KUn088hADbQt7n6fS1+Jsi3EUDvyuZFAsjHGvxcCsJQJCB2c6BYUkAJ5IDg31/ARwxsUwV9rYIbXQMeMpBKAFzKwNFjQognhBBPoWsbfE+qAso0eBkrwyATAV6He+EyEwCOHOBvsN7Kp4x9j6Nz8j8P4CUA54joAyK6ZAF8HwFoLYQgJP27QH+JQFsfwvP8YzP+DbDXgD4F8FPn/I+1+FOiKkBI7ByQwJtCCmJKBaHZAfr1RQQZcKkCC8SZAxfY9AgshBAPoZspcB3A3yQR+FgqBMowqLcQskeACQATAI5qB5ue8ZvA/wy6+v7LRHQfEb2HbuhJmwH+IULgkvht/f1LSzYf09qXKvGHwN5l6MvJ8ENRajRw7PbAfYgCOcgBZZCCFJ+ASx0w71/AMS/AoQrEAn4UCZDXL0jD4PcA3gHwd3Q+ARsROOT2QSYATAA4agC/cvWfM4D/BQn890tj39UA4C89mX7rIAFCvz8R9EOZfwrgKwm/DQA74Df5pYB86rS/ECHIJQCpv5vSVUCe3/epCPoQIHgIQAohcHYNRJCBhYMMmNdd5QEfITgvDYPXJRF4zyAC17DagIiJABMAJgAcvQ4ufXOe01iZ+3Tgf4WIzhHRuxrw9wF/G/DroG9r8wuZ/GJBPwXwfT9TB/PktPhNTeoNzQpIJQgh8Pf+jCAEfciAaydBs0Sw8BCBXBKgiMCLQohrAP5qIQKqa+CeJAI8WZAJABMAjuiDSi1a+gAfZe57BsCLAF4B8DARvSvn87uA3gf+S7iNf60F+FtLhh8y9qUO8/EBvmtufyzYh4A+tfY/tUjxAuQQgxgfgJUoWAhByhChkGHQVAgaCxFo4J8LkEoCFkKIi0KIF9G1C/4VwLuSCCizoD5QiI2CTACYAHAEgV8Z/NQGPRck8D+tAf9TRPQhEX1pAfsQ8MeY/5aaxO8a8GPu0pcK+mb93ibpx5KAFLDv0+I3RTJAPZ8f4zMIkYKQSrAxl8DYNwBw+wJCBkHAPS/gKPuXZMBXEmhigd+8CCEeFUI8i65TQBGBjyURuILVxkNsFGQCwASAw/r9mkN8zqMb4PMUpNQP4AUiuiTH9aYAv6/fPwX4bVv4+giADfSPwNyzZa8vw08x7cVu6FMK2Ida2KnC78TuMeBSAkKEwGk21MiA+fwQGdAJQGi0sI8I+OYDpBCB52TnwHtYlQY+Qbf50FV0rYPsD2ACwASA4+h7VYvKcXRy/wPotuN9Ap2r/xUArxLRARH9WWYRS6wG+fhAP1b+byUYh3byMwFen9i3RMSmPRbQN7P8NhPwc0x+pUb71vx9GuD3U70ALsIQQwganzrgKBXYyMDCuO7zCmxc5N/xdQYsEonAnvy5L4T4oRBiH8BfJBF4H91AoUtYNwryzoNMAJgA7OiBoxaufQn896Pbne9xdHP6XwLwI+ns/w90tcRlwqWVJME56EcD/dD8fjPD14f2tBbQ12+ngH4ok0/p308B4SGIwNBRYl+BWNk/lhD4NhoKkQGbEqBn/6YS4NtmuDFUgdCgoL1Q9m+5nBJCvC6E+A7An9DNEfgAnVFQjRe+hdUWxFwWYALABGBHDhq9n1+v8z+Drs7/IwBPaAa/w4LAvzRMfS4CIBwKgKvOvwbkAdB3bdDjA3Tbc9pEsC4l+8/RBFgK+G3RBJ5LEeqAbS8BkwzYiIPLH9AgomNAB3utPFCKCOxpRsHPALyNdaOg8gfw/AAmAEwAduBgUYuHaut7AF2d/2l0df4fAniJiL4mog+xKfUfIq3mbwK/a+e+JezSvq3Ob2b7RxfNPxCT6Yey/FjATwX7Gtn+2JMAh1AFUu73EYKYuQM+ZaAxBhGFSgQ2Y6CtTdBUBFxEIMYTsGdeF0I8K/0B7wL4Mzp/wMfo/AFqkJDqFuCyABMAJgBbmPWr8b2qn/9JdHX+V9HJ/Qsi+hNW+5CHgN/M+A8TgN8n84dc/UckQIK+gFvO7wP6vt/pC/Y1Zf+h9wKoCfyp4O8D9pJkQO0gGDNR0DZW2FYeiCUCe5GKgEkEjgkhfiyEWEo14C/opgp+ilXb4G0AB6wGMAFgArBdWb/u7n8UXZ3/ZQCvAXiiaZo/ygXABPhU+d8F/KH6fmtRAQDHIB9Htl8D9EsA/thGwLFiSOCPIQQlyUCMKqCXDczsv0HYJ+AjAtFlAMt9Z9u2fQ1dWeCP6KYKfoBu10G9W4DVACYATABmnvUfQ9fTfz86d/9TWNX5f0REV4zteWMA/xBu138I+G1tfeZgH8Ai+Qckfl9dvy/o1yYBqSA/dw9AzHNLg39fMhDrF2gcqoBZEgDWxwa79hLwEQGTFOwlEAK1/fAFqQYof8An6LoFrks14B6rAUwAmADML+tXPf33yaxfuftfBfATInqQiH4nT3IF6rGyvzX7l8C/DAC/bWa/TfIPAX8o2y8N+inA3ifj37VBQH0VgVhCUIsMxKgCrvLAwqIOLAJEYGHpGkhVAva02yeFEG8IIa4A+AO6ssAHWDcJqtkBrAYwAWACMJOs/xS6TXsexmqK32voZvd/SkSfWkDepwKYdf6jer+sKbq27XUBf0x93wf0Mdl+DdCfgwegBjGgyr9bywNQigy4SgRNQBXQ2wkXDiJgMwm69hEgjQj4fAF7vuwfm90CTwohnkQ3N0C1DX6MbszwNciRwqwGMAFgAjDdrH+hZf2qp/8H6Nz9rxPRKdnTf0/L+F1gbwP+tTq/Bvy+HfxigV9gfZe/Utm+D/RzRvWWJAEpQL2NcwD6KAKlwN9GAHLIQIoq4GsdjCUCevugzR9gIwIuUqDuPy5nB9wG8Ht03QJ/x2p2gFID2BvABIAJwMSyfjW/X2X9z6Az+f0EXWvf34nokiXLj6n9b2T/mtzvqveHgH9pZPt9gT+FEKSCOXsAyqsEY3sAYtUAIH5nwlQi4Npq2EUEGg8JiFUBXGqA7g1QswPeQVcWeAfd7AClBnCnwJYQgD3+CGf95ZOW9Z9GN9DncXQ9/T8C8AYRHSeiX6Ob+HUId29/VL+/zPpdm/oI+A1/ucDfRoC+eV/KHP5cEpBKDmLBe8r7APjAVCT8jgg8RzgeE5HPN59Lic8h41gi7ScZxxoF/rdGHvcK3FuZeDVEJIzfXVj+3sI4vtX5pUiAEEIsAAipBqjHWgvo+wZt7QFo5RyQq0KI14QQD6NrGX4A3eyAz9F5A24ul0ulBvAUwZkGE4D5gr9i+CfRbdf7ELpa/8sAXkfn8H+fiL42QD5U57dm/Uad39faZ2b/5ha+Nqnf3MJ3iGy/pNRfywNQGthjX4sqvZaIeJ5IeCyWEPQBfxcZIOMxYVECWuN3zN83iUCjnSeKCDQGEWi0+9Tj6rxrpDK3kK9n2z+jdRCJPY0ECEkEfg/gYSHEf9ZIwDvovAGXIOcGLJdLNggyAeAYEPzNaX6PoXP4/wjAm0T0EBH9FnLEpwP4Qxm/LvebBj/XQB+znS9U4y8F/DVBfxfl/z5/gxJeK0QOfMCfQghEBkEIEQUb4Mfc32qqgEkEhOYR0ImAXipYwl4OaDXSsBRCKG+ATiJaB+jbFIE9+X6+IqJrQog3hRAXpcp4P1adAt9KNYANgjMM9gDML+tXkv9ZdEa/J9Ft3POaBH9XX7+rzc/8GZP127bqbQcE/lC2v0vy/7a1AYaeU7IVMPSc2MecWw0jziew0UHgMQu6NhXSFYK1YUFat4A+OdDlATCNgbo34DkhxIPoDIJ/wGpuwDdSDWCD4PCY0AuXWQGYF/jrRr9HADyLrq//TQDPNU3zB3RO3VDW72r/M939tnq/72LKjH2AP6bunwv0Y7v/h5T/a28HLBJfYyplgNBzUkoE+v/m8gM0PRUBM5PXSYAwfupqQGuUBRbY3EdjzQNgqAC6GvA+EV1q2/bnMvk4j67j6EN0ewpcQ1cSOGASwAoARznw1yV/ZfR7EcCPAfxUzvD/gwX0Y7N+E/zNKX8uud809Y0B/H2y/VJzAHKJQCpAz8VsNbdugNQ5AbGqgC37bwooAuY0QVeXwMbugkbLYKgjQFcD9Mf2pEGwBfBbdOOE30M3WvgK5MZCXBJgBYCj35erTmC1be9FdEa/VwC8AeA12d532QD8kPxvPtYKIWzjfUMb+Oi3oWUb5uY8wnIpCfxjgP4UdgGcAikYuhsg5f5YJSDlNiJUgRhjoE8RMPcTaIUQJLPwRjtfGu21hEUVIOM8XShibpgEXWqAef/RfXKeyENCiH9B50M6i2742MfoNhb6frlc3gF3CUw6mABMF/xVze4EVpK/Mvr9lIieIKLfoBvqc9An8zfAX3kAbKBv28gHjqy/dZCAIYB/6pn/kBsATaEEkAr6fYHfBdx9wT8E+CWJgG4GPCoDCCGgdQzohj6ygLkiAguDtC+EEK1mEjSNgSFC0ALYI6JL0iD4lhBCkYDT6AyCqiRwh7sEmABwpIG/GuxzSrJr1dv/EwA/py5+bcn6QzX/tZ8Oud831MfW3meT+83+/TYD+PtK/kNm/kPN/h8jk8r5mymqQG4ngA/kaysBMX4A12eZQgSgPbbE+gwBs2NA9weYXQHCUAtadDK+ep3WogbYzLzCRgbkWvRDIcT/oZGAE+hmBnwL4Jb0BXBJgAkARwD899DV+8+gq/c/ha63/w0Ab8k5/l84wN+W4R8kZP0m6Pvk/mVEnb8W8NfyANRSA2pm/btWAiiR9fchA31KAqlEoHX8DZtREMbvm2WBhXEbhhrgMgTuBwiBKgm8DeAxIcR/kSTgDDrD8ifofAE3ZKvgIa/yTAA4NoGfsNrBTw32eRbdHP+3ALzSNM3v0e3VfWgAu8/856v1H2Kzxm+T/W1yv03ibz2AHntfaRIQem5pNaAUGSgN7qmvRQVfN3Y2QEwJAAWz/hiwB9I7A/oSAdd9ZsfAkT9AcoBQWUAnAvprKzWgdQwQspUGNtQCIvqciK63bftLdLMC7pMq5oeQg4Pk9MBD9gUwAeBYgb8+1e9+AI+i28Tnx+gk/4tE9K+Q23JqmX0M6JuS/9KR9buA3+fu99X5QyDfFgb+0nX/Xd/+N/fvpCoCfUsAMcAfowSkgH1f8LcRgSZADvT7lAHwyB8gz21bWQBwewMa4/xdiI5NLAJgv2e5rV7ju6Zp/lX6Au6XSsApmdh8CeA6eHogEwCONfBX9f7zAJ5A1+L3OoBfENEtR73fJAFOImAAv2uwjy/rV3K/a0EA/HK/DfhRIfufagdACqjmAu8U9gKIeR8xakCfCYG5MwGGMAO6rpvZvYscuPwBRwqB1i1AOrA7zl1dDTgaL6zOdc84YVjAX2C9S+DXAF6UvgClBJxA1yp4FStfAJMAJgA7C/7K7Hca3WCNp7Aa7PMmEX0gd/DTwT5F8tdr/fpPAf9wHxGR9fvk/pTaf23gn7v5b6pSae6+An3NgEC/EkAfMuBTCfqqAHAQAZ8/wFUWEEKIhoiW2DQJxnYKCMMb4FIBNsBf3U9Ef0PXKvifNCXgmHzuN+hGCLM5kAnAzoL/cXliXMSq3v9zAK82TfNbyG03ETb86YqAafQrmfWbJCBG7h8K+Oe88c8czX8lFIESoN8H+FPIQG0VwEcETNA3P1+zLNA41AAY56mNDOjkQ5UETIPgPuwdA+ZFtQp+L6cHnpEqwHGJPZfRmQPvMglgArBL4K/Mfvdh1d//GjrJ/ykpn91LAH9f1p8C/rFZf6rcLzJIwBitf1OZ/DeVGQAhcE/9W7Xlf9f9Nf0AqS2BMeUA06SXWhZAphqwMH6qkgCwMgiGBnxZL03T/FoI8TMhxCl0Xqdjkkh8DWkO5A4BJgDbDvzK6X8CndNfmf1eB/BLIrpPmv0UqMfI/jbwtxn9XJP9+mb9sWSgL/BPsf9/yMx/zIx/KrMAUoG/b9bfB/xjsvzYx3PKAqlqABwkAA41wDXd03m/XNteF0L8b3INVOWARpKAO+AOASYAWwr+5mQ/Nc//DQC/IqIDuf+2Dv4+t38I/F0b+SyR5vB3Zf195P7aJCD02NBEYIjMfyomwJj3M7QSEHNfzOZA6An+riw/5vGYskCuGqCrCjAUAZ14iAgS4CoHKnPg7wG8LIT4z1iVA/ax2lqYJwcyAdha8FeT/dQWvm/JzP9LIvrMAu56r7+z7z8g+XvH+cp+/jYz6/fJ/bWAv+bmP6VAfxc2AMo1AdZQAkr4AULkoKQ3IEcFgAH6rrJAtBqAbnMg83dtWXyoJGBTDfe119KvCyL6K4AnDCVgX/4d1SHAJIAJwNaAv2rzU5P9XgXwUwA/a5rmb+gmZbkkfxfwH8Dt8g/V+zdq/oGsP8bkFwvotYF/DNf/WHP/p0IOaCQloLQKEAL0EMDHKASxRCB0n6ssEPpMySAPup/Al82bJYE9TQ1oHcqgSQT0DoHPiOhW27b/jHVjYCNJwE1uE2QCsC3gfwZdm9/T6Dbz+TmA17XJfgceAnCYAP6hmf56i8+yZ9bv6gLYhex/zu1/qX36pd5rrBIQA2AphKBUSaBkCSAE9DEqQAj0bcTA5w1YOIDc9AVAu71nlAT2EecLUPddbZrmj3JyoO4JUFMMb8rxwUwCmADMEvyPoevxV21+PwbwSwAvN03za3ST/Q7grvmb15Xkb5v2F5X1a5J/iVp/iUx/DrP/p5T91876a+0eKCKfX7Md0AXWpVSAVPDvqwLEgL7+uz5vALAqCbjImm1+gBojDK2c4PMBmK/1vewQ+KkQQi8FkLaWMglgAjAr8F9omf9DWLX5/YqIntPa/Mys3yb36/cvNfBfm/EPt8vfNdbX1cdbI+ufevY/pbr/mPP/S6gCJSYBhkA/BPAp6kBJFSBGIeijAvh+N6QGwKEG6LdNcBeec94sCUBuVayvE/uOc33fvI+I/g3Az4UQigAs5Pu4hG5WAA8MYgIwG/A/poH/8+i28f0HrcfflPxD0r9u9tNB35T+Xbv4uSR/kZDlp2T9c2j/Gzv7n9Pwn5IbCeUqAb7H+5oDx/QCpDyOHmpAA7uPgDQlAGqTL0dJoLF8Tof6c7TpgS7lQLiOA7k2/lwIsacRAPX+1G6CTAKYAEwe/O8D8LAE/9cB/CMRPST7YH2g7yoD2Fr8bLP9+0j+fbL+UnI/Z//Tyfr7qAE5PoOhhgLVVgGGaAcMEQKbGmC2EJpEAYgvCZj+AGg/hccc6Dr/9DbBXwP4qRwfrEhAg25g0PdMApgATBn8j2vg/wN0Pf7/SETniOg3kZn/RhnAAH9b1u8a8BNy+9fI+vtK/DX6/uee/U+pFTDlvZT2A0xJBehbDsj1B/hAP0cNcBoELb/vOkc3VEWNBJjntrUrwFACfgPgDYMEKCXgex4dzARgyuD/CIAXJPj/ExGdIKLfeYDf7PVfk/4N2d/s73fW+xMl/xSjX60SQN/rJbP9ks7/bcr+U1SA0PspoQLkdgTEAH9s1h/K9IH1rX1jwT+27h+rBoQMgrCQAVeXQOP53NVmYzZPQAxpV22CvwPwY0kCGo0EqDWXSQATgEmB/1kN/N+S4E9E9CcH+Nvc/janv8/stzHZzzPYJ1Xyr5H178K2v3N0/tciGqlegBhSMOQGQaXKAX22Cc4tAZjPCRkEzWx/DejletRok4TNEgBgMQcaHQKxSp/6nT8BeEUI8S8WJeA7JgFMAKaW+b+ogf+SiN4NgL8p/aus3wf+rpp/qN7fR/IvmfXPcdvfXDIwpAIwtVHANRSAHNDvQwbG3Ca4hhpggnyoJKBeo7Fk9ebna5KBtY19DDUg+riVUwNfFEL8s+VYYBLABGBU8D+Gddn/LQD/TER3iOgDeRLciwB9vd6vg79vR7/U6X6lJP8plQA4+5+vClB7LkCtPQJK7BRYuv4fowbklgR8vgDfsWEqAzZfgKssYE4NfBfAc5IEbDyPjYFMAMYE/4cl+L8pM38f+Jsuf1P2N+v9Nqe/1eyn2nfg35/bBfR9+/vHzv5jJMUa7v8ShKAUyE9RARhCBehjDCy9OdDQKkAI9H0RUxIAHL4ASynAdZ5DJwOSBPiOE1c54ANJAv7JssZxdwATgMHAXx/vq7v9/1nK/rngf2AB/EMEnP4Os19svX/srH/Ksj8rAHUVgFiw92XzKY/5DIAxwJ+S9ZcgArUMgSklAfNxwD8vYIHV1uJwZfgeEuA/kLq19QVJApYWEiB4YiATgNrgbw75UW5/0mr+KeB/4Bjw43X6W8x+ter9Y/X+l1IDaoL+1Hb9m1oXgO89pZQGxvQBhLJ8H9gDmx0APsBHJvingH5sSSDGF2AzBzYx57v0BCQfr0T0Hjpj4D9hs/x5g8cGMwGoDf6nNfB/HV2f/0ki+qMH/K1mPwv422R/oRGDHLNfbNvfHKT/qQ/9qbXz3650AeQoAKmgXwv4Y7N+IL0tMFcNoITvyjY4yOcLWFMKHC1/e4YioFQCGL8Tc67rJOCvAF4TQvyjoQSASQATgFrgv4/Vxj7PoRvvq4b8uPr8D2A3/NnA3+b0t/X320x/cIB/br1/m9z/sc9JJQhTVABqvl7pXQKHUABCAJ8K/MB2dAOEfsdWIgiaAyWg62bBQ9jNgsJDAqKOG5lwvWmQgKM1kLcSZgJQEvz3AJxCt6Xvs+g29vkHOd73Nx7wdw34UYa/mNn+Lrd/qtmvlOQ/5KY/U6v/l1AA5qoClNwPIBbo+yoAKdl+DPCnZP0liEDOJkGpoJ/iC7ApBroiYO4Z4Pou96DtIeBpEbR9n6QpAb8H8JYQ4h8MxbRFt5XwIZMAJgB9wJ808L8A4Gl0W/r+ioie1Db2cYG/dZMfzfB3CLvL3wr+Hqd/jtmvj+Q/553/hlQAxlYBcl+fKv+tPtl/rgJgA+8UgjCVHQJ9wJ2a6cNDGny+ABhAf0QQ5GZAoWPjENreAYoFWH6PQscDEf0WwC+EEL8y1lMB4JZUAqZqpGUCMAPwPwHgPICnAPwIwC+1LX29Er8t+w+A/6Er67eAf4zTv6/knyv97+rY3xqkYAwloM/f6DMgKKf+X1MBSAX+GCIQe70U+IeA3vdc0wcQIgUxJEC4cMdRDjDfN5nfndxK+BdybT001lEhlQAmAUwAkmIhwf8cgCcAvArg5wBesezqF5r0t0wE/yMpy9Pm53L6l6r3T0X630Xj35wXq5zMPxfsYwF/aAUgN+v3AT6Qtz1wbinARwp8HQJmm6Dtcz6MIAGUQCiJiP5NCPFLdCbse9oaKwDchjGZkIMJgC/71+f7Pw7gJQA/BfB60zS/hn26n3d7X4/b3wb+oR7/0k7/KZKAMRWAXDKQCt5zHACUk/HnZP6u59dqCayhANiy+xzwr1UKyDUH+joEjkiABPSlY0thHwkghxLg8gMQADRN829t2/4CwF25Rqs1uZUzAnhQEBOAJPB/FKv5/j9rmuZ38uCKafMzwf+wB/j72vxyzH616v+5hGDOCkAtFWAuSkDMe6xlCEwpB0ylJTBECmLmAQDD+AB85kD9to0ErM0KMAYGpSgBMceUun63aZrftW37U5n135XrsCoF8L4BTAC84O+a8verpmn+BuAG1iX/Qw8JMDP/5UjgP3T9v0/Wvy0z/8eW/GsRByr8fvpOCaw1H6C0AhDK9IG68wBSgd73XJcPwDcrIJUEEABykACbCqDfd6Npmnfatv0VgDtawrbUlADuDGAC4AV/1ev/OjrH/1cArnhAf4nwbP/DQuAf4/QfS/Kv3foXem4tBWBMFWBKakDKeyhtBuyT/ddUAHyAnkoEaoF/SR+AjwTo0ZcE6EqALdsn2E2BBOAqEX0lOwPuYFUOaNGNDOYZAUwA1sDfbPdTvf6/IqK7RPQJwsN9XLP9zSE/tcC/Vr1/irv+lQT+mj3/u2T+S/lfpmIGLKEAAPEtgHqmn3p9LPB3kYFQR0AfEkDQTHuGEnDgUQKOfso1+6RBAlQSdpPbA5kAmJ/DSXTtfk+ja/f7BRGdkcMmNqR9C/Af3Wcx/OmgP0fwn/PUv6mpACXBfioLGBV43ynlgFLZfyzoxygAKURg7F0CS/2sRQLUz6VGAsjjCdDB/4ggyL1Z3hBC/MIgAWrNPGDg4+xf9fqfQ9fu9wqAnxHR00T0v+B29pv9/ksASwnkS9in+i1HAv+Sc/9r7fo3lgKQSwZSADgXqOeSoYTeZ245oMRkQBeQx9wfUwqIyfJjHqu1UVCf/QFiXq80CVgaP5UaQJIEmNK/fvvAvI+I/gPAL4UQP8PKGKh3Bux0e+BOEwDp+D+GleP/JXSO/1cDg35sZOBQAvkh3Lv6LQuBf0yb3zYM/ylxO5UM9FUBxlACar7+WFMBYzP/UIbvAu0YMpBTCqgxICi2DRBIq+Xn1P9dJMA1MCiHBNhw6tCiBJigDwsxABH9u1QBbhkkYLnr7YE7SwA009996Bz/L6Bz/L/VNM1v5UGiMnyznm+b7W8z+7k29hETB/+pm/9qAP8Q0v8cZf/SewDUzPxDZKBmKWCoAUG5bYApoN934mAOCRAeIrC+ZaB/WJBNDbjbNM1v27Z9E10n121o5YBd7gzYSQJg7O73IDrH/2sAftE0zXtYTY6yzvLHZgnAZvY7hHtXv7YC+O+C+S+XCIyhAuSC9dyNSTXKAUNtEhQC/VLAH5P1x6oBQ5sBfYpAHxIQSy6XDsA3LzCu326a5j05KOiGoQTs7O6Bu6oALLBu+vshOtPfNQCXYR/l6ywBWHb204FfzBj8hyQBpYlATra/Da7/2gSCKry/El0BtbL/GgrAEK2BNTsBUkyIfUiA2nLYpwKESgD65TIRnZXlgBvoSgL3jASNCcCWZ/976Cb93Y910999cmepQ8QZ/5Ya+C8t4K8A3bbBz5zAf26T/0qRgRgwHbLWPxVloMaOglPpCpjKJkEmsPrUgFKTAUcnAXB3BbQaETh6XW1GQGMB/QMbESCiD9BtIfwzGOWAXTQF7hQBkNK/bvp7EcCbAJ6TG/wcRlx8g358rn/brn5Dgv8Uxv/WVACGUAFSAHAX5f/Y/6VkGSAE9jnZfyzo11IAgLqTAfuOBS5NAtSWwLZdBJeW19JnBJDmCjyIUAGIiH4vNw66rpGAA3R+gHaXSgE7QwCMuv9DAJ4H8BN0G/z8Du56v22a36FlxK8P/AXcW/rWBP+SUv9Uhv6Msc3vnIb+TL0EkJPxpxCCMToC+gA/MKwfIMXdn0IGbM+NJQHA5lbC+k6Dtu9deQFI6wyg2IvcM+ANjQTcUcndLvkBdoIAyEl/C6wm/alhPz8jovexMv3ZgH8Ju+M/lPkfZfcS+IcCf3b+l8n2eehPv/exi8OBTKCOAX6gfEcAkO/mL0UGepEAmak3GglwzQcA1mcE6ERAvaaNBNwmovdlKUAnAYcAbshywNZPCtwVBWCBzWE/PyWie0T0dSD794G/OdRHr/0vtazfB/6YEPjP1flfCvh58M8wRIEyX2ObygBjKQA54J/6GkBcOQBwbyXcGiOA9XkBOiFQ81ZcSsChQQr0UsDXAC4IIX4qScBNrDoDbmu/ywRgxtm/3u//CGS/PxE9Juv+S4Rlf9t9etZvtvqZ4N96wN+2q99cwX/Kdf8pyf9DgX2pv0EDvK9a7YFTKwP4gH8IBSCkDpSaCRC6z7WLoCIBrVQDEPAFmEAPxJcCGiJ6B92kwDdMErALfoCtJgBS+ld1f7XD32sAfkxE/w5Pjd8C/D7Hv63XX1iu24A1drzvEOA/lgKQSgRqqgA1QL8k2I+hEpTeBbAUKRARzx2rDJAD/HNRAHL/TmgrYfN5raEaUOAYMTsDzLkA+n2NUg6I6DeyFHAVwHdYtQe2y+Xy3jaXArZdAVBz/s8DeApdv/9Pm6Z5V5N6DmJIgAT/Fvatfc1ef33KH7BZApgy+PcZ9zulfv85T/yb84JTQvpPzfhTyUCOKjCEH6DWHgG1dwcsRQLMlsCFWkstI4MbuNsDDy1+AF0laLA5KfBvbdv+FMC3kgQcdQZgizcN2loCoM35vx/A4wBeBvAmEbXohv3YhvwsPZm/PtvfVft3DfqxAb7YAvCfS91/qqC/a9uRip7EQET+Tgk/QM3sH6izR8BQ8n9pEiCMTH+DEFgGBdnKAHpnwKFDCTCJwKG87xsielQI8aYkAUemQFkK2Mr9AraSAFjm/KuWv2eI6H86gN42yU+Z/mzb+NqMfwL+QT8uAJ8S+A+95S/X/eetFtRoC5yCH6BE9p9KBEqYAscaDNSHBPgeN1UBWzlgaRAIZQpcarsHNsZP0sBf+QH+LIT4R4kVigTcw2rToK3zA2yrAqC3/D2DruXvNbnJT6ju71IETODfqPVbHP++QT+m8SUlE9/G0b+1yUCJbH+Muv/UVYISs/9jXrOkH6BEl0CJMoANyIF6o4FrKQA5fxcGCYgaFITN9sDWpgBg0/0PS/a/4QeQmwa9BuAbdKZAc1wwE4CJZ/96y5+S/t8ioi/ll3kYCfyxpr/Ydj8f+Ke4/ucy+rcmESgJ/FMB/W0tB/SV/VMAPjXrT1UF5tAWONXRwK77YkgA4G4P9B1X+nwAkxTYygAE4BYRfSmEeAvAFUkCbgM42MZSQLNl4K9G/epb/L5ORPcT0adw1/k3tvH1TPqzmv7gb/cLZfu1RveWVgBSzIC5hsAYMhBzn+/+0GMxjwP2mQ2xYJb7u9tEDEp9fujxPeYcIyLhuPQ9J2V/jNjzcC5rh+v/sM1DCa2z+m1zKuvSGNnuM3svARwS0adEdD+A1yWGPCwx5ZjEGFYAJgj+JP8fU/p/lYh+HQB9W93fBf4bY34djv+YAzv1BJ6zAjCUCpCrBMRm4Vz7L5PRx/xPJc2BYw0KCpUF+rYIphgBx1IAQu/T/M7NQUG6L8DVGaD7AJbYNAU2minQ5gdYUwKI6Hdy18BL6NoDb6DrHFOjgreCtG9TCWCBbpe/c1hN+3uDiN7DqobjGvPrIgStg1X6TH9A2P2fyujnDv671PNfEpzntMiUbP/rQwhKkoEUIhC6b8jZALkjgof2AgDhaYGhVkEy1liTBLRY9waY3QAb4C9v3yOi9+SAoMsArkGbD4AtmRK4FQTAMu3vBwB+QkT7RHQJcQN/Yur+Kaa/WOlrLvI/Cl8vmfWPDfy73Pff5/+rORfAB9ixz5nabIC+A4KGIgEpCgQQ7xUA7KZAkzgsLZ+77gcwWwNdA4IuAXhcCPETSQLUfICtmRK4LQqAkv4fxGqjn5eI6H/Bv8GPDu76sB/vlr7YbO/z1f1D7X4pWf5UFIBUctCXCAwJ/EOA/i7W/Etl+KnZfqoqMBVTYN+hQCkgPIUyQEp7IOBvDTS7AtTPRpsPoJcDnEoAEf1RCPErAF+hMwV+D1kKkGoAE4CRs39d+leu/9fljOcDB+DbdvrT+/2D4K/NB0h1/IdAfergP9XWvyGAf8qb/AxNKmiA/4Eyfzc34x+CCNRSA0rtFDhmGSClPVCRANUVYG4W5CIB5nXTE6BfXwI4IKJ3hBCvA/ga3XyAmwDuLZfL5dy7AmZNADTp/wxWA39eI6JjRPQN3Nv6bgC/Jv37pvyZYO/L6nMBf2ryfy3w54E/81YMYt4TFf4bY5gCU4lASS9ADPCHsv6xSECKAhFSNYKmQLj3DdjwA0jC0Gg4YBsMdPSYxJKnhRCvoTMFqvkAB3MfEDR3BUAf+KNm/b9kcf0f+lQAx5x/qwIghDAn/AHxpr/U1ropDv8ZQwWYA/CXAOltKw2Uqv2nZvkpGX8u2Kdm/ilqwDZ4AVL+ru09m8eL1xSo+QFa47PTFYCj17OMCjazf7MU8AfZFfAluiFB32FVCmACMEL2r6T/swAeA/AiOuOf7vpfRqgANoe/S/63TfZLMf0B0zQA5oB/bj9zKqiXbPWb2qS/XfUC9K3957xGLR+AL/OPAf1YNSDWFxDrBahNAnIVANf/aHuuWR5oAwqAuq7AfunJ/vUSwRKrroDX0fkBrmJVCpjtgKBZDjXQev5PA3gI3Ta/Pyai09K5aTX52QiBzP4PYR8msQb6kXV/n+kvV3KfysY/MeSgjwoQSxDGGPqT81zX7+7q8J+an0vJ77DUcKDc47zP+SV6nuOlfvYpMwqPmgp4hgRpM1nMddpcuw8tHV+HPuwgoktEdBKdyfw5dFvMnwawJzGJFYCBQo37fQDAk+iMf68Q0e8DX+Sau18DdPO5vszfBuzQ7rddn6oCAJTpBMgB+9Ssf46O/yFBfhtMgCUUgpyRwdtmCERitl9DCSihANg8ALF+AL110MzsjxJgOVBIVwR0hUCVAvTWwD/JHQM/x6o18I6GIUwAKmf/atzvWXQ9/8r495XxRbjYnW+DH+tGP7K+lDrsJ3Q9VQEoTQLGAP85bfE7te19p6YY1NgEqDQhGLszYA6GwBokIPXvAHGmQCDODyAAtHI+gGvDoCU22wBdHQJ6m+AduVfAa+j8AFfQlQIO5jgbYI4lgAWAk+iMf08DeBXAw0T0Mfx9/qbxb4lN6d+8LGGX+3Pr/n0UgJJlgCHAP2XuuU8aTVUDSs33T5WeSwL0NuwVUPN/6LN/QJ/nhEoDyDymU/bHyL0+ZIKR+lhKadFXKrCVBUJzXMzR717cQLdXwMdE9JDEnqclFp2U2DSrmBUB0Ix/9wN4FN1GDT9umuZtBJz+FvAPSf+uXv4+df8pKABDgT963E5ZUMcGfgb7cf/XKREBgXwFq48vYGokoM8aI9DfD+BSaFtD9dXbwJeImxmjSMCf0XkBXpBYdD+A4xKjZhOzKQEYxr+LAJ4F8EMp83znYW/mMCBd+m8Dl6Um/7tq+il1/9wTotQJWgr8S7n+a078K/F43+fXeo1tIwV6DNUi2LcrwPf4UBMCfY551/NrlwNSXzd0n3k95Ac4ul8I0RrbBusDgnSfgCoHmKWAA6x3AuiegO+ICEKIHwL4AquuAFUKmMV5PicFoMH6xL8XAbxIRH8KsLZWA31Vz19aGKFt2p9IYJi5WX+O1J8K+qVZeao8OTT413D9s/N/HJWg72uUUAViHkeP47l0V0AptTFnrSkp/8esq16FVlvD28Cav9T2dmmx6RHbwBeJPS/Ky+MSm47PCVdnoQAYE/9U298PpfHPluFvSDxY1Xpa2Hf8c0n/tul/Jev+uQCfmhmNte1vSTIAzKPPf26GPVfQxN57n3kBVOC5cx4T3GcqYMpnnaoOmPfFKBlAeD6AShpdHQK2jYNUV4Ce9bvMgGpM8FdSBfgMXVfA9+hmA8xiQuBcmEqDzmRxHl3b30sAniKij+Bu9fM5/nVzn63nf2k4/0PZb5+6/zaN/o3JUGqA/xT6/IfO8kXCZcp/Y6jPeMg5AblqgEC+kbZWybG28bikH8D2u8JQfV2zAcw28cMYbJEY9JTEpCclRp2cC7ZO/k1qbX9qq9/nAfxIbvYT6vc/+kKNoQ8u2X8Ju/TvavkD0uv+UxjSMRT4l5BDcxfc1Ow3B1iGAMAxAXfK71P0/M5KKCc5w4JyicGcSUDKYynrj7kO29RbsxTgWvePEkTLrrCuxFKRgHfQGQKflxh1H4BjEruYAPQMc6vfV4joDBFdRlzbxtKhANiyf/PgCUn/OVl/3xOk74k4BfCfSstfX+AfAkjnHkP8PzW/w6m2BtYiATXWnFLG49TrtvZtOFQAmwIQ1R5IRJeJ6AyAV6Qa8KDErMmX2CdNAGRLxTGs2v5+AOBVIvpjIOtf29ZXa/Fw9YPq0r8ISHGu632z/tokoNSJCPTv96+Z9c8N+HfNJFjz/x2bCJRWA2rNB5hjO2DoMwity0oFWDqSPNMQaPrFfIrAocSkV7HeFnhs6m2BU1cAVNvfgwCekeB/iG4rRh8rOzS+SO+kP6xL/7aDwyU55Z58YqATEomvm5tV1AD/nGyrBvCXBivuCqj/edQaFNTnGJzKfAAkrjl91p6SfgDfdd8avbaeW0oBthkBNuw4hF9tvkVES0kCnpGYdXrqKsBkCYAx9OcxyaxeIKK3Eaj3W1hbqPYDuNtJcuWn1DJAbPtfajYce2L3UQFybw/l/u8D/GMBEhOCcVSBFCKQowbE/L2aJCCWGIiM9SWWMOTsPFqiFKDfBgJeMFjUZB/2yLbAF+TlMcxgONAkCYBj6M+rRPQF1tv+XOYM1fNvEgPnJj9Gv2iOxJSa9ZfO8rfB+V96l78xgZ9Bf1qf4dATA0vvGjjFjgBUXINS16OYddq21vvGwOulgBZhs/mBxKhXJWZNfrfAqSoADbrd/s6hG7DwAoAn5Lz/kCtzgwjAv8ufVSpCeek/5UTIJQNTN/+l3Fcq6x8L+DnqkIExiMCQakAMMRiCBMxlDcoqBSDOEOgC/kOPCvAxgCckZqnhQCemirWTe1OSKe1r2f8z6Jz/HyFii1+Nuem314x+2DT+tZYDpYT0NCfzX+pCMhb4l876GfiZCAyhBkyJBGCia1AN+d9aCrDMBrB1DdiwxFcKUF0BH6HrCHgW3eC6MwD2p6gCTJGVLCRjekDL/h8moi8js3+b9L/EpuljCb/0H5KXhpLdciW2FHafysBLgH+u5D90dhfz9xj4xyMCJb7DkqShREmgNglI9QGUXoNqlSNj12qzFGDDBnM2gIklPhXgSwAPY+UFUCrA5LwAk3IoWmr/Kvt/F37Tn+1Lc27wg3UTiCkF2aZNAcNI/7XH/5ZQAVLBf9vG/Jb4/WEZdebGJFOtW3q+Dxro90Mjcn2Pux6zjf4F0sYDxz7mejx2Q5+Uz6jmxkApGwbpY4L1x0gCc6t93uTAjYWGM41FCWg0EvCuEOJlAB8A+ArAdQB3lsvlckobBU2tRWEj+yeic3LSkq/f37bVr2vMr8kAbeDex2wytfG/IWYfWwscC/xL1vq3FvhrLCq+15woORiSCMSQAESCPSKBOgfokQisvvdQ6qfrNYH4fQJC123/H7C5Y6AgIp0Y6D/1nQJbbZ8Ac48AGwG4DOB5IcQPAHyMbp+AGxpeTSImUwLQsv8zWvb/MhG9h3DbX2jKn3kB0o1/JbP+1Cy99LjfGDBP+V/HAv+SY11nBf6LxULol137+xP5rvuWBGLvL9EGmHLeD70m9V1DbddzDIEh1TiEO2Yp4D0AL0ssuyixbVIdAVPyAKjs/5zM/n9ARPcDuAK/AUMf+uOr96+1fGjZv036d5UB+pCAIev+KSdy3+t9wH+qM/5L/n4V0J2iAjGx9zbk9z7GngF9BwL16QzoszbV2BPAW+v3rOsC6KRg2IfDbfgCDPPgYQCbrkgM+wHWOwIm4wWYBAGwZP9Py+w/VPv3mfxs/f5K+re1AuaO2RQ9T7QUMoCMkzFlYRka/EsvqCUzwUkA/5RBfybvuQQRqEkC+pwztUnAGGtTn8QqdVy5nhBu4AQ8u8V6MMhUAd6VKsDTU1QBpqIAqL5/NfXveSI6q2X/tg96bTqTJfs3pzoJuI1+scY/EXFATUH6rzn0pyb4l97Wt/aiz6A/n/9lKBJQeuOgnHMOPc/vsdamvkqq7TNuPYqA67beHWDOCTBVgI0uNOPnFYllz2M1HXAycwFGfxOWvv+nALykZf8t3HX/NuI+M/sPTfzrowbEAmwf1l3S9Ncn2x9y6E/JRXiyWf+2gP6E/7+hjo2SvoCpzAUYYm3KWUtz1+3QhEBXxh+DO2tkQGLZSxLb1HTAScwFmAILabCa+f+oZErnZPZv7cn0ZP+2QQ5m9m+bABVii6UktVwfQB+5DT0XgLmB/+yy/m0H/okSgSHUgLmQgD6JSp+1qeY8ABjrvGuN902ENTGkdWCOSwVQ169ITHseq50Cj08Bf6dAAPbQ7Z18AcCT6Fr/PoS7rhLLwjam/zna/mzSUgnjXwkSMGTrX+xitW3gP1rWv2vAP7H/v68aMDcSkHOO1xwNPBT42xI623evJgTahgOJHirAEkArMe0FiXEPSswbvQ1/VAIgd0k6JhnRI5CjE4noa4RHL8Zk/ynz/nONf31aWaYir/UlCGOC/xALOQP/dhOBmgSiNgnIIe59/QC116q+uwTGrt8hXHCNj3epAE7Mkpj2kMS4hyXmHRt7p8CxFYCFZELntez/U88H6duwwZb9C0v2LwJssI08cGrt+ofEkzAV9PssCENO/Su92c/oWT8D/2Q/n9okUiCv66VvJ0AJ4C+5DsWsfSXaA2M2Cwq1BQoHlvhUABdeKRLwqaYCnJfYt5sEYLlcNjL7PysZ0TMAHtcIQOv5MPtm/6ngPvaufznPr7nrX23wr525jQJuDPGT/6zGLAlMhQSUHlc+5i6BOW2BJVQAW4KqCMDjEuselth3TGLhzikACwAnsRr7+zwRXUKglgL/5j42xhYa+hPT9tdHTqvdAlhLPmTw56x/V9WAbSQBff+XEgOBSu4SmEsEotoCteFAPhXA1SXgxC6Jcc9LzHtAYuBoKsAoBEBr/TuDri7yFICniegDhLf6VczL9mXYsn8RyP5LZvyl5bSUk3XqU//GBP/BJX8G/ll/jqLHcTZVElA7gSmxdqX8vyUVAZcKIAIqwNLAIqf8j1UZ4AN0Q4GewgS2Ch5LAdDH/j4K4DkiugP7Fou+uczm1L9S2f8QJ08f5jzU1L/UBacm+NdelIuCFkP3VnymNcnmUCSgxPm/Td0AooAKYGKOb/+Zja3rJdY9J7HvHEYcDzwWAVBb/j4I4AlJAN5BuN1PgbpJBvpm/zlO0lIkIPfE6iPd5Wb7uYtaCfAfagHnrJ/VgKGOPVH4XEol7aLn2tGXFITUgBrg7yNquSqAiUleDJNY95zEvgclFo7SEjg4AZBtD8ehmf+IaA/AHUem7+u7NOc1619USva/bbP/Ywd85C4cKa+3M+DPEL21n/XUSECtczU3ken7+fTdurxEK2DsRkFIwCcbli0B3CGifaybAY+P0RI4hgKgzH/n0c1Gfo6I3kec4c/Gskxzhm3uf0z2n3LApLSl5JxINVWAuWz4Mwvw56x/Z9SAKZGAof0ANbL/EluXl+jcilUBXDgTMgFau9mI6O9SBXhMYuEoZsBBCYBsd9gHcB9Wc//PA7gK9z7MNvlfBLJ/UTD7LzlLW2QsCkP3/qcuLFMC/0HNfgz8O6cG1PShDEECcteCmjMBUtfL1HWtlAogPCqAwOYusz4fWysx7wGs9ge4D50ZcFBMHloB0Hf9ewSd/H8lMfO31f5dfZookP3HylCxj8cy5tjnI+OEKQm8UwL/bQUejml9F1MmAbXO7RLlzBojzEus4SLwt31zZoQDm9qQOkBEV9F1BDyCkXYJHJoAKPPfBXR9kE9pc/99H5he028d0sxRZm/M/K+d/Y+x81+OCjDU8J+ai+Do4M+S/3RJwAxKAjWO/9qTAae8llXZFdDAChum2HBHGBjVhhJaiX1PSyy8gBHMgIMRACltKPPfQxL8CcA9uPv+Y02B+pe2HDj7z5GvhjDP5Mp9fcFfDLj4DQ7+DLWsBoxIAkThczUX+GPXopy1bKhppjkqgA1rQqY/334B9zoIPJoJMPhkwCEVgD2sJv89BuBZInrPA+627N829lf/ImCwOQyU/efIZiUYcy15sIbjn8Gfg0nAcOdJrZ0CU0hFzV1MUxO0XBXAxA3AbTjXh9TFJK5LaYB/VmLiAxh4l8BBCIAx+U/1/ivz3zKCBOgfuPn4GsBLIwawadoYI/uPZbpjZ/9j1/0nC/4s+c+XBAz4vdU4fsci/FOaapqSWNVSAY7u10x+tjKBORjIZwDUL1ckFqqZAINOBhxKAVC9/8r897Q0QLQB0G9NdgX71o1tgLGJxIOidvZfY+JfLlNOyRx2DvwZSlkNmDEJKFEKmPPaljMK2EUGzJZy2wRa2+Z0bYgMWMyAxzFQS+BQBEA3/z0mCcD7gWzfnLW8MebXBH1L61/rYnMZB0TJ7L/UYtMH6FPZMgZe4Bj8OZgElD8HS64PU1jbSqgAwoERMMAchhHd5g9QLYEC7n0BTALwviQAj2FgM2B1AqBt+6t6/58kogZu85/p8g9NALTVYfpk/8g4oMZiyCVYcd/FSgy4sFVfyFny314SMND3OiQJmFopYOoqQC4xsJEBW0ugT8EWcM+3uScxUZ8JMIgZcAgFQN/45xHJdD6Evy7iav2ztf+Zg390ucaX/aPAAVOaIfcdEDSHLX+HXFg56+cY4zse8vif0lbBNYcC9VUBYh8TjqTxKLG0DAay3W4jzID6RbUEPoLODDjIBkFDEABT/r9IRJdhN0/EmP9sk/30Lyj0RcYoA6LAQddn+M/Udv0rvciJCSyoDP5MAqZEAmqfT2PvEpi7FpZQAUL/V+gxW0ugeX9rwaloM6DExIsYeCZAVQJgyP8PAniCiG4j7PY3gT7G/GdKNLEHZJ+d8Upk/zndAX2JQOoCIgouRgz+HEwCpn0elt4lMGet6+Nz6rvGuwiHD3Nck2lTzIA3ATwpicAgGwTVVgDUxj/n0O169BQRfYDNGoj1Q9EkFPgyf6P1LxX0c7Lkmsw45fl9T8TUzyblc2Hw52ASMBwJKLlBUKnEY8i1LnWtjv1srD+NlkDXvjNmGcC3z42aCfCUxMpzEjurYnRtArCHbrDBeQCPSlZzA+7eSVuG7/IHWMcAB6SdUq5/YPiNf0pl/7WkfwZ/DiYB0yMBJTL/mipA6nNz3f992sB9LYGuFsAW7jK3a6bNTXRqud4NUHUmQDUCoI3+VfL/40R0w/OhiIBEYmvxs5n/UroAROEDKZURl9j4Z4yFZuwFkMGfY9dJwFjn+BS8ACUTt5TBQGv4YpgBbYZApWTbklffTIAnoJUBauJ0TQVAd/8/hK7970OEd/hTH5pNEVhaviCR+MWWzv5LAvKUsv+xXP9VF2Nu8+MY8bgY4jyZyjbBpf/PIVWAWDxxlQBcuwf6drpVBOAjdD4AvQxQzQdQkwAo9/8D6FobXPK/yzAB2OV+6xAgz5dU0gfQR/av0Rs7ZIYxhPRfHfwZ6jhGPkaGMPJNZQvwqc48ya3/myBvkgGbCmDDNl+Z+47M+h9FVzo/DWCvVhmgCgEw5P+L6OT/63BL+6ZkEur9t03+E44vrqYPIOXgn3v2z+DPwSRgPiRgG1SAIdqfY+v/LjOgb2t6czJgqPVdLwM8jq50fh8qlgFqKQBK/r8fK/n/A8S1/5kT/toI1oXILy3ngEg94HYp+2fw52ASMD4J2BUVYIypgK5k0pb1A3Zvm7Dgm9M0SESfSALwkMTQE3MjAEr+P49O/j8D4Bb87kgT8G1bLtoG/gDuXf+Q+eWnHPyc/e/2Qs7BJGDsYBWg7vbnttut5TVtSoA5zM7V6aYnwXfQ1f4fxqoMsD8LAmAM/7kA4DEi+g7+tghT/ncxqlDv/5Dmv9rzsVHpRNuZ7J/Bn2Pix9CuqABDroFDmAFDMwFsirU5E0A4lAK9DPCYJABnUGlvgBoKgJL/z2JV/38f/p7+kPxvZv4+NWAo81+q9FWL+U4t+2fw52ASMDwJmLIKMKU1sKQZ0Jb1A3b/GpBWBvgYXRngIlZlgOLdALUIwCmspv+dkZJGaEBCrPzvGsqQC/q1gL8PA55y9s/gz8EkYD4kYK4qQCwhqGEGjCUDNv9ZnzKAfrktsVO1A55Chb0BihIATf4/g07+f5SI7sIu92/IJZHyv7DI/y6ppjTzRYTiUIIBTyH7n8qCx+DPwSSg7GvPQQUQPd5bjbXQ+zlITLIlrGvKtaUM4CIDQu6b8whWZYD90mWA0gqAav87qwgAgE8RuSMS0uX/mJp/n61+cw+4sQYA1V5gxEgLGIM/xy6QgKmch7VVgD7vqYQZMLct0PV4bBnANRTIioWyDPAYKrYDliYAavMf1f53noiuWFiOa4c/IE3+j2n5S/3CSzHfVMlLFDi5x8r+R5f+Gfw5toAE1DyPaiqiU1kLS+8JEPP+XR0CrjIAYB9uZ1PIv0U3SE/5AE6hsA+gGAGQk4r2sZr+9xARHcJf67fNRxa+DzJB/i/d+z/0AKCS7XhjZAUM/hxMAuZ7npVQREu9nzEGAiGAKbllgBD2md0Ah+h8AA9IAlB0c6CSCgBhVf9X43+/QFj214FdOEgCkC7/5zC8Pix3yua/KWT/DNIcHOOeU0P7fsZeE3PWe5HxujFlAOHAuhA+foF1H8AxibWTIwALrO/+9wgRXYZd/m899/uGA9lYWYwk04fBTsX4UorpTiZz5+yfg1WA0UlEToY/lzWxz/+FnpjjM/nZjIBWDCSiS1IBuCCxtWg7YEkCsCclivslAdgHcAB7n79L/rDtrpQq/+fIO6UP8ponMWf/DP4c208C5qgCzG1H1ByfVE4ZAA6g170BNkPgEsChxNIHJbaeRMF2wCIEQKv/q/7/h4joJtyGCHOqn5JL4Mj6bb2XJeX/Kc3/zznJx8j+Gfw5OKZNAoZSAVL/9lz3BQg934dZa6UBB+ZZLxJLH8JqHkAxH0ApBUDv/z8v3+wXiGv9012Qrov5gbfI2zin5vz/UiddKTduKvPnBZeDYzePyZJK4NzXxhTwdw0FsmX9Mc5/V2v8FxJTdR9AEewuRQDU+N/75Js8L2cZ++r85gfndUUWcv/3lYn61rZynlsjY4j93cll/wz+HDtGAmqoAGKA95P6O6XW0RJruo8M5HYDhDDO6Q+QWHpeXor6AEoSANX//yARkZH9+wYiuNr/4FEBxpL/+0pcOQd9rrt17KE9DP4cTAImei4VWi9KzkfJXUNT1+raZQDX5nSt47GlByP1oUCEdR/ANAiAUf9XBsDv4HY/+loiXK1+tg2AUlnjnCSuoVl7jeyfwZ+DScCwUVIFqKUmzqVEGgrftvRRWGao2s5OAHm5rhGAYj6AEgqAqv+fRmdSuEhEXyFu3rGrJcLXYgGHDFN7A6Dcg1oUPgFKqBezy/45OHY8pnRO11pr+m4MVGptz7ntw6mYdsDQQKCvsTICnkYhH0ApAnACqwFADwC4Br/BQTg+IKv076j/l7idyx77SFs55KGGylAr+2fpn4NVgPFKAX1UgBrZ+ZhrZc76XgRjLJglPKQAEXh5TYL/AxJrT0yFAOzJN3MWwAVZq2jhb/9rASwtu//5ZgHY7u/7hc153OVOZOMM/hx87M5SdZhLGaD0FsExGHZ0n8TAZQRethJbL0isPYEC8wBKEYCTWO0AeCMgdwS3+3UwJJfUUmvr3xqz+MeW/0tnEFXJBoM/B5OASZ3DY5QBSqw3tbZLj9l9VoSA3UESbD6AGxoBKDIQqCnw+6r+f79UAL6Ef5xvqAUiZg6AyPzCcg/Y3HGXNd3/JRYLHvHLwbHbx3LpNWKMboDSa2VOQunqCPABf0yirCsAX0oCcD8K+QBKEoBz8s1dQ0RvIyIH/2hlgpwvveQMgJIHWy7rnuoiMWkywcGxZTGX83HoMsCQI4GD90nsiiEENh+ADTOvSYw9NxUCoAYAnZFvag/h/v+Q418gzjABrLcE1gbaseZc55KXUgcyhl5sOPvnYBWg2jk3ROI0FZIxRMLVRoB8DL7FYOZSYuw5rIyAveYB9CUAygB4H7rpfweRGb7e/x/DjAB/q0Xu7SEGXIy1BfAsQZTBn4NJwCwVhz7r1xBrZs5an1sKiPUBAN2+ALHKuJAYq08E7OUD6EMA1ACgk/LNPADgG8T3N/r2Ua5Z/885SGowx5qvWfJ9zNZHwMGx5SEqndNDvccx1sxa7YCu1w5l/fBgog1Dv5FYe5/E3n2JxYMTgAarCYCqBfAS4ichAZtOyLUP0VH/j535P5f2v9TXqSX/c4bEwbG7x/jUOqdKEoEh2gGtmKT5AMzfi5mJY1MALmHVCXBKYnA2jvclAMewGgF8CsBteFyMsHcC+KSSFMCf0kGcI2Gl/F9TOMGKEgkGfw4mAZM+R2uXAfqsnVPzTsV62eDARF8X3W0Dc3sZAfsQgAWA45AtgETUwO30h5Hxhz4cl3tyaGDMrWWlHuRTY/+cFXFwbPcxP7W1p2TiNQWMiO0AAOyDglyqQCuxVrUCHkcPI2BfAqA6AO4HcGj5Z6wDgKQs0kZ8IDlf8JD1/6FbWvqy3jEzAg4OjmmDee5aMUYHVZ/JqUP5AFyEYAPYjZZBmyKgv8ahxNzenQC5BICw6gBQLYBXHPKFzwToJAGO+f+59f9cGarWwVv6RJ/rFqSc/XOwCjDPbH9Su4Uivwsg9vk5PgAblsVgoG9YXiux9pxGAPaQaQTsQwDWOgCI6DLiNzwIPSfE8krKOqV7WedW/x8NgBn8OZgEjHoOTK2EWmIN7ZvclVZPYicEAn5T4NH9EmuLdALkEgBlAFQE4AziDYCuLN71hYvEDzzmdg0wnmv9f7LqAAcHx9ackyV2Bcxdh4faFAgOVSD0P4a2DLYZAc9oBCDbCNiXAJySb2KBPANgjBIgUL/ePef6vxjh8+Dsn4NjXudCjTW0bzY9FR9ALUyJ3QoYSDQCSsy9Dz07AXIJwEIjAGeJaInIqX6aARDIMwCW2tRnajWsEifLlBYSBn8OjmHPiamc/3NbQ0tjiu01fPI+YDcC+soAS6xmARxDphEwlwDsoWs/UArALfidi656RxvBlBBJDPoythpy0tD1/ykuGBwcHNONMdaYuayhJUcBw4FxLfy+OFdH3S1NATiOzJHAfQjACXR9iGex6gBIkfed5j/P9KTYL3moXQCReJDOwfBT9T1y9s/BMdq5MZkSYI+/XXpfgNoDgXyYlrIBnkkarkjsPT00AWg0BUANAbrmePOhcYeI+CBKHKBi5BMqh0ykEpgh/mcGbw6O3cvo57qXypCfY6wRMAX/nL46IrquEQDVCpiM57kEYF9TAE4AuAv/dr+uLYDhuS9EAmqrALlqwFiMfPLtf5z9c3BM8hypsXZMYV+AXCWgD7b4wD+EeSkYegddB4DC4Kw9AfoQAPXH9+B2L7o+DBe4txjGAFjygBry4OWFjYODScA2qBJTXktrGQHN/W9icdHVTbcnMfjk0ATgmGQdZ4goZq6/bQRwTPaPgBJQ0wCYe+DVHGIxZibAAM7BMV/AndN5PKW1tJQRMKQCuEYC+zoBBFbTAAcjAAtNATiDdfk/5mIyoZTsvmTPfWkD4JROzklt/8vZPwfH7M6ZMfYQqb2W9jUC9nlPPkIQmgDoutyVGKyGASW3AuYSADUF8DSA6/DP9fe1+G3M+7cwoBoMrfaBPMRJyaDKwcExNcVhDh6Gmtm/Uw1wdAK0kVhpmwh4HasSwOAEQG0EdANhA19KX2QKixvjwGIDIGf/HBzbdu7skhFwTLzog4vm4zewKgEMQgBUC+CRAkBE32lvyLWzUSijby0fRJ8d/kp1AMydZJQ6oRnEOTjmm5VPdX2ZwppboxMgZqfANqQYwL9TICT26iWA5FbAVAKgtgE+Lv/ocQD3EK5VuGQOF7spfeCMvYvVVBcCzv45OFgFmDLxn/OuqiLif3JhYhuBq/e0ZFwNA0raFTBXAVAEYB9pLYCpLRI+5WCKHQBTOJEYdDk4OOa+tkx5Tc3pBICR1ceSApdSoV5nH+vjgKsqAHoJ4BT8PYwC4fpHLLiX/hLHml41xATASZzEnP1zcMzuXCq1rkx5TR2iEyDlveRgpvmaJ4ckAPtKASCi0CYGR2/ScPe3GQdP3z0ASp4AU/o7tXcRYyDn4Ni+jH2o3QentqbWVgNiMcw5BM/ASi++yl0BlQcgeRZA3xLAvQjwd2X6G5sHZbQADsVoOTj75+Dgc2r7iNBQCZarFdA3HTBmyN690RQAADcRN98/RvaP+RLGZpxi4gcrBwcHxzYCcc2/UXKDtVQDeMgUH8LXGxoBGEwBUHMAbiFsgBARH3Zq21/uFy5GPDDH+LtMJjg4OHiNqpfZ5zwv1djue/5trOYAVFcAFjoBIKJbgaw/JvNvE8hDzhe3Sy2AYuzXY6mSg6NOZJ5bo68JBf/mnFsBXWVwV/IchaMSg/U5AEnDgFIIAGGzBHDb8sZiXIt9ZZNY5jQF0J0SIDI4c3BwbMt6NaVWwFwy4/s7IQ8ANAVALwFEzwJIJQC6AnAcwAHSDAs+lgMPGyr5xe9yCyCTAg4OjiHWEm6v9vvdYmv8IWw9kFisKwBVCcC+/GMN/Nv5wvMP2qId+UAXMzw5J/WeWf7n4Ni5c4zX1vRoI/52zDAgdWmwagMchAAcl9dTHP21a+RzGJbBGTcHBwfHvNfWGkpBDjYq/F1gVQKoRgAarJcAUiYWxf6jKZK/GPhL4uDg4ODYPXKRgzWp94WM76GOOb0EEI3r2SUAInK96Y3rgSmAoQ9UbMEBtPVsmuV/Do5hIuNc2zXVcoqYEfOefNMAXdeFxOLqJQBdAdiHe2CBL5N3fkiOf7jWlzrVXfoYRDk4OHYtE9+2XVNFT6yLVQD0n/tDKAD6IKA2gulEjwEu9EGL3C9lS08sDg4ODl5rymFCjf1nQuOAQzjbYn0QUPU2wH3YWwBjdzXKBWs+2Pn/4ODg4LVibv9HLN6l7KKrtwLqCkA1AqCXAe4ib4c+MeEDoNRwB7FLJwjX/zk4ho2C59zc1qrSQ+Om9PnkYKeQWKzL/9UJwD66XYhKZPI5rYTMQDk4ODi2f83b1jU/Fvdi8PUeVgbAQQjAHoBlxj8Ww3DmlEUPsaGEmPHJwMHBwWRgzPVzSp9DzKZ3qVNxgc4DMKgCsMDKAxDzxn3yRekDQMzwRODg4ODgmNc6XovApO4XcE/D5cEUgMPAG07NXOc0LlLwCcLBwcExi/VwqtiSYgz0/e6hBv5VCYBOAtoMqaLElzPURkAM7hwcHBzbv6bV3BCo5Hv0lQCUAkC1FYAGQENEdyNkClHpH+ZgAsLBwcHn9Bw+91o4KABAYrEu/1chADAUgKXlTc61/s4nR2ZwCyAHB597W0qIpopnJta2WvafhOk5JQD1R9rEDzo07IAPOg4ODg4G5l1XCVI74ZZYr/9XUwBg/IFSbR6CD04ODg4Ojh1Zo0tho7DgchUFAIYC4HuDYgu/MA4ODg4OjjHISghnKYcE5JYACP5BQBwcHBwcHBz1CYMqASSTgNwSADwEIPTGmSBMi1lycHBw8Joyrc845fNeGthcRQGwkQAODg4ODg6OcSMLkxv+3Dg4ODg4OHYvcgkAy0EcHBwcHBzTiCxMbnr8oUXC75DjOked4M+Yg4OD15T5fsYpn/cilwSkEABhXBZ8oHBwcHBwcIxKwtTePMkTeVMVAPXibeBNUYF/ioODg4ODY9cAPfa55EjQqygA5h+K+Qeowgcxly+Mg4ODg4PX6JrYSBZcrkIAhKEANIn/gFnfID44OTg4ODh2fI0k2Ov/sZ+FKgG0GKAE0KIbPLCw/AM00wOKgTkzlsslf3YcHHzubSPRmCqemVjbSEw2y/PFFQDFMlohxPHAh9SXEOyqSsAsnIODg8/p7czuS/4+AYDE4uoKgDAUgKbQP0wDHshzaUfkk5WDg4MJyPDvjSb6P/pwVSkAy9oEQIH/IYA9yz9OPT5UmtEBS3xCcXBwcMxiPZwqtsS+55A/YE9isq4CVCUASwD7kSDv+2epwhdPfIJwcHBwMKmo/J6o0v8ZIggm7u5ruDwIATiEfxAQZX6AhHQH5NwP0pRWSiYSHBwc2wLiQ6yfU8v4YzAwFVcXEpMHJQDHEmUMZPxjc8+kGbA5ODgY9HdnDU3Bsr6lcfXYsaEJwAGA44iT9ceU6IcyGJLj506crNyOxMExbBQ85+a2VvVdY2lGn08MdpLE4oMhCIDK/g+w8gCYF5/UEStn0wQPPGbmHBwcnMXz/1H6vZi4GSqNm5d9iclKBaiqACgCEOP4d/1DTaEviDI+ZD7YOTg4OHitycGE0oo2eV6XEnBWEYBqCoBe/79nAfGQAuD9x4mIenwpczyAaYtPLA4ODo65r8dViIUF60J4SR6cVTh+D+s+gCoKgKr/3xNCIJKZmP9wM5MDYu7gTEP+PvsAODiGiYxzbdC1YALr6FzxowkkxNbrEovvYeUDGEQBsMkTIUYTMnJQ5H0xHyht2QHEwcHBwTH8mp6DNTEtfz4sjFEA9NuDKgB35fWcFr5aTnmawYHE5IKDg4Nj3mtraTIRMgGGiMFyCAVgrQQgWUbIqJDyTzUjf2E0w5NhUu+ZywAcHDt3jvHamh5NxN8OKQj6c1qZlA9CAFQJ4C7srYCAvVXB1x2QOv1oqA2BSrYq9nmtmtMAGbQ5ODjjLv16u7a2xpYBYrJ+H37aWgDvYr0EULUNUJUA7gA46XnDQHzvf+nhDlMaNsQzDTg4OHaVFGzL2loKY1Ln/FMAY09KLFYEoFobIAwF4LYQ4lQgq4+pbTQe1lQi+y/xfMr4YmkLTt7k1+MyAAdHncg8t7ZBMcxZW6eGHTHYl4yjQogTkgCoEkCb8oZTCUCrEQClAISkj1gnZC7DogJf6FigRTN/fQ4ODlYE5rhGDbkRUWzff04p/DSA21gvAVQlAKoEcBvAGaTV+GNr/6mySW3iMOTByqDNwcHBZGK4v1FyRH0fbIuR/M3HzkgsvotVCaC6AqAIwDGkGRbMv712vzH8YOytIhmIM4PLABwcfE5tCfEohUPmgB/9flcZPMZYf0wjAIOUAFQb4G0hxCLw5oC4aYApRkHffds2C4AGPECZCHFw7A7wDZVgDbXV+1hrf6qU73qObwqgE18lBqsSwGAeAKUAAGnTAGONgVT5i6PIL6X0STTVdhXOWDg4OPvfhfbqXByo8dmm7qLr6qobpQRwC6tdARvEjTIMuft9rshUJrarrYAMvBwcHHNfW+bUApgyA6AJ/I2YwXk65h4MSQCEoQDcg98HQMab9SkAfaT/0gfunFsBJ0MIWAXg4Jj1OTSl9WvOLYAu3PVhYhOBq8ckBt/CqgtApLzpPm2AtwHcEEKctbCSBpsGB4r4QHwMqNQezUPKPhjoYK19sjOQc3BsT7Y+lfVlCmtubgdA6nNNTPMpAfo2v+TAVggh7gNwA6s5ANUVAGC18cAd+cfPOD5EV3ZPEc+fykFT8++PaQQc7DNkFYCDYxbnTo01ZCgD4Jzwog8umo/fB+AmVmr8MvUN9yEAt+Ufvz9CsoBH5lj7xzJaAcfyAozNohlYOTg4pqY2bMME1JLY4msBBNzlcSBcUr9fJuGDEwBlPLgB4DjCtQrbP9hY/tnQB0uVDtS5dAKUOnC3OZPh4ODsf7ogu+0dACktgCYJQCKWHtcIwMFQBKDFqgRwUwhBkW9eMSDXP4sAIZhLJ0Do51yY/BQUDw4OjnHO4ymYD6ewlpboAPBh2lpSbFHAnUm0xN6bWG0G1Kb+c7kEQFcADgNA7vtQnMMQAgdEabaYwyBrn7BbBbisAnBw7Oy5Mre1tM/zU1vdY3DRRRwODQVgUAJwR2Mfx7HuUrT5ARrE7RsAhDdNyGFqoS8s5eBhIyAvbBwc23KO7JIBsHYHgAvcYzDP1wLYGBh73MDgQQmAmgVwE8B3QohzDtmi0S5A3OYGMeA/FbCjCr9XaiIgTej/5eDgmCaYl1yvaGLvbSqkKLQ3ju3+xpNMQ2LudxKDs1oAcwkA5B9T7OM7ABdg2dwHaZsE2ToBYtlX6eyfenzBUwXQSdT+WAXg4Bjt3Jhy/T91DR3aAJiLQ74OgNhNf8zXayTmfqcpAIc5H3ofAqDGAX8H4BQ2hxWE2gIbC2kI9UUObQQcciLgGD4ABmMODlYMhn79uayhpQyAPvD3KeQu+Z8k5n4nMfju0ARAzQK4ha4EsEB6J4CPATWRzK+EWWMuoEgTOJmL/A1WATg4qpwTUzn/57aGlsYU1x4Avsw/tQNgoRGArBkAfQhAqxGA7+Ufd9YrDGBPKRP4Mv9cmSbngI3tTBjqpB3CvDKHBY+Dg8G/7Dldopw6FGEJOe6H/jxcSkCqvO/DR/X4UmKvIgBtzgfYlwDclm/iBoCTsDsYmwhy4JN9KPOLoZ4Ha62BQFTwtaasRnBwcPA5mbJ2UsHX6qNa9MGWGBLgel4IM3X5/4bE3ttjEAC1K6AiANeEEBcjmI5P/qcESYUqHZglZbg572K1TZkPB8euZ/9jrRWl6/+lyyRU8bMM4ZnLFxfEUIm11zQCkLwLYAkCoGYB3JBv5nyAtcTMBtC9Ao3jA4zZNKhk9j/VXaxqv59B/kcmARwM/rNUB6bmoSqdPOVgjNXwZ2BZDAa6MFPh63mJuWonwIOhCQDQ1SBUK+A1dHsTwyFhNHCPBE6ZAVC6C6BvD2tNH8DYNazJKgccHByDnNNDeKj6vtcxBwCl/G1fZm8zADYOLIXE2mtYtQAucz/EvgTgnnwT14UQbQDUAbsRMGUHwZIMrwYwl/QBzHEBYRWAg2Mex/zU1p4x6/81MCJ2Zz8b6MNHFiTWXpfYm90B0JcAtFhNA1Rv5gTcZQCnIuD5Z13SSq4a0JcRpr72XHwAJZk9kwAOjuGOdZrIWlBqHRuj/l9rAFCO+78JYOhJA3PvItMAWIIAHGA1DOiqEOIhxz/dOLJ5c1Tw2ocpZZGYWkuqXFPCBzDWCTbkLGtWAjg4duMYn0ryVEo9KLne59T/FYS5Xtc1Jh9w752jDIBXsZoBkLUHQAkCoIyAqhPgKoCLxj/WwD8dEK5/FOHOAPT4wnIPlBpEYCrtgLmmHgZtDo5pgHapc3qo9zinvVRSlQCKUAMax302NUBdFAFQHQDZBsC+BABY7QnwPYBvhRD78E8xOrpIZ2RoBGKNUcCp0wD77guQI2lNqQzAGRIHx24f20N5p2qumTlrfclRwM7WP4mFUbgpMfZbibnZewCUIgCqE+CGfFOHABaWbL9xMJyYDYJ85sCm4IE8hpO1xAlZs5Y1CtlgEsDB4F/tnBvbOzUEORlSyW08GX8KvsVg5kJi7LdYtQAu+3x4fQmAmgioWgGvAjgXkDOi5x0DTh/AUAcyFTrgYg/iKZGM1PfAoM3Bwedj7Fo5VOv0GLsAKuiK7QQwa/+usvn9EmOvYdUB0Pb5EEsRgFvoXInfCCEeQXggUBOhCtT0AVDlgy10kE+lDDBZ0GYVgIOz/0mQiaHl/5w1dewhQK7fj9ndL5T9Hz0uhHgUwDcSa3vtAVCKAEBKEqoT4AqA+xDf/hcC/hgfAKGOCbCko7VG338JNpvynMGzDiYBHAz+kzqHh5D/qdB7Lb1WxuJObP3fthVwE0iU75MYqzoADvt+2KUIgDICXtUGAjWBf3yhyfuu9gc4WJHvQJlrO2CtMsCsQZRJAAcfu7OMsXZQHbr9L5Tx2zDs6D6JgYsIvGyEEAKrDoDeBsBSBEANBPoenTnhOoCzcLcxNEgzTLj2BShxO/fAKbGz1VBlAGR8JpNRAXgh5WDwr5b991kvS6xXtdfKEolc1kwAC2bFbpTnwsuz6Gr/ygDYawBQDQJwU765y7JWkeIDsMkdLpnFJ7WUUAJyDIE5LS6lF4QpOHK3QnXg4JhJRj3W3x9K/s8lD33X9tQEMIRToam4rjK5Wf+/PDkCsFgs1EAgZQS8jM6tGOMDIE8PpO/+vpJSSYNIjex86DJAaRVgLhkVB8c2HatU4Pyutd5MUf7PeV0TO83pftFYZtkl0JcQn5XYqgyAhxJ7R1cAgK4X8TakEVDWKkLzAGx1D8A9Icl1EKVKWzlKQO4BM0Q3ABU6oCefsTAJ4NhR8B/7fB1iYFoJNaCvepGqClDg/dom3pqPLRDXAQCsDIC3UaD+X5oA3MGqE+BbIcR5+OcBwMJ6FpFsyfUFxG4aVEoqSmW2NRyuQ7D23L/DJICDwX8Y8C+Z/Y+xLpVaR0us6T4SECQNmqoNCwlYwK6GO8sCQogL6KT/K1gZAJclDq5SBEAfCPQtgK8BPGqoAD6DwyIgm7iklpSDoaRENEQZoLbENVsgZRLAwcdkVZKxy2tjzGfhmv5n/o5vH5wF/MY/ffrfIxJTVf2/d/9/UQKg+QAUAbgkhLgPcf39jRyY5KqHmB90E/mF1yoDUCITrTXnOvf9lzrpRlMBmARw7Aj400DnaI21peTs/1jllXq8/77yfwizYjDPNf9f1f+vSYw9KFH/L6kAAF1NQvkAvpEsZc+iAiy0nwtslgGc85MjywCpjHZKI4Fr/40h2n6YBHAw+I8P/kOtAVNdE2uNAPa+lsPUbiMJuvq98GT/C5lcKwNgsfp/aQJg+gC+FkI8hHAnQKgd0GRWsMguvrbBUqa/nIMv9+Qc6iBHpUVmGxZgDo45H3tDZf9TXhP7qhp9MMdq5oO7LGA1zAshHgbwFVYGwGL1/9IEQMis/wa6aUVfAXgMcR6AUDsg4PcHxB4MOeyvxIFXU/7faRWAg2MLY47Zf+r7G2K79BLrfar8b3P++/xtrqxf3X4MXf3/Klb1f1HqCypGAIx5AN9KBWAP4YFANlckHL9Xqhug71CgGkw35+Tpo1yMnVGwCsDB2f+8z9WpzkfpO/ynpPy/0fUmx//6OgL09r99SQCuSWwtVv8vrQAAq3kAaiDQt7CPBbY5/9V7WcDfFRAjyeQyvJJEIJXpjjnusnRmwSSAg8G/LnAPuTPolMx/JYE/lQTkyv96rd+mDCwcGHmfxFC9/r8secCVJgAtVhsDfQPgCyHEc3AbHWwGQZdMAqSXAajHF55zItQY95tDOvqc4LVfY44LMwfHlI6xqZ3LQ5r/+kj9OSQg5vGQ/G8bgOfCwKP7hRDPAvhCYqmq/7clP9SiBGCxWLToygBHPgAhxAnEmfway+6AfcsAiPyic4E+lfGOKXkh8fOgkRYwJgEcuwj+Q752re3ShzL/1VqncxJJl/yv4xewvvufEwOxPgDoNDovnar/H0iMnawCAHQtCkc+APnGTyDCCIjN1oiYMkDMUKDSGwP5Hh/b+FJykcktdzAJ4GDwr3MubMsa0WcNLNH7X2Lr38bynMaR9S8QNgDql5MSO9UAoFso2P5XkwDo7YCXAHwmywCuPkdTBkktA4SAeMhdAUsw3ympAKUWISYBHAz+w4N/33N+G9fAHBLgIx6x8j8QVwZX5r+nAHyOrv5fvP2vGgGQEsVddD6AK+h8AGfhngWwVh6ILAM0jv2WCfksb2pmwCkz/KEWNSYBHAz+w5yHU8r+hzT/pWLEhhlQYpFL0gc23f/BuThy/v+XWM3/v1da/q+lAEBKFWos8Ffy+qkA60kpA5hfTINysn/qgT+EGXBuKsCuLOAcDP5TiCln/6X+txLmv9jfjbndOPDU5f5P6f8/gc7x/5XE0JvovHXFoxYBUGWAa4grA5hdArYyQGMB/wb2OkxKGWDK+wLUOtl2RgVgEsAx0WNmV7L/GmvfkPP/fS1/ZvIKT2a/gL/0bZP/L0kMLe7+r0oApFRxD6t2wM8tZQDXKERdKnG1WeiSilOagb0kMLeBQHNWAZgEcDD41z0vOPvvt36H8MFLUCxYZcWsFPkfwAOSAHwjMfTurAiADLU74FV0tYzvAZyBvxOgweZQoFBtJTQTYGwzYOr7mZsKwCSAg8F/WuA/9ew/93mlgL+v+c+nRptY5drwzin/CyHuScy8KjH0sOT0v6EIgCoDXJdSxqdysMEC4RKArQyg3+fL+lMz/b4HEEXeV2tBGFIFqLnt6GAkgIkAx0jHxRDnCWWe20Nm/9TzuZTwXkua/2ABfRODbOr2Av55/7r8/wyAT7Ea/1t8+t8gBEDrBlDbA38uhDgD/+6AR3URzVmpP8/8kEkrAwBhT0AKUahZDijBiIdQAWikha86QDMJ4Bj4WBjrHBhi7HeN7H+I1r8UbNjAF61jzTvYxzP730YAzmO9/a+a/F9bAQBWQ4FUGeA7dPONbeAe/HDgniRIkcBfa0+AUipAjS2J+6gAfTMPJgEcDP7DEfO+iiNn/2HcAMI1fxPHFpYk14Zzp9GVyr/ASv4/qCX/D0EA1OZA19CVAT4x9gZY+OQQjTmZmb/+xbhmAvgOtj7O/9IqQOzBP8R+2DG3mQRwMPjPA/xLt0JPea1LXatzfQCu3n9gs3Xd1fvf2DBQCPE8gE+wav+7XTP7r04AjG6Ay+jaAU8irh2QHOqA+YHqX45ZLkglA333hc5hu6knS+kTpDRwMwngYPAfFvxLv9ZQ2X/uPiolzNgpoO9KQAF3R5vN/OdMdtHJ/6fQ1f8vS8y8u1gsljUP0GaAk0ANBboipY3LQoiLcHcB2AgBwe4ZANwDgkKTAWPr/6kHXJ95ALWZ8RClgDEWRiYBHNsO/qh4vg7hdyqR/ffp/+/zGFlA32b+M3HK2+9vgP9FyB10JVbeQIXZ/2MQAH0o0FcAPgbwbOADOfrAImYCuMyAMV9kH/BPPShL1cdSyUDuIjKVrgAmARzbDv59XpMKnct93kdppTM1+y89+Icc2X93Y9385/QByOfFkoBnJTZ+iU7+rzL7f3ACYCkDfCqEaAEcg9sHEJJOTJnFNiMghuH1Bf8aKsBY87H7nvypi+AQ082yAIKJwHYC/4TBv2bdP+Vvj7XvydSm/9lwA7Ab0G3mdVcp21X/PyYx8ROs5P8qs//HUACAVRngqpQ4PpaGh5APYAFgYWwQpH/ogHsyoEkGbCpAqa6AsfYGKDEfO9X3MMTCNioJYDWAs/4ZgH/u65dcH6awtpV0/duy/zVfmUWRhi0Z1bL/BfzzbhqJhR9j3f1/OMQBOxQBWKLrZ7yGbsDBx7Lf0VcCcDGrkBkwtiUwlvnNZW+AXIacsmD0yQaYBHAw+Jc7N0pL/9u4tvVpA/e1/gF+859NqfaZ/85LAqCG/9zFAPL/YARA9jEeoDM2XAbwmWQ65xGujbjMgNYBQUZLIDm+6L5y0ZgqwNCGwNzFZ+zFk0kAg/+cwb/2+TfFfU/GyP59oL+GIUbrHzx4tAiBvnY5L7HwM4mNN1C5938MBQBS0riNzuDwJYCPhBA/CDAk0wy4gLsOswgwNlMZKD0hcCimPPSCkPJatQaUjE4CmAjMC/i3APz7GmxrbX+eoibMIfu3Sf6uDrOjnBZu/9nC6P1f+JJaiYEfYWX+u42B5P9BCYAxGlgNBQKAfdjNgAsXGbB8kPoXZvoDMLIKkDKTYEwVIGUByVmgZk0CWA3grH/C4F9D+h8q+w/13o+Z/es/GwfWeBNXbG7/q1/2JQZ+IjHxO3S9/+1QB3Az8PmpzwT4HKvJgL6NElwtgc5xwBYz4BAqQJ8SQGmmnCvzpWb+NODiNxkSwERg57P+McA/9/k1dz0dey2rnf2b5j/APwa48WT/NvPfcxL8P5eYOJj5bywC0GI1E0CVAS7AXjNZBJiUa/KSbXOG2ipAyuPIODFKy2a5C0qJqWCzJwGsBux01j8W+A917qWAbsraVIIM1Gj/C2X/tmTThT+u7N+qAEjs+0hi4TWJje2QB/KgBEBKGwfQZgKgq3ucQ9x4YGXys33wa6xtYBUglRyMNRRoyG2CmQRwMPiPC/5jS/81N/4ZOvs3DYAbiaixg23ICHhOYp8++vdgSPl/DAUAWG0Q9K2UPt4XQrxoYUm+D3JhYWEL+Ccz9VEBYp6XI5tNdShQqVbAsUgAlwR2APhHkPznDP61tjovuZaVLAHkrOsx2b9NbY7FLF3+fxHA+xIDlflvOfR5NDgBkJsb3AVwHauZAIcATiC+JdBlulgrARRUAWzPI5QtAeQuLCWHAuUuKCUXt1JZPasBnPWPmfXXAv+a52oqERjCzByztsYMdyuV/dv6/UP4ZD5+UmKe6v2/jgE2/pmKAgCszIDfSAnkfSHEy3CPBl67T5NaFsaXt/B8mSVUgJzrtdoCkXHS9M38cxeznSEBTAS27vOdGviXJPKlWppLrGElHf+ls38YwG/eNjHJh18LiXXvS+z7BiOY/8YmAPoGQV8C+FAIcQLAnocE2IyApuNyw6iRoAI0I4A/Ff6JjBM6RSmotUFQ7GI5eV8AqwFb9ZnWLEGVAv/Sdf++u/6VLgHUIAFNYvZvxRbE7fpn4tmexLoPsW7+W45xXo1CAIzJgJekFPKxbItYwD0X4Oi6ZdayzQuQogLUUARSTqC+i9QQJ8/YJKD2osxqAGf9Q5DNocC/dhIz9ammOaOAQ9n/Ru3fsuufq+9/ITHuY3m5hIEn/2UTgMViYb30VAFMM+DFAHvy7RWwKKQClCYCsSfQ1CYD5i40UyUBo6kBTARm9bnVPrbGAP8S/0utyX9DJDDkWONzs38X9oRwqxFCPISC5r++uDxWCUDfJlhNBvwIwJdCiCd8mb+hAtg2X3C1bPhUgNJjgUvX//s8v8YULSYBGYDGsD75z2pbwb/mQLOhev9LKpcEv6HQ3GDOhS8mBnlxS2LbFxLr1OS/e0O3/k2CAGgqwC10U5A+A/CeEOIp+OX/JvI+nbGF5gKQRwVIIQKEOmOB+xhwSslp20YCWA3grL/E8bBN4B+bOAyxVuWsr7bn+NZ2X/ZPCLf7xdyny/9PAXhPYt0ViX3LMc+5UQmAbHu4h64N4it0xojLshTgIgF7hVSABuO1AtbeJKjmeOCS8mFokahpDhxVDWAiMJnPozaJJNQD/5T/bQpjf0tn/aVaAJtC2f+eB/wfRDfw50OJdddl9r+7BEDGoWRC36Cbi/wegOcQUU+JVAEaiwrQeE5SCqgApWS1nPp/aVmt70KSslDltjVtpRrARGASwD9m1l8C/Gvt+FeiXDk1979P9T3CBCP7bzKzf9tjz0tsU61/tzBS69/UCIDaJfA6uraI94UQ36HbJ3nh+FBjVQDXlo1wqAA12gJLDtXoO0I49wTbdhIwmhqwi0RgAv/vEMfJlMC/5kZmKf/3UJ6lUNufLfu3qQG2MkBM9m+qAOclpr2PzgNwXWJeO/a5ODoB0FoCb2oqwN+EEC8hfptg130b5QCN4dXaFTBVwhpLXkuVClMWoLFJwOzUgF0gAhMB/iGOjTHBv8+5PWaZsu9amrpub6gDBjaYiWRO9q/k/5cA/E1imxr8M1rr39QUAKUCqMFAX0gV4HuLCrCIUAGs8j/8/Z012gJzwH+o4RolJLVaJIAGWoQnqQboQLkNZGBC/8tQWX/u8TvFmf9Dr02lSEBy2x/Cu/7pff+p2f/3WvZ/DSPs+jdpAiCZ0CG6oQiX0bVJvONQAWxqQKg301QBGov8k8Mea+4NkFvDKzFfe0wSUFoNGCNb3GkyMLH3TJi25D8V8J/L2hSz/sau5+a+MWb2H4MtzqE/Wvb/jsS0yxLjDqeQ/U9JAQBW44G/lUzp74YXwDdhKUYFaBC/UVBpQ2Bse2AfuQ2JJ9VUSMCUSwKTIQJTJwMTfG9Dfu99JP+pgn/J2f+xZCAnmfJdD63rettfCDNc2b8Pm1Tt/+8S077FiGN/J00ADBXgkqYCvOhRAVwfvGsus276C7UFljIEjlkKiD0ZYwF16BHBMQvrEGpAid+vBrhjgO7Yf38i3zWhbL2/NPjnnMdjrEk16v8xxj9yYIJv/5lFKOvXsv8Xtez/0tSyf6CrXUxqXcPKC/A5usFALwJ4kIguaR9uK38usarBCMnQhBBiqX1Brby+8ZO6J6svfmkcSEK7LSyPxV43X8f2uOt5JX6aJ7br78U8joTHEHgPSLgfjsdCv5vzWjV/vyogB8h10decaAxJ8kpL/jXAv09CkpPp1yYBJcsAZttfYyR75kY/DRE5PWgW8H9QZv/vSSy7NrXsf3IEYLFYiOVyaaoAfxVC/CMRXbEQAHV9aXms1b4cIa8LrLZzbLX7Wu1LbzUQVLdhXA8BaAqwxoB/zkImMv4uMoA+hgQgghiEgDz0WaR8VltLBLYEyKcM/EOBf63Wv1Ry0Hfjn1QSUEr6hwHowGa518z+Xa7+0LhfV+3//8NEa/+2D2qyKgCAS0KIRzwf+B7W92b2jRK2DQcK1fpzpk2lnFy5bLnGVMCh5gKk1D9TFt6SWRwK/z2O8sA/dNZfw+w3FPiX6PsfcsOfPq1/FCAIFDn0R9X+zdLyngf8H5EJ7LtYbfozuex/kgTA0RHwVyHEs3C3XGxcDEOgq7/TNwgiR2pK6QoYSn5LkdtSTrYSJCBlUUxdhIcyCeqvwWRgONAfkrz1PeaowPlQCvxjEwAqvAb1SYhy5//bjH8xY37X+v8NLAleJFb9derZ/1QVAF0FUFsFvwvgcyHE0wEVwKYKuNo2jr5ojd3FGgLRU7IqNRp46NnbQ5KAvmpAjeyOVYF5Z/u1VKKas/5Lg//QCUjprD92sx+v8U+u+RsT/mAf8rMXwJqj+yVGfT6H7H+yBEBTAW5qKsBfhBCPAdhHXFdAYxkRbF4I8RMC+5QCSuwSGFuLG3r2to+Np0qcY6sBNYCGycA0PsMax0Fu1p9bIhsK/GutPaWz/tR12rbWkwcfFsbMmBDu7EuM+ouW/d+cavY/ZQUAWO0RcE1jVO8JIV6NlGL2YDdx2EweQP9SQI1dAvsO5xhq9nbu7aHVgDGIAJOBcT+rWt97yaw/9VwpDf4115VcEhCT9feR/hGBCw0Sys4Sm97Tsv9rmMjM/9kRAEMF+AYrL8BxAKc8X4w5Iths12jg2DRIMkNzHGRsKaCPGkDoV8svOXs753/KvZ1yXwk1YGwiYAIcE4J6n0dN4M89PnPuKz0ACAnrCnquPb7nlOwACM1v0aV/3yY/TQA7fHhzSmKTqv1/M/Xsf+oKAOReyfew2inwPXSlgNciWJmtTzNUClgYEwL79p3GHNhDGnFKsPEhSUCuGlCTCJQG7V0jBDTQZzkk8OeQ1jHAf05rTknzn2/in2vwTwj4zdr/a+ik//ckVl0HcE9i2GSjmcGCcYhu7+QrAD6WKsBNIcTFCFa20JhcaFqgTf4vUQqYwolY2wyYIlH6ZM8xNg7KAaKaYL1thGCI/6fmd1hjg5/Uen/K+TY181+pxKOP9O/rALBO+dOy/0UIZ4QQF4UQN2X2/7HEqlsSuyYdkycAi8WiRbdd8HcAvkI3V/lPctDCXiRDM79QX+1HN4mULgUQxgP/McyAJSTQPmpAzOMlQKQmUJPjMnWgH/JzqUUWxpjx3+e8mlr3Uc4al7L+xEr/tp5/Z83fkTC6ksw9iUV/ktj0lcSqA4ldTAAKxBLAbQBXAXyKbm/lz2TLRUwpYM/C9mwSkF4KaCyLjOt6rEoQc8CX6AiYEgnI7QgoseDmLvo5wDU0OFPkZS5/p/ZnXPIYoMxjNAboc5W1ocC/5FqTuj7aNmmLkf6tG8IZ676pDu8hTvp/GsBnEpM+lRh1GxNt+5slAdBUADUi+AMAbwshHsV6W6B3FoBjlrPNGGiyyFApIFaWK9kOiEJSXikS0NcXkFISmDIRGIMMlADwqSoNVOi7GAL4a0n+Kedb7ba/2mtNat3fRQ5C0n9jwwINI2JmAKi2v7clJqkNf2aR/c9JAQDsbYHvCiF+bEoyni+tcbBBmyxkKwW42kly/QBDdATEnKB9TswhtgxOlWdrEIESZIBd/8N8XoThgL9v1p8D/qHzr5TxuFSWn7LWxSoANvXV7O4ye/5tsr+p+jaeZHKt5Cyx52+YUdvfbAmAYzjQn+VufmcRZwjcw+aEwAbhQRC+vQL6+AFyT8wpGXJSF6iYkkAJNSDl8aHIABOCep9Hje8y9/Hc4zqlBBDKiue4xvSp+9sG/kSt8Ua2v4cI4x+AsxJ7ZjP0Z+4KgGoLvIuuxeILdC0Xbwshfgi3IXDPI/OklAKsDDPASEvtDTC3dsChWwNLZPxTmBi47aSgpkeh5PNLAn+JrL/U+TjH9r/Yun9IoY2V/hcxWCIx522Z/X8hMenu1Nv+Zk0AZChDoGoL/IsQ4pLFEOgrBZi1HvKwQ9emETX8ACH5f2qtOaVJwBRaA6cwH2AbSEHN/6FWv/8UW/7mBv45a1ipur/Ns+VSAHTpP7SnzBquCCGeFkJcltm/avubjfFv1gRAmivuYb0t8I/SEHgi9ku0PMc1KbAhosayV4Dv+pQVgKFJQK2ugNpmwD4gM8TAoLEJwpDvhwp8HzWBv0/WX6oEgC1ZW3Lq/mvXZTSO5M7W9x+DD+r2Cbnd7x+xavv7Ht3Qn9nU/uesACgVQO0W+CmAd6QS8GP4zYBrYG/s8ewrBZDnoMv1A6QqACVP0CFIwDZ1BfTtOR8CoGngy5D/T63f21bX/5jgn9r2F6MAhNZVWzLmGvNrmsIXDlJgNf9JjHkH3dCfTyUGzTL7ny0BsBgCVVvgbSHEQwEGt/aYHPqwh7iuAFvJINcPgAInztAkIIUQpDyWqgbMsT1wl2r9Y3w2U2z3yz3O+/T+T2XYWCkFILbub27zG3L97xnD4TZc/uZjQoiHhBC30Q39+RAzNf5tgwJgMwS+C+A/hBAvYHM2QOiLtu0XkNIamOMHmIICkLpApEp1MZlMjV0D+xKBmqqAD/h2FfCHUAn6uv5L1f+RcD7kzL1H5rk9RQUgpe4f0/Lnm/MfKgPsS2z5A2Zu/NsKAiBDnxD4MTpTxrtCiJ8YX6KN2ZkqgNkG4pweZWkNDPkBaMIKQJ9JXSnXa+wV0GfBLmkGrAFo20AMSv8/Oa/T97tOfWzsWf9TSChKKACEtLq/0+RngP0e1qV/Fy6sYYfElHcB/BkzN/5tDQHQDIHfA/gawPvoDIGHAB5EoBMA67WgPYcC4OoWSPEDxDL3MU9YZN4Xe72EGpCSbZUmAmOaAac6rW/oMcSllYHSx83Y4377nNNTUQBS1lJbmx85Mn1z1r8PD0wV4EGJKX+UGPM1Zmz802NvC+REfULgZ+gMGg+3bfufmqa5Jh9XX6aQF9f11ri90K6rCwAIIlrIQRCtdiDqB4OQ97fGdXXACu1gFtp95uOu59X6iYj7fO/Zdh2Ox2JvI+I+3/2xj7keT31eCARK1wu3pXxAA/xun82jxpr4lwv4Y4F/SiLjA3yXmhpb97cpAKYa4Lu+ALDftu0rAP4fiS2fYYYT/7aWACwWC7FcLtWWwd+gm8p0AcAjQojXiOj3GggrkG+N6y2AloiE6FB94SEB0H63MYBNB3wbYNoeD4Hr0CQAEcTA955t12NIge/2UERgCDIwBCHYBcAvCfpTAv6+StvYtf/UOQC+rJ4cj9s8WCHwd23z65oWq4x/r6GT/t+W2PKNxJrZGv+2TQHAYrFol8vlgZRl1GyAB4UQDwO4SERfOzL8PTPD10iAsCzQQgNydJ4TCCFEq4G7rga4ABUBwI0B4SEVgJysv5QagMBzahGB2OeUAHQXwGwLMaCRXmdI4I8B9tJZf062P4QCkEIMYh83Jf5Q3d/l9bIBvVUFEEIo6f8/sN7zfzB36X+rCIAM1RVwDatSwENCiH8hom8t4G8qAa4yQKOpAAtjcSaLCiAMNcC8DstrAPMrA5QC/tjsv0/m31f+T832S2X4PnCaGjmgibwmFXhOra1+hyACc5X/Y01/tuw/WPfHSv7fw6pLzEcIjgkhXgLw37Ep/c/a+LeVBMBRCngAwEUhxJtE9G8ekN/T7yciyKxeVwf0UsCaKiD9AEu4ywAIkABgfmWAWFUAyFcDYohBDhEooQqkgHANyX8b2wap8u9R4cdygD8X7FPAPSc7rw3+oXY/E/xt7/UI8C11f5+BWw1987n910iAEOJNdF1lf8IWSv/bqAC4SgEXZCngcSL6zKEEbFw3SgE2EtBoi7kp/YdMgTGAHwvCUykDpNT/Xdl9bvY/BVNgH0IwxYx+yooBFXzuttX+5yr/wwB/p+kP9ro/ecB/Lwb0sZr297gQ4jt0Pf9bKf1vJQGQJOBwuVzewaoUcFaSgP9CRFdhl/sVWO9r10N+gPUzt1MNYGT8PhIgLKrAtpcBck2AczAF5qoDIbCZOzEYo/4/Z9PfnME/R/5Pcfwf7c0C91z/UN1/H/6e/5NCiCcA/Fd0436V9H9nsVgcbhtebh0BkLGUco3aMfAcOlPg60T0r4goA2jgvactxLa2QAXkQh6YyhSoH9RmSSCnPXAKZYDQfS4SE6MMxN4uQQRSH+tLBvoAeQjMxiYINJHXrQX6fYC/FNiHAHUqJCBV/neBv36fPunPtjeLbda/PtTNHPC24fTHuvHvDXSmv7exGvhzC1tU9996AqCVAm6gG9rwdwDnhBAPAniViN6GuwygX/Y1P8Ae1k2BsCgDZmugqzMgpj1wbBLQVwGILQPUzP5rlgBSAb6W7L8NXgCq+DulSwAs/5fZAtjX7rfh+Id/xK85xl2f9rcfAn2spv29KoT4DCvp/2uJIVsn/ZsMbBtJgLlXwN8A/E4IsY/NKYG+mpA5KthlNtFHBS9gH/8bOuhLnFhjSXmpI4FztkLtOzLYN0UwNPVtiImB274nQN//t9R3kXM85N6Xcjt1BHDKxj9TBX/zeVbHv7YRm2/tXWjgn9TyJxXifQC/Q+f634pZ/zupAGhxiNVeAZ8AuA/AA23b/kvTNN9h09WvS/9rw380P8CeJYsTBplydQboqoDZHjglJSDl7wHlhwINUQYYakhQTqY/p/a/morEnOv+KVn+EApALSWgL/jb2v1Cjn9bz/+eAf57AeDfl5c9AMfbtn0ZXcvf2xIrrmILZv3vNAGQrYEH6LZsvATgJDpT4HkhxFtE9Gu4/QAmEVCTAs1FWDgWaVtngA3cTRKQ2hVQgwQg4TEfIQhdTyEFuaA/RT9AHyBPAcVSZGEIVWKMnv+p1f1LAP6UwT9W9Qw5/lNMf8Gxv0KIt9C1+/0R3dbylyRmbK30vysKgPIDqA2DvgLwHoD7hRAPAHiZiP4Cy0RAedk3snwT+Bc+MqB1BgD29kAbCTAX8DFIQE0FYKwRwTGAP5QfoDQpGBO4h3xftev+2zLydwgSkNoO6Bv042z3szj+bQSggbuM6/IAqLr/S0KIL9FJ/+9i1fJ3b9vBfycIgIwlAL018LQkAf8HgIe1UcG2jX/WygES1M0OAJ0QAMa44EB7YMygoNgZACVIQCrJ6KMATK0tMHZQUAisS84HqEUOpgryua8x137/WoBfkwSkPic06CfU7ufq89dNf/qwHxcZWCMGQoiHhBAnAPwPGC1/2HLpf6cIgCwFmK2BZyQJ+N+I6Du4OwE2ygGW+QCHsHcGAPb2wNCMgNiRwS6QRiYZCP0OkFYScJEDnxrgUgdct0sQgb5kIJUQ5AB6LBjWJgrc9leODJRo+8sF+BqgnwL+zl5/Dfhtk/6sWb8E/5hhP3q//w/Q7fK30fK3TdP+WAHAUSngECs/wAlFAtq2fbNpmn91EABbOQAWJUC4CIAE9UZrKexLAmIBPua5qa+XqwDkGgG3tQywy22BU2z7m/JWv6UVgJqdA0OBvznpL1b230O3xe9bAH6Pruf/fazq/oe7IP3vHAGQJGAp/QDfAfgSnSlQKQFvENFvPQQA5k9NCYBFCbAu6gYJMDsEYklAjuTvyxhr7Q+QowDschmASwB5v79t4377KgBTmP4XC/5kAf9Qr7+e+ZvO/r3QRQjxOjqzn63uvxPS/04SAEkCDpfLpZoP8JkkAfcJIe4H8CwRfeABf1MNcHUG6M/fk8TAtpjrg4KGIAFDmwJTFICh/AA5qkAMGUglBKmgvs0lgLlt89sX+Ptk/alAP3Xw9w362YO93c/m+Dd3+Nt3gP+zQoibAP4dXd3/c2zxqF8mAPZYYn0+wCkAZ4QQ/zsRXUdXC4rtCBCG299ciJcwBi7J5y9nQgKA/JJA7vWaRCA3+59LGWDqJYCx5P+hdvvbZud/NfC3DPpRj9na/fY08N+HZ2MfrJcFLgghzgP4v7Be99/6fn8mAOsqQKvNB7gM4JhUAs60bfvPTdP8HusufeECf40Y6I8vQgt5JRKAwmSgD/iP1RFgA+rS2X8s0JcuAwyR4U9BIRhrzG8s6I9JBKY8AKgk+C9gb/czs3xbrX/fcv1k27YvoXP8q37/y9iRfn8mAG4S8D26mc/HpRJwum3bnzZN82vEzQLQVYDYxVmoA78wCRizHbDvTIChFIAxOwKG7AaYi6IwhWl/cwD+PuA/RPtfH/A3SwCuQT97MvsPgb4+6W+BbtLfWwB+g8749x52YM4/E4AwCdBNgV9IEnASwGk5KfDfHCoALKoADBIgIj7ftjIJSFUEYn8HEcQgVQ1IIQI+QJ9bR0Bt499USgLb5vwPPadkF0BJEpD63Njn9AH/0Na+eq//ngXovQqAnPT3VwC/RbcnzBdyzb+7a6Y/JgCbYQ4JOgbghBDiFIDXiej3iDME6jMC1G3bfABRkAQQ7OOFYzsA0IMojLlHQAzIlygFhIC8ZikgBtimUg6ggV9nStJ/abDvC/h9s/zUn7bZ/rng73P928b8Bo1/QojXhRCfA/g3AH/BDg77YQLgVwGEnA9wG50hZE8jAf8ZwItE9DfY6/7WBdhSDtCnBNq6AnJJgG8DoRpmQESCf592QF9GnwLycysF5AD6XHcO3IahP6UUgNz2vyl0AJQG/z0LCbBl/vsWsN83SYAQ4kUhxA0A/4rVJj/K9He4K8N+mADEk4BbAL6Rn8txSQL+C4AniOizQCZvzgiAxxJQiwSkZvJjmAFLtAOmKAAlSgE5ZCAn8+c5AOWUgKFc/ynAP0bWXxr0bWBfGvzXJvkR0b4ny7cZ/vaFEE/IMb//Fd1GPx/Jtf0Wgz8TABsJUKbAG+imQikl4KQcF6zGCNuMgNZJgFo5oDQJgIcElO4I6Av+Nc2AMQrAlLsCUsB9jF0AaysOpZWAMV3/JRSAucz+923pWxr8XfV+n/R/XgjxGID/BuAPWE3623nTHxOAeBLwtUYCTrRt+09N0/xBPmaCv3PxNTYDEoVIgGsrYdfWwrFkwAUmtScD5poBU26nKgAlSwEhUC5VEqgF1GMQh5oqwBT6/lMAvQbQp4L+UOBvjvjdD2T++s99dK3cLwP4f9FN+mPHPxOAJBKwlJMCv9MOvOOSBPxCaw8EPFsBW0iA6KkE6LsTmiRAfz/m7RQzYGlVABnkoBYR6KMA1Mr++5QEamb6QxOOIcb+TmXkb4msv2a27yMEPue/a1e/VPDf84B/iAQcb9v2TQC/Ruf4V2N+2fHPBCCLBFzXDsBj8gD7udw4CIj0A8A9LdBHAoQxLEhn360FIM2OgFRQrw3+ufL/1OYCpHYGpJKCFGCfmwlwG81/Y438HZIEmEDfOJ7jAn99ql8M+Lum+7nq/gsAx9q2/Rm6zX3+HcA76Nr9rjP4MwHICdUe+K12IB4DcFwI8TM5IyApA0skAa36HQCtYxdBgt8MWIoE+F7L9VhMtj/kgKBcVaCEArBLZsDa5r/SPoAxN/3pSwiGqv/HmP9cu/rZLjHg75P6zU6AfSHEz9D1+P8bup5/bvdjAtBLBVCdAXfQ7RnQaAfbPoCfGYOC9AXaWRqIIAFLD3kwSYCvI8BUBmLJABIIRK4aANQZEFRKAajdCjiUGXCqysIUpv/tsvQfA/o+sDfr/xQJ/tbtfD3g79re1xz083MhxEdYb/e7Cm73YwJQiATckgfU0UErhNgD8JbcQti1yIoIJSB6MbSQAPM6RZKCEEhMsRTQlwjkKgA+QM8F+9SSQKnMf8iSwVw2/alBBGq2/tXM9kNgXxr89Z39fBP9Fi4FQAjxlhz087/QtfupDX643Y8JQBES0EoScBPG1pRCiP8E4A0i+l3qQi3BnCwn7zKwgKmMvrUAuwgoAyIR5PuWAvqCfwkiEKsKxJCBlMdSgb6vIbAGYahJHoYw/qUAfAzIDwX8seBegwyE2v500NeJgLmLn29jnwb2Vr9gf7/+UwjxhhDiMoD/D12734foev1vSvBnxz8TgGIkQN898IgISBLwGhH9MXHhJUs5wAb+DVbegJaIGu13Wg3YXbMChAU0UzP90O8A420VTA7ikUoOYshAqgIwZvY/BYNgLRVgrq1/Y43+zQV/n9PfJAONzP518N/D5pa+Prd/Cvj/WAhxDcD/RGf84939mABUJwH3sJoDYJKAV4jor6mLl4UEmIvJEitvAEkSoP+O8hy4ZgUAmz4Agt0s6AKfvvsCDLlRUK4qEEMGSigAOfV/SviupgrsYygAc6r/jz3/PyT5O3v8NfA3s35CeGMfHfxDBsBjEvxfFkLc0cBfH/Rzj8GfCcAQJGBN5hJC/AuAF4jovcgsj3QpQAP0Q8v3YoK36g4gz6wAV4eAb2jQEPsC9NkoqC8R8D2nLxmopQCkgPoU2wKn0vpXS/bvC/ylAb8G+Fud/tjs8Tc39Gk84O8a7+vq/VeZ/wtyrfyf6Lb2ZfBnAjA4CdCnBepKwD8DeI6IPkheJdc9AYcG614aioDaWMgcGKSfrDGgn+ILiFEF+qoBuUQg9NxUcpAL+qVNgVPM/sdQAXZp5n/JrD/0nJQxv4TwgB+91z8k+6eC/74Q4jlpvv4f6Kb8/R085Y8JwAgkYCmVgO/Nk8MgAb5FhBwkIGYBa7XHW8esgNaR5TaOTDV08tTeJbAPEQgRg1RVIJYMpDyWSgpSgX2qQ4GmrADMvf5fMuv31fvXiECgx9/W4tcEwN/XAaBk/+fk5j4m+H8vM3/u9WcCMDoJ6FZsIf4J3TbC71pOLtuCQQAOJAkgqQTAyPxtaoDtdVSHwAJxvoCckkAI9OEA4xLgH5v1l2wLjLk/FvBTpf5tVQCo0POmUP+fm/s/p94P+J3+JvA3GtAvAOxJ8F9EgL4p+78ohFhI8NdH/DL4MwEYnQSofQPWFmNJAmKNgYYQYG0RPDROcPOgNzsETIk/1hcQs4/AlLsBXKBech5ADR/Ato4F5h3/plMCiJX8Ab/Zz9Xm11jAf0+Cv2/3Ph/4vyL3UVGZP8/3ZwIwWRKgO/KXkgT8mIj+FFiYNk5MKeurkoCuCJjg38DoEMC6OVA/qU2QB+JLAlMqAQwl/099ONBYKsBc6/9jjv0NAfTQWX+o7S/W7EeWzN+s9y8QHvDjavW7g25nv9+j29lPZf4M/kwAJkcC9IW4RVeX/0cAbxLR7yMXgTVCYJkauLFzoOV1WnnCunwBfUsCJdQA83EgvRUwtxzge04MGehDCFIVgDmrAFPN/nOy/dzsPxfkS2X9viw/pd7vMvuZCoA+3c8E+ejsXwjxuhDiOlZuf73mz+DPBGCyJEBYlIB/hH1ssJn9q9sH+nXNF0Aa6Ns8Aa0FzH2+gJSSQK4aAA9Ip/gDgOkMB/LdH6MC1PIBDKECTCX7LwH6fbP9MYE/hgzkSv5AuN7vm/Bngr9e9w/2/Mvxvpex3ufPhj8mALMgAcoYKDTgVSTgF3IDIZcpkBwnb2OQgEPYRwhvAI9WEgDsdX5bScCmLOSoAakqQE6m39cEWMIAGMryd1EF2ObsPwa8h8z+Y7L+PpI/YbPH35T7XU7/Pc9l3yAGx+TGPp+hG++rwF/v82fwZwIwKxKgKwH/AODnRPTvngWH4B4YZJsV4Dr5lxqIp5YE+qgBsYQgFfxzywG7qAKUVgNKkgrO/vNJQOpufiUkf1u932X2W1jMfmbG76r7HxNC/Ezu6ve/0M32/0AD/wMGfyYAcyIBAquxwToJOJBKwL8DuGs5MQ8sioC6n4w2QZc5sLW8hlkeCJUEAL9BMKXujwLgX3My4FxUgFRQH0MNqDH/nyf/lcn6AY/kT51caMr8vnq/DvZ7HvC3be2rZ//HJfj/Dd2Wvn9Ct7HPZfCEPyYAMyUB+thgRQAOARwIIe4KIX7RNM3vsBorbNb+XZtx2DoEzJq/Xg5YIwNE1MJdEtBBJmQQjFUDQkCPRPCPKQeUUgFS1YGUx0LAVlIJSCEQtUhDqdn/Q+38N7Xsv1rWD7/kb8r9Zr2/cTj9Y6T/PQBn2rZ9E53c/28A3ka3pa/a1Y/BnwnA7EnAWilAAv3dtm1/2jTNOwCuOk5y8hACpQYoP8Ch8fyl8VNXAfSSgEC8QTBGDYAHdEupAKkKQaoKEAL+kh0BQygBUwP4lN8Zsu9/jtl/k0AGfFm/z+hnSv6x4O/rANgHcL5t25cB/BrAbwD8BcAnAK6Ad/VjArBFJEBtJbxGAADcadv2l0T0BRF9hk3JHwFloLGQAGCzJGAjELb6v0sN8IG+aRocQgUA6ngBcu7LAf0pKAFDxRC7/g0J+lPJ/psEMmDN+hF2+dsk/4UB/jYCEHT9CyGeEEI8hq7H/7cA3gHwmUyGbjP4MwHYRhKglIA1EiCE+BWAU3InQUpRAYCj8cEm2C9hLwmsAbnqEjDUgAabRkCfGmA+d2wVIEQaSpOBVNAvqQT0VQSGzPhLZf41sv9SU/+GyP5zsv61pMEC/C6nv03+V/V+F/B7ZX+5o99JAP8Nq+l+XwC4JsH/kMGfCcA2kwBdCbgH4LYkAW8Q0X8EAL9xKQHoavtmlg/YSwLmYwsiWgohGqzPMohVA2CoATVVgFyFwPV4CeAvYQLs0wY45yFAUx7/O6X2vyaBDLhq/crUB6yc/TGSvzncx/y57yEBR8Y/OeDnBoD/is7pr6b7XQdwR4K/AAcTgC0kAWK5XB5KlqtIwKGmBPwSwK/krIC7HiLgJAhal0ADe0nA5QsQUk0QWG8XjOkUAOLKAjFAn5Pd9ykBDLk5UJ9WwG1oA6w5/ncK7X8xz+sD/LFkwKz129r7XJv52BQAW70/JfvfQ+f0/7kQ4hN0Tv8/omvz+xpyrj+AJYM/E4CtJwEADmWboNBIwD1JAn4uhPh50zS/lYyYPBcb46dAScBGAnRQV2oAZEkgplMgtiyQOvTHBWq1RgPXaAfMkf5jgDJmoRxSFRjaBDjV0b+lNwGKkfv7Zv0+8Dclf7PFL2bYz4m2bd9CV+dXTv+PsOrx59G+TAB2jgio0cGqJ/+IBAC42bbtW03T/BWdKcZXDjg07jvEZkmgwabs7yIBNjXA1ymQWhZIBfMh2gNDZCCkDqQQghRi4AP6bTMBzsEAOOTufylyvy3rt9X6Q07/kMs/J/s/37btK+iMfr/Fyun/DXjADxMAJgFLgW5q4JonQJKAXxDROSL6MEIFsJEBXQ0ws39dzg+pAbo3wMz0c8oCNYkAME5XQAyo11IDYhWBsTP+vll/TvY/VgmgL/AD8XJ/jax/YRj9bKDvzf6FEM8KIc4D+H/QbejzNwCfyqTmFtjpzwSAScBGm6DyBNwCcEMI8Qt0uwn+R4QKYMqBsQZBX0mgjVADdBCKKQuY5KC2ClCKDKSoAzmkIFcNmJoiMLXWv9KgX4MENA5iEJL7Y/r6KTLr9wF/SvavzH43Afxf6Mx+fwc7/ZkAcHhJgDBIwG1JAn4mhPhV0zS/wbovQDf6OVUALStoHAbBxqEG2DoFhCQTrQHmobKAjQiICipAjCrgu50C8jHAn6sE9FEDSigEU5v/v60lAB/Iu+4Lmfxis36C3ejXRIC+zfmv6v0/RWfw+3d09X7d7MdOfyYAHBYSIAAcyJKAbgy8ha5Wdr1t27eI6D0iumQBf1MFaDQwVwvDUmYISzkKuNGAGwbw60RAwNhDQDMJ6nMDSCMxZlkAnvtKEYG+qkCsEjDURMBSZsCaCsE2TAAca/pfau3fbAHWs/6F9hxfb78t69dr/Wb2v2+53zrpTwjxkBDiBXRy/++wqvdfxmor30Ne7ZkAcLiJgOoQMJWA7wFcF0L8DMAFIvob1o1/LmOgvigcqoVB8wboRKI1wN+8vQbYmhpggn5j/FsxpsAcIgAPeJeq/5cqAfQ1A7rAfopmwF2a/pfT/58D/ECcyY+wuXOfjQToWb9L8rcpANa6vxDiJSHECQD/Hd1c/3exmuynxvqy2Y8JAEcECVgabYIHkgTcAHBNCPFTdDsK/lYSBBf4bwA/1ocHNQAONTVA30K4wWY5QCcFtrKAaQ50+QNSiEAIEKcwD6BGCaAE2E9pDkCtrH8o0O+T7acCf0qdH3DL/eZWvmbWb4K9DehDE/+OCyHeFEJ8BeB/oJP830c33OcaeKwvEwCOLBKgfAFmi+ANqQS8IYT4maUk4GsR1EsDuhqwkPsJ6KDvmhfgKwu0xi6DOhHQ/QEpRGBsM2Bq1l9qHkAJ+X8odYAqPL/GwJ+xSEAq8AOWOj/Wa/2AW+4P7uTnqfXHZv665P8ndLL/X9Ht5HcJ68N9GPyZAHDkkAAArVYSOJAk4Ca60ZlXhRBvAXiMiP6YqQSY3gD9+WYpQL8sjcVKdQu49hUw/QG1icAYSkDK/TmgX9IUOBVCULLlbwjQHwr4bXV+E+j17H8B/2Y+C03184F9VOYvhHhNnuP/Hd1Uv/fQSf5qJz+u9zMB4ChEBA4Nc+BdjQRckRLcPzRN8wd0XgGbEuAaCGKqAY02N0BXDGxKgLmj4NF9Dn9ATSKAQNaeszlQDSUgBfT7Av5USgBDjwAee/OfWsAfGujj3cVPSv6xWb+rA+C+tm1/AuBDrIx+H2Il+d9C5/Lnej8TAI6CJED5Am5gfVbAd+iMNj9p2/YNIvqGiD6wKAFNpBrQqkVHDv9ZGkRAZf/mY60B6qWIgHCQAB/gp3YGxJCBvkpAqhoQC5xjzwao0QVQiwjUHv9LkWSgFPATNlv6Ngb8GHJ/E5n1b8j/QojnhBAPohvn+0esBvsol/8dsOTPBICjGgnQSwJ6m+D3kn1/I0sCvyKi32BlENQBP1YNOJRyoRogZIK+6QEwuwTUT5tRMIUI2LJ/83dLtQX2UQJC98cA/651AUxB/q+R7ZcCfmDd4Edwu/v1nn7SMn4T/FOyfvXzuBDip0KIywD+TwB/Rmf0+wLAt1i5/FnyZwLAMQAROFwul/qWwsoceA2dL+An0iD4PhF9bYC/C/h18Ndr/8okuLSoAaY/oMG6P8BGBNqCRCB0f2qJAJh2GWCOg4CmKP/3Af2awK+b/mKA32n2M+T+xpHhB01/QoiHhRDPo3P3/wGbRj+e6scEgGMMNcDoErirqQFXAVwWQryJlUHwwKIGLLFZCtCvH3UCGGWBFpslAbIoBGtEQgehCCIArLwFMX4AszxQQwkA6s8FyAX9oTsCpjIAaOjd/yiBENj6+GOBXz8XdTJgXl+r92vAv/CAfIz0vy+NfvcA/N+SAPwd60Y/3sKXCQDHiCRA31pYVwO+l9LcZakG/JKI3iWiy4grBeiqgCIJC6zKAofavgImGTAnCbaO7N5HBAD7QCGbHwDwlwdKgX9K1r+rA4Hm5v6PJQF9sn0YAJ8C/I1BApxDfTR3v0/uj5L+hRAXhRAvotu+9w/y50foxvlew6q3n41+TAA4JkAElhYScFOerN8A+EoI8QaAZ+SmQnexWRKwlQIabJoJW7mALTWToEkCWkMFaDxEgAAIi0egsRCB1PJAaKhQSQ9A3zbAFNCfw1bAY/gASrv/U7L9FOB3KW4u4DcJgAn+a61+GqDHSv/q/uNyE5/bAP4bulr/3wF8LteR78G9/UwAOCZJApRBUGXfyiD4nZTsvhZC/FgI8XMi+oiIPsdmm9+hRw1Qi9BayyC6gSC6P8AF+uo+dV3A3jUA+TgsGw4tjN8D7AZEmypggmhp8E8xBcYCfx/QLy3LUuXfnUoLYArowwR4GwmQ54nK8uEBfl0FcAH/kdlPk/sbA9Ab+Ov8G9eFEI8LIZ5GV+P/EzqH/8da1n8LXW8/Z/1bFLQ+vZVjG2K5XKoF4DiA+wBcAPAYgOcAvArgJ0R0gYh+L+W8Q40A6D+TLhKwW0MJ0O/Twd8kAsK4rsBdaKoA9Pu16zrgmwAoHIDoen7sbd/9IdWhJHCPfQKXngI4xva/BH/Nv0+2bxvpmwr8a5m/It4ZF5vsf1JOFb2Kbob/X7Fy+F/Rsn42+jEB4JgZEVgAOAbgJID7ATwE4CkALwL4EYAfaXMDUsD/UAP1PkQA2n3QHjOv+4iADuZiIDKQSghKAv/UT9i5tAD2AX1ykAAf8C8s1xeIm+gXA/yhmv8GCdD6+v8ss/730O3e9zW6IWO3OetnAsAxfzVgIdWAMwDOSzXgWQCvAHgNwKNN07yNrlxgAn0METg0AD6GCLQWFaA11AFYFAIhyUBrgGGMKlCCDJTO+mNOwLmepFPbACgV9GOzfSXz2/bdMPv5XTX+JgH4zS19Q8BvPn62bdsfAfgS3UCfv6Kb5vcFug6iG+BaPxMAjq1TA/alGnAWwEUATwL4AYAfSjUARPQ2OiPhYSQRaI3HQ0RAWAiBqzQQWx4QDvAvTQZKqQG1gb/WSU0Vf6/m9L9SoG/epgSZ3yX1uwb6+IDfrPfHAL9q7fuh7OB5GyuT32dY7+tnhz8TAI4tVgOOATgN4AEAjwB4Gl1Z4IcAXiSir4now0QSYLsvhwjYVAHAUhKwqAK5JYIYMgDjtXPBvwbwj3USDzkGOOX+xvO8GNCPlfhtGT+wLvPbsv2+wL8x5z8E/kKIZ4UQDwN4VwL/u+hMfl9hNc3vHmf9TAA4dkMN2ANwAp1J8EEAj6IrCygi8CQRvUNE3yDNFKiXBFxEQFgeExYy0MJeHvCpAoC7RBBLBmz3x6gDfbP+ufsAatf/fVl/ar0/BfR1iR+R2b7ZRmub42+285EH+JuIjN9GAi60bfuKzPLflsD/ITr5X7X23QFv4MMEgGOnSABpaoAyCV4E8Di6boGX0JUFzshJgreQ1hUQIgKtQxGwEQGzJKBPCGwtqoB+GwEyYLsdUgdSCEEp4J/biUoFnuPL+lMG+Ljud8n7OujDkt3rtxvLdZ/MT46MvykI/AsAp+Qkv5tYtfV9gK6n/zI0kx94mh8TAI6dJQK6SfA0gHPougWeBPA8OqPgD4noHhH9GZ0/wFUSaBOJQGshAy6zYGvJ/r1dA5ZLChlIUQdiScMYwN/394fYCjgm40+Zz++7PwX0YyR+U9q3Zf8bFwP0m0Tgb+CW/FWd/zg6qf+v6Or8n6Kr81/D+hhflvuZAHAwETgqC+jdAsof8IIkAj8goi+I6JNIBcB2u7U8fkQKtDq+TRWwlQUAt2nQRQYkF9ggA7bSgIgkBEDaLIHS8wCGVAlq1/5je/hjAN+U9jceN+R9H+jbTH2AW+63Zfu24T4LBxmIrfcv0A3kekoI8Ri6Pv6/oGvrU3V+3d3Pcj8HEwCODRKgFi7VLaD7A55B5w94FcATRPQBEX2VSQJchMAkAkv42wVDBCBEBo5AXZIBBNSBWEIQQxZib08F9EuRAUq87cvufYDvzPIl6jdwewB8oO8iAL72voUF+H2AnwT+QohHhBDPoavz/wVdnf8jdHX+K9Dc/QBalvs5mABw+IiAWqCOATiFzh/wIDp/gCICrwB4iIj+Jo2CscDvKgO4hga1DjIAuMsEqWRgI+OPUAdchAAZpCCVCMzRBEiRzw2BPSIA35Xl+xSBWNB3mfrgAP0G7uE+Lvk/iggIIR4UQryETtr/qwb8anb/dcgRvhL4We7nYALAkUQE9rBqGzyHlVHwGcjSABGdJaL30MmMoYzfBvZLhxqw1i6oEYFQx4AP/G3dA1a/ADZbDGOmC6aSghAxcAH/1E5cSlQFCOm1fxfYwwL45AF72/2+QT42ad/p7DeA37WRz8KhCCwiFIHzQogXhBDfSeB/TwP+y1jV+e+BR/hyMAHg6EkEdH+AbhTUicDLRHQfEb1rEIEcEuAiAqYq4BskhAQyICJIgI0QAP6SQMrcgRDw9zlRc3+37wZAKQbAnF33jh4LAL4L/CkB9PUMf8Phb8n2vTv4ZYL/eSHEi0KI79Ft0WsCv+rn5zo/BxMAjqIkQC1wan7AGQsR+AGAlwwiEFsOaD0qQAvPzIBEMmCdKNiDECCCFPiIQoqpMAfsS3cB9DEApuzC5wJ/E+xRAPDhAH8f6JPm5Hf19jee7D8G/NVtBfw3JPD/XQN+5ey/AdnPD67zczAB4KhMBPYtROAxSQSel0TgLIAPiOgy4roDWo8yYJsc2FqIwNIA7RiDYEgdQMR9yCAFLsCOlfr7EIUi60cEKYidxZ8K9kBY4kdElh9j9NOfp0v8rsvatr2RhGCDAAghLgJ4Tkr9f0Pn7v8I3cx+E/jZ4MfBBIBjUCKwMBSBB9B5BB5Dt+vg8+jKAxeJ6CMi+iKSALiUAfO2aQ7MJQN6R0FIHfCRgShSAMkMEghBKvAPvRdATIbvBXwJqkgE+1jQtw30aTwEIAb0zTp/EwB736CfNQIghHhMCPEMOln/PQn8n0jgV1K/nvHzIB8OJgAckyEC59B1DTyGbqDQs5IIPEZEl4jofYS7AlIJgM0UaJIBwD5HwNx9MMYrgMjbgH3IkA34beQglhTUBv8QCQiBvQvkbb/XOIiFD+hd2b4N4KEBNyygjgDo27L9WALgdP0LIZ4XQjwkgf49dCN7P5W3v8Gm1M/Az8EEgGOSROA0Vu2DjwB4AqvywNNEdIeI3pGLWWhAkAv0bcOCXPMCjm5rLX62rgFgfaqgq40w1SfQejL42JkBG25u4T+BS53cTtDXQN0G4AgoAz4nf+MAfx/QwwP45vQ+082vDwRygb5e498Y8hNBBmx9/yeEEC8LIU6gG9qjZP7P0A3wUe18Nxn4OZgAcMyBCCiPwHGs5ghcwMow+LRUBZ4jomPSMPhtJAFwmQFjyYCpDgD+rgFXy2DrIAIhJSCkCgD+FsNQ9J0wSD0fB+Ja9WKBPibThwPwff37KsuHBfBTQD/G/GcjAA9IY989dDP6P5QEQBn7rmDVx38XXOPnYALAMUMioOYInAJwFp1P4CF00wWVKvAcgPNEdIWIPlCLHcLmQJsPIDQ90Dda2FQHUoYItQ6gbyOyfp9RsE0E85xxw33G9IZ+v/G8Rgj0zd+3gbuZ9QMew5+R5ftG9zqn+iG9/q+u7wshnhNCXEDXIfOBlu1/KYH/W3ST+9QAH3b1czAB4Jg1GVDZj9p5UPcJPIyVafAZdOUBIX0CVyIIgG8DIatBEOGhQUe3HYRAwD9YyKYUuIhBbMYfaxSs1RlAEWSAAtdjJX/zfptnwObk37gYI39tWb5rqI/L6Odz/rtuX5D1fZJZ/kdYmfq+xnp9X9+hj/v4OZgAcGwNEVALo2ohPC1VgfPougceQVcieApdieCCVAXel4tiCgFwmgIdagBg7xqAQyGwgbwIKAVwkAWbIhC7kVAq4PctAcQSgtBgH9vjDezOf9cY3w3Z35LhAxZXv/H7iwABiLmYBOCYBP3zMtv/UIL+5+hq+5fl/d9hVd8/AO/Qx8EEgGPLiYBuGDyO1cZDulfgMXQlgqelKtCgmynwTSYBWAaUAJ8x0Nc6aI4JDpEClz8Alt9zgbdtw6IS4J9KAvTM3UUKzMcajzrgc/jbwD40q9+V6Ye27KXIrH+DAAghHkTXu9/KbP9jrCT+r7Gq7X8vs/27YGMfBxMAjh0lAwuHKvAAVh0Eqp3wGQAPyg6C99FJpilkQESoAi5CAGyaBYHN9kGBVeuhTdL37TAY8gUA4T0HBls7PNm9Tynwjeo1CYIJ9o2FGJjOflgyfJf873P7p2T9p2W2fxKdnP8RVu17ysn/rSPbZ5mfgwkAx84TAbWo6qbBM5oqcFEjA0+gKxOcJaKbkgzcRF45wGUI9BEC36wAFylwTQa0/T4Q9gOkDg8qDfo+EuACfliy+7XHLTP9XWAfM9nPl/H7NvSJAX8F+qclsH8iM30F+kriv4ZVbV9l+7wzHwcTAA4ODxlQpkG1AdF96IyDF7AyDz6KzjPwJIAzRPS97CK4hXBHQBsJ/LEEwDdJ0LzuBH/LroM+lcAF+n0nBFLE/TG9/RuEwTIMyNUB0ASuNwkEIIYI+EoA6vop6eK/T4L6p+hq+srBfxmdxH8NncSvNuY5AO/Kx8EEgIMjiQjoXoFjWJUIFBk4L8nAQwYZOEVE14joI3Ryq2smwNID9KGdBEP+AJchEIHHgIiSgGOPgVzAzyUEZubuyvxdKoDL3Od6LKbOH7ONb2zrX4NuUM8zQohzkliaoP+Nlukr0L+DVQsf1/Y5mABwcBRSBWLJwGOSEJwiontE9AlWuxP6SgExBMDM9kMEIAT+MaAf8gSUUgFSs39fzT+GDIRIQOy2vfBk/D4CYLt9XgjxlBDimAT9z9FJ+y7Qv4VV+54Cfc72OZgAcHBUJgPHLWRAGQgvSkLwiLw8KBPVr+XGRHcRHg4U0x3gIgW2x30qgY0g+DoGbIpBCPRTCEDK5j8UQQJsPgFXvb+Bf/te345+sbV/lfkfF0I8BuCiEKJBJ+N/ia6Wr6R9ZeQzM/27DPocTAA4OMYlA/tYDRo6hVVboSIEFzSFQPkHjhPRXakOfIs4c2COAhBzgYMgAP65AKnKQNL6EJnpux731ft9BCDmEqMA+Gr9D8gs/7jM3HXA/0aSAAX41y2ZPvfsczAB4OCYMBk4gdXkwTMaITiPVWeBIgQXZB37GhFdkou+rUXQVx5AIhFAxm04SEEMEfDdHyP9+7J/FwEosZNfSAGwZf4m4N8vd9o7J/0TV9D15evmPSXrfycBX7n37zDoczAB4OCYHxlQ3QS6OnAGq3KBTgiUSnBRqgZ7RHQLwCUi+hqbpYJUM2DfLYVT2gT7Zv8hFSCmzQ+VCEDI/LcQQjwM4CEhxCl0Ev23WEn5VwzAvy5B/4aR5d+T3zmDPgcTAA6OmZIBvZ1LqQNqp8LTGiE4q6kEihgoc+GDAPaJ6BDAFUkIbiOuJbCEAmADdl/tv+9sAIpQCFwtfzEegBQCEGr9OykB/4IQYk9m6t9gZdZTQK+Dvcrwb2K1495Rlg/egIeDCQAHx9arA2rbYlUuOGmQAtNLcF5TDE5JleAQwLdEdBWr0kGuEXAXCUCKAfB+OV//AQn2hxLAr2hgb9budbC/jZWsf1cDfM7yOZgAcHDssDpgKgSKFNiUAqUW3G8oB/cDOEZESwlMVyUxuAv/YKBUH0DIBAj0KwXEbgEcmvQXe9sc9nNcAv15dAN4FjJDv25k8te167bMXgd7PcPnLJ+DCQATAA4OKyEw5w4oU+EJSQj0iyIHpzXV4KxBDk5KtUBIELqDrpRwC6vJhaHsvy8J8N0fawLMBX9b1n9K1ugvyM91X26ZeyiBWwf577Rs/qYB8vrlDlamvaO+fEm4GPA5OJgAcHAkkQKTEOik4Lh2OWkQhNMGOVDX9cfUfQ2ABRG1GohdAyCkgtAiPBMgdmtgkQD2KcCvg3wjM3iSysgJdFP1Gg2QFYjf1MBcv++G8ZgCeDVX/66W2SuwP9Sye5b0OTiYAHBwVFMJdGKwj/USwnFNNbBdTmLde6CIg7qunqdeZ18C53EAh1JN2JMZMiT4XXMRBOlVuB749+6XtXUXwJ+TfxNS2TiUWfueBONGUzjuamRGyfG3tOu3NUC/47noQH8gL2uZPWf3HBxMADg4xlYK1IS5hUEMlGqgk4R9i5JwQrt+TLvsG7dtF/V3FrDvhqe/L33bXWVQXGoX226ISwm8B1rW7bqYz7mrEQIzcz8wwP2e9ncOjfclOLPn4NhCArBc8tbYHFsZrr3kdZKgKwj72n3mZd/zU72Wq1de39nORgDMHRRtMw4UIB8YIG3+tF0OjAz+0EI6zAuHJxaLBX8IHNmxxx8BB0f18IGZbWrdwkEYTOKwsGT1ZuYP+AfpAP5BRrAoAaZasLQAuQ3QzftcWxtzcHAwAeDg2PowZwQgQBR8s+7NdjoY111b8wLxmw/5NkeykQcODo6JBnsAODg4ODg4djAa/gg4ODg4ODiYAHBwcHBwcHAwAeDg4ODg4OBgAsDBwcHBwcHBBICDg4ODg4ODCQAHBwcHBwcHEwAODg4ODg4OJgAcHBwcHBwcTAA4ODg4ODg4xov/fwAHnhIg2IQLzgAAAABJRU5ErkJggg=="}],"object":{"uuid":"da66c047-c0da-4a53-90dd-589c4e53e868","type":"Group","name":"MagicZoneGreen","visible":false,"layers":1,"matrix":[1,0,0,0,0,1,0,0,0,0,1,0,-0.33236499558859744,0,0,1],"up":[0,1,0],"children":[{"uuid":"1921b779-fac1-42ec-b40a-a1af729bf6fa","type":"Group","name":"PortalDust","layers":1,"matrix":[-1.0000003044148624,7.619931978698374e-8,1.2979021821838232e-8,0,-1.2979033855407715e-8,-1.8384778810981241e-7,-1.0000001522074553,0,-7.619930580271816e-8,-1.0000001522073967,1.8384778922002538e-7,0,0,0,0,1],"up":[0,1,0],"children":[{"uuid":"9ec4a1d9-3f3d-49a6-85fc-577cef6084a2","type":"ParticleEmitter","name":"PortalDustEmitter","layers":1,"matrix":[1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1],"up":[0,1,0],"ps":{"version":"3.0","autoDestroy":false,"looping":true,"prewarm":false,"duration":5,"shape":{"type":"cone","radius":1.21,"arc":6.283185307179586,"thickness":0,"angle":0,"mode":0,"spread":0,"speed":{"type":"ConstantValue","value":1}},"startLife":{"type":"IntervalValue","a":1,"b":1.5},"startSpeed":{"type":"ConstantValue","value":-1},"startRotation":{"type":"IntervalValue","a":0,"b":6.283185},"startSize":{"type":"IntervalValue","a":0.15,"b":0.2},"startColor":{"type":"ConstantColor","color":{"r":1,"g":1,"b":1,"a":1}},"emissionOverTime":{"type":"ConstantValue","value":25},"emissionOverDistance":{"type":"ConstantValue","value":0},"emissionBursts":[],"onlyUsedByOther":false,"instancingGeometry":"780917d8-bd1b-4d63-8aca-f79e3211f964","renderOrder":0,"renderMode":0,"rendererEmitterSettings":{},"material":"769df3ee-4567-40b7-8da4-473fb149f350","layers":1,"startTileIndex":{"type":"ConstantValue","value":0},"uTileCount":1,"vTileCount":1,"blendTiles":false,"softParticles":false,"softFarFade":0,"softNearFade":0,"behaviors":[{"type":"ForceOverLife","x":{"type":"ConstantValue","value":0},"y":{"type":"ConstantValue","value":0},"z":{"type":"ConstantValue","value":0}},{"type":"SizeOverLife","size":{"type":"PiecewiseBezier","functions":[{"function":{"p0":0.8495575,"p1":0.8495575,"p2":1,"p3":1},"start":0},{"function":{"p0":1,"p1":1,"p2":0,"p3":0},"start":0.49871457}]}},{"type":"RotationOverLife","angularVelocity":{"type":"IntervalValue","a":-3.1415925,"b":3.1415925}},{"type":"ColorOverLife","color":{"type":"Gradient","color":{"type":"CLinearFunction","subType":"Color","keys":[{"value":{"r":1,"g":1,"b":1},"pos":0},{"value":{"r":0.59607846,"g":1,"b":0.050980393},"pos":0.4587167162584878},{"value":{"r":0,"g":1,"b":0.047058824},"pos":0.9518272678721293}]},"alpha":{"type":"CLinearFunction","subType":"Number","keys":[{"value":0,"pos":0},{"value":1,"pos":0.41690699626153965},{"value":1,"pos":0.7580224307621881},{"value":0,"pos":1}]}}}],"worldSpace":true}}]},{"uuid":"cfe42db9-925f-4bd2-bc92-3d15a4e2b795","type":"Group","name":"GlowCircle","layers":1,"matrix":[1,0,0,0,0,-5.321248014494817e-8,-1.0000000532124802,0,0,1.0000000532124802,-5.321248014494817e-8,0,0,0,0,1],"up":[0,1,0],"children":[{"uuid":"94cac5fe-52a9-431d-ba8a-19fe44a2cdc1","type":"ParticleEmitter","name":"GlowCircleEmitter","layers":1,"matrix":[1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1],"up":[0,1,0],"ps":{"version":"3.0","autoDestroy":false,"looping":true,"prewarm":false,"duration":2,"shape":{"type":"cone","radius":0.01,"arc":6.283185307179586,"thickness":1,"angle":0.06981317007977318,"mode":0,"spread":0,"speed":{"type":"ConstantValue","value":1}},"startLife":{"type":"ConstantValue","value":2},"startSpeed":{"type":"ConstantValue","value":0},"startRotation":{"type":"Euler","angleX":{"type":"IntervalValue","a":0,"b":0},"angleY":{"type":"IntervalValue","a":0,"b":0},"angleZ":{"type":"IntervalValue","a":0,"b":6.283185},"eulerOrder":"XYZ"},"startSize":{"type":"ConstantValue","value":4.1},"startColor":{"type":"ConstantColor","color":{"r":0.45882353,"g":1,"b":0.28627452,"a":0.4509804}},"emissionOverTime":{"type":"ConstantValue","value":1},"emissionOverDistance":{"type":"ConstantValue","value":0},"emissionBursts":[],"onlyUsedByOther":false,"instancingGeometry":"f40b6ee0-aa01-46e0-b05a-d938b54eec83","renderOrder":0,"renderMode":2,"rendererEmitterSettings":{},"material":"6d9283b7-81c2-4063-84cc-f696054ce6f6","layers":1,"startTileIndex":{"type":"ConstantValue","value":0},"uTileCount":1,"vTileCount":1,"blendTiles":false,"softParticles":false,"softFarFade":0,"softNearFade":0,"behaviors":[{"type":"ForceOverLife","x":{"type":"ConstantValue","value":0},"y":{"type":"ConstantValue","value":0},"z":{"type":"ConstantValue","value":0}},{"type":"ColorOverLife","color":{"type":"Gradient","color":{"type":"CLinearFunction","subType":"Color","keys":[{"value":{"r":1,"g":1,"b":1},"pos":0},{"value":{"r":1,"g":1,"b":1},"pos":1}]},"alpha":{"type":"CLinearFunction","subType":"Number","keys":[{"value":0,"pos":0},{"value":1,"pos":0.5014572365911345},{"value":0,"pos":1}]}}}],"worldSpace":true}}]},{"uuid":"c86a5eb7-2571-4e87-b5fd-e68a0f965b0a","type":"ParticleEmitter","name":"MagicZoneGreenEmitter","layers":1,"matrix":[1,0,0,0,0,-2.220446049250313e-16,-1,0,0,1,-2.220446049250313e-16,0,0,0,0,1],"up":[0,1,0],"ps":{"version":"3.0","autoDestroy":false,"looping":true,"prewarm":false,"duration":5,"shape":{"type":"point"},"startLife":{"type":"ConstantValue","value":1},"startSpeed":{"type":"ConstantValue","value":0},"startRotation":{"type":"Euler","angleX":{"type":"IntervalValue","a":1.5707963,"b":1.5707963},"angleY":{"type":"IntervalValue","a":0,"b":6.283185},"angleZ":{"type":"IntervalValue","a":0,"b":0},"eulerOrder":"XYZ"},"startSize":{"type":"ConstantValue","value":2.8},"startColor":{"type":"ConstantColor","color":{"r":1,"g":1,"b":1,"a":1}},"emissionOverTime":{"type":"ConstantValue","value":2.5},"emissionOverDistance":{"type":"ConstantValue","value":0},"emissionBursts":[],"onlyUsedByOther":false,"instancingGeometry":"780917d8-bd1b-4d63-8aca-f79e3211f964","renderOrder":0,"renderMode":2,"rendererEmitterSettings":{},"material":"7442c205-fb42-4fb9-baec-82a192b81351","layers":1,"startTileIndex":{"type":"ConstantValue","value":0},"uTileCount":1,"vTileCount":1,"blendTiles":false,"softParticles":false,"softFarFade":0,"softNearFade":0,"behaviors":[{"type":"ForceOverLife","x":{"type":"ConstantValue","value":0},"y":{"type":"ConstantValue","value":0},"z":{"type":"ConstantValue","value":0}},{"type":"ColorOverLife","color":{"type":"Gradient","color":{"type":"CLinearFunction","subType":"Color","keys":[{"value":{"r":0.6156863,"g":1,"b":0},"pos":0},{"value":{"r":0.101960786,"g":1,"b":0.10980392},"pos":1}]},"alpha":{"type":"CLinearFunction","subType":"Number","keys":[{"value":0,"pos":0.004592965590905623},{"value":1,"pos":0.5014572365911345},{"value":0,"pos":1}]}}}],"worldSpace":true}}]}} \ No newline at end of file From 9c4b31c8153d4d8793b9b302936d8e274e961538 Mon Sep 17 00:00:00 2001 From: Mikalai Lazitski Date: Thu, 11 Dec 2025 18:17:07 +0300 Subject: [PATCH 10/62] refactor: remove unused loader file and streamline imports in FX Editor components for improved clarity and performance --- .../editor/windows/fx-editor/VFX/VFXEffect.ts | 2 +- .../fx-editor/VFX/behaviors/colorBySpeed.ts | 17 +- .../fx-editor/VFX/behaviors/colorOverLife.ts | 6 +- .../fx-editor/VFX/behaviors/forceOverLife.ts | 4 +- .../fx-editor/VFX/behaviors/frameOverLife.ts | 2 +- .../VFX/behaviors/limitSpeedOverLife.ts | 2 +- .../fx-editor/VFX/behaviors/orbitOverLife.ts | 4 +- .../VFX/behaviors/rotationBySpeed.ts | 8 +- .../VFX/behaviors/rotationOverLife.ts | 4 +- .../fx-editor/VFX/behaviors/sizeBySpeed.ts | 4 +- .../fx-editor/VFX/behaviors/sizeOverLife.ts | 4 +- .../fx-editor/VFX/behaviors/speedOverLife.ts | 4 +- .../factories/VFXBehaviorFunctionFactory.ts | 8 +- .../VFX/factories/VFXEmitterFactory.ts | 18 +- .../VFX/factories/VFXGeometryFactory.ts | 10 +- .../VFX/factories/VFXMaterialFactory.ts | 20 +- .../fx-editor/VFX/loggers/VFXLogger.ts | 4 +- .../fx-editor/VFX/parsers/VFXDataConverter.ts | 4 +- .../fx-editor/VFX/parsers/VFXParser.ts | 4 +- .../fx-editor/VFX/parsers/VFXValueParser.ts | 4 +- .../VFX/processors/VFXHierarchyProcessor.ts | 8 +- .../VFX/systems/VFXParticleSystem.ts | 8 +- .../VFX/systems/VFXSolidParticleSystem.ts | 11 +- .../VFX/types/VFXBehaviorFunction.ts | 6 +- .../windows/fx-editor/VFX/types/context.ts | 4 +- .../windows/fx-editor/VFX/types/emitter.ts | 6 +- .../windows/fx-editor/VFX/types/factories.ts | 16 +- .../windows/fx-editor/VFX/types/hierarchy.ts | 2 +- editor/src/editor/windows/fx-editor/graph.tsx | 166 +++--- .../src/editor/windows/fx-editor/layout.tsx | 8 +- editor/src/editor/windows/fx-editor/loader.ts | 543 ------------------ .../editor/windows/fx-editor/properties.tsx | 1 - .../properties/particle-renderer.tsx | 1 - .../editor/windows/fx-editor/resources.tsx | 5 +- 34 files changed, 165 insertions(+), 753 deletions(-) delete mode 100644 editor/src/editor/windows/fx-editor/loader.ts diff --git a/editor/src/editor/windows/fx-editor/VFX/VFXEffect.ts b/editor/src/editor/windows/fx-editor/VFX/VFXEffect.ts index 8661964f3..14cbc8215 100644 --- a/editor/src/editor/windows/fx-editor/VFX/VFXEffect.ts +++ b/editor/src/editor/windows/fx-editor/VFX/VFXEffect.ts @@ -1,4 +1,4 @@ -import type { Scene } from "@babylonjs/core/scene"; +import type { Scene } from "@babylonjs/core"; import { Tools } from "@babylonjs/core/Misc/tools"; import type { IDisposable } from "@babylonjs/core/scene"; import type { QuarksVFXJSON } from "./types/quarksTypes"; diff --git a/editor/src/editor/windows/fx-editor/VFX/behaviors/colorBySpeed.ts b/editor/src/editor/windows/fx-editor/VFX/behaviors/colorBySpeed.ts index 3d34e8b9d..dd4bf65b3 100644 --- a/editor/src/editor/windows/fx-editor/VFX/behaviors/colorBySpeed.ts +++ b/editor/src/editor/windows/fx-editor/VFX/behaviors/colorBySpeed.ts @@ -1,22 +1,15 @@ -import type { Particle } from "../../particle"; -import type { SolidParticle } from "../../solidParticle"; +import type { SolidParticle } from "@babylonjs/core/Particles/solidParticle"; +import type { Particle } from "@babylonjs/core/Particles/particle"; import type { VFXColorBySpeedBehavior } from "../types/behaviors"; import { interpolateColorKeys } from "./utils"; import { VFXValueParser } from "../parsers/VFXValueParser"; -import type { Color4 } from "../../../Maths/math.color"; -/** - * Extended Particle interface for custom behaviors - */ -interface ExtendedParticle extends Particle { - startSpeed?: number; - startColor?: Color4; -} + /** * Apply ColorBySpeed behavior to Particle */ -export function applyColorBySpeedPS(particle: ExtendedParticle, behavior: VFXColorBySpeedBehavior, currentSpeed: number, valueParser: VFXValueParser): void { +export function applyColorBySpeedPS(particle: Particle, behavior: VFXColorBySpeedBehavior, currentSpeed: number, valueParser: VFXValueParser): void { if (!behavior.color || !behavior.color.keys || !particle.color) { return; } @@ -27,7 +20,7 @@ export function applyColorBySpeedPS(particle: ExtendedParticle, behavior: VFXCol const speedRatio = Math.max(0, Math.min(1, (currentSpeed - minSpeed) / (maxSpeed - minSpeed || 1))); const interpolatedColor = interpolateColorKeys(colorKeys, speedRatio); - const startColor = particle.startColor || particle.initialColor; + const startColor = particle.initialColor; if (startColor) { // Multiply with startColor (matching three.quarks behavior) diff --git a/editor/src/editor/windows/fx-editor/VFX/behaviors/colorOverLife.ts b/editor/src/editor/windows/fx-editor/VFX/behaviors/colorOverLife.ts index cf8fc2140..7f41df43f 100644 --- a/editor/src/editor/windows/fx-editor/VFX/behaviors/colorOverLife.ts +++ b/editor/src/editor/windows/fx-editor/VFX/behaviors/colorOverLife.ts @@ -1,8 +1,8 @@ -import { Color4 } from "../../../Maths/math.color"; -import type { ParticleSystem } from "../../particleSystem"; -import type { SolidParticle } from "../../solidParticle"; +import { Color4 } from "@babylonjs/core/Maths/math.color"; +import type { SolidParticle } from "@babylonjs/core/Particles/solidParticle"; import type { VFXColorOverLifeBehavior } from "../types/behaviors"; import { extractColorFromValue, extractAlphaFromValue, interpolateColorKeys, interpolateGradientKeys } from "./utils"; +import { ParticleSystem } from "@babylonjs/core/Particles/particleSystem"; /** * Apply ColorOverLife behavior to ParticleSystem diff --git a/editor/src/editor/windows/fx-editor/VFX/behaviors/forceOverLife.ts b/editor/src/editor/windows/fx-editor/VFX/behaviors/forceOverLife.ts index e71756db2..261278402 100644 --- a/editor/src/editor/windows/fx-editor/VFX/behaviors/forceOverLife.ts +++ b/editor/src/editor/windows/fx-editor/VFX/behaviors/forceOverLife.ts @@ -1,5 +1,5 @@ -import { Vector3 } from "../../../Maths/math.vector"; -import type { ParticleSystem } from "../../particleSystem"; +import { Vector3 } from "@babylonjs/core/Maths/math.vector"; +import type { ParticleSystem } from "@babylonjs/core/Particles/particleSystem"; import type { VFXForceOverLifeBehavior, VFXGravityForceBehavior } from "../types/behaviors"; import { VFXValueParser } from "../parsers/VFXValueParser"; diff --git a/editor/src/editor/windows/fx-editor/VFX/behaviors/frameOverLife.ts b/editor/src/editor/windows/fx-editor/VFX/behaviors/frameOverLife.ts index 9267ddc46..1c37c3af7 100644 --- a/editor/src/editor/windows/fx-editor/VFX/behaviors/frameOverLife.ts +++ b/editor/src/editor/windows/fx-editor/VFX/behaviors/frameOverLife.ts @@ -1,4 +1,4 @@ -import type { ParticleSystem } from "../../particleSystem"; +import type { ParticleSystem } from "@babylonjs/core/Particles/particleSystem"; import type { VFXFrameOverLifeBehavior } from "../types/behaviors"; import { VFXValueParser } from "../parsers/VFXValueParser"; diff --git a/editor/src/editor/windows/fx-editor/VFX/behaviors/limitSpeedOverLife.ts b/editor/src/editor/windows/fx-editor/VFX/behaviors/limitSpeedOverLife.ts index 5fbfb373e..57b164487 100644 --- a/editor/src/editor/windows/fx-editor/VFX/behaviors/limitSpeedOverLife.ts +++ b/editor/src/editor/windows/fx-editor/VFX/behaviors/limitSpeedOverLife.ts @@ -1,4 +1,4 @@ -import type { ParticleSystem } from "../../particleSystem"; +import type { ParticleSystem } from "@babylonjs/core/Particles/particleSystem"; import type { VFXLimitSpeedOverLifeBehavior } from "../types/behaviors"; import { extractNumberFromValue } from "./utils"; import { VFXValueParser } from "../parsers/VFXValueParser"; diff --git a/editor/src/editor/windows/fx-editor/VFX/behaviors/orbitOverLife.ts b/editor/src/editor/windows/fx-editor/VFX/behaviors/orbitOverLife.ts index 1438b885e..23c75ce42 100644 --- a/editor/src/editor/windows/fx-editor/VFX/behaviors/orbitOverLife.ts +++ b/editor/src/editor/windows/fx-editor/VFX/behaviors/orbitOverLife.ts @@ -1,5 +1,5 @@ -import type { Particle } from "../../particle"; -import type { SolidParticle } from "../../solidParticle"; +import type { Particle } from "@babylonjs/core/Particles/particle"; +import type { SolidParticle } from "@babylonjs/core/Particles/solidParticle"; import type { VFXOrbitOverLifeBehavior } from "../types/behaviors"; import { extractNumberFromValue, interpolateGradientKeys } from "./utils"; import { VFXValueParser } from "../parsers/VFXValueParser"; diff --git a/editor/src/editor/windows/fx-editor/VFX/behaviors/rotationBySpeed.ts b/editor/src/editor/windows/fx-editor/VFX/behaviors/rotationBySpeed.ts index c795c9211..7eca3e7ee 100644 --- a/editor/src/editor/windows/fx-editor/VFX/behaviors/rotationBySpeed.ts +++ b/editor/src/editor/windows/fx-editor/VFX/behaviors/rotationBySpeed.ts @@ -1,6 +1,6 @@ -import type { Particle } from "../../particle"; -import type { SolidParticle } from "../../solidParticle"; -import type { ParticleSystem } from "../../particleSystem"; +import type { Particle } from "@babylonjs/core/Particles/particle"; +import type { SolidParticle } from "@babylonjs/core/Particles/solidParticle"; +import type { ParticleSystem } from "@babylonjs/core/Particles/particleSystem"; import type { VFXRotationBySpeedBehavior } from "../types/behaviors"; import { extractNumberFromValue, interpolateGradientKeys } from "./utils"; import { VFXValueParser } from "../parsers/VFXValueParser"; @@ -15,7 +15,7 @@ interface ExtendedParticle extends Particle { /** * Apply RotationBySpeed behavior to Particle */ -export function applyRotationBySpeedPS(particle: ExtendedParticle, behavior: VFXRotationBySpeedBehavior, currentSpeed: number, particleSystem: ParticleSystem, valueParser: VFXValueParser): void { +export function applyRotationBySpeedPS(particle: ExtendedParticle, behavior: VFXRotationBySpeedBehavior, currentSpeed: number, _particleSystem: ParticleSystem, valueParser: VFXValueParser): void { if (!behavior.angularVelocity) { return; } diff --git a/editor/src/editor/windows/fx-editor/VFX/behaviors/rotationOverLife.ts b/editor/src/editor/windows/fx-editor/VFX/behaviors/rotationOverLife.ts index d0bde6d3f..9f8fd39cf 100644 --- a/editor/src/editor/windows/fx-editor/VFX/behaviors/rotationOverLife.ts +++ b/editor/src/editor/windows/fx-editor/VFX/behaviors/rotationOverLife.ts @@ -1,5 +1,5 @@ -import type { ParticleSystem } from "../../particleSystem"; -import type { SolidParticle } from "../../solidParticle"; +import type { ParticleSystem } from "@babylonjs/core/Particles/particleSystem"; +import type { SolidParticle } from "@babylonjs/core/Particles/solidParticle"; import type { VFXRotationOverLifeBehavior } from "../types/behaviors"; import { VFXValueParser } from "../parsers/VFXValueParser"; diff --git a/editor/src/editor/windows/fx-editor/VFX/behaviors/sizeBySpeed.ts b/editor/src/editor/windows/fx-editor/VFX/behaviors/sizeBySpeed.ts index faeb75d2b..c69ee1bdb 100644 --- a/editor/src/editor/windows/fx-editor/VFX/behaviors/sizeBySpeed.ts +++ b/editor/src/editor/windows/fx-editor/VFX/behaviors/sizeBySpeed.ts @@ -1,5 +1,5 @@ -import type { Particle } from "../../particle"; -import type { SolidParticle } from "../../solidParticle"; +import type { Particle } from "@babylonjs/core/Particles/particle"; +import type { SolidParticle } from "@babylonjs/core/Particles/solidParticle"; import type { VFXSizeBySpeedBehavior } from "../types/behaviors"; import { extractNumberFromValue, interpolateGradientKeys } from "./utils"; import { VFXValueParser } from "../parsers/VFXValueParser"; diff --git a/editor/src/editor/windows/fx-editor/VFX/behaviors/sizeOverLife.ts b/editor/src/editor/windows/fx-editor/VFX/behaviors/sizeOverLife.ts index 80790bd36..ab7994cee 100644 --- a/editor/src/editor/windows/fx-editor/VFX/behaviors/sizeOverLife.ts +++ b/editor/src/editor/windows/fx-editor/VFX/behaviors/sizeOverLife.ts @@ -1,5 +1,5 @@ -import type { ParticleSystem } from "../../particleSystem"; -import type { SolidParticle } from "../../solidParticle"; +import type { ParticleSystem } from "@babylonjs/core/Particles/particleSystem"; +import type { SolidParticle } from "@babylonjs/core/Particles/solidParticle"; import type { VFXSizeOverLifeBehavior } from "../types/behaviors"; import { extractNumberFromValue, interpolateGradientKeys } from "./utils"; diff --git a/editor/src/editor/windows/fx-editor/VFX/behaviors/speedOverLife.ts b/editor/src/editor/windows/fx-editor/VFX/behaviors/speedOverLife.ts index d11eb767e..ed649bab3 100644 --- a/editor/src/editor/windows/fx-editor/VFX/behaviors/speedOverLife.ts +++ b/editor/src/editor/windows/fx-editor/VFX/behaviors/speedOverLife.ts @@ -1,5 +1,5 @@ -import type { ParticleSystem } from "../../particleSystem"; -import type { SolidParticle } from "../../solidParticle"; +import type { ParticleSystem } from "@babylonjs/core/Particles/particleSystem"; +import type { SolidParticle } from "@babylonjs/core/Particles/solidParticle"; import type { VFXSpeedOverLifeBehavior } from "../types/behaviors"; import { extractNumberFromValue, interpolateGradientKeys } from "./utils"; import { VFXValueParser } from "../parsers/VFXValueParser"; diff --git a/editor/src/editor/windows/fx-editor/VFX/factories/VFXBehaviorFunctionFactory.ts b/editor/src/editor/windows/fx-editor/VFX/factories/VFXBehaviorFunctionFactory.ts index b0cf724b1..80093a03e 100644 --- a/editor/src/editor/windows/fx-editor/VFX/factories/VFXBehaviorFunctionFactory.ts +++ b/editor/src/editor/windows/fx-editor/VFX/factories/VFXBehaviorFunctionFactory.ts @@ -1,6 +1,6 @@ -import type { Particle } from "../../particle"; -import type { SolidParticle } from "../../solidParticle"; -import type { ParticleSystem } from "../../particleSystem"; +import type { Particle } from "@babylonjs/core/Particles/particle"; +import type { SolidParticle } from "@babylonjs/core/Particles/solidParticle"; +import type { ParticleSystem } from "@babylonjs/core/Particles/particleSystem"; import type { VFXBehavior, VFXColorBySpeedBehavior, @@ -163,7 +163,7 @@ export class VFXBehaviorFunctionFactory { return functions; } - public static createSystemFunctions(behaviors: VFXBehavior[], valueParser: VFXValueParser): VFXSystemBehaviorFunction[] { + public static createSystemFunctions(behaviors: VFXBehavior[], _valueParser: VFXValueParser): VFXSystemBehaviorFunction[] { const functions: VFXSystemBehaviorFunction[] = []; for (const behavior of behaviors) { diff --git a/editor/src/editor/windows/fx-editor/VFX/factories/VFXEmitterFactory.ts b/editor/src/editor/windows/fx-editor/VFX/factories/VFXEmitterFactory.ts index ba28dd113..7731d9af4 100644 --- a/editor/src/editor/windows/fx-editor/VFX/factories/VFXEmitterFactory.ts +++ b/editor/src/editor/windows/fx-editor/VFX/factories/VFXEmitterFactory.ts @@ -1,11 +1,11 @@ -import type { Nullable } from "../../../types"; -import { Vector3, Matrix, Quaternion } from "../../../Maths/math.vector"; -import { Color4 } from "../../../Maths/math.color"; -import { ParticleSystem } from "../../particleSystem"; -import { SolidParticleSystem } from "../../solidParticleSystem"; -import { CreatePlane } from "../../../Meshes/Builders/planeBuilder"; -import { Mesh } from "../../../Meshes/mesh"; -import { Constants } from "../../../Engines/constants"; +import type { Nullable } from "@babylonjs/core/types"; +import { Vector3, Matrix, Quaternion } from "@babylonjs/core/Maths/math.vector"; +import { Color4 } from "@babylonjs/core/Maths/math.color"; +import { ParticleSystem } from "@babylonjs/core/Particles/particleSystem"; +import { SolidParticleSystem } from "@babylonjs/core/Particles/solidParticleSystem"; +import { CreatePlane } from "@babylonjs/core/Meshes/Builders/planeBuilder"; +import { Mesh } from "@babylonjs/core/Meshes/mesh"; +import { Constants } from "@babylonjs/core/Engines/constants"; import type { VFXEmitterData } from "../types/emitter"; import type { VFXParseContext } from "../types/context"; import type { VFXLoaderOptions } from "../types/loader"; @@ -508,7 +508,7 @@ export class VFXEmitterFactory { bursts: import("../types/emitterConfig").VFXEmissionBurst[], baseEmitRate: number, duration: number, - options?: VFXLoaderOptions + _options?: VFXLoaderOptions ): void { for (const burst of bursts) { if (burst.time !== undefined && burst.count !== undefined) { diff --git a/editor/src/editor/windows/fx-editor/VFX/factories/VFXGeometryFactory.ts b/editor/src/editor/windows/fx-editor/VFX/factories/VFXGeometryFactory.ts index 30cc63183..38f7a6121 100644 --- a/editor/src/editor/windows/fx-editor/VFX/factories/VFXGeometryFactory.ts +++ b/editor/src/editor/windows/fx-editor/VFX/factories/VFXGeometryFactory.ts @@ -1,8 +1,8 @@ -import type { Nullable } from "../../../types"; -import type { Scene } from "../../../scene"; -import { Mesh } from "../../../Meshes/mesh"; -import { VertexData } from "../../../Meshes/mesh.vertexData"; -import { CreatePlane } from "../../../Meshes/Builders/planeBuilder"; +import type { Nullable } from "@babylonjs/core/types"; +import { Scene } from "@babylonjs/core/scene"; +import { Mesh } from "@babylonjs/core/Meshes/mesh"; +import { VertexData } from "@babylonjs/core/Meshes/mesh.vertexData"; +import { CreatePlane } from "@babylonjs/core/Meshes/Builders/planeBuilder"; import type { IVFXGeometryFactory } from "../types/factories"; import type { VFXParseContext } from "../types/context"; import type { VFXLoaderOptions } from "../types/loader"; diff --git a/editor/src/editor/windows/fx-editor/VFX/factories/VFXMaterialFactory.ts b/editor/src/editor/windows/fx-editor/VFX/factories/VFXMaterialFactory.ts index ef2bc7d33..70ecef2ca 100644 --- a/editor/src/editor/windows/fx-editor/VFX/factories/VFXMaterialFactory.ts +++ b/editor/src/editor/windows/fx-editor/VFX/factories/VFXMaterialFactory.ts @@ -1,16 +1,16 @@ -import type { Nullable } from "../../../types"; -import { Color3 } from "../../../Maths/math.color"; -import { Texture } from "../../../Materials/Textures/texture"; -import { PBRMaterial } from "../../../Materials/PBR/pbrMaterial"; -import { Material } from "../../../Materials/material"; -import { Constants } from "../../../Engines/constants"; -import { Tools } from "../../../Misc/tools"; +import type { Nullable } from "@babylonjs/core/types"; +import { Color3 } from "@babylonjs/core/Maths/math.color"; +import { Texture } from "@babylonjs/core/Materials/Textures/texture"; +import { PBRMaterial } from "@babylonjs/core/Materials/PBR/pbrMaterial"; +import { Material } from "@babylonjs/core/Materials/material"; +import { Constants } from "@babylonjs/core/Engines/constants"; +import { Tools } from "@babylonjs/core/Misc/tools"; import type { IVFXMaterialFactory } from "../types/factories"; import type { VFXParseContext } from "../types/context"; import type { VFXLoaderOptions } from "../types/loader"; import { VFXLogger } from "../loggers/VFXLogger"; import type { QuarksTexture } from "../types/quarksTypes"; -import type { Scene } from "../../../scene"; +import type { Scene } from "@babylonjs/core/scene"; /** * Factory for creating materials and textures from Three.js JSON data @@ -83,7 +83,7 @@ export class VFXMaterialFactory implements IVFXMaterialFactory { /** * Helper method to create texture from texture data */ - private _createTextureFromData(textureUrl: string, texture: QuarksTexture, scene: Scene, options?: VFXLoaderOptions): Texture { + private _createTextureFromData(textureUrl: string, texture: QuarksTexture, scene: Scene, _options?: VFXLoaderOptions): Texture { // Determine sampling mode from texture filters let samplingMode = Texture.TRILINEAR_SAMPLINGMODE; // Default if (texture.minFilter !== undefined) { @@ -184,7 +184,7 @@ export class VFXMaterialFactory implements IVFXMaterialFactory { return null; } - const imageInfo = []; + const imageInfo: string[] = []; if (image.url) { const urlParts = image.url.split("/"); let filename = urlParts[urlParts.length - 1] || image.url; diff --git a/editor/src/editor/windows/fx-editor/VFX/loggers/VFXLogger.ts b/editor/src/editor/windows/fx-editor/VFX/loggers/VFXLogger.ts index 0c05d5896..ac640999e 100644 --- a/editor/src/editor/windows/fx-editor/VFX/loggers/VFXLogger.ts +++ b/editor/src/editor/windows/fx-editor/VFX/loggers/VFXLogger.ts @@ -1,4 +1,4 @@ -import { Logger } from "../../../Misc/logger"; +import { Logger } from "@babylonjs/core/Misc/logger"; import type { VFXLoaderOptions } from "../types"; /** @@ -32,7 +32,7 @@ export class VFXLogger { /** * Log an error */ - public error(message: string, options?: VFXLoaderOptions): void { + public error(message: string, _options?: VFXLoaderOptions): void { Logger.Error(`${this._prefix} ${message}`); } } diff --git a/editor/src/editor/windows/fx-editor/VFX/parsers/VFXDataConverter.ts b/editor/src/editor/windows/fx-editor/VFX/parsers/VFXDataConverter.ts index 190e16ecc..7dfb43859 100644 --- a/editor/src/editor/windows/fx-editor/VFX/parsers/VFXDataConverter.ts +++ b/editor/src/editor/windows/fx-editor/VFX/parsers/VFXDataConverter.ts @@ -1,4 +1,4 @@ -import { Vector3, Matrix, Quaternion } from "../../../Maths/math.vector"; +import { Vector3, Matrix, Quaternion } from "@babylonjs/core/Maths/math.vector"; import type { VFXLoaderOptions } from "../types/loader"; import type { QuarksVFXJSON } from "../types/quarksTypes"; import type { @@ -153,7 +153,7 @@ export class VFXDataConverter { * Convert transform from Quarks/Three.js (right-handed) to Babylon.js VFX (left-handed) * This is the ONLY place where handedness conversion happens */ - private _convertTransform(matrixArray?: number[], positionArray?: number[], rotationArray?: number[], scaleArray?: number[], options?: VFXLoaderOptions): VFXTransform { + private _convertTransform(matrixArray?: number[], positionArray?: number[], rotationArray?: number[], scaleArray?: number[], _options?: VFXLoaderOptions): VFXTransform { const position = Vector3.Zero(); const rotation = Quaternion.Identity(); const scale = Vector3.One(); diff --git a/editor/src/editor/windows/fx-editor/VFX/parsers/VFXParser.ts b/editor/src/editor/windows/fx-editor/VFX/parsers/VFXParser.ts index 9732054a4..9babf2407 100644 --- a/editor/src/editor/windows/fx-editor/VFX/parsers/VFXParser.ts +++ b/editor/src/editor/windows/fx-editor/VFX/parsers/VFXParser.ts @@ -1,4 +1,4 @@ -import type { Scene } from "../../../scene"; +import type { Scene } from "@babylonjs/core/scene"; import type { QuarksVFXJSON } from "../types/quarksTypes"; import type { VFXLoaderOptions } from "../types/loader"; import type { VFXParseContext } from "../types/context"; @@ -9,7 +9,7 @@ import { VFXGeometryFactory } from "../factories/VFXGeometryFactory"; import { VFXEmitterFactory } from "../factories/VFXEmitterFactory"; import { VFXHierarchyProcessor } from "../processors/VFXHierarchyProcessor"; import { VFXDataConverter } from "./VFXDataConverter"; -import { TransformNode } from "../../../Meshes/transformNode"; +import { TransformNode } from "@babylonjs/core/Meshes/transformNode"; import { VFXParticleSystem } from "../systems/VFXParticleSystem"; import { VFXSolidParticleSystem } from "../systems/VFXSolidParticleSystem"; diff --git a/editor/src/editor/windows/fx-editor/VFX/parsers/VFXValueParser.ts b/editor/src/editor/windows/fx-editor/VFX/parsers/VFXValueParser.ts index bec68557c..f5bde22d3 100644 --- a/editor/src/editor/windows/fx-editor/VFX/parsers/VFXValueParser.ts +++ b/editor/src/editor/windows/fx-editor/VFX/parsers/VFXValueParser.ts @@ -1,5 +1,5 @@ -import { Color4 } from "../../../Maths/math.color"; -import { ColorGradient } from "../../../Misc/gradients"; +import { Color4 } from "@babylonjs/core/Maths/math.color"; +import { ColorGradient } from "@babylonjs/core/Misc/gradients"; import type { IVFXValueParser } from "../types/factories"; import type { VFXValue } from "../types/values"; import type { VFXColor } from "../types/colors"; diff --git a/editor/src/editor/windows/fx-editor/VFX/processors/VFXHierarchyProcessor.ts b/editor/src/editor/windows/fx-editor/VFX/processors/VFXHierarchyProcessor.ts index a896ed098..497241797 100644 --- a/editor/src/editor/windows/fx-editor/VFX/processors/VFXHierarchyProcessor.ts +++ b/editor/src/editor/windows/fx-editor/VFX/processors/VFXHierarchyProcessor.ts @@ -1,6 +1,6 @@ -import type { Nullable } from "../../../types"; -import { Vector3, Quaternion } from "../../../Maths/math.vector"; -import { TransformNode } from "../../../Meshes/transformNode"; +import type { Nullable } from "@babylonjs/core/types"; +import { Vector3, Quaternion } from "@babylonjs/core/Maths/math.vector"; +import { TransformNode } from "@babylonjs/core/Meshes/transformNode"; import { VFXParticleSystem } from "../systems/VFXParticleSystem"; import { VFXSolidParticleSystem } from "../systems/VFXSolidParticleSystem"; import type { VFXParseContext } from "../types/context"; @@ -263,7 +263,7 @@ export class VFXHierarchyProcessor { * For SPS, transformations are applied in initParticles (after buildMesh) * For ParticleSystem, we need to find and update the emitter mesh */ - private _applyVFXTransformToEmitter(vfxEmitter: VFXEmitter, currentGroup: Nullable, depth: number, options?: VFXLoaderOptions): void { + private _applyVFXTransformToEmitter(vfxEmitter: VFXEmitter, _currentGroup: Nullable, depth: number, options?: VFXLoaderOptions): void { const indent = " ".repeat(depth); const emitterName = vfxEmitter.name; diff --git a/editor/src/editor/windows/fx-editor/VFX/systems/VFXParticleSystem.ts b/editor/src/editor/windows/fx-editor/VFX/systems/VFXParticleSystem.ts index 8fbb88bb4..6c1ff0fcd 100644 --- a/editor/src/editor/windows/fx-editor/VFX/systems/VFXParticleSystem.ts +++ b/editor/src/editor/windows/fx-editor/VFX/systems/VFXParticleSystem.ts @@ -1,20 +1,20 @@ import { Color4 } from "@babylonjs/core/Maths/math.color"; -import { ParticleSystem } from "@babylonjs/core/Particles/particleSystem"; -import type { Scene } from "@babylonjs/core/scene"; +import { ParticleSystem, Scene } from "@babylonjs/core"; import type { VFXValueParser } from "../parsers/VFXValueParser"; import type { VFXPerParticleBehaviorFunction } from "../types/VFXBehaviorFunction"; + /** * Extended ParticleSystem with VFX behaviors support * (logic intentionally minimal, behaviors handled elsewhere) */ export class VFXParticleSystem extends ParticleSystem { - constructor(name: string, capacity: number, scene: Scene, valueParser: VFXValueParser, avgStartSpeed: number, avgStartSize: number, startColor: Color4) { + constructor(name: string, capacity: number, scene: Scene, _valueParser: VFXValueParser, _avgStartSpeed: number, _avgStartSize: number, _startColor: Color4) { super(name, capacity, scene); // behavior wiring omitted by design (see VFXEmitterFactory) } - public setPerParticleBehaviors(functions: VFXPerParticleBehaviorFunction[]): void { + public setPerParticleBehaviors(_functions: VFXPerParticleBehaviorFunction[]): void { // intentionally no-op (kept for API parity) } } diff --git a/editor/src/editor/windows/fx-editor/VFX/systems/VFXSolidParticleSystem.ts b/editor/src/editor/windows/fx-editor/VFX/systems/VFXSolidParticleSystem.ts index 54148092c..608e32b6c 100644 --- a/editor/src/editor/windows/fx-editor/VFX/systems/VFXSolidParticleSystem.ts +++ b/editor/src/editor/windows/fx-editor/VFX/systems/VFXSolidParticleSystem.ts @@ -1,8 +1,8 @@ -import { Vector3, Quaternion, Matrix } from "../../../Maths/math.vector"; -import { Color4 } from "../../../Maths/math.color"; -import { SolidParticleSystem } from "../../solidParticleSystem"; -import { SolidParticle } from "../../solidParticle"; -import type { TransformNode } from "../../../Meshes/transformNode"; +import { Vector3, Quaternion, Matrix } from "@babylonjs/core/Maths/math.vector"; +import { Color4 } from "@babylonjs/core/Maths/math.color"; +import { SolidParticleSystem } from "@babylonjs/core/Particles/solidParticleSystem"; +import { SolidParticle } from "@babylonjs/core/Particles/solidParticle"; +import type { TransformNode } from "@babylonjs/core/Meshes/transformNode"; import type { VFXParticleEmitterConfig, VFXEmissionBurst } from "../types/emitterConfig"; import type { VFXValueParser } from "../parsers/VFXValueParser"; import { VFXLogger } from "../loggers/VFXLogger"; @@ -259,6 +259,7 @@ export class VFXSolidParticleSystem extends SolidParticleSystem { } private _initializeEmitterShape(particle: SolidParticle, emissionState: EmissionState): void { + console.log("initializeEmitterShape", particle, emissionState); const config = this._config; const startSpeed = particle.props?.startSpeed ?? 0; diff --git a/editor/src/editor/windows/fx-editor/VFX/types/VFXBehaviorFunction.ts b/editor/src/editor/windows/fx-editor/VFX/types/VFXBehaviorFunction.ts index e7115e73b..a894a2bb8 100644 --- a/editor/src/editor/windows/fx-editor/VFX/types/VFXBehaviorFunction.ts +++ b/editor/src/editor/windows/fx-editor/VFX/types/VFXBehaviorFunction.ts @@ -1,6 +1,6 @@ -import type { Particle } from "../../particle"; -import type { SolidParticle } from "../../solidParticle"; -import type { ParticleSystem } from "../../particleSystem"; +import type { Particle } from "@babylonjs/core/Particles/particle"; +import type { SolidParticle } from "@babylonjs/core/Particles/solidParticle"; +import type { ParticleSystem } from "@babylonjs/core/Particles/particleSystem"; import type { VFXValueParser } from "../parsers/VFXValueParser"; /** diff --git a/editor/src/editor/windows/fx-editor/VFX/types/context.ts b/editor/src/editor/windows/fx-editor/VFX/types/context.ts index 24142cbda..3d6a04fde 100644 --- a/editor/src/editor/windows/fx-editor/VFX/types/context.ts +++ b/editor/src/editor/windows/fx-editor/VFX/types/context.ts @@ -1,5 +1,5 @@ -import type { Scene } from "../../../scene"; -import type { TransformNode } from "../../../Meshes/transformNode"; +import type { Scene } from "@babylonjs/core/scene"; +import type { TransformNode } from "@babylonjs/core/Meshes/transformNode"; import type { QuarksVFXJSON } from "./quarksTypes"; import type { VFXHierarchy } from "./hierarchy"; import type { VFXLoaderOptions } from "./loader"; diff --git a/editor/src/editor/windows/fx-editor/VFX/types/emitter.ts b/editor/src/editor/windows/fx-editor/VFX/types/emitter.ts index 931883e21..8c6c913da 100644 --- a/editor/src/editor/windows/fx-editor/VFX/types/emitter.ts +++ b/editor/src/editor/windows/fx-editor/VFX/types/emitter.ts @@ -1,6 +1,6 @@ -import type { Nullable } from "../../../types"; -import type { TransformNode } from "../../../Meshes/transformNode"; -import type { Vector3 } from "../../../Maths/math.vector"; +import type { Nullable } from "@babylonjs/core/types"; +import type { TransformNode } from "@babylonjs/core/Meshes/transformNode"; +import type { Vector3 } from "@babylonjs/core/Maths/math.vector"; import type { VFXParticleEmitterConfig } from "./emitterConfig"; import type { VFXEmitter } from "./hierarchy"; diff --git a/editor/src/editor/windows/fx-editor/VFX/types/factories.ts b/editor/src/editor/windows/fx-editor/VFX/types/factories.ts index c5ad41366..99af4a4cb 100644 --- a/editor/src/editor/windows/fx-editor/VFX/types/factories.ts +++ b/editor/src/editor/windows/fx-editor/VFX/types/factories.ts @@ -1,11 +1,11 @@ -import type { Nullable } from "../../../types"; -import type { Mesh } from "../../../Meshes/mesh"; -import type { ParticleSystem } from "../../particleSystem"; -import type { SolidParticleSystem } from "../../solidParticleSystem"; -import { PBRMaterial } from "../../../Materials/PBR/pbrMaterial"; -import type { Color4 } from "../../../Maths/math.color"; -import type { Texture } from "../../../Materials/Textures/texture"; -import type { ColorGradient } from "../../../Misc/gradients"; +import type { Nullable } from "@babylonjs/core/types"; +import type { Mesh } from "@babylonjs/core/Meshes/mesh"; +import type { ParticleSystem } from "@babylonjs/core/Particles/particleSystem"; +import type { SolidParticleSystem } from "@babylonjs/core/Particles/solidParticleSystem"; +import { PBRMaterial } from "@babylonjs/core/Materials/PBR/pbrMaterial"; +import type { Color4 } from "@babylonjs/core/Maths/math.color"; +import type { Texture } from "@babylonjs/core/Materials/Textures/texture"; +import type { ColorGradient } from "@babylonjs/core/Misc/gradients"; import type { VFXValue } from "./values"; import type { VFXColor } from "./colors"; import type { VFXGradientKey } from "./gradients"; diff --git a/editor/src/editor/windows/fx-editor/VFX/types/hierarchy.ts b/editor/src/editor/windows/fx-editor/VFX/types/hierarchy.ts index 6dd0b9a63..8618ecd53 100644 --- a/editor/src/editor/windows/fx-editor/VFX/types/hierarchy.ts +++ b/editor/src/editor/windows/fx-editor/VFX/types/hierarchy.ts @@ -1,4 +1,4 @@ -import { Vector3, Quaternion } from "../../../Maths/math.vector"; +import { Vector3, Quaternion } from "@babylonjs/core/Maths/math.vector"; import type { VFXParticleEmitterConfig } from "./emitterConfig"; /** diff --git a/editor/src/editor/windows/fx-editor/graph.tsx b/editor/src/editor/windows/fx-editor/graph.tsx index 3cec0b2ee..d385b2b06 100644 --- a/editor/src/editor/windows/fx-editor/graph.tsx +++ b/editor/src/editor/windows/fx-editor/graph.tsx @@ -1,9 +1,10 @@ import { Component, ReactNode } from "react"; import { Tree, TreeNodeInfo } from "@blueprintjs/core"; -import { Scene, AbstractMesh, ParticleSystem, SolidParticleSystem, Vector3, Color4, ParticleSystemSet } from "babylonjs"; +import { Scene, + + // AbstractMesh, + Vector3, Color4 } from "@babylonjs/core"; import { IFXParticleData, IFXGroupData, IFXNodeData, isGroupData, isParticleData } from "./properties/types"; -import { IConvertedNode, convertThreeJSJSONToFXEditor } from "./loader"; -import { ThreeJSParticleLoader } from "./threeJSParticleLoader"; import { AiOutlinePlus, AiOutlineClose } from "react-icons/ai"; import { IoSparklesSharp } from "react-icons/io5"; @@ -19,18 +20,12 @@ import { ContextMenuSubTrigger, ContextMenuSubContent, } from "../../../ui/shadcn/ui/context-menu"; -import { FXEditorPreview } from "./preview"; import { IFXEditor } from "."; - -// Maps to track created particle systems and meshes -const createdParticleSystemsMap = new Map(); -const createdMeshesMap = new Map(); -const particleSystemSetsMap = new Map(); +import { VFXEffect } from "./VFX"; export interface IFXEditorGraphProps { filePath: string | null; onNodeSelected?: (nodeId: string | number | null) => void; - onResourcesLoaded?: (resources: IConvertedNode[]) => void; editor: IFXEditor; } @@ -114,18 +109,18 @@ export class FXEditorGraph extends Component { - if (ps instanceof ParticleSystem) { - const nodeId = `particle-${setId}-${index}`; - createdParticleSystemsMap.set(nodeId, ps); - // Start the particle system - if (!ps.isStarted()) { - ps.start(); - } - } else if (ps instanceof SolidParticleSystem) { - const nodeId = `particle-${setId}-${index}`; - createdParticleSystemsMap.set(nodeId, ps); - // For SPS, call setParticles to update them - ps.setParticles(); - } - }); - - // // Also convert to our node structure for tree view - // const convertedData = await convertThreeJSJSONToFXEditor(filePath); - // const { nodes, resources } = convertedData; + const vfxEffect = await VFXEffect.LoadAsync(filePath, this.props.editor.preview!.scene as unknown as Scene, dirname + "/"); - // // Filter out resource nodes (texture, geometry) - they will be shown in Resources tab - // const particleNodes = nodes.filter((n) => n.type === "particle" || n.type === "group"); - // const treeNodes = this._convertToTreeNodeInfo(particleNodes, null); - // this.setState({ nodes: treeNodes }); - - // Notify parent about loaded resources - // if (this.props.onResourcesLoaded) { - // this.props.onResourcesLoaded(resources); - // } + vfxEffect.systems.forEach((system) => { + system.start(); + }); } catch (error) { console.error("Failed to load FX file:", error); } @@ -320,10 +284,10 @@ export class FXEditorGraph extends Component { // Create node data based on type (only particle and group nodes should be here) - let nodeData: IFXNodeData; + let nodeData: any; if (node.type === "particle" && node.particleData) { nodeData = { ...node.particleData, type: "particle" }; } else if (node.type === "group" && node.groupData) { @@ -336,9 +300,9 @@ export class FXEditorGraph extends Component, - isExpanded: false, - childNodes: undefined, - isSelected: false, - hasCaret: false, - nodeData: particleData, - }; + private _handleAddParticles(_parentId?: string | number): void { + // const _nodeId = `particle-${Date.now()}`; + // const particleData = this.getOrCreateParticleData(nodeId); + + // const newNode: TreeNodeInfo = { + // id: nodeId, + // label: this._getNodeLabelComponent({ id: nodeId, nodeData: particleData } as any, particleData.name), + // icon: , + // isExpanded: false, + // childNodes: undefined, + // isSelected: false, + // hasCaret: false, + // nodeData: particleData, + // }; } - private _handleAddGroup(parentId?: string | number): void { - const nodeId = `group-${Date.now()}`; - const groupData = this.getOrCreateGroupData(nodeId); - - const newNode: TreeNodeInfo = { - id: nodeId, - label: this._getNodeLabelComponent({ id: nodeId, nodeData: groupData } as any, groupData.name), - icon: , - isExpanded: true, - childNodes: [], - isSelected: false, - hasCaret: true, - nodeData: groupData, - }; + private _handleAddGroup(_parentId?: string | number): void { + // const _nodeId = `group-${Date.now()}`; + // const groupData = this.getOrCreateGroupData(nodeId); + + // const newNode: TreeNodeInfo = { + // id: nodeId, + // label: this._getNodeLabelComponent({ id: nodeId, nodeData: groupData } as any, groupData.name), + // icon: , + // isExpanded: true, + // childNodes: [], + // isSelected: false, + // hasCaret: true, + // nodeData: groupData, + // }; } private _handleAddParticlesToNode(node: TreeNodeInfo): void { @@ -594,24 +558,24 @@ export class FXEditorGraph extends Component (this.props.editor.preview = r!)} filePath={this.props.filePath} - onSceneReady={(scene) => { + onSceneReady={() => { // Update graph when scene is ready if (this.props.editor.graph) { this.props.editor.graph.forceUpdate(); @@ -177,9 +177,9 @@ export class FXEditorLayout extends Component { - this.setState({ resources }); - }} + // onResourcesLoaded={(resources) => { + // this.setState({ resources }); + // }} /> ), resources: ( diff --git a/editor/src/editor/windows/fx-editor/loader.ts b/editor/src/editor/windows/fx-editor/loader.ts deleted file mode 100644 index 17a6209b6..000000000 --- a/editor/src/editor/windows/fx-editor/loader.ts +++ /dev/null @@ -1,543 +0,0 @@ -import { Vector3, Matrix, Quaternion, Color4 } from "babylonjs"; -import { readJSON } from "fs-extra"; -import { IFXParticleData, IFXGroupData } from "./properties/types"; - -interface IThreeJSObject { - type: string; - name?: string; - uuid?: string; - matrix?: number[]; - visible?: boolean; - children?: IThreeJSObject[]; - ps?: IQuarksParticleSystem; -} - -interface IQuarksParticleSystem { - version?: string; - autoDestroy?: boolean; - looping?: boolean; - prewarm?: boolean; - duration?: number; - shape?: IQuarksShape; - startLife?: IQuarksValue; - startSpeed?: IQuarksValue; - startRotation?: IQuarksValue; - startSize?: IQuarksValue; - startColor?: IQuarksColor; - emissionOverTime?: IQuarksValue; - emissionOverDistance?: IQuarksValue; - emissionBursts?: IQuarksBurst[]; - onlyUsedByOther?: boolean; - instancingGeometry?: string; - renderOrder?: number; - renderMode?: number; - rendererEmitterSettings?: any; - material?: string; - layers?: number; - startTileIndex?: IQuarksValue; - uTileCount?: number; - vTileCount?: number; - blendTiles?: boolean; - softParticles?: boolean; - softFarFade?: number; - softNearFade?: number; - behaviors?: IQuarksBehavior[]; - worldSpace?: boolean; -} - -interface IQuarksShape { - type: string; - radius?: number; - arc?: number; - thickness?: number; - angle?: number; - mode?: number; - spread?: number; - speed?: IQuarksValue; - width?: number; - height?: number; - depth?: number; - boxWidth?: number; - boxHeight?: number; - boxDepth?: number; -} - -interface IQuarksValue { - type: string; - value?: number; - a?: number; - b?: number; - functions?: Array<{ - function: { - p0: number; - p1: number; - p2: number; - p3: number; - }; - start: number; - }>; -} - -interface IQuarksColor { - type: string; - color?: { - r: number; - g: number; - b: number; - a?: number; - } | { - type: string; - subType?: string; - keys?: Array<{ - value: { - r: number; - g: number; - b: number; - } | number; - pos: number; - }>; - }; - color1?: { - r: number; - g: number; - b: number; - a?: number; - }; - color2?: { - r: number; - g: number; - b: number; - a?: number; - }; - alpha?: { - type: string; - subType?: string; - keys?: Array<{ - value: number; - pos: number; - }>; - }; - keys?: Array<{ - value: { - r: number; - g: number; - b: number; - } | number; - pos: number; - }>; -} - -interface IQuarksBurst { - time?: number; - count?: IQuarksValue; - cycle?: number; - interval?: number; - probability?: number; -} - -interface IQuarksBehavior { - type: string; - [key: string]: any; -} - -interface IThreeJSJSON { - metadata?: { - version?: number; - type?: string; - generator?: string; - }; - object?: IThreeJSObject; - materials?: any[]; - textures?: any[]; - images?: any[]; - geometries?: any[]; -} - -export interface IConvertedNode { - id: string; - name: string; - type: "particle" | "group" | "texture" | "geometry"; - parentId?: string; - particleData?: IFXParticleData; - groupData?: IFXGroupData; - resourceData?: { - uuid: string; - path?: string; - type: "texture" | "geometry"; - }; - children?: IConvertedNode[]; -} - -export interface IConvertedData { - nodes: IConvertedNode[]; - resources: IConvertedNode[]; - materials: Array<{ - uuid: string; - type: string; - color?: number; - map?: string; - blending?: number; - side?: number; - transparent?: boolean; - depthWrite?: boolean; - opacity?: number; - }>; - textures: Array<{ - uuid: string; - name?: string; - image?: string; - imageUrl?: string; - }>; - geometries: Array<{ - uuid: string; - type: string; - name?: string; - data?: any; - [name: string]: any; - }>; - images: Array<{ - uuid: string; - url?: string; - name?: string; - data?: string; - format?: string; - }>; -} - -/** - * Converts a Three.js JSON file to FX editor node structure for UI tree view - * Note: Actual particle systems are created by ThreeJSParticleLoader - */ -export async function convertThreeJSJSONToFXEditor(filePath: string): Promise { - const json: IThreeJSJSON = await readJSON(filePath); - - if (!json.object) { - return { nodes: [], resources: [], materials: [], textures: [], geometries: [], images: [] }; - } - - const convertedNodes: IConvertedNode[] = []; - const usedResources = new Set(); // Track used texture and geometry UUIDs - - // Simple conversion - just create node structure for tree view - _convertObjectToNodes(json.object, null, convertedNodes, json, usedResources); - - // Add resource nodes for textures and geometries - const resourceNodes: IConvertedNode[] = []; - - // Extract materials data - const materialsData: IConvertedData["materials"] = []; - if (json.materials) { - json.materials.forEach((material) => { - if (usedResources.has(material.uuid)) { - materialsData.push({ - uuid: material.uuid, - type: material.type || "MeshStandardMaterial", - color: material.color, - map: material.map, - blending: material.blending, - side: material.side, - transparent: material.transparent, - depthWrite: material.depthWrite, - opacity: material.opacity, - }); - } - }); - } - - // Extract textures and images data - const texturesData: IConvertedData["textures"] = []; - const imagesData: IConvertedData["images"] = []; - - // Store all images first - if (json.images) { - json.images.forEach((image) => { - imagesData.push({ - uuid: image.uuid, - url: image.url, - name: image.name, - data: image.data, - format: image.format, - }); - }); - } - - if (json.textures) { - json.textures.forEach((texture) => { - if (usedResources.has(texture.uuid)) { - const image = json.images?.find((img) => img.uuid === texture.image); - const imagePath = image?.url || image?.name || texture.name || texture.uuid; - - texturesData.push({ - uuid: texture.uuid, - name: texture.name, - image: texture.image, - imageUrl: imagePath, - }); - - resourceNodes.push({ - id: `texture-${texture.uuid}`, - name: texture.name || imagePath || `Texture ${texture.uuid.substring(0, 8)}`, - type: "texture", - resourceData: { - uuid: texture.uuid, - path: imagePath, - type: "texture", - }, - }); - } - }); - } - - // Extract geometries data - const geometriesData: IConvertedData["geometries"] = []; - if (json.geometries) { - json.geometries.forEach((geometry) => { - if (usedResources.has(geometry.uuid)) { - geometriesData.push({ - uuid: geometry.uuid, - type: geometry.type || "BufferGeometry", - name: geometry.name, - data: geometry.data || geometry, - ...geometry, - }); - - resourceNodes.push({ - id: `geometry-${geometry.uuid}`, - name: geometry.name || `Geometry ${geometry.uuid.substring(0, 8)}`, - type: "geometry", - resourceData: { - uuid: geometry.uuid, - type: "geometry", - }, - }); - } - }); - } - - return { - nodes: convertedNodes, - resources: resourceNodes, - materials: materialsData, - textures: texturesData, - geometries: geometriesData, - images: imagesData, - }; -} - -/** - * Decomposes a 4x4 transformation matrix into position, rotation (Euler angles), and scale - * Three.js uses column-major matrices and XYZ order for Euler angles - */ -function _decomposeMatrix(matrixArray: number[]): { position: Vector3; rotation: Vector3; scale: Vector3 } { - const position = Vector3.Zero(); - const rotationQuat = Quaternion.Identity(); - const scaling = Vector3.Zero(); - - console.log("[_decomposeMatrix] Input matrix (column-major):", matrixArray); - - // Three.js matrices are stored in column-major order - // Try without transposing first - Matrix.FromArray might handle it correctly - // If this doesn't work, we'll transpose - const matrix = Matrix.FromArray(matrixArray); - console.log("[_decomposeMatrix] Matrix after FromArray:", matrix.m); - - matrix.decompose(scaling, rotationQuat, position); - - console.log("[_decomposeMatrix] Decomposed values:"); - console.log(" Position:", position.x, position.y, position.z); - console.log(" Scale:", scaling.x, scaling.y, scaling.z); - console.log(" Quaternion:", rotationQuat.x, rotationQuat.y, rotationQuat.z, rotationQuat.w); - - // Babylon.js toEulerAngles() returns angles in YXZ order (yaw-pitch-roll) - // Three.js uses XYZ order (roll-pitch-yaw) - // We'll use Babylon's method and then convert the order - const eulerYXZ = rotationQuat.toEulerAngles(); - console.log("[_decomposeMatrix] Babylon YXZ Euler (rad):", eulerYXZ.x, eulerYXZ.y, eulerYXZ.z); - console.log("[_decomposeMatrix] Babylon YXZ Euler (deg):", eulerYXZ.x * 180 / Math.PI, eulerYXZ.y * 180 / Math.PI, eulerYXZ.z * 180 / Math.PI); - - // Convert from YXZ to XYZ order - // YXZ: first Y (yaw), then X (pitch), then Z (roll) - // XYZ: first X (roll), then Y (pitch), then Z (yaw) - // We need to recompute from quaternion using XYZ order - const qx = rotationQuat.x; - const qy = rotationQuat.y; - const qz = rotationQuat.z; - const qw = rotationQuat.w; - - // XYZ order Euler angles from quaternion - // Roll (X) - const sinr_cosp = 2 * (qw * qx + qy * qz); - const cosr_cosp = 1 - 2 * (qx * qx + qy * qy); - const roll = Math.atan2(sinr_cosp, cosr_cosp); - - // Pitch (Y) - const sinp = 2 * (qw * qy - qz * qx); - let pitch: number; - if (Math.abs(sinp) >= 1) { - pitch = Math.sign(sinp) * Math.PI / 2; - } else { - pitch = Math.asin(sinp); - } - - // Yaw (Z) - const siny_cosp = 2 * (qw * qz + qx * qy); - const cosy_cosp = 1 - 2 * (qy * qy + qz * qz); - const yaw = Math.atan2(siny_cosp, cosy_cosp); - - console.log("[_decomposeMatrix] XYZ Euler (rad):", roll, pitch, yaw); - - // Store rotation in RADIANS (not degrees) because EditorInspectorNumberField with asDegrees expects radians - // The component will automatically convert radians to degrees for display - const rotation = new Vector3(roll, pitch, yaw); - - console.log("[_decomposeMatrix] Final rotation (rad):", rotation.x, rotation.y, rotation.z); - console.log("[_decomposeMatrix] Final rotation (deg for reference):", rotation.x * 180 / Math.PI, rotation.y * 180 / Math.PI, rotation.z * 180 / Math.PI); - - return { - position, - rotation, - scale: scaling, - }; -} - -/** - * Recursively converts Three.js objects to FX editor nodes (simplified - only for UI tree) - */ -function _convertObjectToNodes( - obj: IThreeJSObject, - parentId: string | null, - convertedNodes: IConvertedNode[], - json: IThreeJSJSON, - usedResources: Set -): void { - if (obj.type === "ParticleEmitter" && obj.ps) { - // Create simple particle node for tree view - const nodeId = `particle-${obj.uuid || Date.now()}-${Math.random()}`; - - const node: IConvertedNode = { - id: nodeId, - name: obj.name || "Particle", - type: "particle", - parentId: parentId || undefined, - // Store minimal particle data - actual creation is done by ThreeJSParticleLoader - particleData: { - type: "particle", - id: nodeId, - name: obj.name || "Particle", - visibility: obj.visible !== false, - position: new Vector3(0, 0, 0), - rotation: new Vector3(0, 0, 0), - scale: new Vector3(1, 1, 1), - emitterShape: { shape: "Box" }, - particleRenderer: { - renderMode: "Billboard", - worldSpace: false, - material: null, - materialType: "MeshStandardMaterial", - transparent: true, - opacity: 1.0, - side: "Double", - blending: "Add", - color: new Color4(1, 1, 1, 1), - renderOrder: 0, - uvTile: { column: 1, row: 1, startTileIndex: 0, blendTiles: false }, - texture: null, - meshPath: null, - softParticles: false, - }, - emission: { looping: true, duration: 5, prewarm: false, onlyUsedByOtherSystem: false, emitOverTime: 10, emitOverDistance: 0 }, - bursts: [], - particleInitialization: { - startLife: { functionType: "IntervalValue", data: { min: 1, max: 2 } }, - startSize: { functionType: "IntervalValue", data: { min: 0.1, max: 0.2 } }, - startSpeed: { functionType: "IntervalValue", data: { min: 1, max: 2 } }, - startColor: { colorFunctionType: "ConstantColor", data: { color: { r: 1, g: 1, b: 1, a: 1 } } }, - startRotation: { functionType: "IntervalValue", data: { min: 0, max: 360 } }, - }, - behaviors: [], - }, - }; - - // Extract position, rotation, scale from matrix if available - if (obj.matrix && obj.matrix.length >= 16) { - const { position, rotation, scale } = _decomposeMatrix(obj.matrix); - if (node.particleData) { - node.particleData.position = position; - node.particleData.rotation = rotation; - node.particleData.scale = scale; - } - } - - // Track used resources - if (obj.ps) { - const ps = obj.ps; - if (ps.material && json.materials) { - usedResources.add(ps.material); - const material = json.materials.find((m) => m.uuid === ps.material); - if (material?.map && json.textures) { - usedResources.add(material.map); - } - } - if (ps.instancingGeometry) { - usedResources.add(ps.instancingGeometry); - } - } - - // Process children - if (obj.children) { - node.children = []; - obj.children.forEach((child) => { - _convertObjectToNodes(child, nodeId, node.children!, json, usedResources); - }); - } - - convertedNodes.push(node); - } else if (obj.type === "Group") { - // Convert group - const nodeId = `group-${obj.uuid || Date.now()}-${Math.random()}`; - - const groupData: IFXGroupData = { - type: "group", - id: nodeId, - name: obj.name || "Group", - visibility: obj.visible !== false, - position: new Vector3(0, 0, 0), - rotation: new Vector3(0, 0, 0), - scale: new Vector3(1, 1, 1), - }; - - // Extract position, rotation, scale from matrix if available - if (obj.matrix && obj.matrix.length >= 16) { - const { position, rotation, scale } = _decomposeMatrix(obj.matrix); - groupData.position = position; - groupData.rotation = rotation; - groupData.scale = scale; - } - - const node: IConvertedNode = { - id: nodeId, - name: obj.name || "Group", - type: "group", - parentId: parentId || undefined, - groupData, - children: [], - }; - - // Process children - if (obj.children) { - obj.children.forEach((child) => { - _convertObjectToNodes(child, nodeId, node.children!, json, usedResources); - }); - } - - convertedNodes.push(node); - } else if (obj.children) { - // Process children of other object types - obj.children.forEach((child) => { - _convertObjectToNodes(child, parentId, convertedNodes, json, usedResources); - }); - } -} - - - diff --git a/editor/src/editor/windows/fx-editor/properties.tsx b/editor/src/editor/windows/fx-editor/properties.tsx index 254344d40..e10b034fe 100644 --- a/editor/src/editor/windows/fx-editor/properties.tsx +++ b/editor/src/editor/windows/fx-editor/properties.tsx @@ -1,5 +1,4 @@ import { Component, ReactNode } from "react"; -import { Scene } from "babylonjs"; import { EditorInspectorSectionField } from "../../layout/inspector/fields/section"; diff --git a/editor/src/editor/windows/fx-editor/properties/particle-renderer.tsx b/editor/src/editor/windows/fx-editor/properties/particle-renderer.tsx index fe759fe04..c0a0d2b79 100644 --- a/editor/src/editor/windows/fx-editor/properties/particle-renderer.tsx +++ b/editor/src/editor/windows/fx-editor/properties/particle-renderer.tsx @@ -1,5 +1,4 @@ import { Component, ReactNode, DragEvent } from "react"; -import { Scene } from "babylonjs"; import { extname } from "path/posix"; import { EditorInspectorSectionField } from "../../../layout/inspector/fields/section"; diff --git a/editor/src/editor/windows/fx-editor/resources.tsx b/editor/src/editor/windows/fx-editor/resources.tsx index 68bd51559..8d3644d68 100644 --- a/editor/src/editor/windows/fx-editor/resources.tsx +++ b/editor/src/editor/windows/fx-editor/resources.tsx @@ -1,6 +1,5 @@ import { Component, ReactNode } from "react"; import { Tree, TreeNodeInfo } from "@blueprintjs/core"; -import { IConvertedNode } from "./loader"; import { IoImageOutline, IoCubeOutline } from "react-icons/io5"; @@ -12,7 +11,7 @@ import { } from "../../../ui/shadcn/ui/context-menu"; export interface IFXEditorResourcesProps { - resources: IConvertedNode[]; + resources: any[]; } export interface IFXEditorResourcesState { @@ -36,7 +35,7 @@ export class FXEditorResources extends Component { const icon = resource.type === "texture" ? ( From 1c534525381309b56569ca082fabf37552efaac6 Mon Sep 17 00:00:00 2001 From: Mikalai Lazitski Date: Fri, 12 Dec 2025 11:04:13 +0300 Subject: [PATCH 11/62] refactor: clean up imports and formatting in FX Editor components for improved readability and consistency --- .../editor/windows/fx-editor/VFX/VFXEffect.ts | 149 +- .../fx-editor/VFX/behaviors/colorBySpeed.ts | 95 +- .../fx-editor/VFX/behaviors/colorOverLife.ts | 113 +- .../fx-editor/VFX/behaviors/forceOverLife.ts | 39 +- .../fx-editor/VFX/behaviors/frameOverLife.ts | 51 +- .../windows/fx-editor/VFX/behaviors/index.ts | 3 +- .../VFX/behaviors/limitSpeedOverLife.ts | 49 +- .../fx-editor/VFX/behaviors/orbitOverLife.ts | 145 +- .../VFX/behaviors/rotationBySpeed.ts | 93 +- .../VFX/behaviors/rotationOverLife.ts | 35 +- .../fx-editor/VFX/behaviors/sizeBySpeed.ts | 55 +- .../fx-editor/VFX/behaviors/sizeOverLife.ts | 81 +- .../fx-editor/VFX/behaviors/speedOverLife.ts | 142 +- .../windows/fx-editor/VFX/behaviors/utils.ts | 253 ++- .../factories/VFXBehaviorFunctionFactory.ts | 353 ++-- .../VFX/factories/VFXEmitterFactory.ts | 1142 +++++------ .../VFX/factories/VFXGeometryFactory.ts | 268 +-- .../VFX/factories/VFXMaterialFactory.ts | 694 +++---- .../fx-editor/VFX/loggers/VFXLogger.ts | 52 +- .../fx-editor/VFX/parsers/VFXDataConverter.ts | 1096 +++++------ .../fx-editor/VFX/parsers/VFXParser.ts | 204 +- .../fx-editor/VFX/parsers/VFXValueParser.ts | 348 ++-- .../VFX/processors/VFXHierarchyProcessor.ts | 554 +++--- .../VFX/systems/VFXParticleSystem.ts | 16 +- .../VFX/systems/VFXSolidParticleSystem.ts | 1032 +++++----- .../VFX/treejs3dobject.particle.json | 1736 ++++++++--------- .../VFX/types/VFXBehaviorFunction.ts | 13 +- .../windows/fx-editor/VFX/types/behaviors.ts | 189 +- .../windows/fx-editor/VFX/types/colors.ts | 5 +- .../windows/fx-editor/VFX/types/context.ts | 12 +- .../windows/fx-editor/VFX/types/emitter.ts | 17 +- .../fx-editor/VFX/types/emitterConfig.ts | 65 +- .../windows/fx-editor/VFX/types/factories.ts | 19 +- .../windows/fx-editor/VFX/types/gradients.ts | 7 +- .../windows/fx-editor/VFX/types/hierarchy.ts | 33 +- .../windows/fx-editor/VFX/types/index.ts | 26 +- .../windows/fx-editor/VFX/types/loader.ts | 17 +- .../fx-editor/VFX/types/quarksTypes.ts | 478 ++--- .../windows/fx-editor/VFX/types/rotations.ts | 9 +- .../windows/fx-editor/VFX/types/shapes.ts | 21 +- .../windows/fx-editor/VFX/types/values.ts | 30 +- editor/src/editor/windows/fx-editor/graph.tsx | 22 +- editor/src/editor/windows/fx-editor/index.tsx | 2 +- .../src/editor/windows/fx-editor/layout.tsx | 8 +- .../src/editor/windows/fx-editor/preview.tsx | 5 +- .../editor/windows/fx-editor/properties.tsx | 10 +- .../fx-editor/properties/behaviors.tsx | 1 - .../properties/behaviors/bezier-editor.tsx | 4 +- .../windows/fx-editor/properties/object.tsx | 7 +- .../properties/particle-initialization.tsx | 34 +- .../properties/particle-renderer.tsx | 10 +- .../editor/windows/fx-editor/resources.tsx | 14 +- 52 files changed, 4906 insertions(+), 4950 deletions(-) diff --git a/editor/src/editor/windows/fx-editor/VFX/VFXEffect.ts b/editor/src/editor/windows/fx-editor/VFX/VFXEffect.ts index 14cbc8215..56b5ba74c 100644 --- a/editor/src/editor/windows/fx-editor/VFX/VFXEffect.ts +++ b/editor/src/editor/windows/fx-editor/VFX/VFXEffect.ts @@ -7,90 +7,89 @@ import { VFXParser } from "./parsers/VFXParser"; import type { VFXParticleSystem } from "./systems/VFXParticleSystem"; import type { VFXSolidParticleSystem } from "./systems/VFXSolidParticleSystem"; - /** * VFX Effect containing multiple particle systems * Main entry point for loading and creating VFX from Three.js particle JSON files */ export class VFXEffect implements IDisposable { - public readonly systems: (VFXParticleSystem | VFXSolidParticleSystem)[] = []; + public readonly systems: (VFXParticleSystem | VFXSolidParticleSystem)[] = []; - /** - * Load a Three.js particle JSON file and create particle systems - * @param url URL to the JSON file - * @param scene The Babylon.js scene - * @param rootUrl Root URL for loading textures - * @param options Optional parsing options - * @returns Promise that resolves to a VFXEffect - */ - public static async LoadAsync(url: string, scene: Scene, rootUrl: string = "", options?: VFXLoaderOptions): Promise { - return new Promise((resolve, reject) => { - Tools.LoadFile( - url, - (data) => { - try { - const jsonData = JSON.parse(data.toString()); - const effect = VFXEffect.Parse(jsonData, scene, rootUrl, options); - resolve(effect); - } catch (error) { - reject(error); - } - }, - undefined, - undefined, - undefined, - (error) => { - reject(error); - } - ); - }); - } + /** + * Load a Three.js particle JSON file and create particle systems + * @param url URL to the JSON file + * @param scene The Babylon.js scene + * @param rootUrl Root URL for loading textures + * @param options Optional parsing options + * @returns Promise that resolves to a VFXEffect + */ + public static async LoadAsync(url: string, scene: Scene, rootUrl: string = "", options?: VFXLoaderOptions): Promise { + return new Promise((resolve, reject) => { + Tools.LoadFile( + url, + (data) => { + try { + const jsonData = JSON.parse(data.toString()); + const effect = VFXEffect.Parse(jsonData, scene, rootUrl, options); + resolve(effect); + } catch (error) { + reject(error); + } + }, + undefined, + undefined, + undefined, + (error) => { + reject(error); + } + ); + }); + } - /** - * Parse a Three.js particle JSON file and create Babylon.js particle systems - * @param jsonData The Three.js JSON data - * @param scene The Babylon.js scene - * @param rootUrl Root URL for loading textures - * @param options Optional parsing options - * @returns A VFXEffect containing all particle systems - */ - public static Parse(jsonData: QuarksVFXJSON, scene: Scene, rootUrl: string = "", options?: VFXLoaderOptions): VFXEffect { - const particleSystems = new VFXParser(scene, rootUrl, jsonData, options).parse(); - const effect = new VFXEffect(); - effect.systems.push(...particleSystems); - return effect; - } + /** + * Parse a Three.js particle JSON file and create Babylon.js particle systems + * @param jsonData The Three.js JSON data + * @param scene The Babylon.js scene + * @param rootUrl Root URL for loading textures + * @param options Optional parsing options + * @returns A VFXEffect containing all particle systems + */ + public static Parse(jsonData: QuarksVFXJSON, scene: Scene, rootUrl: string = "", options?: VFXLoaderOptions): VFXEffect { + const particleSystems = new VFXParser(scene, rootUrl, jsonData, options).parse(); + const effect = new VFXEffect(); + effect.systems.push(...particleSystems); + return effect; + } - /** - * Create a VFXEffect directly from JSON data - * @param jsonData The Three.js JSON data - * @param scene The Babylon.js scene - * @param rootUrl Root URL for loading textures - * @param options Optional parsing options - */ - constructor(jsonData?: QuarksVFXJSON, scene?: Scene, rootUrl: string = "", options?: VFXLoaderOptions) { - if (jsonData && scene) { - const effect = VFXEffect.Parse(jsonData, scene, rootUrl, options); - this.systems.push(...effect.systems); - } - } + /** + * Create a VFXEffect directly from JSON data + * @param jsonData The Three.js JSON data + * @param scene The Babylon.js scene + * @param rootUrl Root URL for loading textures + * @param options Optional parsing options + */ + constructor(jsonData?: QuarksVFXJSON, scene?: Scene, rootUrl: string = "", options?: VFXLoaderOptions) { + if (jsonData && scene) { + const effect = VFXEffect.Parse(jsonData, scene, rootUrl, options); + this.systems.push(...effect.systems); + } + } - public start(): void { - for (const system of this.systems) { - system.start(); - } - } + public start(): void { + for (const system of this.systems) { + system.start(); + } + } - public stop(): void { - for (const system of this.systems) { - system.stop(); - } - } + public stop(): void { + for (const system of this.systems) { + system.stop(); + } + } - public dispose(): void { - for (const system of this.systems) { - system.dispose(); - } - this.systems.length = 0; - } + public dispose(): void { + for (const system of this.systems) { + system.dispose(); + } + this.systems.length = 0; + } } diff --git a/editor/src/editor/windows/fx-editor/VFX/behaviors/colorBySpeed.ts b/editor/src/editor/windows/fx-editor/VFX/behaviors/colorBySpeed.ts index dd4bf65b3..e090041d3 100644 --- a/editor/src/editor/windows/fx-editor/VFX/behaviors/colorBySpeed.ts +++ b/editor/src/editor/windows/fx-editor/VFX/behaviors/colorBySpeed.ts @@ -4,63 +4,60 @@ import type { VFXColorBySpeedBehavior } from "../types/behaviors"; import { interpolateColorKeys } from "./utils"; import { VFXValueParser } from "../parsers/VFXValueParser"; - - /** * Apply ColorBySpeed behavior to Particle */ export function applyColorBySpeedPS(particle: Particle, behavior: VFXColorBySpeedBehavior, currentSpeed: number, valueParser: VFXValueParser): void { - if (!behavior.color || !behavior.color.keys || !particle.color) { - return; - } - - const colorKeys = behavior.color.keys; - const minSpeed = behavior.minSpeed !== undefined ? valueParser.parseConstantValue(behavior.minSpeed) : 0; - const maxSpeed = behavior.maxSpeed !== undefined ? valueParser.parseConstantValue(behavior.maxSpeed) : 1; - const speedRatio = Math.max(0, Math.min(1, (currentSpeed - minSpeed) / (maxSpeed - minSpeed || 1))); - - const interpolatedColor = interpolateColorKeys(colorKeys, speedRatio); - const startColor = particle.initialColor; - - if (startColor) { - // Multiply with startColor (matching three.quarks behavior) - particle.color.r = interpolatedColor.r * startColor.r; - particle.color.g = interpolatedColor.g * startColor.g; - particle.color.b = interpolatedColor.b * startColor.b; - particle.color.a = startColor.a; // Keep original alpha - } else { - particle.color.r = interpolatedColor.r; - particle.color.g = interpolatedColor.g; - particle.color.b = interpolatedColor.b; - } + if (!behavior.color || !behavior.color.keys || !particle.color) { + return; + } + + const colorKeys = behavior.color.keys; + const minSpeed = behavior.minSpeed !== undefined ? valueParser.parseConstantValue(behavior.minSpeed) : 0; + const maxSpeed = behavior.maxSpeed !== undefined ? valueParser.parseConstantValue(behavior.maxSpeed) : 1; + const speedRatio = Math.max(0, Math.min(1, (currentSpeed - minSpeed) / (maxSpeed - minSpeed || 1))); + + const interpolatedColor = interpolateColorKeys(colorKeys, speedRatio); + const startColor = particle.initialColor; + + if (startColor) { + // Multiply with startColor (matching three.quarks behavior) + particle.color.r = interpolatedColor.r * startColor.r; + particle.color.g = interpolatedColor.g * startColor.g; + particle.color.b = interpolatedColor.b * startColor.b; + particle.color.a = startColor.a; // Keep original alpha + } else { + particle.color.r = interpolatedColor.r; + particle.color.g = interpolatedColor.g; + particle.color.b = interpolatedColor.b; + } } /** * Apply ColorBySpeed behavior to SolidParticle */ export function applyColorBySpeedSPS(particle: SolidParticle, behavior: VFXColorBySpeedBehavior, currentSpeed: number, valueParser: VFXValueParser): void { - if (!behavior.color || !behavior.color.keys || !particle.color) { - return; - } - - const colorKeys = behavior.color.keys; - const minSpeed = behavior.minSpeed !== undefined ? valueParser.parseConstantValue(behavior.minSpeed) : 0; - const maxSpeed = behavior.maxSpeed !== undefined ? valueParser.parseConstantValue(behavior.maxSpeed) : 1; - const speedRatio = Math.max(0, Math.min(1, (currentSpeed - minSpeed) / (maxSpeed - minSpeed || 1))); - - const interpolatedColor = interpolateColorKeys(colorKeys, speedRatio); - const startColor = particle.props?.startColor; - - if (startColor) { - // Multiply with startColor (matching three.quarks behavior) - particle.color.r = interpolatedColor.r * startColor.r; - particle.color.g = interpolatedColor.g * startColor.g; - particle.color.b = interpolatedColor.b * startColor.b; - particle.color.a = startColor.a; // Keep original alpha - } else { - particle.color.r = interpolatedColor.r; - particle.color.g = interpolatedColor.g; - particle.color.b = interpolatedColor.b; - } + if (!behavior.color || !behavior.color.keys || !particle.color) { + return; + } + + const colorKeys = behavior.color.keys; + const minSpeed = behavior.minSpeed !== undefined ? valueParser.parseConstantValue(behavior.minSpeed) : 0; + const maxSpeed = behavior.maxSpeed !== undefined ? valueParser.parseConstantValue(behavior.maxSpeed) : 1; + const speedRatio = Math.max(0, Math.min(1, (currentSpeed - minSpeed) / (maxSpeed - minSpeed || 1))); + + const interpolatedColor = interpolateColorKeys(colorKeys, speedRatio); + const startColor = particle.props?.startColor; + + if (startColor) { + // Multiply with startColor (matching three.quarks behavior) + particle.color.r = interpolatedColor.r * startColor.r; + particle.color.g = interpolatedColor.g * startColor.g; + particle.color.b = interpolatedColor.b * startColor.b; + particle.color.a = startColor.a; // Keep original alpha + } else { + particle.color.r = interpolatedColor.r; + particle.color.g = interpolatedColor.g; + particle.color.b = interpolatedColor.b; + } } - diff --git a/editor/src/editor/windows/fx-editor/VFX/behaviors/colorOverLife.ts b/editor/src/editor/windows/fx-editor/VFX/behaviors/colorOverLife.ts index 7f41df43f..bbeef65b6 100644 --- a/editor/src/editor/windows/fx-editor/VFX/behaviors/colorOverLife.ts +++ b/editor/src/editor/windows/fx-editor/VFX/behaviors/colorOverLife.ts @@ -8,71 +8,70 @@ import { ParticleSystem } from "@babylonjs/core/Particles/particleSystem"; * Apply ColorOverLife behavior to ParticleSystem */ export function applyColorOverLifePS(particleSystem: ParticleSystem, behavior: VFXColorOverLifeBehavior): void { - if (behavior.color && behavior.color.color && behavior.color.color.keys) { - const colorKeys = behavior.color.color.keys; - for (const key of colorKeys) { - if (key.value !== undefined && key.pos !== undefined) { - const color = extractColorFromValue(key.value); - const alpha = extractAlphaFromValue(key.value); - particleSystem.addColorGradient(key.pos, new Color4(color.r, color.g, color.b, alpha)); - } - } - } + if (behavior.color && behavior.color.color && behavior.color.color.keys) { + const colorKeys = behavior.color.color.keys; + for (const key of colorKeys) { + if (key.value !== undefined && key.pos !== undefined) { + const color = extractColorFromValue(key.value); + const alpha = extractAlphaFromValue(key.value); + particleSystem.addColorGradient(key.pos, new Color4(color.r, color.g, color.b, alpha)); + } + } + } - if (behavior.color && behavior.color.alpha && behavior.color.alpha.keys) { - const alphaKeys = behavior.color.alpha.keys; - for (const key of alphaKeys) { - if (key.value !== undefined && key.pos !== undefined) { - const alpha = extractAlphaFromValue(key.value); - const existingGradients = particleSystem.getColorGradients(); - const existingGradient = existingGradients?.find((g) => key.pos !== undefined && Math.abs(g.gradient - key.pos) < 0.001); - if (existingGradient) { - existingGradient.color1.a = alpha; - if (existingGradient.color2) { - existingGradient.color2.a = alpha; - } - } else { - particleSystem.addColorGradient(key.pos ?? 0, new Color4(1, 1, 1, alpha)); - } - } - } - } + if (behavior.color && behavior.color.alpha && behavior.color.alpha.keys) { + const alphaKeys = behavior.color.alpha.keys; + for (const key of alphaKeys) { + if (key.value !== undefined && key.pos !== undefined) { + const alpha = extractAlphaFromValue(key.value); + const existingGradients = particleSystem.getColorGradients(); + const existingGradient = existingGradients?.find((g) => key.pos !== undefined && Math.abs(g.gradient - key.pos) < 0.001); + if (existingGradient) { + existingGradient.color1.a = alpha; + if (existingGradient.color2) { + existingGradient.color2.a = alpha; + } + } else { + particleSystem.addColorGradient(key.pos ?? 0, new Color4(1, 1, 1, alpha)); + } + } + } + } } /** * Apply ColorOverLife behavior to SolidParticle */ export function applyColorOverLifeSPS(particle: SolidParticle, behavior: VFXColorOverLifeBehavior, lifeRatio: number): void { - if (!behavior.color || !particle.color) { - return; - } + if (!behavior.color || !particle.color) { + return; + } - const colorKeys = behavior.color.color?.keys ?? behavior.color.keys; - if (!colorKeys || !Array.isArray(colorKeys)) { - return; - } + const colorKeys = behavior.color.color?.keys ?? behavior.color.keys; + if (!colorKeys || !Array.isArray(colorKeys)) { + return; + } - const interpolatedColor = interpolateColorKeys(colorKeys, lifeRatio); - const startColor = particle.props?.startColor; - - if (startColor) { - // Multiply with startColor (matching three.quarks behavior) - particle.color.r = interpolatedColor.r * startColor.r; - particle.color.g = interpolatedColor.g * startColor.g; - particle.color.b = interpolatedColor.b * startColor.b; - } else { - particle.color.r = interpolatedColor.r; - particle.color.g = interpolatedColor.g; - particle.color.b = interpolatedColor.b; - } + const interpolatedColor = interpolateColorKeys(colorKeys, lifeRatio); + const startColor = particle.props?.startColor; - // Apply alpha if specified - if (behavior.color.alpha?.keys) { - const alphaKeys = behavior.color.alpha.keys; - const alpha = interpolateGradientKeys(alphaKeys, lifeRatio, extractAlphaFromValue); - particle.color.a = alpha; - } else { - particle.color.a = interpolatedColor.a; - } -} + if (startColor) { + // Multiply with startColor (matching three.quarks behavior) + particle.color.r = interpolatedColor.r * startColor.r; + particle.color.g = interpolatedColor.g * startColor.g; + particle.color.b = interpolatedColor.b * startColor.b; + } else { + particle.color.r = interpolatedColor.r; + particle.color.g = interpolatedColor.g; + particle.color.b = interpolatedColor.b; + } + // Apply alpha if specified + if (behavior.color.alpha?.keys) { + const alphaKeys = behavior.color.alpha.keys; + const alpha = interpolateGradientKeys(alphaKeys, lifeRatio, extractAlphaFromValue); + particle.color.a = alpha; + } else { + particle.color.a = interpolatedColor.a; + } +} diff --git a/editor/src/editor/windows/fx-editor/VFX/behaviors/forceOverLife.ts b/editor/src/editor/windows/fx-editor/VFX/behaviors/forceOverLife.ts index 261278402..ca623b9fe 100644 --- a/editor/src/editor/windows/fx-editor/VFX/behaviors/forceOverLife.ts +++ b/editor/src/editor/windows/fx-editor/VFX/behaviors/forceOverLife.ts @@ -7,30 +7,29 @@ import { VFXValueParser } from "../parsers/VFXValueParser"; * Apply ForceOverLife behavior to ParticleSystem */ export function applyForceOverLifePS(particleSystem: ParticleSystem, behavior: VFXForceOverLifeBehavior, valueParser: VFXValueParser): void { - if (behavior.force) { - const forceX = behavior.force.x !== undefined ? valueParser.parseConstantValue(behavior.force.x) : 0; - const forceY = behavior.force.y !== undefined ? valueParser.parseConstantValue(behavior.force.y) : 0; - const forceZ = behavior.force.z !== undefined ? valueParser.parseConstantValue(behavior.force.z) : 0; - if (Math.abs(forceY) > 0.01 || Math.abs(forceX) > 0.01 || Math.abs(forceZ) > 0.01) { - particleSystem.gravity = new Vector3(forceX, forceY, forceZ); - } - } else if (behavior.x !== undefined || behavior.y !== undefined || behavior.z !== undefined) { - const forceX = behavior.x !== undefined ? valueParser.parseConstantValue(behavior.x) : 0; - const forceY = behavior.y !== undefined ? valueParser.parseConstantValue(behavior.y) : 0; - const forceZ = behavior.z !== undefined ? valueParser.parseConstantValue(behavior.z) : 0; - if (Math.abs(forceY) > 0.01 || Math.abs(forceX) > 0.01 || Math.abs(forceZ) > 0.01) { - particleSystem.gravity = new Vector3(forceX, forceY, forceZ); - } - } + if (behavior.force) { + const forceX = behavior.force.x !== undefined ? valueParser.parseConstantValue(behavior.force.x) : 0; + const forceY = behavior.force.y !== undefined ? valueParser.parseConstantValue(behavior.force.y) : 0; + const forceZ = behavior.force.z !== undefined ? valueParser.parseConstantValue(behavior.force.z) : 0; + if (Math.abs(forceY) > 0.01 || Math.abs(forceX) > 0.01 || Math.abs(forceZ) > 0.01) { + particleSystem.gravity = new Vector3(forceX, forceY, forceZ); + } + } else if (behavior.x !== undefined || behavior.y !== undefined || behavior.z !== undefined) { + const forceX = behavior.x !== undefined ? valueParser.parseConstantValue(behavior.x) : 0; + const forceY = behavior.y !== undefined ? valueParser.parseConstantValue(behavior.y) : 0; + const forceZ = behavior.z !== undefined ? valueParser.parseConstantValue(behavior.z) : 0; + if (Math.abs(forceY) > 0.01 || Math.abs(forceX) > 0.01 || Math.abs(forceZ) > 0.01) { + particleSystem.gravity = new Vector3(forceX, forceY, forceZ); + } + } } /** * Apply GravityForce behavior to ParticleSystem */ export function applyGravityForcePS(particleSystem: ParticleSystem, behavior: VFXGravityForceBehavior, valueParser: VFXValueParser): void { - if (behavior.gravity !== undefined) { - const gravity = valueParser.parseConstantValue(behavior.gravity); - particleSystem.gravity = new Vector3(0, -gravity, 0); - } + if (behavior.gravity !== undefined) { + const gravity = valueParser.parseConstantValue(behavior.gravity); + particleSystem.gravity = new Vector3(0, -gravity, 0); + } } - diff --git a/editor/src/editor/windows/fx-editor/VFX/behaviors/frameOverLife.ts b/editor/src/editor/windows/fx-editor/VFX/behaviors/frameOverLife.ts index 1c37c3af7..59669ccd4 100644 --- a/editor/src/editor/windows/fx-editor/VFX/behaviors/frameOverLife.ts +++ b/editor/src/editor/windows/fx-editor/VFX/behaviors/frameOverLife.ts @@ -6,31 +6,30 @@ import { VFXValueParser } from "../parsers/VFXValueParser"; * Apply FrameOverLife behavior to ParticleSystem */ export function applyFrameOverLifePS(particleSystem: ParticleSystem, behavior: VFXFrameOverLifeBehavior, valueParser: VFXValueParser): void { - if (!behavior.frame) { - return; - } + if (!behavior.frame) { + return; + } - particleSystem.isAnimationSheetEnabled = true; - if (typeof behavior.frame === "object" && behavior.frame !== null && "keys" in behavior.frame && behavior.frame.keys && Array.isArray(behavior.frame.keys)) { - const frames = behavior.frame.keys.map((k) => { - const val = k.value; - const pos = k.pos ?? k.time ?? 0; - if (typeof val === "number") { - return val; - } else if (Array.isArray(val)) { - return val[0] || 0; - } else { - return pos; - } - }); - if (frames.length > 0) { - particleSystem.startSpriteCellID = Math.floor(frames[0]); - particleSystem.endSpriteCellID = Math.floor(frames[frames.length - 1] || frames[0]); - } - } else if (typeof behavior.frame === "number" || (typeof behavior.frame === "object" && behavior.frame !== null && "type" in behavior.frame)) { - const frameValue = valueParser.parseConstantValue(behavior.frame); - particleSystem.startSpriteCellID = Math.floor(frameValue); - particleSystem.endSpriteCellID = Math.floor(frameValue); - } + particleSystem.isAnimationSheetEnabled = true; + if (typeof behavior.frame === "object" && behavior.frame !== null && "keys" in behavior.frame && behavior.frame.keys && Array.isArray(behavior.frame.keys)) { + const frames = behavior.frame.keys.map((k) => { + const val = k.value; + const pos = k.pos ?? k.time ?? 0; + if (typeof val === "number") { + return val; + } else if (Array.isArray(val)) { + return val[0] || 0; + } else { + return pos; + } + }); + if (frames.length > 0) { + particleSystem.startSpriteCellID = Math.floor(frames[0]); + particleSystem.endSpriteCellID = Math.floor(frames[frames.length - 1] || frames[0]); + } + } else if (typeof behavior.frame === "number" || (typeof behavior.frame === "object" && behavior.frame !== null && "type" in behavior.frame)) { + const frameValue = valueParser.parseConstantValue(behavior.frame); + particleSystem.startSpriteCellID = Math.floor(frameValue); + particleSystem.endSpriteCellID = Math.floor(frameValue); + } } - diff --git a/editor/src/editor/windows/fx-editor/VFX/behaviors/index.ts b/editor/src/editor/windows/fx-editor/VFX/behaviors/index.ts index 368005c38..849a07acf 100644 --- a/editor/src/editor/windows/fx-editor/VFX/behaviors/index.ts +++ b/editor/src/editor/windows/fx-editor/VFX/behaviors/index.ts @@ -1,6 +1,6 @@ /** * Behavior modules for VFX particle systems - * + * * Each behavior module exports functions for both ParticleSystem (PS) and SolidParticleSystem (SPS) */ @@ -16,4 +16,3 @@ export * from "./orbitOverLife"; export * from "./frameOverLife"; export * from "./limitSpeedOverLife"; export * from "./utils"; - diff --git a/editor/src/editor/windows/fx-editor/VFX/behaviors/limitSpeedOverLife.ts b/editor/src/editor/windows/fx-editor/VFX/behaviors/limitSpeedOverLife.ts index 57b164487..f159a9efa 100644 --- a/editor/src/editor/windows/fx-editor/VFX/behaviors/limitSpeedOverLife.ts +++ b/editor/src/editor/windows/fx-editor/VFX/behaviors/limitSpeedOverLife.ts @@ -7,30 +7,29 @@ import { VFXValueParser } from "../parsers/VFXValueParser"; * Apply LimitSpeedOverLife behavior to ParticleSystem */ export function applyLimitSpeedOverLifePS(particleSystem: ParticleSystem, behavior: VFXLimitSpeedOverLifeBehavior, valueParser: VFXValueParser): void { - if (behavior.dampen !== undefined) { - const dampen = valueParser.parseConstantValue(behavior.dampen); - particleSystem.limitVelocityDamping = dampen; - } + if (behavior.dampen !== undefined) { + const dampen = valueParser.parseConstantValue(behavior.dampen); + particleSystem.limitVelocityDamping = dampen; + } - if (behavior.maxSpeed !== undefined) { - const speedLimit = valueParser.parseConstantValue(behavior.maxSpeed); - particleSystem.addLimitVelocityGradient(0, speedLimit); - particleSystem.addLimitVelocityGradient(1, speedLimit); - } else if (behavior.speed !== undefined) { - if (typeof behavior.speed === "object" && behavior.speed !== null && "keys" in behavior.speed && behavior.speed.keys && Array.isArray(behavior.speed.keys)) { - for (const key of behavior.speed.keys) { - const pos = key.pos ?? key.time ?? 0; - const val = key.value; - if (val !== undefined && pos !== undefined) { - const numVal = extractNumberFromValue(val); - particleSystem.addLimitVelocityGradient(pos, numVal); - } - } - } else if (typeof behavior.speed === "number" || (typeof behavior.speed === "object" && behavior.speed !== null && "type" in behavior.speed)) { - const speedLimit = valueParser.parseConstantValue(behavior.speed); - particleSystem.addLimitVelocityGradient(0, speedLimit); - particleSystem.addLimitVelocityGradient(1, speedLimit); - } - } + if (behavior.maxSpeed !== undefined) { + const speedLimit = valueParser.parseConstantValue(behavior.maxSpeed); + particleSystem.addLimitVelocityGradient(0, speedLimit); + particleSystem.addLimitVelocityGradient(1, speedLimit); + } else if (behavior.speed !== undefined) { + if (typeof behavior.speed === "object" && behavior.speed !== null && "keys" in behavior.speed && behavior.speed.keys && Array.isArray(behavior.speed.keys)) { + for (const key of behavior.speed.keys) { + const pos = key.pos ?? key.time ?? 0; + const val = key.value; + if (val !== undefined && pos !== undefined) { + const numVal = extractNumberFromValue(val); + particleSystem.addLimitVelocityGradient(pos, numVal); + } + } + } else if (typeof behavior.speed === "number" || (typeof behavior.speed === "object" && behavior.speed !== null && "type" in behavior.speed)) { + const speedLimit = valueParser.parseConstantValue(behavior.speed); + particleSystem.addLimitVelocityGradient(0, speedLimit); + particleSystem.addLimitVelocityGradient(1, speedLimit); + } + } } - diff --git a/editor/src/editor/windows/fx-editor/VFX/behaviors/orbitOverLife.ts b/editor/src/editor/windows/fx-editor/VFX/behaviors/orbitOverLife.ts index 23c75ce42..fb1a66c2e 100644 --- a/editor/src/editor/windows/fx-editor/VFX/behaviors/orbitOverLife.ts +++ b/editor/src/editor/windows/fx-editor/VFX/behaviors/orbitOverLife.ts @@ -8,93 +8,92 @@ import { VFXValueParser } from "../parsers/VFXValueParser"; * Apply OrbitOverLife behavior to Particle */ export function applyOrbitOverLifePS(particle: Particle, behavior: VFXOrbitOverLifeBehavior, lifeRatio: number, valueParser: VFXValueParser): void { - if (!behavior.radius) { - return; - } + if (!behavior.radius) { + return; + } - // Parse radius (can be VFXValue with keys or constant/interval) - let radius = 1; - const radiusValue = behavior.radius; - - // Check if radius is an object with keys (gradient) - if ( - radiusValue !== undefined && - radiusValue !== null && - typeof radiusValue === "object" && - "keys" in radiusValue && - Array.isArray(radiusValue.keys) && - radiusValue.keys.length > 0 - ) { - radius = interpolateGradientKeys(radiusValue.keys, lifeRatio, extractNumberFromValue); - } else if (radiusValue !== undefined && radiusValue !== null) { - // Parse as VFXValue (number, VFXConstantValue, or VFXIntervalValue) - const parsedRadius = valueParser.parseIntervalValue(radiusValue as import("../types/values").VFXValue); - radius = parsedRadius.min + (parsedRadius.max - parsedRadius.min) * lifeRatio; - } + // Parse radius (can be VFXValue with keys or constant/interval) + let radius = 1; + const radiusValue = behavior.radius; - const speed = behavior.speed !== undefined ? valueParser.parseConstantValue(behavior.speed) : 1; - const angle = lifeRatio * speed * Math.PI * 2; + // Check if radius is an object with keys (gradient) + if ( + radiusValue !== undefined && + radiusValue !== null && + typeof radiusValue === "object" && + "keys" in radiusValue && + Array.isArray(radiusValue.keys) && + radiusValue.keys.length > 0 + ) { + radius = interpolateGradientKeys(radiusValue.keys, lifeRatio, extractNumberFromValue); + } else if (radiusValue !== undefined && radiusValue !== null) { + // Parse as VFXValue (number, VFXConstantValue, or VFXIntervalValue) + const parsedRadius = valueParser.parseIntervalValue(radiusValue as import("../types/values").VFXValue); + radius = parsedRadius.min + (parsedRadius.max - parsedRadius.min) * lifeRatio; + } - // Calculate orbit offset relative to center - const centerX = behavior.center?.x ?? 0; - const centerY = behavior.center?.y ?? 0; - const centerZ = behavior.center?.z ?? 0; + const speed = behavior.speed !== undefined ? valueParser.parseConstantValue(behavior.speed) : 1; + const angle = lifeRatio * speed * Math.PI * 2; - const orbitX = Math.cos(angle) * radius; - const orbitY = Math.sin(angle) * radius; - const orbitZ = 0; // 2D orbit + // Calculate orbit offset relative to center + const centerX = behavior.center?.x ?? 0; + const centerY = behavior.center?.y ?? 0; + const centerZ = behavior.center?.z ?? 0; - // Apply orbit offset to particle position - if (particle.position) { - particle.position.x = centerX + orbitX; - particle.position.y = centerY + orbitY; - particle.position.z = centerZ + orbitZ; - } + const orbitX = Math.cos(angle) * radius; + const orbitY = Math.sin(angle) * radius; + const orbitZ = 0; // 2D orbit + + // Apply orbit offset to particle position + if (particle.position) { + particle.position.x = centerX + orbitX; + particle.position.y = centerY + orbitY; + particle.position.z = centerZ + orbitZ; + } } /** * Apply OrbitOverLife behavior to SolidParticle */ export function applyOrbitOverLifeSPS(particle: SolidParticle, behavior: VFXOrbitOverLifeBehavior, lifeRatio: number, valueParser: VFXValueParser): void { - if (!behavior.radius) { - return; - } + if (!behavior.radius) { + return; + } - // Parse radius (can be VFXValue with keys or constant/interval) - let radius = 1; - const radiusValue = behavior.radius; - - // Check if radius is an object with keys (gradient) - if ( - radiusValue !== undefined && - radiusValue !== null && - typeof radiusValue === "object" && - "keys" in radiusValue && - Array.isArray(radiusValue.keys) && - radiusValue.keys.length > 0 - ) { - radius = interpolateGradientKeys(radiusValue.keys, lifeRatio, extractNumberFromValue); - } else if (radiusValue !== undefined && radiusValue !== null) { - // Parse as VFXValue (number, VFXConstantValue, or VFXIntervalValue) - const parsedRadius = valueParser.parseIntervalValue(radiusValue as import("../types/values").VFXValue); - radius = parsedRadius.min + (parsedRadius.max - parsedRadius.min) * lifeRatio; - } + // Parse radius (can be VFXValue with keys or constant/interval) + let radius = 1; + const radiusValue = behavior.radius; - const speed = behavior.speed !== undefined ? valueParser.parseConstantValue(behavior.speed) : 1; - const angle = lifeRatio * speed * Math.PI * 2; + // Check if radius is an object with keys (gradient) + if ( + radiusValue !== undefined && + radiusValue !== null && + typeof radiusValue === "object" && + "keys" in radiusValue && + Array.isArray(radiusValue.keys) && + radiusValue.keys.length > 0 + ) { + radius = interpolateGradientKeys(radiusValue.keys, lifeRatio, extractNumberFromValue); + } else if (radiusValue !== undefined && radiusValue !== null) { + // Parse as VFXValue (number, VFXConstantValue, or VFXIntervalValue) + const parsedRadius = valueParser.parseIntervalValue(radiusValue as import("../types/values").VFXValue); + radius = parsedRadius.min + (parsedRadius.max - parsedRadius.min) * lifeRatio; + } - // Calculate orbit offset relative to center - const centerX = behavior.center?.x ?? 0; - const centerY = behavior.center?.y ?? 0; - const centerZ = behavior.center?.z ?? 0; + const speed = behavior.speed !== undefined ? valueParser.parseConstantValue(behavior.speed) : 1; + const angle = lifeRatio * speed * Math.PI * 2; - const orbitX = Math.cos(angle) * radius; - const orbitY = Math.sin(angle) * radius; - const orbitZ = 0; // 2D orbit + // Calculate orbit offset relative to center + const centerX = behavior.center?.x ?? 0; + const centerY = behavior.center?.y ?? 0; + const centerZ = behavior.center?.z ?? 0; - // Apply orbit offset to particle position - particle.position.x = centerX + orbitX; - particle.position.y = centerY + orbitY; - particle.position.z = centerZ + orbitZ; -} + const orbitX = Math.cos(angle) * radius; + const orbitY = Math.sin(angle) * radius; + const orbitZ = 0; // 2D orbit + // Apply orbit offset to particle position + particle.position.x = centerX + orbitX; + particle.position.y = centerY + orbitY; + particle.position.z = centerZ + orbitZ; +} diff --git a/editor/src/editor/windows/fx-editor/VFX/behaviors/rotationBySpeed.ts b/editor/src/editor/windows/fx-editor/VFX/behaviors/rotationBySpeed.ts index 7eca3e7ee..dda7a996e 100644 --- a/editor/src/editor/windows/fx-editor/VFX/behaviors/rotationBySpeed.ts +++ b/editor/src/editor/windows/fx-editor/VFX/behaviors/rotationBySpeed.ts @@ -9,53 +9,76 @@ import { VFXValueParser } from "../parsers/VFXValueParser"; * Extended Particle interface for custom behaviors */ interface ExtendedParticle extends Particle { - startSpeed?: number; + startSpeed?: number; } /** * Apply RotationBySpeed behavior to Particle */ -export function applyRotationBySpeedPS(particle: ExtendedParticle, behavior: VFXRotationBySpeedBehavior, currentSpeed: number, _particleSystem: ParticleSystem, valueParser: VFXValueParser): void { - if (!behavior.angularVelocity) { - return; - } +export function applyRotationBySpeedPS( + particle: ExtendedParticle, + behavior: VFXRotationBySpeedBehavior, + currentSpeed: number, + _particleSystem: ParticleSystem, + valueParser: VFXValueParser +): void { + if (!behavior.angularVelocity) { + return; + } - // angularVelocity can be VFXValue (constant/interval) or object with keys - let angularSpeed = 0; - if (typeof behavior.angularVelocity === "object" && behavior.angularVelocity !== null && "keys" in behavior.angularVelocity && Array.isArray(behavior.angularVelocity.keys) && behavior.angularVelocity.keys.length > 0) { - const minSpeed = behavior.minSpeed !== undefined ? valueParser.parseConstantValue(behavior.minSpeed) : 0; - const maxSpeed = behavior.maxSpeed !== undefined ? valueParser.parseConstantValue(behavior.maxSpeed) : 1; - const speedRatio = Math.max(0, Math.min(1, (currentSpeed - minSpeed) / (maxSpeed - minSpeed || 1))); - angularSpeed = interpolateGradientKeys(behavior.angularVelocity.keys, speedRatio, extractNumberFromValue); - } else { - const angularVel = valueParser.parseIntervalValue(behavior.angularVelocity); - angularSpeed = angularVel.min + (angularVel.max - angularVel.min) * 0.5; // Use middle value - } + // angularVelocity can be VFXValue (constant/interval) or object with keys + let angularSpeed = 0; + if ( + typeof behavior.angularVelocity === "object" && + behavior.angularVelocity !== null && + "keys" in behavior.angularVelocity && + Array.isArray(behavior.angularVelocity.keys) && + behavior.angularVelocity.keys.length > 0 + ) { + const minSpeed = behavior.minSpeed !== undefined ? valueParser.parseConstantValue(behavior.minSpeed) : 0; + const maxSpeed = behavior.maxSpeed !== undefined ? valueParser.parseConstantValue(behavior.maxSpeed) : 1; + const speedRatio = Math.max(0, Math.min(1, (currentSpeed - minSpeed) / (maxSpeed - minSpeed || 1))); + angularSpeed = interpolateGradientKeys(behavior.angularVelocity.keys, speedRatio, extractNumberFromValue); + } else { + const angularVel = valueParser.parseIntervalValue(behavior.angularVelocity); + angularSpeed = angularVel.min + (angularVel.max - angularVel.min) * 0.5; // Use middle value + } - particle.angle += angularSpeed * 0.016; // Assuming ~60fps + particle.angle += angularSpeed * 0.016; // Assuming ~60fps } /** * Apply RotationBySpeed behavior to SolidParticle */ -export function applyRotationBySpeedSPS(particle: SolidParticle, behavior: VFXRotationBySpeedBehavior, currentSpeed: number, valueParser: VFXValueParser, updateSpeed: number = 0.016): void { - if (!behavior.angularVelocity) { - return; - } +export function applyRotationBySpeedSPS( + particle: SolidParticle, + behavior: VFXRotationBySpeedBehavior, + currentSpeed: number, + valueParser: VFXValueParser, + updateSpeed: number = 0.016 +): void { + if (!behavior.angularVelocity) { + return; + } - // angularVelocity can be VFXValue (constant/interval) or object with keys - let angularSpeed = 0; - if (typeof behavior.angularVelocity === "object" && behavior.angularVelocity !== null && "keys" in behavior.angularVelocity && Array.isArray(behavior.angularVelocity.keys) && behavior.angularVelocity.keys.length > 0) { - const minSpeed = behavior.minSpeed !== undefined ? valueParser.parseConstantValue(behavior.minSpeed) : 0; - const maxSpeed = behavior.maxSpeed !== undefined ? valueParser.parseConstantValue(behavior.maxSpeed) : 1; - const speedRatio = Math.max(0, Math.min(1, (currentSpeed - minSpeed) / (maxSpeed - minSpeed || 1))); - angularSpeed = interpolateGradientKeys(behavior.angularVelocity.keys, speedRatio, extractNumberFromValue); - } else { - const angularVel = valueParser.parseIntervalValue(behavior.angularVelocity); - angularSpeed = angularVel.min + (angularVel.max - angularVel.min) * 0.5; // Use middle value - } + // angularVelocity can be VFXValue (constant/interval) or object with keys + let angularSpeed = 0; + if ( + typeof behavior.angularVelocity === "object" && + behavior.angularVelocity !== null && + "keys" in behavior.angularVelocity && + Array.isArray(behavior.angularVelocity.keys) && + behavior.angularVelocity.keys.length > 0 + ) { + const minSpeed = behavior.minSpeed !== undefined ? valueParser.parseConstantValue(behavior.minSpeed) : 0; + const maxSpeed = behavior.maxSpeed !== undefined ? valueParser.parseConstantValue(behavior.maxSpeed) : 1; + const speedRatio = Math.max(0, Math.min(1, (currentSpeed - minSpeed) / (maxSpeed - minSpeed || 1))); + angularSpeed = interpolateGradientKeys(behavior.angularVelocity.keys, speedRatio, extractNumberFromValue); + } else { + const angularVel = valueParser.parseIntervalValue(behavior.angularVelocity); + angularSpeed = angularVel.min + (angularVel.max - angularVel.min) * 0.5; // Use middle value + } - // SolidParticle uses rotation.z for 2D rotation - particle.rotation.z += angularSpeed * updateSpeed; + // SolidParticle uses rotation.z for 2D rotation + particle.rotation.z += angularSpeed * updateSpeed; } - diff --git a/editor/src/editor/windows/fx-editor/VFX/behaviors/rotationOverLife.ts b/editor/src/editor/windows/fx-editor/VFX/behaviors/rotationOverLife.ts index 9f8fd39cf..f1be1c2cb 100644 --- a/editor/src/editor/windows/fx-editor/VFX/behaviors/rotationOverLife.ts +++ b/editor/src/editor/windows/fx-editor/VFX/behaviors/rotationOverLife.ts @@ -7,26 +7,31 @@ import { VFXValueParser } from "../parsers/VFXValueParser"; * Apply RotationOverLife behavior to ParticleSystem */ export function applyRotationOverLifePS(particleSystem: ParticleSystem, behavior: VFXRotationOverLifeBehavior, valueParser: VFXValueParser): void { - if (behavior.angularVelocity) { - const angularVel = valueParser.parseIntervalValue(behavior.angularVelocity); - particleSystem.minAngularSpeed = angularVel.min; - particleSystem.maxAngularSpeed = angularVel.max; - } + if (behavior.angularVelocity) { + const angularVel = valueParser.parseIntervalValue(behavior.angularVelocity); + particleSystem.minAngularSpeed = angularVel.min; + particleSystem.maxAngularSpeed = angularVel.max; + } } /** * Apply RotationOverLife behavior to SolidParticle */ -export function applyRotationOverLifeSPS(particle: SolidParticle, behavior: VFXRotationOverLifeBehavior, lifeRatio: number, valueParser: VFXValueParser, updateSpeed: number = 0.016): void { - if (!behavior.angularVelocity) { - return; - } +export function applyRotationOverLifeSPS( + particle: SolidParticle, + behavior: VFXRotationOverLifeBehavior, + lifeRatio: number, + valueParser: VFXValueParser, + updateSpeed: number = 0.016 +): void { + if (!behavior.angularVelocity) { + return; + } - const angularVel = valueParser.parseIntervalValue(behavior.angularVelocity); - const angularSpeed = angularVel.min + (angularVel.max - angularVel.min) * lifeRatio; + const angularVel = valueParser.parseIntervalValue(behavior.angularVelocity); + const angularSpeed = angularVel.min + (angularVel.max - angularVel.min) * lifeRatio; - // Apply rotation around Z axis (2D rotation) - // SolidParticle uses rotation.z for 2D rotation - particle.rotation.z += angularSpeed * updateSpeed; + // Apply rotation around Z axis (2D rotation) + // SolidParticle uses rotation.z for 2D rotation + particle.rotation.z += angularSpeed * updateSpeed; } - diff --git a/editor/src/editor/windows/fx-editor/VFX/behaviors/sizeBySpeed.ts b/editor/src/editor/windows/fx-editor/VFX/behaviors/sizeBySpeed.ts index c69ee1bdb..f825bf4d9 100644 --- a/editor/src/editor/windows/fx-editor/VFX/behaviors/sizeBySpeed.ts +++ b/editor/src/editor/windows/fx-editor/VFX/behaviors/sizeBySpeed.ts @@ -8,44 +8,43 @@ import { VFXValueParser } from "../parsers/VFXValueParser"; * Extended Particle interface for custom behaviors */ interface ExtendedParticle extends Particle { - startSpeed?: number; - startSize?: number; + startSpeed?: number; + startSize?: number; } /** * Apply SizeBySpeed behavior to Particle */ export function applySizeBySpeedPS(particle: ExtendedParticle, behavior: VFXSizeBySpeedBehavior, currentSpeed: number, valueParser: VFXValueParser): void { - if (!behavior.size || !behavior.size.keys) { - return; - } - - const sizeKeys = behavior.size.keys; - const minSpeed = behavior.minSpeed !== undefined ? valueParser.parseConstantValue(behavior.minSpeed) : 0; - const maxSpeed = behavior.maxSpeed !== undefined ? valueParser.parseConstantValue(behavior.maxSpeed) : 1; - const speedRatio = Math.max(0, Math.min(1, (currentSpeed - minSpeed) / (maxSpeed - minSpeed || 1))); - - const sizeMultiplier = interpolateGradientKeys(sizeKeys, speedRatio, extractNumberFromValue); - const startSize = particle.startSize || particle.size || 1; - particle.size = startSize * sizeMultiplier; + if (!behavior.size || !behavior.size.keys) { + return; + } + + const sizeKeys = behavior.size.keys; + const minSpeed = behavior.minSpeed !== undefined ? valueParser.parseConstantValue(behavior.minSpeed) : 0; + const maxSpeed = behavior.maxSpeed !== undefined ? valueParser.parseConstantValue(behavior.maxSpeed) : 1; + const speedRatio = Math.max(0, Math.min(1, (currentSpeed - minSpeed) / (maxSpeed - minSpeed || 1))); + + const sizeMultiplier = interpolateGradientKeys(sizeKeys, speedRatio, extractNumberFromValue); + const startSize = particle.startSize || particle.size || 1; + particle.size = startSize * sizeMultiplier; } /** * Apply SizeBySpeed behavior to SolidParticle */ export function applySizeBySpeedSPS(particle: SolidParticle, behavior: VFXSizeBySpeedBehavior, currentSpeed: number, valueParser: VFXValueParser): void { - if (!behavior.size || !behavior.size.keys) { - return; - } - - const sizeKeys = behavior.size.keys; - const minSpeed = behavior.minSpeed !== undefined ? valueParser.parseConstantValue(behavior.minSpeed) : 0; - const maxSpeed = behavior.maxSpeed !== undefined ? valueParser.parseConstantValue(behavior.maxSpeed) : 1; - const speedRatio = Math.max(0, Math.min(1, (currentSpeed - minSpeed) / (maxSpeed - minSpeed || 1))); - - const sizeMultiplier = interpolateGradientKeys(sizeKeys, speedRatio, extractNumberFromValue); - const startSize = particle.props?.startSize ?? 1; - const newSize = startSize * sizeMultiplier; - particle.scaling.setAll(newSize); + if (!behavior.size || !behavior.size.keys) { + return; + } + + const sizeKeys = behavior.size.keys; + const minSpeed = behavior.minSpeed !== undefined ? valueParser.parseConstantValue(behavior.minSpeed) : 0; + const maxSpeed = behavior.maxSpeed !== undefined ? valueParser.parseConstantValue(behavior.maxSpeed) : 1; + const speedRatio = Math.max(0, Math.min(1, (currentSpeed - minSpeed) / (maxSpeed - minSpeed || 1))); + + const sizeMultiplier = interpolateGradientKeys(sizeKeys, speedRatio, extractNumberFromValue); + const startSize = particle.props?.startSize ?? 1; + const newSize = startSize * sizeMultiplier; + particle.scaling.setAll(newSize); } - diff --git a/editor/src/editor/windows/fx-editor/VFX/behaviors/sizeOverLife.ts b/editor/src/editor/windows/fx-editor/VFX/behaviors/sizeOverLife.ts index ab7994cee..01fe06071 100644 --- a/editor/src/editor/windows/fx-editor/VFX/behaviors/sizeOverLife.ts +++ b/editor/src/editor/windows/fx-editor/VFX/behaviors/sizeOverLife.ts @@ -7,54 +7,53 @@ import { extractNumberFromValue, interpolateGradientKeys } from "./utils"; * Apply SizeOverLife behavior to ParticleSystem */ export function applySizeOverLifePS(particleSystem: ParticleSystem, behavior: VFXSizeOverLifeBehavior): void { - if (behavior.size && behavior.size.functions) { - const functions = behavior.size.functions; - for (const func of functions) { - if (func.function && func.start !== undefined) { - const startSize = func.function.p0 || 1; - const endSize = func.function.p3 !== undefined ? func.function.p3 : startSize; - particleSystem.addSizeGradient(func.start, startSize); - if (func.function.p3 !== undefined) { - particleSystem.addSizeGradient(func.start + 0.5, endSize); - } - } - } - } else if (behavior.size && behavior.size.keys) { - for (const key of behavior.size.keys) { - if (key.value !== undefined && key.pos !== undefined) { - const size = extractNumberFromValue(key.value); - particleSystem.addSizeGradient(key.pos, size); - } - } - } + if (behavior.size && behavior.size.functions) { + const functions = behavior.size.functions; + for (const func of functions) { + if (func.function && func.start !== undefined) { + const startSize = func.function.p0 || 1; + const endSize = func.function.p3 !== undefined ? func.function.p3 : startSize; + particleSystem.addSizeGradient(func.start, startSize); + if (func.function.p3 !== undefined) { + particleSystem.addSizeGradient(func.start + 0.5, endSize); + } + } + } + } else if (behavior.size && behavior.size.keys) { + for (const key of behavior.size.keys) { + if (key.value !== undefined && key.pos !== undefined) { + const size = extractNumberFromValue(key.value); + particleSystem.addSizeGradient(key.pos, size); + } + } + } } /** * Apply SizeOverLife behavior to SolidParticle */ export function applySizeOverLifeSPS(particle: SolidParticle, behavior: VFXSizeOverLifeBehavior, lifeRatio: number): void { - if (!behavior.size) { - return; - } + if (!behavior.size) { + return; + } - let sizeMultiplier = 1; + let sizeMultiplier = 1; - if (behavior.size.keys && Array.isArray(behavior.size.keys)) { - sizeMultiplier = interpolateGradientKeys(behavior.size.keys, lifeRatio, extractNumberFromValue); - } else if (behavior.size.functions && Array.isArray(behavior.size.functions)) { - // Handle functions (simplified - use first function) - const func = behavior.size.functions[0]; - if (func && func.function && func.start !== undefined) { - const startSize = func.function.p0 || 1; - const endSize = func.function.p3 !== undefined ? func.function.p3 : startSize; - const t = Math.max(0, Math.min(1, (lifeRatio - func.start) / 0.5)); - sizeMultiplier = startSize + (endSize - startSize) * t; - } - } + if (behavior.size.keys && Array.isArray(behavior.size.keys)) { + sizeMultiplier = interpolateGradientKeys(behavior.size.keys, lifeRatio, extractNumberFromValue); + } else if (behavior.size.functions && Array.isArray(behavior.size.functions)) { + // Handle functions (simplified - use first function) + const func = behavior.size.functions[0]; + if (func && func.function && func.start !== undefined) { + const startSize = func.function.p0 || 1; + const endSize = func.function.p3 !== undefined ? func.function.p3 : startSize; + const t = Math.max(0, Math.min(1, (lifeRatio - func.start) / 0.5)); + sizeMultiplier = startSize + (endSize - startSize) * t; + } + } - // Multiply startSize by the gradient value (matching three.quarks behavior) - const startSize = particle.props?.startSize ?? 1; - const newSize = startSize * sizeMultiplier; - particle.scaling.setAll(newSize); + // Multiply startSize by the gradient value (matching three.quarks behavior) + const startSize = particle.props?.startSize ?? 1; + const newSize = startSize * sizeMultiplier; + particle.scaling.setAll(newSize); } - diff --git a/editor/src/editor/windows/fx-editor/VFX/behaviors/speedOverLife.ts b/editor/src/editor/windows/fx-editor/VFX/behaviors/speedOverLife.ts index ed649bab3..a4ba2ac02 100644 --- a/editor/src/editor/windows/fx-editor/VFX/behaviors/speedOverLife.ts +++ b/editor/src/editor/windows/fx-editor/VFX/behaviors/speedOverLife.ts @@ -8,85 +8,85 @@ import { VFXValueParser } from "../parsers/VFXValueParser"; * Apply SpeedOverLife behavior to ParticleSystem */ export function applySpeedOverLifePS(particleSystem: ParticleSystem, behavior: VFXSpeedOverLifeBehavior, valueParser: VFXValueParser): void { - if (behavior.speed) { - if (typeof behavior.speed === "object" && behavior.speed !== null && "keys" in behavior.speed && behavior.speed.keys && Array.isArray(behavior.speed.keys)) { - for (const key of behavior.speed.keys) { - const pos = key.pos ?? key.time ?? 0; - const val = key.value; - if (val !== undefined && pos !== undefined) { - const numVal = extractNumberFromValue(val); - particleSystem.addVelocityGradient(pos, numVal); - } - } - } else if ( - typeof behavior.speed === "object" && - behavior.speed !== null && - "functions" in behavior.speed && - behavior.speed.functions && - Array.isArray(behavior.speed.functions) - ) { - for (const func of behavior.speed.functions) { - if (func.function && func.start !== undefined) { - const startSpeed = func.function.p0 || 1; - const endSpeed = func.function.p3 !== undefined ? func.function.p3 : startSpeed; - particleSystem.addVelocityGradient(func.start, startSpeed); - if (func.function.p3 !== undefined) { - particleSystem.addVelocityGradient(Math.min(func.start + 0.5, 1), endSpeed); - } - } - } - } else if (typeof behavior.speed === "number" || (typeof behavior.speed === "object" && behavior.speed !== null && "type" in behavior.speed)) { - const speedValue = valueParser.parseIntervalValue(behavior.speed); - particleSystem.addVelocityGradient(0, speedValue.min); - particleSystem.addVelocityGradient(1, speedValue.max); - } - } + if (behavior.speed) { + if (typeof behavior.speed === "object" && behavior.speed !== null && "keys" in behavior.speed && behavior.speed.keys && Array.isArray(behavior.speed.keys)) { + for (const key of behavior.speed.keys) { + const pos = key.pos ?? key.time ?? 0; + const val = key.value; + if (val !== undefined && pos !== undefined) { + const numVal = extractNumberFromValue(val); + particleSystem.addVelocityGradient(pos, numVal); + } + } + } else if ( + typeof behavior.speed === "object" && + behavior.speed !== null && + "functions" in behavior.speed && + behavior.speed.functions && + Array.isArray(behavior.speed.functions) + ) { + for (const func of behavior.speed.functions) { + if (func.function && func.start !== undefined) { + const startSpeed = func.function.p0 || 1; + const endSpeed = func.function.p3 !== undefined ? func.function.p3 : startSpeed; + particleSystem.addVelocityGradient(func.start, startSpeed); + if (func.function.p3 !== undefined) { + particleSystem.addVelocityGradient(Math.min(func.start + 0.5, 1), endSpeed); + } + } + } + } else if (typeof behavior.speed === "number" || (typeof behavior.speed === "object" && behavior.speed !== null && "type" in behavior.speed)) { + const speedValue = valueParser.parseIntervalValue(behavior.speed); + particleSystem.addVelocityGradient(0, speedValue.min); + particleSystem.addVelocityGradient(1, speedValue.max); + } + } } /** * Apply SpeedOverLife behavior to SolidParticle */ export function applySpeedOverLifeSPS(particle: SolidParticle, behavior: VFXSpeedOverLifeBehavior, lifeRatio: number, valueParser: VFXValueParser): void { - if (!behavior.speed) { - return; - } + if (!behavior.speed) { + return; + } - let speedMultiplier = 1; + let speedMultiplier = 1; - if (typeof behavior.speed === "object" && behavior.speed !== null && "keys" in behavior.speed && behavior.speed.keys && Array.isArray(behavior.speed.keys)) { - speedMultiplier = interpolateGradientKeys(behavior.speed.keys, lifeRatio, extractNumberFromValue); - } else if ( - typeof behavior.speed === "object" && - behavior.speed !== null && - "functions" in behavior.speed && - behavior.speed.functions && - Array.isArray(behavior.speed.functions) - ) { - // Handle functions (simplified - use first function) - const func = behavior.speed.functions[0]; - if (func && func.function && func.start !== undefined) { - const startSpeed = func.function.p0 || 1; - const endSpeed = func.function.p3 !== undefined ? func.function.p3 : startSpeed; - const t = Math.max(0, Math.min(1, (lifeRatio - func.start) / 0.5)); - speedMultiplier = startSpeed + (endSpeed - startSpeed) * t; - } - } else if (typeof behavior.speed === "number" || (typeof behavior.speed === "object" && behavior.speed !== null && "type" in behavior.speed)) { - const speedValue = valueParser.parseIntervalValue(behavior.speed); - speedMultiplier = speedValue.min + (speedValue.max - speedValue.min) * lifeRatio; - } + if (typeof behavior.speed === "object" && behavior.speed !== null && "keys" in behavior.speed && behavior.speed.keys && Array.isArray(behavior.speed.keys)) { + speedMultiplier = interpolateGradientKeys(behavior.speed.keys, lifeRatio, extractNumberFromValue); + } else if ( + typeof behavior.speed === "object" && + behavior.speed !== null && + "functions" in behavior.speed && + behavior.speed.functions && + Array.isArray(behavior.speed.functions) + ) { + // Handle functions (simplified - use first function) + const func = behavior.speed.functions[0]; + if (func && func.function && func.start !== undefined) { + const startSpeed = func.function.p0 || 1; + const endSpeed = func.function.p3 !== undefined ? func.function.p3 : startSpeed; + const t = Math.max(0, Math.min(1, (lifeRatio - func.start) / 0.5)); + speedMultiplier = startSpeed + (endSpeed - startSpeed) * t; + } + } else if (typeof behavior.speed === "number" || (typeof behavior.speed === "object" && behavior.speed !== null && "type" in behavior.speed)) { + const speedValue = valueParser.parseIntervalValue(behavior.speed); + speedMultiplier = speedValue.min + (speedValue.max - speedValue.min) * lifeRatio; + } - // Apply speed modifier to velocity - const startSpeed = particle.props?.startSpeed ?? 1; - const speedModifier = particle.props?.speedModifier ?? 1; - const newSpeedModifier = speedModifier * speedMultiplier; - particle.props = particle.props || {}; - particle.props.speedModifier = newSpeedModifier; + // Apply speed modifier to velocity + const startSpeed = particle.props?.startSpeed ?? 1; + const speedModifier = particle.props?.speedModifier ?? 1; + const newSpeedModifier = speedModifier * speedMultiplier; + particle.props = particle.props || {}; + particle.props.speedModifier = newSpeedModifier; - // Update velocity magnitude - const velocityLength = Math.sqrt(particle.velocity.x * particle.velocity.x + particle.velocity.y * particle.velocity.y + particle.velocity.z * particle.velocity.z); - if (velocityLength > 0) { - const newLength = startSpeed * newSpeedModifier; - const scale = newLength / velocityLength; - particle.velocity.scaleInPlace(scale); - } + // Update velocity magnitude + const velocityLength = Math.sqrt(particle.velocity.x * particle.velocity.x + particle.velocity.y * particle.velocity.y + particle.velocity.z * particle.velocity.z); + if (velocityLength > 0) { + const newLength = startSpeed * newSpeedModifier; + const scale = newLength / velocityLength; + particle.velocity.scaleInPlace(scale); + } } diff --git a/editor/src/editor/windows/fx-editor/VFX/behaviors/utils.ts b/editor/src/editor/windows/fx-editor/VFX/behaviors/utils.ts index 917724716..cf14a2376 100644 --- a/editor/src/editor/windows/fx-editor/VFX/behaviors/utils.ts +++ b/editor/src/editor/windows/fx-editor/VFX/behaviors/utils.ts @@ -4,163 +4,162 @@ import type { VFXGradientKey } from "../types/gradients"; * Extract RGB color from gradient key value */ export function extractColorFromValue(value: number | number[] | { r: number; g: number; b: number; a?: number } | undefined): { r: number; g: number; b: number } { - if (value === undefined) { - return { r: 1, g: 1, b: 1 }; - } - - if (typeof value === "number") { - return { r: value, g: value, b: value }; - } - - if (Array.isArray(value)) { - return { - r: value[0] || 0, - g: value[1] || 0, - b: value[2] || 0, - }; - } - - if (typeof value === "object" && "r" in value) { - return { - r: value.r || 0, - g: value.g || 0, - b: value.b || 0, - }; - } - - return { r: 1, g: 1, b: 1 }; + if (value === undefined) { + return { r: 1, g: 1, b: 1 }; + } + + if (typeof value === "number") { + return { r: value, g: value, b: value }; + } + + if (Array.isArray(value)) { + return { + r: value[0] || 0, + g: value[1] || 0, + b: value[2] || 0, + }; + } + + if (typeof value === "object" && "r" in value) { + return { + r: value.r || 0, + g: value.g || 0, + b: value.b || 0, + }; + } + + return { r: 1, g: 1, b: 1 }; } /** * Extract alpha from gradient key value */ export function extractAlphaFromValue(value: number | number[] | { r: number; g: number; b: number; a?: number } | undefined): number { - if (value === undefined) { - return 1; - } + if (value === undefined) { + return 1; + } - if (typeof value === "number") { - return value; - } + if (typeof value === "number") { + return value; + } - if (Array.isArray(value)) { - return value[3] !== undefined ? value[3] : 1; - } + if (Array.isArray(value)) { + return value[3] !== undefined ? value[3] : 1; + } - if (typeof value === "object" && "a" in value) { - return value.a !== undefined ? value.a : 1; - } + if (typeof value === "object" && "a" in value) { + return value.a !== undefined ? value.a : 1; + } - return 1; + return 1; } /** * Extract number from gradient key value */ export function extractNumberFromValue(value: number | number[] | { r: number; g: number; b: number; a?: number } | undefined): number { - if (value === undefined) { - return 1; - } + if (value === undefined) { + return 1; + } - if (typeof value === "number") { - return value; - } + if (typeof value === "number") { + return value; + } - if (Array.isArray(value)) { - return value[0] || 0; - } + if (Array.isArray(value)) { + return value[0] || 0; + } - return 1; + return 1; } /** * Interpolate between two gradient keys */ export function interpolateGradientKeys( - keys: VFXGradientKey[], - ratio: number, - extractValue: (value: number | number[] | { r: number; g: number; b: number; a?: number } | undefined) => number + keys: VFXGradientKey[], + ratio: number, + extractValue: (value: number | number[] | { r: number; g: number; b: number; a?: number } | undefined) => number ): number { - if (!keys || keys.length === 0) { - return 1; - } - - if (keys.length === 1) { - return extractValue(keys[0].value); - } - - // Find the two keys to interpolate between - for (let i = 0; i < keys.length - 1; i++) { - const pos1 = keys[i].pos ?? keys[i].time ?? 0; - const pos2 = keys[i + 1].pos ?? keys[i + 1].time ?? 1; - - if (ratio >= pos1 && ratio <= pos2) { - const t = pos2 - pos1 !== 0 ? (ratio - pos1) / (pos2 - pos1) : 0; - const val1 = extractValue(keys[i].value); - const val2 = extractValue(keys[i + 1].value); - return val1 + (val2 - val1) * t; - } - } - - // Clamp to first or last key - if (ratio <= (keys[0].pos ?? keys[0].time ?? 0)) { - return extractValue(keys[0].value); - } - return extractValue(keys[keys.length - 1].value); + if (!keys || keys.length === 0) { + return 1; + } + + if (keys.length === 1) { + return extractValue(keys[0].value); + } + + // Find the two keys to interpolate between + for (let i = 0; i < keys.length - 1; i++) { + const pos1 = keys[i].pos ?? keys[i].time ?? 0; + const pos2 = keys[i + 1].pos ?? keys[i + 1].time ?? 1; + + if (ratio >= pos1 && ratio <= pos2) { + const t = pos2 - pos1 !== 0 ? (ratio - pos1) / (pos2 - pos1) : 0; + const val1 = extractValue(keys[i].value); + const val2 = extractValue(keys[i + 1].value); + return val1 + (val2 - val1) * t; + } + } + + // Clamp to first or last key + if (ratio <= (keys[0].pos ?? keys[0].time ?? 0)) { + return extractValue(keys[0].value); + } + return extractValue(keys[keys.length - 1].value); } /** * Interpolate color between two gradient keys */ export function interpolateColorKeys(keys: VFXGradientKey[], ratio: number): { r: number; g: number; b: number; a: number } { - if (!keys || keys.length === 0) { - return { r: 1, g: 1, b: 1, a: 1 }; - } - - if (keys.length === 1) { - const val = keys[0].value; - return { - ...extractColorFromValue(val), - a: extractAlphaFromValue(val), - }; - } - - // Find the two keys to interpolate between - for (let i = 0; i < keys.length - 1; i++) { - const pos1 = keys[i].pos ?? keys[i].time ?? 0; - const pos2 = keys[i + 1].pos ?? keys[i + 1].time ?? 1; - - if (ratio >= pos1 && ratio <= pos2) { - const t = pos2 - pos1 !== 0 ? (ratio - pos1) / (pos2 - pos1) : 0; - const val1 = keys[i].value; - const val2 = keys[i + 1].value; - - const c1 = extractColorFromValue(val1); - const c2 = extractColorFromValue(val2); - const a1 = extractAlphaFromValue(val1); - const a2 = extractAlphaFromValue(val2); - - return { - r: c1.r + (c2.r - c1.r) * t, - g: c1.g + (c2.g - c1.g) * t, - b: c1.b + (c2.b - c1.b) * t, - a: a1 + (a2 - a1) * t, - }; - } - } - - // Clamp to first or last key - if (ratio <= (keys[0].pos ?? keys[0].time ?? 0)) { - const val = keys[0].value; - return { - ...extractColorFromValue(val), - a: extractAlphaFromValue(val), - }; - } - const val = keys[keys.length - 1].value; - return { - ...extractColorFromValue(val), - a: extractAlphaFromValue(val), - }; + if (!keys || keys.length === 0) { + return { r: 1, g: 1, b: 1, a: 1 }; + } + + if (keys.length === 1) { + const val = keys[0].value; + return { + ...extractColorFromValue(val), + a: extractAlphaFromValue(val), + }; + } + + // Find the two keys to interpolate between + for (let i = 0; i < keys.length - 1; i++) { + const pos1 = keys[i].pos ?? keys[i].time ?? 0; + const pos2 = keys[i + 1].pos ?? keys[i + 1].time ?? 1; + + if (ratio >= pos1 && ratio <= pos2) { + const t = pos2 - pos1 !== 0 ? (ratio - pos1) / (pos2 - pos1) : 0; + const val1 = keys[i].value; + const val2 = keys[i + 1].value; + + const c1 = extractColorFromValue(val1); + const c2 = extractColorFromValue(val2); + const a1 = extractAlphaFromValue(val1); + const a2 = extractAlphaFromValue(val2); + + return { + r: c1.r + (c2.r - c1.r) * t, + g: c1.g + (c2.g - c1.g) * t, + b: c1.b + (c2.b - c1.b) * t, + a: a1 + (a2 - a1) * t, + }; + } + } + + // Clamp to first or last key + if (ratio <= (keys[0].pos ?? keys[0].time ?? 0)) { + const val = keys[0].value; + return { + ...extractColorFromValue(val), + a: extractAlphaFromValue(val), + }; + } + const val = keys[keys.length - 1].value; + return { + ...extractColorFromValue(val), + a: extractAlphaFromValue(val), + }; } - diff --git a/editor/src/editor/windows/fx-editor/VFX/factories/VFXBehaviorFunctionFactory.ts b/editor/src/editor/windows/fx-editor/VFX/factories/VFXBehaviorFunctionFactory.ts index 80093a03e..e436d7e96 100644 --- a/editor/src/editor/windows/fx-editor/VFX/factories/VFXBehaviorFunctionFactory.ts +++ b/editor/src/editor/windows/fx-editor/VFX/factories/VFXBehaviorFunctionFactory.ts @@ -2,188 +2,187 @@ import type { Particle } from "@babylonjs/core/Particles/particle"; import type { SolidParticle } from "@babylonjs/core/Particles/solidParticle"; import type { ParticleSystem } from "@babylonjs/core/Particles/particleSystem"; import type { - VFXBehavior, - VFXColorBySpeedBehavior, - VFXSizeBySpeedBehavior, - VFXRotationBySpeedBehavior, - VFXOrbitOverLifeBehavior, - VFXForceOverLifeBehavior, + VFXBehavior, + VFXColorBySpeedBehavior, + VFXSizeBySpeedBehavior, + VFXRotationBySpeedBehavior, + VFXOrbitOverLifeBehavior, + VFXForceOverLifeBehavior, } from "../types/behaviors"; import type { VFXValueParser } from "../parsers/VFXValueParser"; import type { VFXPerParticleBehaviorFunction, VFXPerSolidParticleBehaviorFunction, VFXSystemBehaviorFunction, VFXPerParticleContext } from "../types/VFXBehaviorFunction"; import { - applyColorOverLifeSPS, - applySizeOverLifeSPS, - applyRotationOverLifeSPS, - applySpeedOverLifeSPS, - applyColorBySpeedSPS, - applySizeBySpeedSPS, - applyRotationBySpeedSPS, - applyOrbitOverLifeSPS, - applyColorBySpeedPS, - applySizeBySpeedPS, - applyRotationBySpeedPS, - applyOrbitOverLifePS, + applyColorOverLifeSPS, + applySizeOverLifeSPS, + applyRotationOverLifeSPS, + applySpeedOverLifeSPS, + applyColorBySpeedSPS, + applySizeBySpeedSPS, + applyRotationBySpeedSPS, + applyOrbitOverLifeSPS, + applyColorBySpeedPS, + applySizeBySpeedPS, + applyRotationBySpeedPS, + applyOrbitOverLifePS, } from "../behaviors"; export class VFXBehaviorFunctionFactory { - public static createPerParticleFunctionsSPS(behaviors: VFXBehavior[], valueParser: VFXValueParser): VFXPerSolidParticleBehaviorFunction[] { - const functions: VFXPerSolidParticleBehaviorFunction[] = []; - - for (const behavior of behaviors) { - switch (behavior.type) { - case "ColorOverLife": { - const b = behavior as any; - functions.push((particle: SolidParticle, context: VFXPerParticleContext) => { - applyColorOverLifeSPS(particle, b, context.lifeRatio); - }); - break; - } - - case "SizeOverLife": { - const b = behavior as any; - functions.push((particle: SolidParticle, context: VFXPerParticleContext) => { - applySizeOverLifeSPS(particle, b, context.lifeRatio); - }); - break; - } - - case "RotationOverLife": - case "Rotation3DOverLife": { - const b = behavior as any; - functions.push((particle: SolidParticle, context: VFXPerParticleContext) => { - applyRotationOverLifeSPS(particle, b, context.lifeRatio, valueParser, context.updateSpeed); - }); - break; - } - - case "ForceOverLife": - case "ApplyForce": { - const b = behavior as VFXForceOverLifeBehavior; - functions.push((particle: SolidParticle, context: VFXPerParticleContext) => { - const forceX = b.x ?? b.force?.x; - const forceY = b.y ?? b.force?.y; - const forceZ = b.z ?? b.force?.z; - if (forceX !== undefined || forceY !== undefined || forceZ !== undefined) { - const fx = forceX !== undefined ? valueParser.parseConstantValue(forceX) : 0; - const fy = forceY !== undefined ? valueParser.parseConstantValue(forceY) : 0; - const fz = forceZ !== undefined ? valueParser.parseConstantValue(forceZ) : 0; - particle.velocity.x += fx * context.updateSpeed; - particle.velocity.y += fy * context.updateSpeed; - particle.velocity.z += fz * context.updateSpeed; - } - }); - break; - } - - case "SpeedOverLife": { - const b = behavior as any; - functions.push((particle: SolidParticle, context: VFXPerParticleContext) => { - applySpeedOverLifeSPS(particle, b, context.lifeRatio, valueParser); - }); - break; - } - - case "ColorBySpeed": { - const b = behavior as VFXColorBySpeedBehavior; - functions.push((particle: SolidParticle, context: VFXPerParticleContext) => { - applyColorBySpeedSPS(particle, b, context.startSpeed, valueParser); - }); - break; - } - - case "SizeBySpeed": { - const b = behavior as VFXSizeBySpeedBehavior; - functions.push((particle: SolidParticle, context: VFXPerParticleContext) => { - applySizeBySpeedSPS(particle, b, context.startSpeed, valueParser); - }); - break; - } - - case "RotationBySpeed": { - const b = behavior as VFXRotationBySpeedBehavior; - functions.push((particle: SolidParticle, context: VFXPerParticleContext) => { - applyRotationBySpeedSPS(particle, b, context.startSpeed, valueParser, context.updateSpeed); - }); - break; - } - - case "OrbitOverLife": { - const b = behavior as VFXOrbitOverLifeBehavior; - functions.push((particle: SolidParticle, context: VFXPerParticleContext) => { - applyOrbitOverLifeSPS(particle, b, context.lifeRatio, valueParser); - }); - break; - } - } - } - - return functions; - } - - public static createPerParticleFunctionsPS(behaviors: VFXBehavior[], valueParser: VFXValueParser, particleSystem: ParticleSystem): VFXPerParticleBehaviorFunction[] { - const functions: VFXPerParticleBehaviorFunction[] = []; - - for (const behavior of behaviors) { - switch (behavior.type) { - case "ColorBySpeed": { - const b = behavior as VFXColorBySpeedBehavior; - functions.push((particle: Particle, context: VFXPerParticleContext) => { - applyColorBySpeedPS(particle as any, b, context.startSpeed, valueParser); - }); - break; - } - - case "SizeBySpeed": { - const b = behavior as VFXSizeBySpeedBehavior; - functions.push((particle: Particle, context: VFXPerParticleContext) => { - applySizeBySpeedPS(particle as any, b, context.startSpeed, valueParser); - }); - break; - } - - case "RotationBySpeed": { - const b = behavior as VFXRotationBySpeedBehavior; - functions.push((particle: Particle, context: VFXPerParticleContext) => { - applyRotationBySpeedPS(particle as any, b, context.startSpeed, particleSystem, valueParser); - }); - break; - } - - case "OrbitOverLife": { - const b = behavior as VFXOrbitOverLifeBehavior; - functions.push((particle: Particle, context: VFXPerParticleContext) => { - applyOrbitOverLifePS(particle, b, context.lifeRatio, valueParser); - }); - break; - } - } - } - - return functions; - } - - public static createSystemFunctions(behaviors: VFXBehavior[], _valueParser: VFXValueParser): VFXSystemBehaviorFunction[] { - const functions: VFXSystemBehaviorFunction[] = []; - - for (const behavior of behaviors) { - switch (behavior.type) { - case "ColorOverLife": - case "SizeOverLife": - case "RotationOverLife": - case "Rotation3DOverLife": - case "ForceOverLife": - case "ApplyForce": - case "GravityForce": - case "SpeedOverLife": - case "FrameOverLife": - case "LimitSpeedOverLife": - // handled at emitter level - break; - } - } - - return functions; - } + public static createPerParticleFunctionsSPS(behaviors: VFXBehavior[], valueParser: VFXValueParser): VFXPerSolidParticleBehaviorFunction[] { + const functions: VFXPerSolidParticleBehaviorFunction[] = []; + + for (const behavior of behaviors) { + switch (behavior.type) { + case "ColorOverLife": { + const b = behavior as any; + functions.push((particle: SolidParticle, context: VFXPerParticleContext) => { + applyColorOverLifeSPS(particle, b, context.lifeRatio); + }); + break; + } + + case "SizeOverLife": { + const b = behavior as any; + functions.push((particle: SolidParticle, context: VFXPerParticleContext) => { + applySizeOverLifeSPS(particle, b, context.lifeRatio); + }); + break; + } + + case "RotationOverLife": + case "Rotation3DOverLife": { + const b = behavior as any; + functions.push((particle: SolidParticle, context: VFXPerParticleContext) => { + applyRotationOverLifeSPS(particle, b, context.lifeRatio, valueParser, context.updateSpeed); + }); + break; + } + + case "ForceOverLife": + case "ApplyForce": { + const b = behavior as VFXForceOverLifeBehavior; + functions.push((particle: SolidParticle, context: VFXPerParticleContext) => { + const forceX = b.x ?? b.force?.x; + const forceY = b.y ?? b.force?.y; + const forceZ = b.z ?? b.force?.z; + if (forceX !== undefined || forceY !== undefined || forceZ !== undefined) { + const fx = forceX !== undefined ? valueParser.parseConstantValue(forceX) : 0; + const fy = forceY !== undefined ? valueParser.parseConstantValue(forceY) : 0; + const fz = forceZ !== undefined ? valueParser.parseConstantValue(forceZ) : 0; + particle.velocity.x += fx * context.updateSpeed; + particle.velocity.y += fy * context.updateSpeed; + particle.velocity.z += fz * context.updateSpeed; + } + }); + break; + } + + case "SpeedOverLife": { + const b = behavior as any; + functions.push((particle: SolidParticle, context: VFXPerParticleContext) => { + applySpeedOverLifeSPS(particle, b, context.lifeRatio, valueParser); + }); + break; + } + + case "ColorBySpeed": { + const b = behavior as VFXColorBySpeedBehavior; + functions.push((particle: SolidParticle, context: VFXPerParticleContext) => { + applyColorBySpeedSPS(particle, b, context.startSpeed, valueParser); + }); + break; + } + + case "SizeBySpeed": { + const b = behavior as VFXSizeBySpeedBehavior; + functions.push((particle: SolidParticle, context: VFXPerParticleContext) => { + applySizeBySpeedSPS(particle, b, context.startSpeed, valueParser); + }); + break; + } + + case "RotationBySpeed": { + const b = behavior as VFXRotationBySpeedBehavior; + functions.push((particle: SolidParticle, context: VFXPerParticleContext) => { + applyRotationBySpeedSPS(particle, b, context.startSpeed, valueParser, context.updateSpeed); + }); + break; + } + + case "OrbitOverLife": { + const b = behavior as VFXOrbitOverLifeBehavior; + functions.push((particle: SolidParticle, context: VFXPerParticleContext) => { + applyOrbitOverLifeSPS(particle, b, context.lifeRatio, valueParser); + }); + break; + } + } + } + + return functions; + } + + public static createPerParticleFunctionsPS(behaviors: VFXBehavior[], valueParser: VFXValueParser, particleSystem: ParticleSystem): VFXPerParticleBehaviorFunction[] { + const functions: VFXPerParticleBehaviorFunction[] = []; + + for (const behavior of behaviors) { + switch (behavior.type) { + case "ColorBySpeed": { + const b = behavior as VFXColorBySpeedBehavior; + functions.push((particle: Particle, context: VFXPerParticleContext) => { + applyColorBySpeedPS(particle as any, b, context.startSpeed, valueParser); + }); + break; + } + + case "SizeBySpeed": { + const b = behavior as VFXSizeBySpeedBehavior; + functions.push((particle: Particle, context: VFXPerParticleContext) => { + applySizeBySpeedPS(particle as any, b, context.startSpeed, valueParser); + }); + break; + } + + case "RotationBySpeed": { + const b = behavior as VFXRotationBySpeedBehavior; + functions.push((particle: Particle, context: VFXPerParticleContext) => { + applyRotationBySpeedPS(particle as any, b, context.startSpeed, particleSystem, valueParser); + }); + break; + } + + case "OrbitOverLife": { + const b = behavior as VFXOrbitOverLifeBehavior; + functions.push((particle: Particle, context: VFXPerParticleContext) => { + applyOrbitOverLifePS(particle, b, context.lifeRatio, valueParser); + }); + break; + } + } + } + + return functions; + } + + public static createSystemFunctions(behaviors: VFXBehavior[], _valueParser: VFXValueParser): VFXSystemBehaviorFunction[] { + const functions: VFXSystemBehaviorFunction[] = []; + + for (const behavior of behaviors) { + switch (behavior.type) { + case "ColorOverLife": + case "SizeOverLife": + case "RotationOverLife": + case "Rotation3DOverLife": + case "ForceOverLife": + case "ApplyForce": + case "GravityForce": + case "SpeedOverLife": + case "FrameOverLife": + case "LimitSpeedOverLife": + // handled at emitter level + break; + } + } + + return functions; + } } - diff --git a/editor/src/editor/windows/fx-editor/VFX/factories/VFXEmitterFactory.ts b/editor/src/editor/windows/fx-editor/VFX/factories/VFXEmitterFactory.ts index 7731d9af4..8773da440 100644 --- a/editor/src/editor/windows/fx-editor/VFX/factories/VFXEmitterFactory.ts +++ b/editor/src/editor/windows/fx-editor/VFX/factories/VFXEmitterFactory.ts @@ -16,581 +16,581 @@ import { VFXSolidParticleSystem } from "../systems/VFXSolidParticleSystem"; import { VFXParticleSystem } from "../systems/VFXParticleSystem"; import { VFXBehaviorFunctionFactory } from "./VFXBehaviorFunctionFactory"; import { - applyColorOverLifePS, - applySizeOverLifePS, - applyRotationOverLifePS, - applyForceOverLifePS, - applyGravityForcePS, - applySpeedOverLifePS, - applyFrameOverLifePS, - applyLimitSpeedOverLifePS, + applyColorOverLifePS, + applySizeOverLifePS, + applyRotationOverLifePS, + applyForceOverLifePS, + applyGravityForcePS, + applySpeedOverLifePS, + applyFrameOverLifePS, + applyLimitSpeedOverLifePS, } from "../behaviors"; /** * Factory for creating particle emitters (ParticleSystem and SolidParticleSystem) */ export class VFXEmitterFactory { - private _logger: VFXLogger; - private _context: VFXParseContext; - private _valueParser: VFXValueParser; - private _materialFactory: IVFXMaterialFactory; - private _geometryFactory: IVFXGeometryFactory; - - constructor(context: VFXParseContext, valueParser: VFXValueParser, materialFactory: IVFXMaterialFactory, geometryFactory: IVFXGeometryFactory) { - this._context = context; - this._logger = new VFXLogger("[VFXEmitterFactory]"); - this._valueParser = valueParser; - this._materialFactory = materialFactory; - this._geometryFactory = geometryFactory; - } - - /** - * Create a particle emitter from emitter data - */ - public createEmitter(emitterData: VFXEmitterData): Nullable { - const { config } = emitterData; - const { options } = this._context; - - // Check if we need SolidParticleSystem (mesh-based particles) - const useSolidParticles = config.renderMode === 2; - this._logger.log(`Using ${useSolidParticles ? "SolidParticleSystem" : "ParticleSystem"}`, options); - - if (useSolidParticles) { - return this._createSolidParticleSystem(emitterData); - } else { - return this._createParticleSystem(emitterData); - } - } - - /** - * Create a ParticleSystem (billboard-based particles) - */ - private _createParticleSystem(emitterData: VFXEmitterData): Nullable { - const { name, config } = emitterData; - const { scene, options } = this._context; - - this._logger.log(`Creating ParticleSystem: ${name}`, options); - - // Calculate capacity based on emission rate and duration - const emissionRate = config.emissionOverTime !== undefined ? this._valueParser.parseConstantValue(config.emissionOverTime) : 10; - const duration = config.duration || 5; - const capacity = Math.ceil(emissionRate * duration * 2); // Add some buffer - this._logger.log(` Emission rate: ${emissionRate}, Duration: ${duration}, Capacity: ${capacity}`, options); - - // Parse life time - const lifeTime = config.startLife !== undefined ? this._valueParser.parseIntervalValue(config.startLife) : { min: 1, max: 1 }; - this._logger.log(` Life time: ${lifeTime.min} - ${lifeTime.max}`, options); - - // Parse speed - const speed = config.startSpeed !== undefined ? this._valueParser.parseIntervalValue(config.startSpeed) : { min: 1, max: 1 }; - const avgStartSpeed = (speed.min + speed.max) / 2; - this._logger.log(` Speed: ${speed.min} - ${speed.max}`, options); - - // Parse size - const size = config.startSize !== undefined ? this._valueParser.parseIntervalValue(config.startSize) : { min: 1, max: 1 }; - const avgStartSize = (size.min + size.max) / 2; - this._logger.log(` Size: ${size.min} - ${size.max}`, options); - - // Parse start color - const startColor = config.startColor !== undefined ? this._valueParser.parseConstantColor(config.startColor) : new Color4(1, 1, 1, 1); - this._logger.log(` Start color: R=${startColor.r}, G=${startColor.g}, B=${startColor.b}, A=${startColor.a}`, options); - - // Create VFXParticleSystem instead of regular ParticleSystem - const particleSystem = new VFXParticleSystem(name, capacity, scene, this._valueParser, avgStartSpeed, avgStartSize, startColor); - - // Set basic properties - particleSystem.targetStopDuration = duration; - particleSystem.emitRate = emissionRate; - particleSystem.manualEmitCount = -1; - - // Set life time - particleSystem.minLifeTime = lifeTime.min; - particleSystem.maxLifeTime = lifeTime.max; - - // Set speed and size - particleSystem.minEmitPower = speed.min; - particleSystem.maxEmitPower = speed.max; - particleSystem.minSize = size.min; - particleSystem.maxSize = size.max; - - // Set colors - particleSystem.color1 = startColor; - particleSystem.color2 = startColor; - particleSystem.colorDead = new Color4(startColor.r, startColor.g, startColor.b, 0); - - // Parse start rotation - if (config.startRotation) { - if (typeof config.startRotation === "object" && config.startRotation !== null && "type" in config.startRotation && config.startRotation.type === "Euler") { - const eulerRotation = config.startRotation; - if (eulerRotation.angleZ !== undefined) { - const angleZ = this._valueParser.parseIntervalValue(eulerRotation.angleZ); - particleSystem.minInitialRotation = angleZ.min; - particleSystem.maxInitialRotation = angleZ.max; - } - } else { - const rotation = this._valueParser.parseIntervalValue(config.startRotation); - particleSystem.minInitialRotation = rotation.min; - particleSystem.maxInitialRotation = rotation.max; - } - } - - // Set sprite tiles if specified - if (config.uTileCount !== undefined && config.vTileCount !== undefined) { - if (config.uTileCount > 1 || config.vTileCount > 1) { - particleSystem.isAnimationSheetEnabled = true; - particleSystem.spriteCellWidth = config.uTileCount; - particleSystem.spriteCellHeight = config.vTileCount; - if (config.startTileIndex !== undefined) { - const startTile = this._valueParser.parseConstantValue(config.startTileIndex); - particleSystem.startSpriteCellID = Math.floor(startTile); - particleSystem.endSpriteCellID = Math.floor(startTile); - } - } - } - - // Set render order and layers - if (config.renderOrder !== undefined) { - particleSystem.renderingGroupId = config.renderOrder; - } - if (config.layers !== undefined) { - particleSystem.layerMask = config.layers; - } - - // Set emitter shape (pass matrix to extract rotation for emitter direction) - this._setEmitterShape(particleSystem, config.shape, emitterData.cumulativeScale, emitterData.matrix, options); - - // Load texture (ParticleSystem only needs texture, not material) - if (emitterData.materialId) { - const texture = this._materialFactory.createTexture(emitterData.materialId); - if (texture) { - particleSystem.particleTexture = texture; - // Get blend mode from material - const { jsonData } = this._context; - const material = jsonData.materials?.find((m: any) => m.uuid === emitterData.materialId); - if (material?.blending !== undefined) { - if (material.blending === 2) { - // Additive blending (Three.js AdditiveBlending) - particleSystem.blendMode = Constants.ALPHA_ADD; - } else if (material.blending === 1) { - // Normal blending (Three.js NormalBlending) - particleSystem.blendMode = Constants.ALPHA_COMBINE; - } else if (material.blending === 0) { - // No blending (Three.js NoBlending) - particleSystem.blendMode = Constants.ALPHA_DISABLE; - } - } - } - } - - // Handle emission bursts - if (config.emissionBursts && Array.isArray(config.emissionBursts) && config.emissionBursts.length > 0) { - this._applyEmissionBursts(particleSystem, config.emissionBursts, emissionRate, duration, options); - } - - // Apply behaviors - if (config.behaviors && Array.isArray(config.behaviors) && config.behaviors.length > 0) { - this._applyBehaviorsToPS(particleSystem, config.behaviors); - } - - // Set world space - if (config.worldSpace !== undefined) { - particleSystem.isLocal = !config.worldSpace; - this._logger.log(` World space: ${config.worldSpace}`, options); - } - - // Set looping - if (config.looping !== undefined) { - particleSystem.targetStopDuration = config.looping ? 0 : duration; - this._logger.log(` Looping: ${config.looping}`, options); - } - - // Set render mode - if (config.renderMode !== undefined) { - if (config.renderMode === 0) { - particleSystem.isBillboardBased = true; - this._logger.log(` Render mode: Billboard`, options); - } else if (config.renderMode === 1) { - particleSystem.billboardMode = ParticleSystem.BILLBOARDMODE_STRETCHED; - this._logger.log(` Render mode: Stretched Billboard`, options); - } - } - - // Set soft particles and auto destroy - if (config.softParticles !== undefined) { - this._logger.log(` Soft particles: ${config.softParticles} (not fully supported)`, options); - } - if (config.autoDestroy !== undefined) { - particleSystem.disposeOnStop = config.autoDestroy; - this._logger.log(` Auto destroy: ${config.autoDestroy}`, options); - } - - this._logger.log(`ParticleSystem created: ${name}`, options); - return particleSystem; - } - - /** - * Create a SolidParticleSystem (mesh-based particles) - */ - private _createSolidParticleSystem(emitterData: VFXEmitterData): Nullable { - const { name, config } = emitterData; - const { scene, options } = this._context; - - this._logger.log(`Creating SolidParticleSystem: ${name}`, options); - - // Calculate capacity based on emission rate and particle lifetime - // duration = particle lifetime (how long each particle lives) - // startLife = when particle becomes "alive" (for behaviors that depend on age) - // emissionOverTime = particles per second (e.g., 2.5 means 2.5 particles per second) - const emissionRate = config.emissionOverTime !== undefined ? this._valueParser.parseConstantValue(config.emissionOverTime) : 10; // particles per second - const particleLifetime = config.duration || 5; // duration is the particle lifetime - const isLooping = config.looping !== false; - - let capacity: number; - if (isLooping) { - // For looping systems: capacity = emissionRate * particleLifetime - // This gives the steady-state number of particles needed for perfect looping - // Example: emissionRate=2.5 particles/sec, particleLifetime=5 sec - // -> capacity = 2.5 * 5 = 12.5 -> 13 particles - // This ensures we have enough particles to cover the lifetime at the emission rate - capacity = Math.ceil(emissionRate * particleLifetime); - // Ensure minimum capacity of at least 1 - capacity = Math.max(capacity, 1); - this._logger.log(` Looping system: Emission rate: ${emissionRate} particles/sec, Particle lifetime: ${particleLifetime} sec, Capacity: ${capacity}`, options); - } else { - // For non-looping: capacity = emissionRate * particleLifetime * 2 (buffer for particles still alive) - capacity = Math.ceil(emissionRate * particleLifetime * 2); - this._logger.log(` Non-looping system: Emission rate: ${emissionRate} particles/sec, Particle lifetime: ${particleLifetime} sec, Capacity: ${capacity}`, options); - } - - // Get VFX transform from emitter data (stored during conversion) - // This is the clean way - transform is already in left-handed coordinate system - let vfxTransform: { position: Vector3; rotation: Quaternion; scale: Vector3 } | null = null; - const vfxEmitter = emitterData.vfxEmitter; - if (vfxEmitter && vfxEmitter.transform) { - vfxTransform = vfxEmitter.transform; - } - - const sps = new VFXSolidParticleSystem(name, scene, config, this._valueParser, { - updatable: true, - isPickable: false, - enableDepthSort: false, - particleIntersection: false, - useModelMaterial: true, - parentGroup: emitterData.parentGroup, - vfxTransform: vfxTransform, - logger: this._logger, - loaderOptions: options, - }); - - // Load geometry for particle shape - let particleMesh: Nullable = null; - if (config.instancingGeometry) { - this._logger.log(` Loading geometry: ${config.instancingGeometry}`, options); - particleMesh = this._geometryFactory.createMesh(config.instancingGeometry, emitterData.materialId, name + "_shape"); - if (!particleMesh) { - this._logger.warn(` Failed to load geometry ${config.instancingGeometry}, will create default plane`, options); - } - } - - // Default to plane if no geometry found - if (!particleMesh) { - this._logger.log(` Creating default plane geometry`, options); - particleMesh = CreatePlane(name + "_shape", { width: 1, height: 1 }, scene); - if (emitterData.materialId && particleMesh) { - const particleMaterial = this._materialFactory.createMaterial(emitterData.materialId, name); - if (particleMaterial) { - particleMesh.material = particleMaterial; - } - } - } else { - // Ensure material is applied - if (emitterData.materialId && particleMesh && !particleMesh.material) { - const particleMaterial = this._materialFactory.createMaterial(emitterData.materialId, name); - if (particleMaterial) { - particleMesh.material = particleMaterial; - } - } - } - - if (!particleMesh) { - this._logger.warn(` Cannot add shape to SPS: particleMesh is null`, options); - return null; - } - - this._logger.log(` Adding shape to SPS: mesh name=${particleMesh.name}, hasMaterial=${!!particleMesh.material}`, options); - sps.addShape(particleMesh, capacity); - - // Set billboard mode if needed - if (config.renderMode === 0 || config.renderMode === 1) { - sps.billboard = true; - } - - // Apply behaviors to SPS - if (config.behaviors && Array.isArray(config.behaviors) && config.behaviors.length > 0) { - this._applyBehaviorsToSPS(sps, config.behaviors); - this._logger.log(` Set SPS behaviors (${config.behaviors.length})`, options); - } - - // Cleanup temporary mesh - if (particleMesh) { - particleMesh.dispose(); - } - - this._logger.log(`SolidParticleSystem created: ${name}`, options); - return sps; - } - - /** - * Set the emitter shape based on Three.js shape configuration - * @param matrix Optional 4x4 matrix array from Three.js to extract rotation - */ - private _setEmitterShape(particleSystem: ParticleSystem, shape: any, cumulativeScale: Vector3, matrix?: number[], options?: VFXLoaderOptions): void { - if (!shape || !shape.type) { - particleSystem.createPointEmitter(Vector3.Zero(), Vector3.Zero()); - return; - } - - const scaleX = cumulativeScale.x; - const scaleY = cumulativeScale.y; - const scaleZ = cumulativeScale.z; - - // Extract rotation from matrix if provided - let rotationMatrix: Matrix | null = null; - if (matrix && matrix.length >= 16) { - // Three.js uses column-major order, Babylon.js uses row-major - const mat = Matrix.FromArray(matrix); - mat.transpose(); - - // Extract rotation matrix (remove scale and translation) - rotationMatrix = mat.getRotationMatrix(); - this._logger.log(` Extracted rotation from matrix`, options); - } - - // Helper function to apply rotation to default direction - const applyRotation = (defaultDir: Vector3): Vector3 => { - if (rotationMatrix) { - const rotatedDir = Vector3.Zero(); - Vector3.TransformNormalToRef(defaultDir, rotationMatrix, rotatedDir); - return rotatedDir; - } - return defaultDir; - }; - - switch (shape.type.toLowerCase()) { - case "cone": { - let radius = shape.radius || 1; - const angle = shape.angle !== undefined ? shape.angle : Math.PI / 4; - const coneScale = (scaleX + scaleZ) / 2; - radius = radius * coneScale; - - // Default direction for cone is up (0, 1, 0) - const defaultDir = new Vector3(0, 1, 0); - const rotatedDir = applyRotation(defaultDir); - - if (rotationMatrix) { - // Use directed emitter with rotated direction - particleSystem.createDirectedConeEmitter(radius, angle, rotatedDir, rotatedDir); - this._logger.log( - ` Created directed cone emitter with rotated direction: (${rotatedDir.x.toFixed(2)}, ${rotatedDir.y.toFixed(2)}, ${rotatedDir.z.toFixed(2)})`, - options - ); - } else { - particleSystem.createConeEmitter(radius, angle); - } - break; - } - - case "sphere": { - let sphereRadius = shape.radius || 1; - const sphereScale = (scaleX + scaleY + scaleZ) / 3; - sphereRadius = sphereRadius * sphereScale; - - // Default direction for sphere is up (0, 1, 0) - const defaultDir = new Vector3(0, 1, 0); - const rotatedDir = applyRotation(defaultDir); - - if (rotationMatrix) { - particleSystem.createDirectedSphereEmitter(sphereRadius, rotatedDir, rotatedDir); - this._logger.log( - ` Created directed sphere emitter with rotated direction: (${rotatedDir.x.toFixed(2)}, ${rotatedDir.y.toFixed(2)}, ${rotatedDir.z.toFixed(2)})`, - options - ); - } else { - particleSystem.createSphereEmitter(sphereRadius); - } - break; - } - - case "point": { - const defaultDir = new Vector3(0, 1, 0); - const rotatedDir = applyRotation(defaultDir); - - if (rotationMatrix) { - particleSystem.createPointEmitter(rotatedDir, rotatedDir); - this._logger.log( - ` Created point emitter with rotated direction: (${rotatedDir.x.toFixed(2)}, ${rotatedDir.y.toFixed(2)}, ${rotatedDir.z.toFixed(2)})`, - options - ); - } else { - particleSystem.createPointEmitter(Vector3.Zero(), Vector3.Zero()); - } - break; - } - - case "box": { - let boxSize = shape.size || [1, 1, 1]; - boxSize = [boxSize[0] * scaleX, boxSize[1] * scaleY, boxSize[2] * scaleZ]; - const minBox = new Vector3(-boxSize[0] / 2, -boxSize[1] / 2, -boxSize[2] / 2); - const maxBox = new Vector3(boxSize[0] / 2, boxSize[1] / 2, boxSize[2] / 2); - - const defaultDir = new Vector3(0, 1, 0); - const rotatedDir = applyRotation(defaultDir); - - if (rotationMatrix) { - particleSystem.createBoxEmitter(rotatedDir, rotatedDir, minBox, maxBox); - this._logger.log(` Created box emitter with rotated direction: (${rotatedDir.x.toFixed(2)}, ${rotatedDir.y.toFixed(2)}, ${rotatedDir.z.toFixed(2)})`, options); - } else { - particleSystem.createBoxEmitter(Vector3.Zero(), Vector3.Zero(), minBox, maxBox); - } - break; - } - - case "hemisphere": { - let hemRadius = shape.radius || 1; - const hemScale = (scaleX + scaleY + scaleZ) / 3; - hemRadius = hemRadius * hemScale; - particleSystem.createHemisphericEmitter(hemRadius); - break; - } - - case "cylinder": { - let cylRadius = shape.radius || 1; - let height = shape.height || 1; - const cylRadiusScale = (scaleX + scaleZ) / 2; - cylRadius = cylRadius * cylRadiusScale; - height = height * scaleY; - - // Default direction for cylinder is up (0, 1, 0) - const defaultDir = new Vector3(0, 1, 0); - const rotatedDir = applyRotation(defaultDir); - - if (rotationMatrix) { - particleSystem.createDirectedCylinderEmitter(cylRadius, height, 1, rotatedDir, rotatedDir); - this._logger.log( - ` Created directed cylinder emitter with rotated direction: (${rotatedDir.x.toFixed(2)}, ${rotatedDir.y.toFixed(2)}, ${rotatedDir.z.toFixed(2)})`, - options - ); - } else { - particleSystem.createCylinderEmitter(cylRadius, height); - } - break; - } - - default: { - const defaultDir = new Vector3(0, 1, 0); - const rotatedDir = applyRotation(defaultDir); - - if (rotationMatrix) { - particleSystem.createPointEmitter(rotatedDir, rotatedDir); - } else { - particleSystem.createPointEmitter(Vector3.Zero(), Vector3.Zero()); - } - break; - } - } - } - - /** - * Apply emission bursts via emit rate gradients - */ - private _applyEmissionBursts( - particleSystem: ParticleSystem, - bursts: import("../types/emitterConfig").VFXEmissionBurst[], - baseEmitRate: number, - duration: number, - _options?: VFXLoaderOptions - ): void { - for (const burst of bursts) { - if (burst.time !== undefined && burst.count !== undefined) { - const burstTime = this._valueParser.parseConstantValue(burst.time); - const burstCount = this._valueParser.parseConstantValue(burst.count); - const timeRatio = Math.min(Math.max(burstTime / duration, 0), 1); - - const windowSize = 0.02; - const burstEmitRate = burstCount / windowSize; - - const beforeTime = Math.max(0, timeRatio - windowSize); - const afterTime = Math.min(1, timeRatio + windowSize); - - particleSystem.addEmitRateGradient(beforeTime, baseEmitRate); - particleSystem.addEmitRateGradient(timeRatio, burstEmitRate); - particleSystem.addEmitRateGradient(afterTime, baseEmitRate); - } - } - } - - /** - * Apply behaviors to ParticleSystem - */ - private _applyBehaviorsToPS(particleSystem: ParticleSystem, behaviors: import("../types/behaviors").VFXBehavior[]): void { - const { options } = this._context; - const valueParser = this._valueParser; - - const vfxPS = particleSystem as any as VFXParticleSystem; - if (vfxPS && typeof vfxPS.setPerParticleBehaviors === "function") { - // Apply system-level behaviors (gradients, etc.) - for (const behavior of behaviors) { - if (!behavior.type) { - this._logger.warn(`Behavior missing type: ${JSON.stringify(behavior)}`, options); - continue; - } - - this._logger.log(` Processing behavior: ${behavior.type}`, options); - - switch (behavior.type) { - case "ColorOverLife": - applyColorOverLifePS(particleSystem, behavior as any); - break; - case "SizeOverLife": - applySizeOverLifePS(particleSystem, behavior as any); - break; - case "RotationOverLife": - case "Rotation3DOverLife": - applyRotationOverLifePS(particleSystem, behavior as any, valueParser); - break; - case "ForceOverLife": - case "ApplyForce": - applyForceOverLifePS(particleSystem, behavior as any, valueParser); - break; - case "GravityForce": - applyGravityForcePS(particleSystem, behavior as any, valueParser); - break; - case "SpeedOverLife": - applySpeedOverLifePS(particleSystem, behavior as any, valueParser); - break; - case "FrameOverLife": - applyFrameOverLifePS(particleSystem, behavior as any, valueParser); - break; - case "LimitSpeedOverLife": - applyLimitSpeedOverLifePS(particleSystem, behavior as any, valueParser); - break; - } - } - - // Create and set per-particle behavior functions - const perParticleFunctions = VFXBehaviorFunctionFactory.createPerParticleFunctionsPS(behaviors, valueParser, particleSystem); - vfxPS.setPerParticleBehaviors(perParticleFunctions); - } - } - - /** - * Apply behaviors to SolidParticleSystem - */ - private _applyBehaviorsToSPS(sps: SolidParticleSystem, behaviors: import("../types/behaviors").VFXBehavior[]): void { - const vfxSPS = sps as any as VFXSolidParticleSystem; - if (vfxSPS && typeof vfxSPS.setPerParticleBehaviors === "function") { - const perParticleFunctions = VFXBehaviorFunctionFactory.createPerParticleFunctionsSPS(behaviors, this._valueParser); - vfxSPS.setPerParticleBehaviors(perParticleFunctions); - } - } + private _logger: VFXLogger; + private _context: VFXParseContext; + private _valueParser: VFXValueParser; + private _materialFactory: IVFXMaterialFactory; + private _geometryFactory: IVFXGeometryFactory; + + constructor(context: VFXParseContext, valueParser: VFXValueParser, materialFactory: IVFXMaterialFactory, geometryFactory: IVFXGeometryFactory) { + this._context = context; + this._logger = new VFXLogger("[VFXEmitterFactory]"); + this._valueParser = valueParser; + this._materialFactory = materialFactory; + this._geometryFactory = geometryFactory; + } + + /** + * Create a particle emitter from emitter data + */ + public createEmitter(emitterData: VFXEmitterData): Nullable { + const { config } = emitterData; + const { options } = this._context; + + // Check if we need SolidParticleSystem (mesh-based particles) + const useSolidParticles = config.renderMode === 2; + this._logger.log(`Using ${useSolidParticles ? "SolidParticleSystem" : "ParticleSystem"}`, options); + + if (useSolidParticles) { + return this._createSolidParticleSystem(emitterData); + } else { + return this._createParticleSystem(emitterData); + } + } + + /** + * Create a ParticleSystem (billboard-based particles) + */ + private _createParticleSystem(emitterData: VFXEmitterData): Nullable { + const { name, config } = emitterData; + const { scene, options } = this._context; + + this._logger.log(`Creating ParticleSystem: ${name}`, options); + + // Calculate capacity based on emission rate and duration + const emissionRate = config.emissionOverTime !== undefined ? this._valueParser.parseConstantValue(config.emissionOverTime) : 10; + const duration = config.duration || 5; + const capacity = Math.ceil(emissionRate * duration * 2); // Add some buffer + this._logger.log(` Emission rate: ${emissionRate}, Duration: ${duration}, Capacity: ${capacity}`, options); + + // Parse life time + const lifeTime = config.startLife !== undefined ? this._valueParser.parseIntervalValue(config.startLife) : { min: 1, max: 1 }; + this._logger.log(` Life time: ${lifeTime.min} - ${lifeTime.max}`, options); + + // Parse speed + const speed = config.startSpeed !== undefined ? this._valueParser.parseIntervalValue(config.startSpeed) : { min: 1, max: 1 }; + const avgStartSpeed = (speed.min + speed.max) / 2; + this._logger.log(` Speed: ${speed.min} - ${speed.max}`, options); + + // Parse size + const size = config.startSize !== undefined ? this._valueParser.parseIntervalValue(config.startSize) : { min: 1, max: 1 }; + const avgStartSize = (size.min + size.max) / 2; + this._logger.log(` Size: ${size.min} - ${size.max}`, options); + + // Parse start color + const startColor = config.startColor !== undefined ? this._valueParser.parseConstantColor(config.startColor) : new Color4(1, 1, 1, 1); + this._logger.log(` Start color: R=${startColor.r}, G=${startColor.g}, B=${startColor.b}, A=${startColor.a}`, options); + + // Create VFXParticleSystem instead of regular ParticleSystem + const particleSystem = new VFXParticleSystem(name, capacity, scene, this._valueParser, avgStartSpeed, avgStartSize, startColor); + + // Set basic properties + particleSystem.targetStopDuration = duration; + particleSystem.emitRate = emissionRate; + particleSystem.manualEmitCount = -1; + + // Set life time + particleSystem.minLifeTime = lifeTime.min; + particleSystem.maxLifeTime = lifeTime.max; + + // Set speed and size + particleSystem.minEmitPower = speed.min; + particleSystem.maxEmitPower = speed.max; + particleSystem.minSize = size.min; + particleSystem.maxSize = size.max; + + // Set colors + particleSystem.color1 = startColor; + particleSystem.color2 = startColor; + particleSystem.colorDead = new Color4(startColor.r, startColor.g, startColor.b, 0); + + // Parse start rotation + if (config.startRotation) { + if (typeof config.startRotation === "object" && config.startRotation !== null && "type" in config.startRotation && config.startRotation.type === "Euler") { + const eulerRotation = config.startRotation; + if (eulerRotation.angleZ !== undefined) { + const angleZ = this._valueParser.parseIntervalValue(eulerRotation.angleZ); + particleSystem.minInitialRotation = angleZ.min; + particleSystem.maxInitialRotation = angleZ.max; + } + } else { + const rotation = this._valueParser.parseIntervalValue(config.startRotation); + particleSystem.minInitialRotation = rotation.min; + particleSystem.maxInitialRotation = rotation.max; + } + } + + // Set sprite tiles if specified + if (config.uTileCount !== undefined && config.vTileCount !== undefined) { + if (config.uTileCount > 1 || config.vTileCount > 1) { + particleSystem.isAnimationSheetEnabled = true; + particleSystem.spriteCellWidth = config.uTileCount; + particleSystem.spriteCellHeight = config.vTileCount; + if (config.startTileIndex !== undefined) { + const startTile = this._valueParser.parseConstantValue(config.startTileIndex); + particleSystem.startSpriteCellID = Math.floor(startTile); + particleSystem.endSpriteCellID = Math.floor(startTile); + } + } + } + + // Set render order and layers + if (config.renderOrder !== undefined) { + particleSystem.renderingGroupId = config.renderOrder; + } + if (config.layers !== undefined) { + particleSystem.layerMask = config.layers; + } + + // Set emitter shape (pass matrix to extract rotation for emitter direction) + this._setEmitterShape(particleSystem, config.shape, emitterData.cumulativeScale, emitterData.matrix, options); + + // Load texture (ParticleSystem only needs texture, not material) + if (emitterData.materialId) { + const texture = this._materialFactory.createTexture(emitterData.materialId); + if (texture) { + particleSystem.particleTexture = texture; + // Get blend mode from material + const { jsonData } = this._context; + const material = jsonData.materials?.find((m: any) => m.uuid === emitterData.materialId); + if (material?.blending !== undefined) { + if (material.blending === 2) { + // Additive blending (Three.js AdditiveBlending) + particleSystem.blendMode = Constants.ALPHA_ADD; + } else if (material.blending === 1) { + // Normal blending (Three.js NormalBlending) + particleSystem.blendMode = Constants.ALPHA_COMBINE; + } else if (material.blending === 0) { + // No blending (Three.js NoBlending) + particleSystem.blendMode = Constants.ALPHA_DISABLE; + } + } + } + } + + // Handle emission bursts + if (config.emissionBursts && Array.isArray(config.emissionBursts) && config.emissionBursts.length > 0) { + this._applyEmissionBursts(particleSystem, config.emissionBursts, emissionRate, duration, options); + } + + // Apply behaviors + if (config.behaviors && Array.isArray(config.behaviors) && config.behaviors.length > 0) { + this._applyBehaviorsToPS(particleSystem, config.behaviors); + } + + // Set world space + if (config.worldSpace !== undefined) { + particleSystem.isLocal = !config.worldSpace; + this._logger.log(` World space: ${config.worldSpace}`, options); + } + + // Set looping + if (config.looping !== undefined) { + particleSystem.targetStopDuration = config.looping ? 0 : duration; + this._logger.log(` Looping: ${config.looping}`, options); + } + + // Set render mode + if (config.renderMode !== undefined) { + if (config.renderMode === 0) { + particleSystem.isBillboardBased = true; + this._logger.log(` Render mode: Billboard`, options); + } else if (config.renderMode === 1) { + particleSystem.billboardMode = ParticleSystem.BILLBOARDMODE_STRETCHED; + this._logger.log(` Render mode: Stretched Billboard`, options); + } + } + + // Set soft particles and auto destroy + if (config.softParticles !== undefined) { + this._logger.log(` Soft particles: ${config.softParticles} (not fully supported)`, options); + } + if (config.autoDestroy !== undefined) { + particleSystem.disposeOnStop = config.autoDestroy; + this._logger.log(` Auto destroy: ${config.autoDestroy}`, options); + } + + this._logger.log(`ParticleSystem created: ${name}`, options); + return particleSystem; + } + + /** + * Create a SolidParticleSystem (mesh-based particles) + */ + private _createSolidParticleSystem(emitterData: VFXEmitterData): Nullable { + const { name, config } = emitterData; + const { scene, options } = this._context; + + this._logger.log(`Creating SolidParticleSystem: ${name}`, options); + + // Calculate capacity based on emission rate and particle lifetime + // duration = particle lifetime (how long each particle lives) + // startLife = when particle becomes "alive" (for behaviors that depend on age) + // emissionOverTime = particles per second (e.g., 2.5 means 2.5 particles per second) + const emissionRate = config.emissionOverTime !== undefined ? this._valueParser.parseConstantValue(config.emissionOverTime) : 10; // particles per second + const particleLifetime = config.duration || 5; // duration is the particle lifetime + const isLooping = config.looping !== false; + + let capacity: number; + if (isLooping) { + // For looping systems: capacity = emissionRate * particleLifetime + // This gives the steady-state number of particles needed for perfect looping + // Example: emissionRate=2.5 particles/sec, particleLifetime=5 sec + // -> capacity = 2.5 * 5 = 12.5 -> 13 particles + // This ensures we have enough particles to cover the lifetime at the emission rate + capacity = Math.ceil(emissionRate * particleLifetime); + // Ensure minimum capacity of at least 1 + capacity = Math.max(capacity, 1); + this._logger.log(` Looping system: Emission rate: ${emissionRate} particles/sec, Particle lifetime: ${particleLifetime} sec, Capacity: ${capacity}`, options); + } else { + // For non-looping: capacity = emissionRate * particleLifetime * 2 (buffer for particles still alive) + capacity = Math.ceil(emissionRate * particleLifetime * 2); + this._logger.log(` Non-looping system: Emission rate: ${emissionRate} particles/sec, Particle lifetime: ${particleLifetime} sec, Capacity: ${capacity}`, options); + } + + // Get VFX transform from emitter data (stored during conversion) + // This is the clean way - transform is already in left-handed coordinate system + let vfxTransform: { position: Vector3; rotation: Quaternion; scale: Vector3 } | null = null; + const vfxEmitter = emitterData.vfxEmitter; + if (vfxEmitter && vfxEmitter.transform) { + vfxTransform = vfxEmitter.transform; + } + + const sps = new VFXSolidParticleSystem(name, scene, config, this._valueParser, { + updatable: true, + isPickable: false, + enableDepthSort: false, + particleIntersection: false, + useModelMaterial: true, + parentGroup: emitterData.parentGroup, + vfxTransform: vfxTransform, + logger: this._logger, + loaderOptions: options, + }); + + // Load geometry for particle shape + let particleMesh: Nullable = null; + if (config.instancingGeometry) { + this._logger.log(` Loading geometry: ${config.instancingGeometry}`, options); + particleMesh = this._geometryFactory.createMesh(config.instancingGeometry, emitterData.materialId, name + "_shape"); + if (!particleMesh) { + this._logger.warn(` Failed to load geometry ${config.instancingGeometry}, will create default plane`, options); + } + } + + // Default to plane if no geometry found + if (!particleMesh) { + this._logger.log(` Creating default plane geometry`, options); + particleMesh = CreatePlane(name + "_shape", { width: 1, height: 1 }, scene); + if (emitterData.materialId && particleMesh) { + const particleMaterial = this._materialFactory.createMaterial(emitterData.materialId, name); + if (particleMaterial) { + particleMesh.material = particleMaterial; + } + } + } else { + // Ensure material is applied + if (emitterData.materialId && particleMesh && !particleMesh.material) { + const particleMaterial = this._materialFactory.createMaterial(emitterData.materialId, name); + if (particleMaterial) { + particleMesh.material = particleMaterial; + } + } + } + + if (!particleMesh) { + this._logger.warn(` Cannot add shape to SPS: particleMesh is null`, options); + return null; + } + + this._logger.log(` Adding shape to SPS: mesh name=${particleMesh.name}, hasMaterial=${!!particleMesh.material}`, options); + sps.addShape(particleMesh, capacity); + + // Set billboard mode if needed + if (config.renderMode === 0 || config.renderMode === 1) { + sps.billboard = true; + } + + // Apply behaviors to SPS + if (config.behaviors && Array.isArray(config.behaviors) && config.behaviors.length > 0) { + this._applyBehaviorsToSPS(sps, config.behaviors); + this._logger.log(` Set SPS behaviors (${config.behaviors.length})`, options); + } + + // Cleanup temporary mesh + if (particleMesh) { + particleMesh.dispose(); + } + + this._logger.log(`SolidParticleSystem created: ${name}`, options); + return sps; + } + + /** + * Set the emitter shape based on Three.js shape configuration + * @param matrix Optional 4x4 matrix array from Three.js to extract rotation + */ + private _setEmitterShape(particleSystem: ParticleSystem, shape: any, cumulativeScale: Vector3, matrix?: number[], options?: VFXLoaderOptions): void { + if (!shape || !shape.type) { + particleSystem.createPointEmitter(Vector3.Zero(), Vector3.Zero()); + return; + } + + const scaleX = cumulativeScale.x; + const scaleY = cumulativeScale.y; + const scaleZ = cumulativeScale.z; + + // Extract rotation from matrix if provided + let rotationMatrix: Matrix | null = null; + if (matrix && matrix.length >= 16) { + // Three.js uses column-major order, Babylon.js uses row-major + const mat = Matrix.FromArray(matrix); + mat.transpose(); + + // Extract rotation matrix (remove scale and translation) + rotationMatrix = mat.getRotationMatrix(); + this._logger.log(` Extracted rotation from matrix`, options); + } + + // Helper function to apply rotation to default direction + const applyRotation = (defaultDir: Vector3): Vector3 => { + if (rotationMatrix) { + const rotatedDir = Vector3.Zero(); + Vector3.TransformNormalToRef(defaultDir, rotationMatrix, rotatedDir); + return rotatedDir; + } + return defaultDir; + }; + + switch (shape.type.toLowerCase()) { + case "cone": { + let radius = shape.radius || 1; + const angle = shape.angle !== undefined ? shape.angle : Math.PI / 4; + const coneScale = (scaleX + scaleZ) / 2; + radius = radius * coneScale; + + // Default direction for cone is up (0, 1, 0) + const defaultDir = new Vector3(0, 1, 0); + const rotatedDir = applyRotation(defaultDir); + + if (rotationMatrix) { + // Use directed emitter with rotated direction + particleSystem.createDirectedConeEmitter(radius, angle, rotatedDir, rotatedDir); + this._logger.log( + ` Created directed cone emitter with rotated direction: (${rotatedDir.x.toFixed(2)}, ${rotatedDir.y.toFixed(2)}, ${rotatedDir.z.toFixed(2)})`, + options + ); + } else { + particleSystem.createConeEmitter(radius, angle); + } + break; + } + + case "sphere": { + let sphereRadius = shape.radius || 1; + const sphereScale = (scaleX + scaleY + scaleZ) / 3; + sphereRadius = sphereRadius * sphereScale; + + // Default direction for sphere is up (0, 1, 0) + const defaultDir = new Vector3(0, 1, 0); + const rotatedDir = applyRotation(defaultDir); + + if (rotationMatrix) { + particleSystem.createDirectedSphereEmitter(sphereRadius, rotatedDir, rotatedDir); + this._logger.log( + ` Created directed sphere emitter with rotated direction: (${rotatedDir.x.toFixed(2)}, ${rotatedDir.y.toFixed(2)}, ${rotatedDir.z.toFixed(2)})`, + options + ); + } else { + particleSystem.createSphereEmitter(sphereRadius); + } + break; + } + + case "point": { + const defaultDir = new Vector3(0, 1, 0); + const rotatedDir = applyRotation(defaultDir); + + if (rotationMatrix) { + particleSystem.createPointEmitter(rotatedDir, rotatedDir); + this._logger.log( + ` Created point emitter with rotated direction: (${rotatedDir.x.toFixed(2)}, ${rotatedDir.y.toFixed(2)}, ${rotatedDir.z.toFixed(2)})`, + options + ); + } else { + particleSystem.createPointEmitter(Vector3.Zero(), Vector3.Zero()); + } + break; + } + + case "box": { + let boxSize = shape.size || [1, 1, 1]; + boxSize = [boxSize[0] * scaleX, boxSize[1] * scaleY, boxSize[2] * scaleZ]; + const minBox = new Vector3(-boxSize[0] / 2, -boxSize[1] / 2, -boxSize[2] / 2); + const maxBox = new Vector3(boxSize[0] / 2, boxSize[1] / 2, boxSize[2] / 2); + + const defaultDir = new Vector3(0, 1, 0); + const rotatedDir = applyRotation(defaultDir); + + if (rotationMatrix) { + particleSystem.createBoxEmitter(rotatedDir, rotatedDir, minBox, maxBox); + this._logger.log(` Created box emitter with rotated direction: (${rotatedDir.x.toFixed(2)}, ${rotatedDir.y.toFixed(2)}, ${rotatedDir.z.toFixed(2)})`, options); + } else { + particleSystem.createBoxEmitter(Vector3.Zero(), Vector3.Zero(), minBox, maxBox); + } + break; + } + + case "hemisphere": { + let hemRadius = shape.radius || 1; + const hemScale = (scaleX + scaleY + scaleZ) / 3; + hemRadius = hemRadius * hemScale; + particleSystem.createHemisphericEmitter(hemRadius); + break; + } + + case "cylinder": { + let cylRadius = shape.radius || 1; + let height = shape.height || 1; + const cylRadiusScale = (scaleX + scaleZ) / 2; + cylRadius = cylRadius * cylRadiusScale; + height = height * scaleY; + + // Default direction for cylinder is up (0, 1, 0) + const defaultDir = new Vector3(0, 1, 0); + const rotatedDir = applyRotation(defaultDir); + + if (rotationMatrix) { + particleSystem.createDirectedCylinderEmitter(cylRadius, height, 1, rotatedDir, rotatedDir); + this._logger.log( + ` Created directed cylinder emitter with rotated direction: (${rotatedDir.x.toFixed(2)}, ${rotatedDir.y.toFixed(2)}, ${rotatedDir.z.toFixed(2)})`, + options + ); + } else { + particleSystem.createCylinderEmitter(cylRadius, height); + } + break; + } + + default: { + const defaultDir = new Vector3(0, 1, 0); + const rotatedDir = applyRotation(defaultDir); + + if (rotationMatrix) { + particleSystem.createPointEmitter(rotatedDir, rotatedDir); + } else { + particleSystem.createPointEmitter(Vector3.Zero(), Vector3.Zero()); + } + break; + } + } + } + + /** + * Apply emission bursts via emit rate gradients + */ + private _applyEmissionBursts( + particleSystem: ParticleSystem, + bursts: import("../types/emitterConfig").VFXEmissionBurst[], + baseEmitRate: number, + duration: number, + _options?: VFXLoaderOptions + ): void { + for (const burst of bursts) { + if (burst.time !== undefined && burst.count !== undefined) { + const burstTime = this._valueParser.parseConstantValue(burst.time); + const burstCount = this._valueParser.parseConstantValue(burst.count); + const timeRatio = Math.min(Math.max(burstTime / duration, 0), 1); + + const windowSize = 0.02; + const burstEmitRate = burstCount / windowSize; + + const beforeTime = Math.max(0, timeRatio - windowSize); + const afterTime = Math.min(1, timeRatio + windowSize); + + particleSystem.addEmitRateGradient(beforeTime, baseEmitRate); + particleSystem.addEmitRateGradient(timeRatio, burstEmitRate); + particleSystem.addEmitRateGradient(afterTime, baseEmitRate); + } + } + } + + /** + * Apply behaviors to ParticleSystem + */ + private _applyBehaviorsToPS(particleSystem: ParticleSystem, behaviors: import("../types/behaviors").VFXBehavior[]): void { + const { options } = this._context; + const valueParser = this._valueParser; + + const vfxPS = particleSystem as any as VFXParticleSystem; + if (vfxPS && typeof vfxPS.setPerParticleBehaviors === "function") { + // Apply system-level behaviors (gradients, etc.) + for (const behavior of behaviors) { + if (!behavior.type) { + this._logger.warn(`Behavior missing type: ${JSON.stringify(behavior)}`, options); + continue; + } + + this._logger.log(` Processing behavior: ${behavior.type}`, options); + + switch (behavior.type) { + case "ColorOverLife": + applyColorOverLifePS(particleSystem, behavior as any); + break; + case "SizeOverLife": + applySizeOverLifePS(particleSystem, behavior as any); + break; + case "RotationOverLife": + case "Rotation3DOverLife": + applyRotationOverLifePS(particleSystem, behavior as any, valueParser); + break; + case "ForceOverLife": + case "ApplyForce": + applyForceOverLifePS(particleSystem, behavior as any, valueParser); + break; + case "GravityForce": + applyGravityForcePS(particleSystem, behavior as any, valueParser); + break; + case "SpeedOverLife": + applySpeedOverLifePS(particleSystem, behavior as any, valueParser); + break; + case "FrameOverLife": + applyFrameOverLifePS(particleSystem, behavior as any, valueParser); + break; + case "LimitSpeedOverLife": + applyLimitSpeedOverLifePS(particleSystem, behavior as any, valueParser); + break; + } + } + + // Create and set per-particle behavior functions + const perParticleFunctions = VFXBehaviorFunctionFactory.createPerParticleFunctionsPS(behaviors, valueParser, particleSystem); + vfxPS.setPerParticleBehaviors(perParticleFunctions); + } + } + + /** + * Apply behaviors to SolidParticleSystem + */ + private _applyBehaviorsToSPS(sps: SolidParticleSystem, behaviors: import("../types/behaviors").VFXBehavior[]): void { + const vfxSPS = sps as any as VFXSolidParticleSystem; + if (vfxSPS && typeof vfxSPS.setPerParticleBehaviors === "function") { + const perParticleFunctions = VFXBehaviorFunctionFactory.createPerParticleFunctionsSPS(behaviors, this._valueParser); + vfxSPS.setPerParticleBehaviors(perParticleFunctions); + } + } } diff --git a/editor/src/editor/windows/fx-editor/VFX/factories/VFXGeometryFactory.ts b/editor/src/editor/windows/fx-editor/VFX/factories/VFXGeometryFactory.ts index 38f7a6121..c98c0bf6b 100644 --- a/editor/src/editor/windows/fx-editor/VFX/factories/VFXGeometryFactory.ts +++ b/editor/src/editor/windows/fx-editor/VFX/factories/VFXGeometryFactory.ts @@ -13,138 +13,138 @@ import { VFXMaterialFactory } from "./VFXMaterialFactory"; * Factory for creating meshes from Three.js geometry data */ export class VFXGeometryFactory implements IVFXGeometryFactory { - private _logger: VFXLogger; - private _context: VFXParseContext; - private _materialFactory: VFXMaterialFactory; - - constructor(context: VFXParseContext, materialFactory: VFXMaterialFactory) { - this._context = context; - this._logger = new VFXLogger("[VFXGeometryFactory]"); - this._materialFactory = materialFactory; - } - - /** - * Create a mesh from geometry ID with material applied - */ - public createMesh(geometryId: string, materialId: string | undefined, name: string): Nullable { - const { jsonData, scene, options } = this._context; - - this._logger.log(`Creating mesh from geometry ID: ${geometryId}, name: ${name}`, options); - if (!jsonData.geometries) { - this._logger.warn("No geometries data available", options); - return null; - } - - // Find geometry - const geometryData = jsonData.geometries.find((g) => g.uuid === geometryId); - if (!geometryData) { - this._logger.warn(`Geometry not found: ${geometryId}`, options); - return null; - } - - this._logger.log(`Found geometry: ${geometryData.name || geometryData.type || geometryId} (type: ${geometryData.type})`, options); - - // Create mesh from geometry - const mesh = this._createMeshFromGeometry(geometryData, scene, name, options); - if (!mesh) { - this._logger.warn(`Failed to create mesh from geometry ${geometryId}`, options); - return null; - } - - // Apply material if provided - if (materialId) { - const material = this._materialFactory.createMaterial(materialId, name); - if (material) { - mesh.material = material; - this._logger.log(`Applied material to mesh: ${name}`, options); - } - } - - return mesh; - } - - /** - * Create a mesh from Three.js geometry data - */ - private _createMeshFromGeometry( - geometryData: import("../types/quarksTypes").QuarksGeometry, - scene: Scene, - name: string = "ParticleMesh", - options?: VFXLoaderOptions - ): Nullable { - if (!geometryData) { - this._logger.warn(`createMeshFromGeometry: geometryData is null`, options); - return null; - } - - this._logger.log(`createMeshFromGeometry: type=${geometryData.type}, name=${name}`, options); - - // Handle PlaneGeometry - if (geometryData.type === "PlaneGeometry") { - const width = typeof geometryData.width === "number" ? geometryData.width : 1; - const height = typeof geometryData.height === "number" ? geometryData.height : 1; - this._logger.log(` Creating PlaneGeometry: width=${width}, height=${height}`, options); - const mesh = CreatePlane(name, { width, height }, scene); - if (mesh) { - this._logger.log(` PlaneGeometry created successfully`, options); - } else { - this._logger.warn(` Failed to create PlaneGeometry`, options); - } - return mesh; - } - - // Handle BufferGeometry - if (geometryData.type === "BufferGeometry" && geometryData.data && geometryData.data.attributes) { - const attrs = geometryData.data.attributes; - const positions = attrs.position; - const normals = attrs.normal; - const uvs = attrs.uv; - const colors = attrs.color; - const indices = geometryData.data.index; - - if (!positions || !positions.array) { - return null; - } - - const vertexData = new VertexData(); - vertexData.positions = Array.from(positions.array); - - if (normals && normals.array) { - vertexData.normals = Array.from(normals.array); - } - - if (uvs && uvs.array) { - vertexData.uvs = Array.from(uvs.array); - } - - if (colors && colors.array) { - vertexData.colors = Array.from(colors.array); - } - - if (indices && indices.array) { - vertexData.indices = Array.from(indices.array); - } else { - // Generate indices if not provided - const vertexCount = vertexData.positions.length / 3; - const generatedIndices: number[] = []; - for (let i = 0; i < vertexCount; i++) { - generatedIndices.push(i); - } - vertexData.indices = generatedIndices; - } - - const mesh = new Mesh(name, scene); - vertexData.applyToMesh(mesh); - - // Convert from Three.js (right-handed) to Babylon.js (left-handed) coordinate system - // This inverts Z coordinates, flips face winding, and negates normal Z - if (mesh.geometry) { - mesh.geometry.toLeftHanded(); - } - - return mesh; - } - - return null; - } + private _logger: VFXLogger; + private _context: VFXParseContext; + private _materialFactory: VFXMaterialFactory; + + constructor(context: VFXParseContext, materialFactory: VFXMaterialFactory) { + this._context = context; + this._logger = new VFXLogger("[VFXGeometryFactory]"); + this._materialFactory = materialFactory; + } + + /** + * Create a mesh from geometry ID with material applied + */ + public createMesh(geometryId: string, materialId: string | undefined, name: string): Nullable { + const { jsonData, scene, options } = this._context; + + this._logger.log(`Creating mesh from geometry ID: ${geometryId}, name: ${name}`, options); + if (!jsonData.geometries) { + this._logger.warn("No geometries data available", options); + return null; + } + + // Find geometry + const geometryData = jsonData.geometries.find((g) => g.uuid === geometryId); + if (!geometryData) { + this._logger.warn(`Geometry not found: ${geometryId}`, options); + return null; + } + + this._logger.log(`Found geometry: ${geometryData.name || geometryData.type || geometryId} (type: ${geometryData.type})`, options); + + // Create mesh from geometry + const mesh = this._createMeshFromGeometry(geometryData, scene, name, options); + if (!mesh) { + this._logger.warn(`Failed to create mesh from geometry ${geometryId}`, options); + return null; + } + + // Apply material if provided + if (materialId) { + const material = this._materialFactory.createMaterial(materialId, name); + if (material) { + mesh.material = material; + this._logger.log(`Applied material to mesh: ${name}`, options); + } + } + + return mesh; + } + + /** + * Create a mesh from Three.js geometry data + */ + private _createMeshFromGeometry( + geometryData: import("../types/quarksTypes").QuarksGeometry, + scene: Scene, + name: string = "ParticleMesh", + options?: VFXLoaderOptions + ): Nullable { + if (!geometryData) { + this._logger.warn(`createMeshFromGeometry: geometryData is null`, options); + return null; + } + + this._logger.log(`createMeshFromGeometry: type=${geometryData.type}, name=${name}`, options); + + // Handle PlaneGeometry + if (geometryData.type === "PlaneGeometry") { + const width = typeof geometryData.width === "number" ? geometryData.width : 1; + const height = typeof geometryData.height === "number" ? geometryData.height : 1; + this._logger.log(` Creating PlaneGeometry: width=${width}, height=${height}`, options); + const mesh = CreatePlane(name, { width, height }, scene); + if (mesh) { + this._logger.log(` PlaneGeometry created successfully`, options); + } else { + this._logger.warn(` Failed to create PlaneGeometry`, options); + } + return mesh; + } + + // Handle BufferGeometry + if (geometryData.type === "BufferGeometry" && geometryData.data && geometryData.data.attributes) { + const attrs = geometryData.data.attributes; + const positions = attrs.position; + const normals = attrs.normal; + const uvs = attrs.uv; + const colors = attrs.color; + const indices = geometryData.data.index; + + if (!positions || !positions.array) { + return null; + } + + const vertexData = new VertexData(); + vertexData.positions = Array.from(positions.array); + + if (normals && normals.array) { + vertexData.normals = Array.from(normals.array); + } + + if (uvs && uvs.array) { + vertexData.uvs = Array.from(uvs.array); + } + + if (colors && colors.array) { + vertexData.colors = Array.from(colors.array); + } + + if (indices && indices.array) { + vertexData.indices = Array.from(indices.array); + } else { + // Generate indices if not provided + const vertexCount = vertexData.positions.length / 3; + const generatedIndices: number[] = []; + for (let i = 0; i < vertexCount; i++) { + generatedIndices.push(i); + } + vertexData.indices = generatedIndices; + } + + const mesh = new Mesh(name, scene); + vertexData.applyToMesh(mesh); + + // Convert from Three.js (right-handed) to Babylon.js (left-handed) coordinate system + // This inverts Z coordinates, flips face winding, and negates normal Z + if (mesh.geometry) { + mesh.geometry.toLeftHanded(); + } + + return mesh; + } + + return null; + } } diff --git a/editor/src/editor/windows/fx-editor/VFX/factories/VFXMaterialFactory.ts b/editor/src/editor/windows/fx-editor/VFX/factories/VFXMaterialFactory.ts index 70ecef2ca..2e655dcb9 100644 --- a/editor/src/editor/windows/fx-editor/VFX/factories/VFXMaterialFactory.ts +++ b/editor/src/editor/windows/fx-editor/VFX/factories/VFXMaterialFactory.ts @@ -16,351 +16,351 @@ import type { Scene } from "@babylonjs/core/scene"; * Factory for creating materials and textures from Three.js JSON data */ export class VFXMaterialFactory implements IVFXMaterialFactory { - private _logger: VFXLogger; - private _context: VFXParseContext; - - constructor(context: VFXParseContext) { - this._context = context; - this._logger = new VFXLogger("[VFXMaterialFactory]"); - } - - /** - * Create a texture from material ID (for ParticleSystem - no material needed) - */ - public createTexture(materialId: string): Nullable { - const { jsonData, scene, rootUrl, options } = this._context; - - if (!jsonData.materials || !jsonData.textures || !jsonData.images) { - this._logger.warn(`Missing materials/textures/images data for material ${materialId}`, options); - return null; - } - - // Find material - const material = jsonData.materials.find((m) => m.uuid === materialId); - if (!material) { - this._logger.warn(`Material not found: ${materialId}`, options); - return null; - } - if (!material.map) { - this._logger.warn(`Material ${materialId} has no texture map`, options); - return null; - } - - // Find texture - const texture = jsonData.textures.find((t) => t.uuid === material.map); - if (!texture) { - this._logger.warn(`Texture not found: ${material.map}`, options); - return null; - } - if (!texture.image) { - this._logger.warn(`Texture ${material.map} has no image`, options); - return null; - } - - // Find image - const image = jsonData.images.find((img) => img.uuid === texture.image); - if (!image) { - this._logger.warn(`Image not found: ${texture.image}`, options); - return null; - } - - // Create texture URL from image data - let textureUrl: string; - if (image.url) { - textureUrl = Tools.GetAssetUrl(rootUrl + image.url); - } else if (image.data) { - // Base64 embedded texture - textureUrl = `data:image/${image.format || "png"};base64,${image.data}`; - } else { - this._logger.warn(`Image ${texture.image} has neither URL nor data`, options); - return null; - } - - // Create texture using helper method - return this._createTextureFromData(textureUrl, texture, scene, options); - } - - /** - * Helper method to create texture from texture data - */ - private _createTextureFromData(textureUrl: string, texture: QuarksTexture, scene: Scene, _options?: VFXLoaderOptions): Texture { - // Determine sampling mode from texture filters - let samplingMode = Texture.TRILINEAR_SAMPLINGMODE; // Default - if (texture.minFilter !== undefined) { - if (texture.minFilter === 1008 || texture.minFilter === 1009) { - samplingMode = Texture.TRILINEAR_SAMPLINGMODE; - } else if (texture.minFilter === 1007 || texture.minFilter === 1006) { - samplingMode = Texture.BILINEAR_SAMPLINGMODE; - } else { - samplingMode = Texture.NEAREST_SAMPLINGMODE; - } - } else if (texture.magFilter !== undefined) { - if (texture.magFilter === 1006) { - samplingMode = Texture.BILINEAR_SAMPLINGMODE; - } else { - samplingMode = Texture.NEAREST_SAMPLINGMODE; - } - } - - // Create texture with proper settings - const babylonTexture = new Texture(textureUrl, scene, { - noMipmap: !texture.generateMipmaps, - invertY: texture.flipY !== false, // Three.js flipY defaults to true - samplingMode: samplingMode, - }); - - // Configure texture properties from Three.js JSON - if (texture.wrap && Array.isArray(texture.wrap)) { - const wrapU = texture.wrap[0] === 1000 ? Texture.WRAP_ADDRESSMODE : texture.wrap[0] === 1001 ? Texture.CLAMP_ADDRESSMODE : Texture.MIRROR_ADDRESSMODE; - const wrapV = texture.wrap[1] === 1000 ? Texture.WRAP_ADDRESSMODE : texture.wrap[1] === 1001 ? Texture.CLAMP_ADDRESSMODE : Texture.MIRROR_ADDRESSMODE; - babylonTexture.wrapU = wrapU; - babylonTexture.wrapV = wrapV; - } - - if (texture.repeat && Array.isArray(texture.repeat)) { - babylonTexture.uScale = texture.repeat[0] || 1; - babylonTexture.vScale = texture.repeat[1] || 1; - } - - if (texture.offset && Array.isArray(texture.offset)) { - babylonTexture.uOffset = texture.offset[0] || 0; - babylonTexture.vOffset = texture.offset[1] || 0; - } - - if (texture.channel !== undefined && typeof texture.channel === "number") { - babylonTexture.coordinatesIndex = texture.channel; - } - - if (texture.rotation !== undefined) { - babylonTexture.uAng = texture.rotation; - } - - return babylonTexture; - } - - /** - * Create a material with texture from material ID - */ - public createMaterial(materialId: string, name: string): Nullable { - const { jsonData, scene, rootUrl, options } = this._context; - - this._logger.log(`Creating material for ID: ${materialId}, name: ${name}`, options); - if (!jsonData.materials || !jsonData.textures || !jsonData.images) { - this._logger.warn(`Missing materials/textures/images data for material ${materialId}`, options); - return null; - } - - // Find material - const material = jsonData.materials.find((m) => m.uuid === materialId); - if (!material) { - this._logger.warn(`Material not found: ${materialId}`, options); - return null; - } - if (!material.map) { - this._logger.warn(`Material ${materialId} has no texture map`, options); - return null; - } - - const materialType = material.type || "MeshStandardMaterial"; - this._logger.log(`Found material: type=${materialType}, uuid=${material.uuid}, transparent=${material.transparent}, blending=${material.blending}`, options); - - // Find texture - const texture = jsonData.textures.find((t) => t.uuid === material.map); - if (!texture) { - this._logger.warn(`Texture not found: ${material.map}`, options); - return null; - } - if (!texture.image) { - this._logger.warn(`Texture ${material.map} has no image`, options); - return null; - } - - this._logger.log(`Found texture: ${JSON.stringify({ uuid: texture.uuid, image: texture.image })}`, options); - - // Find image - const image = jsonData.images.find((img) => img.uuid === texture.image); - if (!image) { - this._logger.warn(`Image not found: ${texture.image}`, options); - return null; - } - - const imageInfo: string[] = []; - if (image.url) { - const urlParts = image.url.split("/"); - let filename = urlParts[urlParts.length - 1] || image.url; - // If filename looks like base64 data (very long), truncate it - if (filename.length > 50) { - filename = filename.substring(0, 20) + "..."; - } - imageInfo.push(`file: ${filename}`); - } - if (image.data) { - imageInfo.push("embedded"); - } - if (image.format) { - imageInfo.push(`format: ${image.format}`); - } - this._logger.log(`Found image: ${imageInfo.join(", ") || "unknown"}`, options); - - // Create texture URL from image data - let textureUrl: string; - if (image.url) { - textureUrl = Tools.GetAssetUrl(rootUrl + image.url); - // Extract filename from URL for logging - const urlParts = image.url.split("/"); - let filename = urlParts[urlParts.length - 1] || image.url; - // If filename looks like base64 data (very long), truncate it - if (filename.length > 50) { - filename = filename.substring(0, 20) + "..."; - } - this._logger.log(`Using external texture: ${filename}`, options); - } else if (image.data) { - // Base64 embedded texture - textureUrl = `data:image/${image.format || "png"};base64,${image.data}`; - this._logger.log(`Using base64 embedded texture (format: ${image.format || "png"})`, options); - } else { - this._logger.warn(`Image ${texture.image} has neither URL nor data`, options); - return null; - } - - // Determine sampling mode from texture filters - let samplingMode = Texture.TRILINEAR_SAMPLINGMODE; // Default - if (texture.minFilter !== undefined) { - if (texture.minFilter === 1008 || texture.minFilter === 1009) { - samplingMode = Texture.TRILINEAR_SAMPLINGMODE; - } else if (texture.minFilter === 1007 || texture.minFilter === 1006) { - samplingMode = Texture.BILINEAR_SAMPLINGMODE; - } else { - samplingMode = Texture.NEAREST_SAMPLINGMODE; - } - } else if (texture.magFilter !== undefined) { - if (texture.magFilter === 1006) { - samplingMode = Texture.BILINEAR_SAMPLINGMODE; - } else { - samplingMode = Texture.NEAREST_SAMPLINGMODE; - } - } - - // Create texture with proper settings - const babylonTexture = new Texture(textureUrl, scene, { - noMipmap: !texture.generateMipmaps, - invertY: texture.flipY !== false, // Three.js flipY defaults to true - samplingMode: samplingMode, - }); - - // Configure texture properties from Three.js JSON - // wrap: [1001, 1001] = WRAP_ADDRESSMODE (repeat) - if (texture.wrap && Array.isArray(texture.wrap)) { - // Three.js wrap: 1000 = RepeatWrapping, 1001 = ClampToEdgeWrapping, 1002 = MirroredRepeatWrapping - // Babylon.js: WRAP_ADDRESSMODE = 0, CLAMP_ADDRESSMODE = 1, MIRROR_ADDRESSMODE = 2 - const wrapU = texture.wrap[0] === 1000 ? Texture.WRAP_ADDRESSMODE : texture.wrap[0] === 1001 ? Texture.CLAMP_ADDRESSMODE : Texture.MIRROR_ADDRESSMODE; - const wrapV = texture.wrap[1] === 1000 ? Texture.WRAP_ADDRESSMODE : texture.wrap[1] === 1001 ? Texture.CLAMP_ADDRESSMODE : Texture.MIRROR_ADDRESSMODE; - babylonTexture.wrapU = wrapU; - babylonTexture.wrapV = wrapV; - } - - // repeat: [1, 1] -> uScale, vScale - if (texture.repeat && Array.isArray(texture.repeat)) { - babylonTexture.uScale = texture.repeat[0] || 1; - babylonTexture.vScale = texture.repeat[1] || 1; - } - - // offset: [0, 0] -> uOffset, vOffset - if (texture.offset && Array.isArray(texture.offset)) { - babylonTexture.uOffset = texture.offset[0] || 0; - babylonTexture.vOffset = texture.offset[1] || 0; - } - - // channel: 0 -> coordinatesIndex - if (texture.channel !== undefined && typeof texture.channel === "number") { - babylonTexture.coordinatesIndex = texture.channel; - } - - // rotation: 0 -> uAng (rotation in radians) - if (texture.rotation !== undefined) { - babylonTexture.uAng = texture.rotation; - } - - // Parse color from Three.js material (default is white 0xffffff) - let materialColor = new Color3(1, 1, 1); - if (material.color !== undefined) { - // Three.js color is stored as hex number (e.g., 16777215 = 0xffffff) or hex string - let colorHex: number; - if (typeof material.color === "number") { - colorHex = material.color; - } else if (typeof material.color === "string") { - colorHex = parseInt((material.color as string).replace("#", ""), 16); - } else { - colorHex = 0xffffff; - } - const r = ((colorHex >> 16) & 0xff) / 255; - const g = ((colorHex >> 8) & 0xff) / 255; - const b = (colorHex & 0xff) / 255; - materialColor = new Color3(r, g, b); - this._logger.log(`Parsed material color: R=${r.toFixed(2)}, G=${g.toFixed(2)}, B=${b.toFixed(2)}`, options); - } - - // Handle different Three.js material types - if (materialType === "MeshBasicMaterial") { - // MeshBasicMaterial: Use PBRMaterial with unlit = true (equivalent to UnlitMaterial) - const unlitMaterial = new PBRMaterial(name + "_material", scene); - unlitMaterial.unlit = true; - unlitMaterial.albedoColor = materialColor; - unlitMaterial.albedoTexture = babylonTexture; - - // Transparency - if (material.transparent !== undefined && material.transparent) { - unlitMaterial.transparencyMode = Material.MATERIAL_ALPHABLEND; - unlitMaterial.needDepthPrePass = false; - babylonTexture.hasAlpha = true; - unlitMaterial.useAlphaFromAlbedoTexture = true; - this._logger.log(`Material is transparent (transparencyMode: ALPHABLEND, alphaMode: COMBINE)`, options); - } else { - unlitMaterial.transparencyMode = Material.MATERIAL_OPAQUE; - unlitMaterial.alpha = 1.0; - } - - // Depth write - if (material.depthWrite !== undefined) { - unlitMaterial.disableDepthWrite = !material.depthWrite; - this._logger.log(`Set disableDepthWrite: ${!material.depthWrite}`, options); - } else { - unlitMaterial.disableDepthWrite = true; // Default to false depthWrite = true disableDepthWrite - } - - // Double sided - unlitMaterial.backFaceCulling = false; - - // Side orientation - if (material.side !== undefined) { - // Three.js: 0 = FrontSide, 1 = BackSide, 2 = DoubleSide - // Babylon.js: 0 = Front, 1 = Back, 2 = Double - unlitMaterial.sideOrientation = material.side; - this._logger.log(`Set sideOrientation: ${material.side}`, options); - } - - // Blend mode - if (material.blending !== undefined) { - if (material.blending === 2) { - // Additive blending (Three.js AdditiveBlending) - unlitMaterial.alphaMode = Constants.ALPHA_ADD; - this._logger.log("Set blend mode: ADDITIVE", options); - } else if (material.blending === 1) { - // Normal blending (Three.js NormalBlending) - unlitMaterial.alphaMode = Constants.ALPHA_COMBINE; - this._logger.log("Set blend mode: NORMAL", options); - } else if (material.blending === 0) { - // No blending (Three.js NoBlending) - unlitMaterial.alphaMode = Constants.ALPHA_DISABLE; - this._logger.log("Set blend mode: NO_BLENDING", options); - } - } - - this._logger.log(`Using MeshBasicMaterial: PBRMaterial with unlit=true, albedoTexture`, options); - this._logger.log(`Material created successfully: ${name}_material`, options); - return unlitMaterial; - } else { - return new PBRMaterial(name + "_material", scene); - } - } + private _logger: VFXLogger; + private _context: VFXParseContext; + + constructor(context: VFXParseContext) { + this._context = context; + this._logger = new VFXLogger("[VFXMaterialFactory]"); + } + + /** + * Create a texture from material ID (for ParticleSystem - no material needed) + */ + public createTexture(materialId: string): Nullable { + const { jsonData, scene, rootUrl, options } = this._context; + + if (!jsonData.materials || !jsonData.textures || !jsonData.images) { + this._logger.warn(`Missing materials/textures/images data for material ${materialId}`, options); + return null; + } + + // Find material + const material = jsonData.materials.find((m) => m.uuid === materialId); + if (!material) { + this._logger.warn(`Material not found: ${materialId}`, options); + return null; + } + if (!material.map) { + this._logger.warn(`Material ${materialId} has no texture map`, options); + return null; + } + + // Find texture + const texture = jsonData.textures.find((t) => t.uuid === material.map); + if (!texture) { + this._logger.warn(`Texture not found: ${material.map}`, options); + return null; + } + if (!texture.image) { + this._logger.warn(`Texture ${material.map} has no image`, options); + return null; + } + + // Find image + const image = jsonData.images.find((img) => img.uuid === texture.image); + if (!image) { + this._logger.warn(`Image not found: ${texture.image}`, options); + return null; + } + + // Create texture URL from image data + let textureUrl: string; + if (image.url) { + textureUrl = Tools.GetAssetUrl(rootUrl + image.url); + } else if (image.data) { + // Base64 embedded texture + textureUrl = `data:image/${image.format || "png"};base64,${image.data}`; + } else { + this._logger.warn(`Image ${texture.image} has neither URL nor data`, options); + return null; + } + + // Create texture using helper method + return this._createTextureFromData(textureUrl, texture, scene, options); + } + + /** + * Helper method to create texture from texture data + */ + private _createTextureFromData(textureUrl: string, texture: QuarksTexture, scene: Scene, _options?: VFXLoaderOptions): Texture { + // Determine sampling mode from texture filters + let samplingMode = Texture.TRILINEAR_SAMPLINGMODE; // Default + if (texture.minFilter !== undefined) { + if (texture.minFilter === 1008 || texture.minFilter === 1009) { + samplingMode = Texture.TRILINEAR_SAMPLINGMODE; + } else if (texture.minFilter === 1007 || texture.minFilter === 1006) { + samplingMode = Texture.BILINEAR_SAMPLINGMODE; + } else { + samplingMode = Texture.NEAREST_SAMPLINGMODE; + } + } else if (texture.magFilter !== undefined) { + if (texture.magFilter === 1006) { + samplingMode = Texture.BILINEAR_SAMPLINGMODE; + } else { + samplingMode = Texture.NEAREST_SAMPLINGMODE; + } + } + + // Create texture with proper settings + const babylonTexture = new Texture(textureUrl, scene, { + noMipmap: !texture.generateMipmaps, + invertY: texture.flipY !== false, // Three.js flipY defaults to true + samplingMode: samplingMode, + }); + + // Configure texture properties from Three.js JSON + if (texture.wrap && Array.isArray(texture.wrap)) { + const wrapU = texture.wrap[0] === 1000 ? Texture.WRAP_ADDRESSMODE : texture.wrap[0] === 1001 ? Texture.CLAMP_ADDRESSMODE : Texture.MIRROR_ADDRESSMODE; + const wrapV = texture.wrap[1] === 1000 ? Texture.WRAP_ADDRESSMODE : texture.wrap[1] === 1001 ? Texture.CLAMP_ADDRESSMODE : Texture.MIRROR_ADDRESSMODE; + babylonTexture.wrapU = wrapU; + babylonTexture.wrapV = wrapV; + } + + if (texture.repeat && Array.isArray(texture.repeat)) { + babylonTexture.uScale = texture.repeat[0] || 1; + babylonTexture.vScale = texture.repeat[1] || 1; + } + + if (texture.offset && Array.isArray(texture.offset)) { + babylonTexture.uOffset = texture.offset[0] || 0; + babylonTexture.vOffset = texture.offset[1] || 0; + } + + if (texture.channel !== undefined && typeof texture.channel === "number") { + babylonTexture.coordinatesIndex = texture.channel; + } + + if (texture.rotation !== undefined) { + babylonTexture.uAng = texture.rotation; + } + + return babylonTexture; + } + + /** + * Create a material with texture from material ID + */ + public createMaterial(materialId: string, name: string): Nullable { + const { jsonData, scene, rootUrl, options } = this._context; + + this._logger.log(`Creating material for ID: ${materialId}, name: ${name}`, options); + if (!jsonData.materials || !jsonData.textures || !jsonData.images) { + this._logger.warn(`Missing materials/textures/images data for material ${materialId}`, options); + return null; + } + + // Find material + const material = jsonData.materials.find((m) => m.uuid === materialId); + if (!material) { + this._logger.warn(`Material not found: ${materialId}`, options); + return null; + } + if (!material.map) { + this._logger.warn(`Material ${materialId} has no texture map`, options); + return null; + } + + const materialType = material.type || "MeshStandardMaterial"; + this._logger.log(`Found material: type=${materialType}, uuid=${material.uuid}, transparent=${material.transparent}, blending=${material.blending}`, options); + + // Find texture + const texture = jsonData.textures.find((t) => t.uuid === material.map); + if (!texture) { + this._logger.warn(`Texture not found: ${material.map}`, options); + return null; + } + if (!texture.image) { + this._logger.warn(`Texture ${material.map} has no image`, options); + return null; + } + + this._logger.log(`Found texture: ${JSON.stringify({ uuid: texture.uuid, image: texture.image })}`, options); + + // Find image + const image = jsonData.images.find((img) => img.uuid === texture.image); + if (!image) { + this._logger.warn(`Image not found: ${texture.image}`, options); + return null; + } + + const imageInfo: string[] = []; + if (image.url) { + const urlParts = image.url.split("/"); + let filename = urlParts[urlParts.length - 1] || image.url; + // If filename looks like base64 data (very long), truncate it + if (filename.length > 50) { + filename = filename.substring(0, 20) + "..."; + } + imageInfo.push(`file: ${filename}`); + } + if (image.data) { + imageInfo.push("embedded"); + } + if (image.format) { + imageInfo.push(`format: ${image.format}`); + } + this._logger.log(`Found image: ${imageInfo.join(", ") || "unknown"}`, options); + + // Create texture URL from image data + let textureUrl: string; + if (image.url) { + textureUrl = Tools.GetAssetUrl(rootUrl + image.url); + // Extract filename from URL for logging + const urlParts = image.url.split("/"); + let filename = urlParts[urlParts.length - 1] || image.url; + // If filename looks like base64 data (very long), truncate it + if (filename.length > 50) { + filename = filename.substring(0, 20) + "..."; + } + this._logger.log(`Using external texture: ${filename}`, options); + } else if (image.data) { + // Base64 embedded texture + textureUrl = `data:image/${image.format || "png"};base64,${image.data}`; + this._logger.log(`Using base64 embedded texture (format: ${image.format || "png"})`, options); + } else { + this._logger.warn(`Image ${texture.image} has neither URL nor data`, options); + return null; + } + + // Determine sampling mode from texture filters + let samplingMode = Texture.TRILINEAR_SAMPLINGMODE; // Default + if (texture.minFilter !== undefined) { + if (texture.minFilter === 1008 || texture.minFilter === 1009) { + samplingMode = Texture.TRILINEAR_SAMPLINGMODE; + } else if (texture.minFilter === 1007 || texture.minFilter === 1006) { + samplingMode = Texture.BILINEAR_SAMPLINGMODE; + } else { + samplingMode = Texture.NEAREST_SAMPLINGMODE; + } + } else if (texture.magFilter !== undefined) { + if (texture.magFilter === 1006) { + samplingMode = Texture.BILINEAR_SAMPLINGMODE; + } else { + samplingMode = Texture.NEAREST_SAMPLINGMODE; + } + } + + // Create texture with proper settings + const babylonTexture = new Texture(textureUrl, scene, { + noMipmap: !texture.generateMipmaps, + invertY: texture.flipY !== false, // Three.js flipY defaults to true + samplingMode: samplingMode, + }); + + // Configure texture properties from Three.js JSON + // wrap: [1001, 1001] = WRAP_ADDRESSMODE (repeat) + if (texture.wrap && Array.isArray(texture.wrap)) { + // Three.js wrap: 1000 = RepeatWrapping, 1001 = ClampToEdgeWrapping, 1002 = MirroredRepeatWrapping + // Babylon.js: WRAP_ADDRESSMODE = 0, CLAMP_ADDRESSMODE = 1, MIRROR_ADDRESSMODE = 2 + const wrapU = texture.wrap[0] === 1000 ? Texture.WRAP_ADDRESSMODE : texture.wrap[0] === 1001 ? Texture.CLAMP_ADDRESSMODE : Texture.MIRROR_ADDRESSMODE; + const wrapV = texture.wrap[1] === 1000 ? Texture.WRAP_ADDRESSMODE : texture.wrap[1] === 1001 ? Texture.CLAMP_ADDRESSMODE : Texture.MIRROR_ADDRESSMODE; + babylonTexture.wrapU = wrapU; + babylonTexture.wrapV = wrapV; + } + + // repeat: [1, 1] -> uScale, vScale + if (texture.repeat && Array.isArray(texture.repeat)) { + babylonTexture.uScale = texture.repeat[0] || 1; + babylonTexture.vScale = texture.repeat[1] || 1; + } + + // offset: [0, 0] -> uOffset, vOffset + if (texture.offset && Array.isArray(texture.offset)) { + babylonTexture.uOffset = texture.offset[0] || 0; + babylonTexture.vOffset = texture.offset[1] || 0; + } + + // channel: 0 -> coordinatesIndex + if (texture.channel !== undefined && typeof texture.channel === "number") { + babylonTexture.coordinatesIndex = texture.channel; + } + + // rotation: 0 -> uAng (rotation in radians) + if (texture.rotation !== undefined) { + babylonTexture.uAng = texture.rotation; + } + + // Parse color from Three.js material (default is white 0xffffff) + let materialColor = new Color3(1, 1, 1); + if (material.color !== undefined) { + // Three.js color is stored as hex number (e.g., 16777215 = 0xffffff) or hex string + let colorHex: number; + if (typeof material.color === "number") { + colorHex = material.color; + } else if (typeof material.color === "string") { + colorHex = parseInt((material.color as string).replace("#", ""), 16); + } else { + colorHex = 0xffffff; + } + const r = ((colorHex >> 16) & 0xff) / 255; + const g = ((colorHex >> 8) & 0xff) / 255; + const b = (colorHex & 0xff) / 255; + materialColor = new Color3(r, g, b); + this._logger.log(`Parsed material color: R=${r.toFixed(2)}, G=${g.toFixed(2)}, B=${b.toFixed(2)}`, options); + } + + // Handle different Three.js material types + if (materialType === "MeshBasicMaterial") { + // MeshBasicMaterial: Use PBRMaterial with unlit = true (equivalent to UnlitMaterial) + const unlitMaterial = new PBRMaterial(name + "_material", scene); + unlitMaterial.unlit = true; + unlitMaterial.albedoColor = materialColor; + unlitMaterial.albedoTexture = babylonTexture; + + // Transparency + if (material.transparent !== undefined && material.transparent) { + unlitMaterial.transparencyMode = Material.MATERIAL_ALPHABLEND; + unlitMaterial.needDepthPrePass = false; + babylonTexture.hasAlpha = true; + unlitMaterial.useAlphaFromAlbedoTexture = true; + this._logger.log(`Material is transparent (transparencyMode: ALPHABLEND, alphaMode: COMBINE)`, options); + } else { + unlitMaterial.transparencyMode = Material.MATERIAL_OPAQUE; + unlitMaterial.alpha = 1.0; + } + + // Depth write + if (material.depthWrite !== undefined) { + unlitMaterial.disableDepthWrite = !material.depthWrite; + this._logger.log(`Set disableDepthWrite: ${!material.depthWrite}`, options); + } else { + unlitMaterial.disableDepthWrite = true; // Default to false depthWrite = true disableDepthWrite + } + + // Double sided + unlitMaterial.backFaceCulling = false; + + // Side orientation + if (material.side !== undefined) { + // Three.js: 0 = FrontSide, 1 = BackSide, 2 = DoubleSide + // Babylon.js: 0 = Front, 1 = Back, 2 = Double + unlitMaterial.sideOrientation = material.side; + this._logger.log(`Set sideOrientation: ${material.side}`, options); + } + + // Blend mode + if (material.blending !== undefined) { + if (material.blending === 2) { + // Additive blending (Three.js AdditiveBlending) + unlitMaterial.alphaMode = Constants.ALPHA_ADD; + this._logger.log("Set blend mode: ADDITIVE", options); + } else if (material.blending === 1) { + // Normal blending (Three.js NormalBlending) + unlitMaterial.alphaMode = Constants.ALPHA_COMBINE; + this._logger.log("Set blend mode: NORMAL", options); + } else if (material.blending === 0) { + // No blending (Three.js NoBlending) + unlitMaterial.alphaMode = Constants.ALPHA_DISABLE; + this._logger.log("Set blend mode: NO_BLENDING", options); + } + } + + this._logger.log(`Using MeshBasicMaterial: PBRMaterial with unlit=true, albedoTexture`, options); + this._logger.log(`Material created successfully: ${name}_material`, options); + return unlitMaterial; + } else { + return new PBRMaterial(name + "_material", scene); + } + } } diff --git a/editor/src/editor/windows/fx-editor/VFX/loggers/VFXLogger.ts b/editor/src/editor/windows/fx-editor/VFX/loggers/VFXLogger.ts index ac640999e..4d51628d1 100644 --- a/editor/src/editor/windows/fx-editor/VFX/loggers/VFXLogger.ts +++ b/editor/src/editor/windows/fx-editor/VFX/loggers/VFXLogger.ts @@ -5,34 +5,34 @@ import type { VFXLoaderOptions } from "../types"; * Logger utility for VFX operations */ export class VFXLogger { - private _prefix: string; + private _prefix: string; - constructor(prefix: string = "[VFX]") { - this._prefix = prefix; - } + constructor(prefix: string = "[VFX]") { + this._prefix = prefix; + } - /** - * Log a message if verbose mode is enabled - */ - public log(message: string, options?: VFXLoaderOptions): void { - if (options?.verbose) { - Logger.Log(`${this._prefix} ${message}`); - } - } + /** + * Log a message if verbose mode is enabled + */ + public log(message: string, options?: VFXLoaderOptions): void { + if (options?.verbose) { + Logger.Log(`${this._prefix} ${message}`); + } + } - /** - * Log a warning if verbose or validate mode is enabled - */ - public warn(message: string, options?: VFXLoaderOptions): void { - if (options?.verbose || options?.validate) { - Logger.Warn(`${this._prefix} ${message}`); - } - } + /** + * Log a warning if verbose or validate mode is enabled + */ + public warn(message: string, options?: VFXLoaderOptions): void { + if (options?.verbose || options?.validate) { + Logger.Warn(`${this._prefix} ${message}`); + } + } - /** - * Log an error - */ - public error(message: string, _options?: VFXLoaderOptions): void { - Logger.Error(`${this._prefix} ${message}`); - } + /** + * Log an error + */ + public error(message: string, _options?: VFXLoaderOptions): void { + Logger.Error(`${this._prefix} ${message}`); + } } diff --git a/editor/src/editor/windows/fx-editor/VFX/parsers/VFXDataConverter.ts b/editor/src/editor/windows/fx-editor/VFX/parsers/VFXDataConverter.ts index 7dfb43859..6252d153a 100644 --- a/editor/src/editor/windows/fx-editor/VFX/parsers/VFXDataConverter.ts +++ b/editor/src/editor/windows/fx-editor/VFX/parsers/VFXDataConverter.ts @@ -2,38 +2,38 @@ import { Vector3, Matrix, Quaternion } from "@babylonjs/core/Maths/math.vector"; import type { VFXLoaderOptions } from "../types/loader"; import type { QuarksVFXJSON } from "../types/quarksTypes"; import type { - QuarksObject, - QuarksParticleEmitterConfig, - QuarksBehavior, - QuarksValue, - QuarksColor, - QuarksRotation, - QuarksGradientKey, - QuarksShape, - QuarksColorOverLifeBehavior, - QuarksSizeOverLifeBehavior, - QuarksRotationOverLifeBehavior, - QuarksForceOverLifeBehavior, - QuarksGravityForceBehavior, - QuarksSpeedOverLifeBehavior, - QuarksFrameOverLifeBehavior, - QuarksLimitSpeedOverLifeBehavior, - QuarksColorBySpeedBehavior, - QuarksSizeBySpeedBehavior, - QuarksRotationBySpeedBehavior, - QuarksOrbitOverLifeBehavior, + QuarksObject, + QuarksParticleEmitterConfig, + QuarksBehavior, + QuarksValue, + QuarksColor, + QuarksRotation, + QuarksGradientKey, + QuarksShape, + QuarksColorOverLifeBehavior, + QuarksSizeOverLifeBehavior, + QuarksRotationOverLifeBehavior, + QuarksForceOverLifeBehavior, + QuarksGravityForceBehavior, + QuarksSpeedOverLifeBehavior, + QuarksFrameOverLifeBehavior, + QuarksLimitSpeedOverLifeBehavior, + QuarksColorBySpeedBehavior, + QuarksSizeBySpeedBehavior, + QuarksRotationBySpeedBehavior, + QuarksOrbitOverLifeBehavior, } from "../types/quarksTypes"; import type { VFXTransform, VFXGroup, VFXEmitter, VFXHierarchy } from "../types/hierarchy"; import type { VFXParticleEmitterConfig } from "../types/emitterConfig"; import type { - VFXBehavior, - VFXColorOverLifeBehavior, - VFXSizeOverLifeBehavior, - VFXForceOverLifeBehavior, - VFXSpeedOverLifeBehavior, - VFXLimitSpeedOverLifeBehavior, - VFXColorBySpeedBehavior, - VFXSizeBySpeedBehavior, + VFXBehavior, + VFXColorOverLifeBehavior, + VFXSizeOverLifeBehavior, + VFXForceOverLifeBehavior, + VFXSpeedOverLifeBehavior, + VFXLimitSpeedOverLifeBehavior, + VFXColorBySpeedBehavior, + VFXSizeBySpeedBehavior, } from "../types/behaviors"; import type { VFXValue } from "../types/values"; import type { VFXColor } from "../types/colors"; @@ -47,524 +47,524 @@ import { VFXLogger } from "../loggers/VFXLogger"; * All coordinate system conversions happen here, once */ export class VFXDataConverter { - private _logger: VFXLogger; - private _options?: VFXLoaderOptions; - - constructor(options?: VFXLoaderOptions) { - this._logger = new VFXLogger("[VFXDataConverter]"); - this._options = options; - } - - /** - * Convert Quarks/Three.js VFX JSON to Babylon.js VFX format - */ - public convert(quarksVFXData: QuarksVFXJSON): VFXHierarchy { - this._logger.log("=== Converting Quarks VFX to Babylon.js VFX format ===", this._options); - - const groups = new Map(); - const emitters = new Map(); - - let root: VFXGroup | VFXEmitter | null = null; - - if (quarksVFXData.object) { - root = this._convertObject(quarksVFXData.object, null, groups, emitters, 0); - } - - this._logger.log(`=== Conversion complete. Groups: ${groups.size}, Emitters: ${emitters.size} ===`, this._options); - - return { - root, - groups, - emitters, - }; - } - - /** - * Convert a Quarks/Three.js object to Babylon.js VFX format - */ - private _convertObject( - obj: QuarksObject, - parentUuid: string | null, - groups: Map, - emitters: Map, - depth: number - ): VFXGroup | VFXEmitter | null { - const indent = " ".repeat(depth); - const options = this._options; - - if (!obj || typeof obj !== "object") { - return null; - } - - this._logger.log(`${indent}Converting object: ${obj.type || "unknown"} (name: ${obj.name || "unnamed"})`, options); - - // Convert transform from right-handed to left-handed - const transform = this._convertTransform(obj.matrix, obj.position, obj.rotation, obj.scale, options); - - if (obj.type === "Group") { - const group: VFXGroup = { - uuid: obj.uuid || `group_${groups.size}`, - name: obj.name || "Group", - transform, - children: [], - }; - - // Convert children - if (obj.children && Array.isArray(obj.children)) { - for (const child of obj.children) { - const convertedChild = this._convertObject(child, group.uuid, groups, emitters, depth + 1); - if (convertedChild) { - if ("config" in convertedChild) { - // It's an emitter - group.children.push(convertedChild as VFXEmitter); - } else { - // It's a group - group.children.push(convertedChild as VFXGroup); - } - } - } - } - - groups.set(group.uuid, group); - this._logger.log(`${indent}Converted Group: ${group.name} (uuid: ${group.uuid})`, options); - return group; - } else if (obj.type === "ParticleEmitter" && obj.ps) { - // Convert emitter config from Quarks to VFX format - const vfxConfig = this._convertEmitterConfig(obj.ps); - - const emitter: VFXEmitter = { - uuid: obj.uuid || `emitter_${emitters.size}`, - name: obj.name || "ParticleEmitter", - transform, - config: vfxConfig, - materialId: obj.ps.material, - parentUuid: parentUuid || undefined, - }; - - emitters.set(emitter.uuid, emitter); - this._logger.log(`${indent}Converted Emitter: ${emitter.name} (uuid: ${emitter.uuid})`, options); - return emitter; - } - - return null; - } - - /** - * Convert transform from Quarks/Three.js (right-handed) to Babylon.js VFX (left-handed) - * This is the ONLY place where handedness conversion happens - */ - private _convertTransform(matrixArray?: number[], positionArray?: number[], rotationArray?: number[], scaleArray?: number[], _options?: VFXLoaderOptions): VFXTransform { - const position = Vector3.Zero(); - const rotation = Quaternion.Identity(); - const scale = Vector3.One(); - - if (matrixArray && Array.isArray(matrixArray) && matrixArray.length >= 16) { - // Use matrix (most accurate) - const matrix = Matrix.FromArray(matrixArray); - const tempPos = Vector3.Zero(); - const tempRot = Quaternion.Zero(); - const tempScale = Vector3.Zero(); - matrix.decompose(tempScale, tempRot, tempPos); - - // Convert from right-handed to left-handed - position.copyFrom(tempPos); - position.z = -position.z; // Negate Z position - - rotation.copyFrom(tempRot); - // Convert rotation quaternion: invert X component for proper X-axis rotation conversion - // This handles the case where X=-90° in RH looks like X=0° in LH - rotation.x *= -1; - - scale.copyFrom(tempScale); - } else { - // Use individual components - if (positionArray && Array.isArray(positionArray)) { - position.set(positionArray[0] || 0, positionArray[1] || 0, positionArray[2] || 0); - position.z = -position.z; // Convert to left-handed - } - - if (rotationArray && Array.isArray(rotationArray)) { - // If rotation is Euler angles, convert to quaternion - const eulerX = rotationArray[0] || 0; - const eulerY = rotationArray[1] || 0; - const eulerZ = rotationArray[2] || 0; - Quaternion.RotationYawPitchRollToRef(eulerY, eulerX, -eulerZ, rotation); // Negate Z for handedness - rotation.x *= -1; // Adjust X rotation component - } - - if (scaleArray && Array.isArray(scaleArray)) { - scale.set(scaleArray[0] || 1, scaleArray[1] || 1, scaleArray[2] || 1); - } - } - - return { - position, - rotation, - scale, - }; - } - - /** - * Convert emitter config from Quarks to VFX format - */ - private _convertEmitterConfig(quarksConfig: QuarksParticleEmitterConfig): VFXParticleEmitterConfig { - const vfxConfig: VFXParticleEmitterConfig = { - version: quarksConfig.version, - autoDestroy: quarksConfig.autoDestroy, - looping: quarksConfig.looping, - prewarm: quarksConfig.prewarm, - duration: quarksConfig.duration, - onlyUsedByOther: quarksConfig.onlyUsedByOther, - instancingGeometry: quarksConfig.instancingGeometry, - renderOrder: quarksConfig.renderOrder, - renderMode: quarksConfig.renderMode, - rendererEmitterSettings: quarksConfig.rendererEmitterSettings, - material: quarksConfig.material, - layers: quarksConfig.layers, - uTileCount: quarksConfig.uTileCount, - vTileCount: quarksConfig.vTileCount, - blendTiles: quarksConfig.blendTiles, - softParticles: quarksConfig.softParticles, - softFarFade: quarksConfig.softFarFade, - softNearFade: quarksConfig.softNearFade, - worldSpace: quarksConfig.worldSpace, - }; - - // Convert values - if (quarksConfig.startLife !== undefined) { - vfxConfig.startLife = this._convertValue(quarksConfig.startLife); - } - if (quarksConfig.startSpeed !== undefined) { - vfxConfig.startSpeed = this._convertValue(quarksConfig.startSpeed); - } - if (quarksConfig.startRotation !== undefined) { - vfxConfig.startRotation = this._convertRotation(quarksConfig.startRotation); - } - if (quarksConfig.startSize !== undefined) { - vfxConfig.startSize = this._convertValue(quarksConfig.startSize); - } - if (quarksConfig.startColor !== undefined) { - vfxConfig.startColor = this._convertColor(quarksConfig.startColor); - } - if (quarksConfig.emissionOverTime !== undefined) { - vfxConfig.emissionOverTime = this._convertValue(quarksConfig.emissionOverTime); - } - if (quarksConfig.emissionOverDistance !== undefined) { - vfxConfig.emissionOverDistance = this._convertValue(quarksConfig.emissionOverDistance); - } - if (quarksConfig.startTileIndex !== undefined) { - vfxConfig.startTileIndex = this._convertValue(quarksConfig.startTileIndex); - } - - // Convert shape - if (quarksConfig.shape !== undefined) { - vfxConfig.shape = this._convertShape(quarksConfig.shape); - } - - // Convert emission bursts - if (quarksConfig.emissionBursts !== undefined && Array.isArray(quarksConfig.emissionBursts)) { - vfxConfig.emissionBursts = quarksConfig.emissionBursts.map((burst) => ({ - time: this._convertValue(burst.time), - count: this._convertValue(burst.count), - })); - } - - // Convert behaviors - if (quarksConfig.behaviors !== undefined && Array.isArray(quarksConfig.behaviors)) { - vfxConfig.behaviors = quarksConfig.behaviors.map((behavior) => this._convertBehavior(behavior)); - } - - return vfxConfig; - } - - /** - * Convert Quarks value to VFX value - */ - private _convertValue(quarksValue: QuarksValue): VFXValue { - if (typeof quarksValue === "number") { - return quarksValue; - } - if (quarksValue.type === "ConstantValue") { - return { - type: "ConstantValue", - value: quarksValue.value, - }; - } - if (quarksValue.type === "IntervalValue") { - return { - type: "IntervalValue", - min: quarksValue.a ?? 0, - max: quarksValue.b ?? 0, - }; - } - if (quarksValue.type === "PiecewiseBezier") { - return { - type: "PiecewiseBezier", - functions: quarksValue.functions.map((f) => ({ - function: f.function, - start: f.start, - })), - }; - } - return quarksValue; - } - - /** - * Convert Quarks color to VFX color - */ - private _convertColor(quarksColor: QuarksColor): VFXColor { - if (typeof quarksColor === "string" || Array.isArray(quarksColor)) { - return quarksColor; - } - if (quarksColor.type === "ConstantColor") { - if (quarksColor.value && Array.isArray(quarksColor.value)) { - return { - type: "ConstantColor", - value: quarksColor.value, - }; - } else if (quarksColor.color) { - return { - type: "ConstantColor", - value: [quarksColor.color.r || 0, quarksColor.color.g || 0, quarksColor.color.b || 0, quarksColor.color.a !== undefined ? quarksColor.color.a : 1], - }; - } else { - // Fallback: return default color if neither value nor color is present - return { - type: "ConstantColor", - value: [1, 1, 1, 1], - }; - } - } - return quarksColor as VFXColor; - } - - /** - * Convert Quarks rotation to VFX rotation - */ - private _convertRotation(quarksRotation: QuarksRotation): VFXRotation { - if (typeof quarksRotation === "number" || (typeof quarksRotation === "object" && quarksRotation !== null && "type" in quarksRotation && quarksRotation.type !== "Euler")) { - return this._convertValue(quarksRotation as QuarksValue); - } - if (typeof quarksRotation === "object" && quarksRotation !== null && "type" in quarksRotation && quarksRotation.type === "Euler") { - return { - type: "Euler", - angleX: quarksRotation.angleX !== undefined ? this._convertValue(quarksRotation.angleX) : undefined, - angleY: quarksRotation.angleY !== undefined ? this._convertValue(quarksRotation.angleY) : undefined, - angleZ: quarksRotation.angleZ !== undefined ? this._convertValue(quarksRotation.angleZ) : undefined, - }; - } - return this._convertValue(quarksRotation as QuarksValue); - } - - /** - * Convert Quarks gradient key to VFX gradient key - */ - private _convertGradientKey(quarksKey: QuarksGradientKey): VFXGradientKey { - return { - time: quarksKey.time, - value: quarksKey.value, - pos: quarksKey.pos, - }; - } - - /** - * Convert Quarks shape to VFX shape - */ - private _convertShape(quarksShape: QuarksShape): VFXShape { - const vfxShape: VFXShape = { - type: quarksShape.type, - radius: quarksShape.radius, - arc: quarksShape.arc, - thickness: quarksShape.thickness, - angle: quarksShape.angle, - mode: quarksShape.mode, - spread: quarksShape.spread, - size: quarksShape.size, - height: quarksShape.height, - }; - if (quarksShape.speed !== undefined) { - vfxShape.speed = this._convertValue(quarksShape.speed); - } - return vfxShape; - } - - /** - * Convert Quarks behavior to VFX behavior - */ - private _convertBehavior(quarksBehavior: QuarksBehavior): VFXBehavior { - switch (quarksBehavior.type) { - case "ColorOverLife": { - const behavior = quarksBehavior as QuarksColorOverLifeBehavior; - if (behavior.color) { - const vfxColor: VFXColorOverLifeBehavior["color"] = {}; - if (behavior.color.color?.keys) { - vfxColor.color = { keys: behavior.color.color.keys.map((k) => this._convertGradientKey(k)) }; - } - if (behavior.color.alpha?.keys) { - vfxColor.alpha = { keys: behavior.color.alpha.keys.map((k) => this._convertGradientKey(k)) }; - } - if (behavior.color.keys) { - vfxColor.keys = behavior.color.keys.map((k) => this._convertGradientKey(k)); - } - return { type: "ColorOverLife", color: vfxColor }; - } - return { type: "ColorOverLife" }; - } - - case "SizeOverLife": { - const behavior = quarksBehavior as QuarksSizeOverLifeBehavior; - if (behavior.size) { - const vfxSize: VFXSizeOverLifeBehavior["size"] = {}; - if (behavior.size.keys) { - vfxSize.keys = behavior.size.keys.map((k: QuarksGradientKey) => this._convertGradientKey(k)); - } - if (behavior.size.functions) { - vfxSize.functions = behavior.size.functions; - } - return { type: "SizeOverLife", size: vfxSize }; - } - return { type: "SizeOverLife" }; - } - - case "RotationOverLife": - case "Rotation3DOverLife": { - const behavior = quarksBehavior as QuarksRotationOverLifeBehavior; - return { - type: behavior.type, - angularVelocity: behavior.angularVelocity !== undefined ? this._convertValue(behavior.angularVelocity) : undefined, - }; - } - - case "ForceOverLife": - case "ApplyForce": { - const behavior = quarksBehavior as QuarksForceOverLifeBehavior; - const vfxBehavior: VFXForceOverLifeBehavior = { type: behavior.type }; - if (behavior.force) { - vfxBehavior.force = { - x: behavior.force.x !== undefined ? this._convertValue(behavior.force.x) : undefined, - y: behavior.force.y !== undefined ? this._convertValue(behavior.force.y) : undefined, - z: behavior.force.z !== undefined ? this._convertValue(behavior.force.z) : undefined, - }; - } - if (behavior.x !== undefined) vfxBehavior.x = this._convertValue(behavior.x); - if (behavior.y !== undefined) vfxBehavior.y = this._convertValue(behavior.y); - if (behavior.z !== undefined) vfxBehavior.z = this._convertValue(behavior.z); - return vfxBehavior; - } - - case "GravityForce": { - const behavior = quarksBehavior as QuarksGravityForceBehavior; - const vfxBehavior: { type: string; gravity?: VFXValue } = { - type: "GravityForce", - gravity: behavior.gravity !== undefined ? this._convertValue(behavior.gravity) : undefined, - }; - return vfxBehavior as VFXBehavior; - } - - case "SpeedOverLife": { - const behavior = quarksBehavior as QuarksSpeedOverLifeBehavior; - if (behavior.speed) { - if (typeof behavior.speed === "object" && behavior.speed !== null && "keys" in behavior.speed) { - const vfxSpeed: VFXSpeedOverLifeBehavior["speed"] = {}; - if (behavior.speed.keys) { - vfxSpeed.keys = behavior.speed.keys.map((k: QuarksGradientKey) => this._convertGradientKey(k)); - } - if (behavior.speed.functions) { - vfxSpeed.functions = behavior.speed.functions; - } - return { type: "SpeedOverLife", speed: vfxSpeed }; - } else if (typeof behavior.speed === "number" || (typeof behavior.speed === "object" && behavior.speed !== null && "type" in behavior.speed)) { - return { type: "SpeedOverLife", speed: this._convertValue(behavior.speed as QuarksValue) }; - } - } - return { type: "SpeedOverLife" }; - } - - case "FrameOverLife": { - const behavior = quarksBehavior as QuarksFrameOverLifeBehavior; - const vfxBehavior: { type: string; frame?: VFXValue | { keys?: VFXGradientKey[] } } = { type: "FrameOverLife" }; - if (behavior.frame) { - if (typeof behavior.frame === "object" && behavior.frame !== null && "keys" in behavior.frame) { - vfxBehavior.frame = { - keys: behavior.frame.keys?.map((k: QuarksGradientKey) => this._convertGradientKey(k)), - }; - } else if (typeof behavior.frame === "number" || (typeof behavior.frame === "object" && behavior.frame !== null && "type" in behavior.frame)) { - vfxBehavior.frame = this._convertValue(behavior.frame as QuarksValue); - } - } - return vfxBehavior as VFXBehavior; - } - - case "LimitSpeedOverLife": { - const behavior = quarksBehavior as QuarksLimitSpeedOverLifeBehavior; - const vfxBehavior: VFXLimitSpeedOverLifeBehavior = { type: "LimitSpeedOverLife" }; - if (behavior.maxSpeed !== undefined) { - vfxBehavior.maxSpeed = this._convertValue(behavior.maxSpeed); - } - if (behavior.speed !== undefined) { - if (typeof behavior.speed === "object" && behavior.speed !== null && "keys" in behavior.speed) { - vfxBehavior.speed = { keys: behavior.speed.keys?.map((k: QuarksGradientKey) => this._convertGradientKey(k)) }; - } else if (typeof behavior.speed === "number" || (typeof behavior.speed === "object" && behavior.speed !== null && "type" in behavior.speed)) { - vfxBehavior.speed = this._convertValue(behavior.speed as QuarksValue); - } - } - if (behavior.dampen !== undefined) { - vfxBehavior.dampen = this._convertValue(behavior.dampen); - } - return vfxBehavior; - } - - case "ColorBySpeed": { - const behavior = quarksBehavior as QuarksColorBySpeedBehavior; - const vfxBehavior: VFXColorBySpeedBehavior = { - type: "ColorBySpeed", - minSpeed: behavior.minSpeed !== undefined ? this._convertValue(behavior.minSpeed) : undefined, - maxSpeed: behavior.maxSpeed !== undefined ? this._convertValue(behavior.maxSpeed) : undefined, - }; - if (behavior.color?.keys) { - vfxBehavior.color = { keys: behavior.color.keys.map((k: QuarksGradientKey) => this._convertGradientKey(k)) }; - } - return vfxBehavior; - } - - case "SizeBySpeed": { - const behavior = quarksBehavior as QuarksSizeBySpeedBehavior; - const vfxBehavior: VFXSizeBySpeedBehavior = { - type: "SizeBySpeed", - minSpeed: behavior.minSpeed !== undefined ? this._convertValue(behavior.minSpeed) : undefined, - maxSpeed: behavior.maxSpeed !== undefined ? this._convertValue(behavior.maxSpeed) : undefined, - }; - if (behavior.size?.keys) { - vfxBehavior.size = { keys: behavior.size.keys.map((k: QuarksGradientKey) => this._convertGradientKey(k)) }; - } - return vfxBehavior; - } - - case "RotationBySpeed": { - const behavior = quarksBehavior as QuarksRotationBySpeedBehavior; - const vfxBehavior: { type: string; angularVelocity?: VFXValue; minSpeed?: VFXValue; maxSpeed?: VFXValue } = { - type: "RotationBySpeed", - angularVelocity: behavior.angularVelocity !== undefined ? this._convertValue(behavior.angularVelocity) : undefined, - minSpeed: behavior.minSpeed !== undefined ? this._convertValue(behavior.minSpeed) : undefined, - maxSpeed: behavior.maxSpeed !== undefined ? this._convertValue(behavior.maxSpeed) : undefined, - }; - return vfxBehavior as VFXBehavior; - } - - case "OrbitOverLife": { - const behavior = quarksBehavior as QuarksOrbitOverLifeBehavior; - const vfxBehavior: { type: string; center?: { x?: number; y?: number; z?: number }; radius?: VFXValue; speed?: VFXValue } = { - type: "OrbitOverLife", - center: behavior.center, - radius: behavior.radius !== undefined ? this._convertValue(behavior.radius) : undefined, - speed: behavior.speed !== undefined ? this._convertValue(behavior.speed) : undefined, - }; - return vfxBehavior as VFXBehavior; - } - - default: - // Fallback for unknown behaviors - copy as-is - return quarksBehavior as VFXBehavior; - } - } + private _logger: VFXLogger; + private _options?: VFXLoaderOptions; + + constructor(options?: VFXLoaderOptions) { + this._logger = new VFXLogger("[VFXDataConverter]"); + this._options = options; + } + + /** + * Convert Quarks/Three.js VFX JSON to Babylon.js VFX format + */ + public convert(quarksVFXData: QuarksVFXJSON): VFXHierarchy { + this._logger.log("=== Converting Quarks VFX to Babylon.js VFX format ===", this._options); + + const groups = new Map(); + const emitters = new Map(); + + let root: VFXGroup | VFXEmitter | null = null; + + if (quarksVFXData.object) { + root = this._convertObject(quarksVFXData.object, null, groups, emitters, 0); + } + + this._logger.log(`=== Conversion complete. Groups: ${groups.size}, Emitters: ${emitters.size} ===`, this._options); + + return { + root, + groups, + emitters, + }; + } + + /** + * Convert a Quarks/Three.js object to Babylon.js VFX format + */ + private _convertObject( + obj: QuarksObject, + parentUuid: string | null, + groups: Map, + emitters: Map, + depth: number + ): VFXGroup | VFXEmitter | null { + const indent = " ".repeat(depth); + const options = this._options; + + if (!obj || typeof obj !== "object") { + return null; + } + + this._logger.log(`${indent}Converting object: ${obj.type || "unknown"} (name: ${obj.name || "unnamed"})`, options); + + // Convert transform from right-handed to left-handed + const transform = this._convertTransform(obj.matrix, obj.position, obj.rotation, obj.scale, options); + + if (obj.type === "Group") { + const group: VFXGroup = { + uuid: obj.uuid || `group_${groups.size}`, + name: obj.name || "Group", + transform, + children: [], + }; + + // Convert children + if (obj.children && Array.isArray(obj.children)) { + for (const child of obj.children) { + const convertedChild = this._convertObject(child, group.uuid, groups, emitters, depth + 1); + if (convertedChild) { + if ("config" in convertedChild) { + // It's an emitter + group.children.push(convertedChild as VFXEmitter); + } else { + // It's a group + group.children.push(convertedChild as VFXGroup); + } + } + } + } + + groups.set(group.uuid, group); + this._logger.log(`${indent}Converted Group: ${group.name} (uuid: ${group.uuid})`, options); + return group; + } else if (obj.type === "ParticleEmitter" && obj.ps) { + // Convert emitter config from Quarks to VFX format + const vfxConfig = this._convertEmitterConfig(obj.ps); + + const emitter: VFXEmitter = { + uuid: obj.uuid || `emitter_${emitters.size}`, + name: obj.name || "ParticleEmitter", + transform, + config: vfxConfig, + materialId: obj.ps.material, + parentUuid: parentUuid || undefined, + }; + + emitters.set(emitter.uuid, emitter); + this._logger.log(`${indent}Converted Emitter: ${emitter.name} (uuid: ${emitter.uuid})`, options); + return emitter; + } + + return null; + } + + /** + * Convert transform from Quarks/Three.js (right-handed) to Babylon.js VFX (left-handed) + * This is the ONLY place where handedness conversion happens + */ + private _convertTransform(matrixArray?: number[], positionArray?: number[], rotationArray?: number[], scaleArray?: number[], _options?: VFXLoaderOptions): VFXTransform { + const position = Vector3.Zero(); + const rotation = Quaternion.Identity(); + const scale = Vector3.One(); + + if (matrixArray && Array.isArray(matrixArray) && matrixArray.length >= 16) { + // Use matrix (most accurate) + const matrix = Matrix.FromArray(matrixArray); + const tempPos = Vector3.Zero(); + const tempRot = Quaternion.Zero(); + const tempScale = Vector3.Zero(); + matrix.decompose(tempScale, tempRot, tempPos); + + // Convert from right-handed to left-handed + position.copyFrom(tempPos); + position.z = -position.z; // Negate Z position + + rotation.copyFrom(tempRot); + // Convert rotation quaternion: invert X component for proper X-axis rotation conversion + // This handles the case where X=-90° in RH looks like X=0° in LH + rotation.x *= -1; + + scale.copyFrom(tempScale); + } else { + // Use individual components + if (positionArray && Array.isArray(positionArray)) { + position.set(positionArray[0] || 0, positionArray[1] || 0, positionArray[2] || 0); + position.z = -position.z; // Convert to left-handed + } + + if (rotationArray && Array.isArray(rotationArray)) { + // If rotation is Euler angles, convert to quaternion + const eulerX = rotationArray[0] || 0; + const eulerY = rotationArray[1] || 0; + const eulerZ = rotationArray[2] || 0; + Quaternion.RotationYawPitchRollToRef(eulerY, eulerX, -eulerZ, rotation); // Negate Z for handedness + rotation.x *= -1; // Adjust X rotation component + } + + if (scaleArray && Array.isArray(scaleArray)) { + scale.set(scaleArray[0] || 1, scaleArray[1] || 1, scaleArray[2] || 1); + } + } + + return { + position, + rotation, + scale, + }; + } + + /** + * Convert emitter config from Quarks to VFX format + */ + private _convertEmitterConfig(quarksConfig: QuarksParticleEmitterConfig): VFXParticleEmitterConfig { + const vfxConfig: VFXParticleEmitterConfig = { + version: quarksConfig.version, + autoDestroy: quarksConfig.autoDestroy, + looping: quarksConfig.looping, + prewarm: quarksConfig.prewarm, + duration: quarksConfig.duration, + onlyUsedByOther: quarksConfig.onlyUsedByOther, + instancingGeometry: quarksConfig.instancingGeometry, + renderOrder: quarksConfig.renderOrder, + renderMode: quarksConfig.renderMode, + rendererEmitterSettings: quarksConfig.rendererEmitterSettings, + material: quarksConfig.material, + layers: quarksConfig.layers, + uTileCount: quarksConfig.uTileCount, + vTileCount: quarksConfig.vTileCount, + blendTiles: quarksConfig.blendTiles, + softParticles: quarksConfig.softParticles, + softFarFade: quarksConfig.softFarFade, + softNearFade: quarksConfig.softNearFade, + worldSpace: quarksConfig.worldSpace, + }; + + // Convert values + if (quarksConfig.startLife !== undefined) { + vfxConfig.startLife = this._convertValue(quarksConfig.startLife); + } + if (quarksConfig.startSpeed !== undefined) { + vfxConfig.startSpeed = this._convertValue(quarksConfig.startSpeed); + } + if (quarksConfig.startRotation !== undefined) { + vfxConfig.startRotation = this._convertRotation(quarksConfig.startRotation); + } + if (quarksConfig.startSize !== undefined) { + vfxConfig.startSize = this._convertValue(quarksConfig.startSize); + } + if (quarksConfig.startColor !== undefined) { + vfxConfig.startColor = this._convertColor(quarksConfig.startColor); + } + if (quarksConfig.emissionOverTime !== undefined) { + vfxConfig.emissionOverTime = this._convertValue(quarksConfig.emissionOverTime); + } + if (quarksConfig.emissionOverDistance !== undefined) { + vfxConfig.emissionOverDistance = this._convertValue(quarksConfig.emissionOverDistance); + } + if (quarksConfig.startTileIndex !== undefined) { + vfxConfig.startTileIndex = this._convertValue(quarksConfig.startTileIndex); + } + + // Convert shape + if (quarksConfig.shape !== undefined) { + vfxConfig.shape = this._convertShape(quarksConfig.shape); + } + + // Convert emission bursts + if (quarksConfig.emissionBursts !== undefined && Array.isArray(quarksConfig.emissionBursts)) { + vfxConfig.emissionBursts = quarksConfig.emissionBursts.map((burst) => ({ + time: this._convertValue(burst.time), + count: this._convertValue(burst.count), + })); + } + + // Convert behaviors + if (quarksConfig.behaviors !== undefined && Array.isArray(quarksConfig.behaviors)) { + vfxConfig.behaviors = quarksConfig.behaviors.map((behavior) => this._convertBehavior(behavior)); + } + + return vfxConfig; + } + + /** + * Convert Quarks value to VFX value + */ + private _convertValue(quarksValue: QuarksValue): VFXValue { + if (typeof quarksValue === "number") { + return quarksValue; + } + if (quarksValue.type === "ConstantValue") { + return { + type: "ConstantValue", + value: quarksValue.value, + }; + } + if (quarksValue.type === "IntervalValue") { + return { + type: "IntervalValue", + min: quarksValue.a ?? 0, + max: quarksValue.b ?? 0, + }; + } + if (quarksValue.type === "PiecewiseBezier") { + return { + type: "PiecewiseBezier", + functions: quarksValue.functions.map((f) => ({ + function: f.function, + start: f.start, + })), + }; + } + return quarksValue; + } + + /** + * Convert Quarks color to VFX color + */ + private _convertColor(quarksColor: QuarksColor): VFXColor { + if (typeof quarksColor === "string" || Array.isArray(quarksColor)) { + return quarksColor; + } + if (quarksColor.type === "ConstantColor") { + if (quarksColor.value && Array.isArray(quarksColor.value)) { + return { + type: "ConstantColor", + value: quarksColor.value, + }; + } else if (quarksColor.color) { + return { + type: "ConstantColor", + value: [quarksColor.color.r || 0, quarksColor.color.g || 0, quarksColor.color.b || 0, quarksColor.color.a !== undefined ? quarksColor.color.a : 1], + }; + } else { + // Fallback: return default color if neither value nor color is present + return { + type: "ConstantColor", + value: [1, 1, 1, 1], + }; + } + } + return quarksColor as VFXColor; + } + + /** + * Convert Quarks rotation to VFX rotation + */ + private _convertRotation(quarksRotation: QuarksRotation): VFXRotation { + if (typeof quarksRotation === "number" || (typeof quarksRotation === "object" && quarksRotation !== null && "type" in quarksRotation && quarksRotation.type !== "Euler")) { + return this._convertValue(quarksRotation as QuarksValue); + } + if (typeof quarksRotation === "object" && quarksRotation !== null && "type" in quarksRotation && quarksRotation.type === "Euler") { + return { + type: "Euler", + angleX: quarksRotation.angleX !== undefined ? this._convertValue(quarksRotation.angleX) : undefined, + angleY: quarksRotation.angleY !== undefined ? this._convertValue(quarksRotation.angleY) : undefined, + angleZ: quarksRotation.angleZ !== undefined ? this._convertValue(quarksRotation.angleZ) : undefined, + }; + } + return this._convertValue(quarksRotation as QuarksValue); + } + + /** + * Convert Quarks gradient key to VFX gradient key + */ + private _convertGradientKey(quarksKey: QuarksGradientKey): VFXGradientKey { + return { + time: quarksKey.time, + value: quarksKey.value, + pos: quarksKey.pos, + }; + } + + /** + * Convert Quarks shape to VFX shape + */ + private _convertShape(quarksShape: QuarksShape): VFXShape { + const vfxShape: VFXShape = { + type: quarksShape.type, + radius: quarksShape.radius, + arc: quarksShape.arc, + thickness: quarksShape.thickness, + angle: quarksShape.angle, + mode: quarksShape.mode, + spread: quarksShape.spread, + size: quarksShape.size, + height: quarksShape.height, + }; + if (quarksShape.speed !== undefined) { + vfxShape.speed = this._convertValue(quarksShape.speed); + } + return vfxShape; + } + + /** + * Convert Quarks behavior to VFX behavior + */ + private _convertBehavior(quarksBehavior: QuarksBehavior): VFXBehavior { + switch (quarksBehavior.type) { + case "ColorOverLife": { + const behavior = quarksBehavior as QuarksColorOverLifeBehavior; + if (behavior.color) { + const vfxColor: VFXColorOverLifeBehavior["color"] = {}; + if (behavior.color.color?.keys) { + vfxColor.color = { keys: behavior.color.color.keys.map((k) => this._convertGradientKey(k)) }; + } + if (behavior.color.alpha?.keys) { + vfxColor.alpha = { keys: behavior.color.alpha.keys.map((k) => this._convertGradientKey(k)) }; + } + if (behavior.color.keys) { + vfxColor.keys = behavior.color.keys.map((k) => this._convertGradientKey(k)); + } + return { type: "ColorOverLife", color: vfxColor }; + } + return { type: "ColorOverLife" }; + } + + case "SizeOverLife": { + const behavior = quarksBehavior as QuarksSizeOverLifeBehavior; + if (behavior.size) { + const vfxSize: VFXSizeOverLifeBehavior["size"] = {}; + if (behavior.size.keys) { + vfxSize.keys = behavior.size.keys.map((k: QuarksGradientKey) => this._convertGradientKey(k)); + } + if (behavior.size.functions) { + vfxSize.functions = behavior.size.functions; + } + return { type: "SizeOverLife", size: vfxSize }; + } + return { type: "SizeOverLife" }; + } + + case "RotationOverLife": + case "Rotation3DOverLife": { + const behavior = quarksBehavior as QuarksRotationOverLifeBehavior; + return { + type: behavior.type, + angularVelocity: behavior.angularVelocity !== undefined ? this._convertValue(behavior.angularVelocity) : undefined, + }; + } + + case "ForceOverLife": + case "ApplyForce": { + const behavior = quarksBehavior as QuarksForceOverLifeBehavior; + const vfxBehavior: VFXForceOverLifeBehavior = { type: behavior.type }; + if (behavior.force) { + vfxBehavior.force = { + x: behavior.force.x !== undefined ? this._convertValue(behavior.force.x) : undefined, + y: behavior.force.y !== undefined ? this._convertValue(behavior.force.y) : undefined, + z: behavior.force.z !== undefined ? this._convertValue(behavior.force.z) : undefined, + }; + } + if (behavior.x !== undefined) vfxBehavior.x = this._convertValue(behavior.x); + if (behavior.y !== undefined) vfxBehavior.y = this._convertValue(behavior.y); + if (behavior.z !== undefined) vfxBehavior.z = this._convertValue(behavior.z); + return vfxBehavior; + } + + case "GravityForce": { + const behavior = quarksBehavior as QuarksGravityForceBehavior; + const vfxBehavior: { type: string; gravity?: VFXValue } = { + type: "GravityForce", + gravity: behavior.gravity !== undefined ? this._convertValue(behavior.gravity) : undefined, + }; + return vfxBehavior as VFXBehavior; + } + + case "SpeedOverLife": { + const behavior = quarksBehavior as QuarksSpeedOverLifeBehavior; + if (behavior.speed) { + if (typeof behavior.speed === "object" && behavior.speed !== null && "keys" in behavior.speed) { + const vfxSpeed: VFXSpeedOverLifeBehavior["speed"] = {}; + if (behavior.speed.keys) { + vfxSpeed.keys = behavior.speed.keys.map((k: QuarksGradientKey) => this._convertGradientKey(k)); + } + if (behavior.speed.functions) { + vfxSpeed.functions = behavior.speed.functions; + } + return { type: "SpeedOverLife", speed: vfxSpeed }; + } else if (typeof behavior.speed === "number" || (typeof behavior.speed === "object" && behavior.speed !== null && "type" in behavior.speed)) { + return { type: "SpeedOverLife", speed: this._convertValue(behavior.speed as QuarksValue) }; + } + } + return { type: "SpeedOverLife" }; + } + + case "FrameOverLife": { + const behavior = quarksBehavior as QuarksFrameOverLifeBehavior; + const vfxBehavior: { type: string; frame?: VFXValue | { keys?: VFXGradientKey[] } } = { type: "FrameOverLife" }; + if (behavior.frame) { + if (typeof behavior.frame === "object" && behavior.frame !== null && "keys" in behavior.frame) { + vfxBehavior.frame = { + keys: behavior.frame.keys?.map((k: QuarksGradientKey) => this._convertGradientKey(k)), + }; + } else if (typeof behavior.frame === "number" || (typeof behavior.frame === "object" && behavior.frame !== null && "type" in behavior.frame)) { + vfxBehavior.frame = this._convertValue(behavior.frame as QuarksValue); + } + } + return vfxBehavior as VFXBehavior; + } + + case "LimitSpeedOverLife": { + const behavior = quarksBehavior as QuarksLimitSpeedOverLifeBehavior; + const vfxBehavior: VFXLimitSpeedOverLifeBehavior = { type: "LimitSpeedOverLife" }; + if (behavior.maxSpeed !== undefined) { + vfxBehavior.maxSpeed = this._convertValue(behavior.maxSpeed); + } + if (behavior.speed !== undefined) { + if (typeof behavior.speed === "object" && behavior.speed !== null && "keys" in behavior.speed) { + vfxBehavior.speed = { keys: behavior.speed.keys?.map((k: QuarksGradientKey) => this._convertGradientKey(k)) }; + } else if (typeof behavior.speed === "number" || (typeof behavior.speed === "object" && behavior.speed !== null && "type" in behavior.speed)) { + vfxBehavior.speed = this._convertValue(behavior.speed as QuarksValue); + } + } + if (behavior.dampen !== undefined) { + vfxBehavior.dampen = this._convertValue(behavior.dampen); + } + return vfxBehavior; + } + + case "ColorBySpeed": { + const behavior = quarksBehavior as QuarksColorBySpeedBehavior; + const vfxBehavior: VFXColorBySpeedBehavior = { + type: "ColorBySpeed", + minSpeed: behavior.minSpeed !== undefined ? this._convertValue(behavior.minSpeed) : undefined, + maxSpeed: behavior.maxSpeed !== undefined ? this._convertValue(behavior.maxSpeed) : undefined, + }; + if (behavior.color?.keys) { + vfxBehavior.color = { keys: behavior.color.keys.map((k: QuarksGradientKey) => this._convertGradientKey(k)) }; + } + return vfxBehavior; + } + + case "SizeBySpeed": { + const behavior = quarksBehavior as QuarksSizeBySpeedBehavior; + const vfxBehavior: VFXSizeBySpeedBehavior = { + type: "SizeBySpeed", + minSpeed: behavior.minSpeed !== undefined ? this._convertValue(behavior.minSpeed) : undefined, + maxSpeed: behavior.maxSpeed !== undefined ? this._convertValue(behavior.maxSpeed) : undefined, + }; + if (behavior.size?.keys) { + vfxBehavior.size = { keys: behavior.size.keys.map((k: QuarksGradientKey) => this._convertGradientKey(k)) }; + } + return vfxBehavior; + } + + case "RotationBySpeed": { + const behavior = quarksBehavior as QuarksRotationBySpeedBehavior; + const vfxBehavior: { type: string; angularVelocity?: VFXValue; minSpeed?: VFXValue; maxSpeed?: VFXValue } = { + type: "RotationBySpeed", + angularVelocity: behavior.angularVelocity !== undefined ? this._convertValue(behavior.angularVelocity) : undefined, + minSpeed: behavior.minSpeed !== undefined ? this._convertValue(behavior.minSpeed) : undefined, + maxSpeed: behavior.maxSpeed !== undefined ? this._convertValue(behavior.maxSpeed) : undefined, + }; + return vfxBehavior as VFXBehavior; + } + + case "OrbitOverLife": { + const behavior = quarksBehavior as QuarksOrbitOverLifeBehavior; + const vfxBehavior: { type: string; center?: { x?: number; y?: number; z?: number }; radius?: VFXValue; speed?: VFXValue } = { + type: "OrbitOverLife", + center: behavior.center, + radius: behavior.radius !== undefined ? this._convertValue(behavior.radius) : undefined, + speed: behavior.speed !== undefined ? this._convertValue(behavior.speed) : undefined, + }; + return vfxBehavior as VFXBehavior; + } + + default: + // Fallback for unknown behaviors - copy as-is + return quarksBehavior as VFXBehavior; + } + } } diff --git a/editor/src/editor/windows/fx-editor/VFX/parsers/VFXParser.ts b/editor/src/editor/windows/fx-editor/VFX/parsers/VFXParser.ts index 9babf2407..e52186651 100644 --- a/editor/src/editor/windows/fx-editor/VFX/parsers/VFXParser.ts +++ b/editor/src/editor/windows/fx-editor/VFX/parsers/VFXParser.ts @@ -18,106 +18,106 @@ import { VFXSolidParticleSystem } from "../systems/VFXSolidParticleSystem"; * Orchestrates the parsing process using modular components */ export class VFXParser { - private _context: VFXParseContext; - private _logger: VFXLogger; - private _valueParser: VFXValueParser; - private _materialFactory: VFXMaterialFactory; - private _geometryFactory: VFXGeometryFactory; - private _emitterFactory: VFXEmitterFactory; - private _hierarchyProcessor: VFXHierarchyProcessor; - - constructor(scene: Scene, rootUrl: string, jsonData: QuarksVFXJSON, options?: VFXLoaderOptions) { - const opts = options || {}; - this._context = { - scene, - rootUrl, - jsonData, - options: opts, - groupNodesMap: new Map(), - }; - - this._logger = new VFXLogger("[VFXParser]"); - this._valueParser = new VFXValueParser(); - this._materialFactory = new VFXMaterialFactory(this._context); - this._geometryFactory = new VFXGeometryFactory(this._context, this._materialFactory); - this._emitterFactory = new VFXEmitterFactory(this._context, this._valueParser, this._materialFactory, this._geometryFactory); - this._hierarchyProcessor = new VFXHierarchyProcessor(this._context, this._emitterFactory); - } - - /** - * Parse the JSON data and create particle systems - */ - public parse(): (VFXParticleSystem | VFXSolidParticleSystem)[] { - const { jsonData, options } = this._context; - this._logger.log("=== Starting Particle System Parsing ===", options); - - if (options.validate) { - this._validateJSONStructure(jsonData, options); - } - - const dataConverter = new VFXDataConverter(options); - const vfxData = dataConverter.convert(jsonData); - this._context.vfxData = vfxData; - const particleSystems = this._hierarchyProcessor.processHierarchy(vfxData); - - this._logger.log(`=== Parsing complete. Created ${particleSystems.length} particle system(s) ===`, options); - return particleSystems; - } - - /** - * Validate JSON structure - */ - private _validateJSONStructure(jsonData: QuarksVFXJSON, options: VFXLoaderOptions): void { - this._logger.log("Validating JSON structure...", options); - - if (!jsonData.object) { - this._logger.warn("JSON missing 'object' property", options); - } - - if (!jsonData.materials || jsonData.materials.length === 0) { - this._logger.warn("JSON has no materials", options); - } - - if (!jsonData.textures || jsonData.textures.length === 0) { - this._logger.warn("JSON has no textures", options); - } - - if (!jsonData.images || jsonData.images.length === 0) { - this._logger.warn("JSON has no images", options); - } - - if (!jsonData.geometries || jsonData.geometries.length === 0) { - this._logger.warn("JSON has no geometries", options); - } - - this._logger.log("Validation complete", options); - } - - /** - * Get the parse context (for use by other components) - */ - public getContext(): VFXParseContext { - return this._context; - } - - /** - * Get the value parser - */ - public getValueParser(): VFXValueParser { - return this._valueParser; - } - - /** - * Get the material factory - */ - public getMaterialFactory(): VFXMaterialFactory { - return this._materialFactory; - } - - /** - * Get the geometry factory - */ - public getGeometryFactory(): VFXGeometryFactory { - return this._geometryFactory; - } + private _context: VFXParseContext; + private _logger: VFXLogger; + private _valueParser: VFXValueParser; + private _materialFactory: VFXMaterialFactory; + private _geometryFactory: VFXGeometryFactory; + private _emitterFactory: VFXEmitterFactory; + private _hierarchyProcessor: VFXHierarchyProcessor; + + constructor(scene: Scene, rootUrl: string, jsonData: QuarksVFXJSON, options?: VFXLoaderOptions) { + const opts = options || {}; + this._context = { + scene, + rootUrl, + jsonData, + options: opts, + groupNodesMap: new Map(), + }; + + this._logger = new VFXLogger("[VFXParser]"); + this._valueParser = new VFXValueParser(); + this._materialFactory = new VFXMaterialFactory(this._context); + this._geometryFactory = new VFXGeometryFactory(this._context, this._materialFactory); + this._emitterFactory = new VFXEmitterFactory(this._context, this._valueParser, this._materialFactory, this._geometryFactory); + this._hierarchyProcessor = new VFXHierarchyProcessor(this._context, this._emitterFactory); + } + + /** + * Parse the JSON data and create particle systems + */ + public parse(): (VFXParticleSystem | VFXSolidParticleSystem)[] { + const { jsonData, options } = this._context; + this._logger.log("=== Starting Particle System Parsing ===", options); + + if (options.validate) { + this._validateJSONStructure(jsonData, options); + } + + const dataConverter = new VFXDataConverter(options); + const vfxData = dataConverter.convert(jsonData); + this._context.vfxData = vfxData; + const particleSystems = this._hierarchyProcessor.processHierarchy(vfxData); + + this._logger.log(`=== Parsing complete. Created ${particleSystems.length} particle system(s) ===`, options); + return particleSystems; + } + + /** + * Validate JSON structure + */ + private _validateJSONStructure(jsonData: QuarksVFXJSON, options: VFXLoaderOptions): void { + this._logger.log("Validating JSON structure...", options); + + if (!jsonData.object) { + this._logger.warn("JSON missing 'object' property", options); + } + + if (!jsonData.materials || jsonData.materials.length === 0) { + this._logger.warn("JSON has no materials", options); + } + + if (!jsonData.textures || jsonData.textures.length === 0) { + this._logger.warn("JSON has no textures", options); + } + + if (!jsonData.images || jsonData.images.length === 0) { + this._logger.warn("JSON has no images", options); + } + + if (!jsonData.geometries || jsonData.geometries.length === 0) { + this._logger.warn("JSON has no geometries", options); + } + + this._logger.log("Validation complete", options); + } + + /** + * Get the parse context (for use by other components) + */ + public getContext(): VFXParseContext { + return this._context; + } + + /** + * Get the value parser + */ + public getValueParser(): VFXValueParser { + return this._valueParser; + } + + /** + * Get the material factory + */ + public getMaterialFactory(): VFXMaterialFactory { + return this._materialFactory; + } + + /** + * Get the geometry factory + */ + public getGeometryFactory(): VFXGeometryFactory { + return this._geometryFactory; + } } diff --git a/editor/src/editor/windows/fx-editor/VFX/parsers/VFXValueParser.ts b/editor/src/editor/windows/fx-editor/VFX/parsers/VFXValueParser.ts index f5bde22d3..0b25206f4 100644 --- a/editor/src/editor/windows/fx-editor/VFX/parsers/VFXValueParser.ts +++ b/editor/src/editor/windows/fx-editor/VFX/parsers/VFXValueParser.ts @@ -10,178 +10,178 @@ import type { VFXPiecewiseBezier } from "../types/values"; * Parser for Three.js value types (ConstantValue, IntervalValue, Gradient, etc.) */ export class VFXValueParser implements IVFXValueParser { - /** - * Parse a constant value - */ - public parseConstantValue(value: VFXValue): number { - if (value && typeof value === "object" && value.type === "ConstantValue") { - return value.value || 0; - } - return typeof value === "number" ? value : 0; - } - - /** - * Parse an interval value (returns min and max) - */ - public parseIntervalValue(value: VFXValue): { min: number; max: number } { - if (value && typeof value === "object" && "type" in value && value.type === "IntervalValue") { - return { - min: value.min ?? 0, - max: value.max ?? 0, - }; - } - const constant = this.parseConstantValue(value); - return { min: constant, max: constant }; - } - - /** - * Parse a constant color - */ - public parseConstantColor(value: VFXColor): Color4 { - if (value && typeof value === "object" && !Array.isArray(value)) { - if ("type" in value && value.type === "ConstantColor") { - if (value.value && Array.isArray(value.value)) { - return new Color4(value.value[0] || 0, value.value[1] || 0, value.value[2] || 0, value.value[3] !== undefined ? value.value[3] : 1); - } - } else if (Array.isArray(value) && value.length >= 3) { - // Array format [r, g, b, a?] - return new Color4(value[0] || 0, value[1] || 0, value[2] || 0, value[3] !== undefined ? value[3] : 1); - } - } - return new Color4(1, 1, 1, 1); - } - - /** - * Parse gradient color keys - */ - public parseGradientColorKeys(keys: VFXGradientKey[]): ColorGradient[] { - const gradients: ColorGradient[] = []; - for (const key of keys) { - const pos = key.pos ?? key.time ?? 0; - if (key.value !== undefined && pos !== undefined) { - let color4: Color4; - if (typeof key.value === "number") { - // Single number - grayscale - color4 = new Color4(key.value, key.value, key.value, 1); - } else if (Array.isArray(key.value)) { - // Array format [r, g, b, a?] - color4 = new Color4(key.value[0] || 0, key.value[1] || 0, key.value[2] || 0, key.value[3] !== undefined ? key.value[3] : 1); - } else { - // Object format { r, g, b, a? } - color4 = new Color4(key.value.r || 0, key.value.g || 0, key.value.b || 0, key.value.a !== undefined ? key.value.a : 1); - } - gradients.push(new ColorGradient(pos, color4)); - } - } - return gradients; - } - - /** - * Parse gradient alpha keys - */ - public parseGradientAlphaKeys(keys: VFXGradientKey[]): { gradient: number; factor: number }[] { - const gradients: { gradient: number; factor: number }[] = []; - for (const key of keys) { - const pos = key.pos ?? key.time ?? 0; - if (key.value !== undefined && pos !== undefined) { - let factor: number; - if (typeof key.value === "number") { - factor = key.value; - } else if (Array.isArray(key.value)) { - factor = key.value[3] !== undefined ? key.value[3] : 1; - } else { - factor = key.value.a !== undefined ? key.value.a : 1; - } - gradients.push({ gradient: pos, factor }); - } - } - return gradients; - } - - /** - * Parse a value for particle spawn (returns a single value based on type) - * Handles ConstantValue, IntervalValue, PiecewiseBezier, and number - * @param value The value to parse - * @param normalizedTime Normalized time (0-1) for PiecewiseBezier, default is random for IntervalValue - */ - public parseValue(value: VFXValue, normalizedTime?: number): number { - if (!value || typeof value === "number") { - return typeof value === "number" ? value : 0; - } - - if (value.type === "ConstantValue") { - return value.value || 0; - } - - if (value.type === "IntervalValue") { - const min = value.min ?? 0; - const max = value.max ?? 0; - return min + Math.random() * (max - min); - } - - if (value.type === "PiecewiseBezier") { - // Use provided normalizedTime or random for spawn - const t = normalizedTime !== undefined ? normalizedTime : Math.random(); - return this._evaluatePiecewiseBezier(value, t); - } - - // Fallback - return 0; - } - - /** - * Evaluate PiecewiseBezier at normalized time t (0-1) - */ - private _evaluatePiecewiseBezier(bezier: VFXPiecewiseBezier, t: number): number { - if (!bezier.functions || bezier.functions.length === 0) { - return 0; - } - - // Clamp t to [0, 1] - const clampedT = Math.max(0, Math.min(1, t)); - - // Find which function segment contains t - let segmentIndex = -1; - for (let i = 0; i < bezier.functions.length; i++) { - const func = bezier.functions[i]; - const start = func.start; - const end = i < bezier.functions.length - 1 ? bezier.functions[i + 1].start : 1; - - if (clampedT >= start && clampedT < end) { - segmentIndex = i; - break; - } - } - - // If t is at the end (1.0), use last segment - if (segmentIndex === -1 && clampedT >= 1) { - segmentIndex = bezier.functions.length - 1; - } - - // If still not found, use first segment - if (segmentIndex === -1) { - segmentIndex = 0; - } - - const func = bezier.functions[segmentIndex]; - const start = func.start; - const end = segmentIndex < bezier.functions.length - 1 ? bezier.functions[segmentIndex + 1].start : 1; - - // Normalize t within this segment - const segmentT = end > start ? (clampedT - start) / (end - start) : 0; - - // Evaluate cubic Bezier: B(t) = (1-t)³P₀ + 3(1-t)²tP₁ + 3(1-t)t²P₂ + t³P₃ - const p0 = func.function.p0; - const p1 = func.function.p1; - const p2 = func.function.p2; - const p3 = func.function.p3; - - const t2 = segmentT * segmentT; - const t3 = t2 * segmentT; - const mt = 1 - segmentT; - const mt2 = mt * mt; - const mt3 = mt2 * mt; - - return mt3 * p0 + 3 * mt2 * segmentT * p1 + 3 * mt * t2 * p2 + t3 * p3; - } + /** + * Parse a constant value + */ + public parseConstantValue(value: VFXValue): number { + if (value && typeof value === "object" && value.type === "ConstantValue") { + return value.value || 0; + } + return typeof value === "number" ? value : 0; + } + + /** + * Parse an interval value (returns min and max) + */ + public parseIntervalValue(value: VFXValue): { min: number; max: number } { + if (value && typeof value === "object" && "type" in value && value.type === "IntervalValue") { + return { + min: value.min ?? 0, + max: value.max ?? 0, + }; + } + const constant = this.parseConstantValue(value); + return { min: constant, max: constant }; + } + + /** + * Parse a constant color + */ + public parseConstantColor(value: VFXColor): Color4 { + if (value && typeof value === "object" && !Array.isArray(value)) { + if ("type" in value && value.type === "ConstantColor") { + if (value.value && Array.isArray(value.value)) { + return new Color4(value.value[0] || 0, value.value[1] || 0, value.value[2] || 0, value.value[3] !== undefined ? value.value[3] : 1); + } + } else if (Array.isArray(value) && value.length >= 3) { + // Array format [r, g, b, a?] + return new Color4(value[0] || 0, value[1] || 0, value[2] || 0, value[3] !== undefined ? value[3] : 1); + } + } + return new Color4(1, 1, 1, 1); + } + + /** + * Parse gradient color keys + */ + public parseGradientColorKeys(keys: VFXGradientKey[]): ColorGradient[] { + const gradients: ColorGradient[] = []; + for (const key of keys) { + const pos = key.pos ?? key.time ?? 0; + if (key.value !== undefined && pos !== undefined) { + let color4: Color4; + if (typeof key.value === "number") { + // Single number - grayscale + color4 = new Color4(key.value, key.value, key.value, 1); + } else if (Array.isArray(key.value)) { + // Array format [r, g, b, a?] + color4 = new Color4(key.value[0] || 0, key.value[1] || 0, key.value[2] || 0, key.value[3] !== undefined ? key.value[3] : 1); + } else { + // Object format { r, g, b, a? } + color4 = new Color4(key.value.r || 0, key.value.g || 0, key.value.b || 0, key.value.a !== undefined ? key.value.a : 1); + } + gradients.push(new ColorGradient(pos, color4)); + } + } + return gradients; + } + + /** + * Parse gradient alpha keys + */ + public parseGradientAlphaKeys(keys: VFXGradientKey[]): { gradient: number; factor: number }[] { + const gradients: { gradient: number; factor: number }[] = []; + for (const key of keys) { + const pos = key.pos ?? key.time ?? 0; + if (key.value !== undefined && pos !== undefined) { + let factor: number; + if (typeof key.value === "number") { + factor = key.value; + } else if (Array.isArray(key.value)) { + factor = key.value[3] !== undefined ? key.value[3] : 1; + } else { + factor = key.value.a !== undefined ? key.value.a : 1; + } + gradients.push({ gradient: pos, factor }); + } + } + return gradients; + } + + /** + * Parse a value for particle spawn (returns a single value based on type) + * Handles ConstantValue, IntervalValue, PiecewiseBezier, and number + * @param value The value to parse + * @param normalizedTime Normalized time (0-1) for PiecewiseBezier, default is random for IntervalValue + */ + public parseValue(value: VFXValue, normalizedTime?: number): number { + if (!value || typeof value === "number") { + return typeof value === "number" ? value : 0; + } + + if (value.type === "ConstantValue") { + return value.value || 0; + } + + if (value.type === "IntervalValue") { + const min = value.min ?? 0; + const max = value.max ?? 0; + return min + Math.random() * (max - min); + } + + if (value.type === "PiecewiseBezier") { + // Use provided normalizedTime or random for spawn + const t = normalizedTime !== undefined ? normalizedTime : Math.random(); + return this._evaluatePiecewiseBezier(value, t); + } + + // Fallback + return 0; + } + + /** + * Evaluate PiecewiseBezier at normalized time t (0-1) + */ + private _evaluatePiecewiseBezier(bezier: VFXPiecewiseBezier, t: number): number { + if (!bezier.functions || bezier.functions.length === 0) { + return 0; + } + + // Clamp t to [0, 1] + const clampedT = Math.max(0, Math.min(1, t)); + + // Find which function segment contains t + let segmentIndex = -1; + for (let i = 0; i < bezier.functions.length; i++) { + const func = bezier.functions[i]; + const start = func.start; + const end = i < bezier.functions.length - 1 ? bezier.functions[i + 1].start : 1; + + if (clampedT >= start && clampedT < end) { + segmentIndex = i; + break; + } + } + + // If t is at the end (1.0), use last segment + if (segmentIndex === -1 && clampedT >= 1) { + segmentIndex = bezier.functions.length - 1; + } + + // If still not found, use first segment + if (segmentIndex === -1) { + segmentIndex = 0; + } + + const func = bezier.functions[segmentIndex]; + const start = func.start; + const end = segmentIndex < bezier.functions.length - 1 ? bezier.functions[segmentIndex + 1].start : 1; + + // Normalize t within this segment + const segmentT = end > start ? (clampedT - start) / (end - start) : 0; + + // Evaluate cubic Bezier: B(t) = (1-t)³P₀ + 3(1-t)²tP₁ + 3(1-t)t²P₂ + t³P₃ + const p0 = func.function.p0; + const p1 = func.function.p1; + const p2 = func.function.p2; + const p3 = func.function.p3; + + const t2 = segmentT * segmentT; + const t3 = t2 * segmentT; + const mt = 1 - segmentT; + const mt2 = mt * mt; + const mt3 = mt2 * mt; + + return mt3 * p0 + 3 * mt2 * segmentT * p1 + 3 * mt * t2 * p2 + t3 * p3; + } } diff --git a/editor/src/editor/windows/fx-editor/VFX/processors/VFXHierarchyProcessor.ts b/editor/src/editor/windows/fx-editor/VFX/processors/VFXHierarchyProcessor.ts index 497241797..3f6e221ef 100644 --- a/editor/src/editor/windows/fx-editor/VFX/processors/VFXHierarchyProcessor.ts +++ b/editor/src/editor/windows/fx-editor/VFX/processors/VFXHierarchyProcessor.ts @@ -14,281 +14,281 @@ import type { IVFXEmitterFactory } from "../types/factories"; * Processor for Three.js object hierarchy (Groups and ParticleEmitters) */ export class VFXHierarchyProcessor { - private _logger: VFXLogger; - private _context: VFXParseContext; - private _emitterFactory: IVFXEmitterFactory; - - constructor(context: VFXParseContext, emitterFactory: IVFXEmitterFactory) { - this._context = context; - this._logger = new VFXLogger("[VFXHierarchyProcessor]"); - this._emitterFactory = emitterFactory; - } - - /** - * Process the VFX hierarchy and create particle systems - * Uses pre-converted data (already in left-handed coordinate system) - */ - public processHierarchy(vfxData: VFXHierarchy): (VFXParticleSystem | VFXSolidParticleSystem)[] { - const { options } = this._context; - const particleSystems: (VFXParticleSystem | VFXSolidParticleSystem)[] = []; - - if (!vfxData.root) { - this._logger.warn("No root object found in VFX data", options); - return particleSystems; - } - - this._logger.log("Phase 1: Creating nodes and building hierarchy", options); - // Phase 1: Create all nodes without transformations, build hierarchy - this._processVFXObject(vfxData.root, null, 0, particleSystems, false, vfxData); - - this._logger.log("Phase 2: Applying transformations", options); - // Phase 2: Apply transformations after hierarchy is established - this._processVFXObject(vfxData.root, null, 0, particleSystems, true, vfxData); - - return particleSystems; - } - - /** - * Recursively process VFX object hierarchy - * @param applyTransformations If true, applies transformations. If false, creates nodes and builds hierarchy. - */ - private _processVFXObject( - vfxObj: VFXGroup | VFXEmitter, - parentGroup: Nullable, - depth: number, - particleSystems: (VFXParticleSystem | VFXSolidParticleSystem)[], - applyTransformations: boolean, - vfxData: VFXHierarchy - ): void { - const { options } = this._context; - const indent = " ".repeat(depth); - - if (!applyTransformations) { - this._logger.log(`${indent}Creating object: ${vfxObj.name}`, options); - } else { - this._logger.log(`${indent}Applying transformations to: ${vfxObj.name}`, options); - } - - let currentGroup: Nullable = parentGroup; - - // Handle Group objects - if ("children" in vfxObj) { - const vfxGroup = vfxObj as VFXGroup; - if (!applyTransformations) { - // Phase 1: Create group without transformations, set parent - currentGroup = this._createGroupFromVFX(vfxGroup, parentGroup, depth, options); - } else { - // Phase 2: Apply transformations to group - currentGroup = this._context.groupNodesMap.get(vfxGroup.uuid) || null; - if (currentGroup) { - this._applyVFXTransformToGroup(currentGroup, vfxGroup.transform, depth, options); - } - } - - // Process children recursively - if (vfxGroup.children && vfxGroup.children.length > 0) { - if (!applyTransformations) { - this._logger.log(`${indent}Processing ${vfxGroup.children.length} children`, options); - } - for (const child of vfxGroup.children) { - this._processVFXObject(child, currentGroup, depth + 1, particleSystems, applyTransformations, vfxData); - } - } - } else { - // Handle Emitter objects - const vfxEmitter = vfxObj as VFXEmitter; - if (!applyTransformations) { - // Phase 1: Create particle system without transformations - const particleSystem = this._processVFXEmitter(vfxEmitter, currentGroup, depth, options, false); - if (particleSystem) { - particleSystems.push(particleSystem); - } - } else { - // Phase 2: Apply transformations to particle system - this._applyVFXTransformToEmitter(vfxEmitter, currentGroup, depth, options); - } - } - } - - /** - * Create a Group (TransformNode) from VFX Group data - * Phase 1: Creates node without transformations, sets parent - */ - private _createGroupFromVFX(vfxGroup: VFXGroup, parentGroup: Nullable, depth: number, options?: VFXLoaderOptions): TransformNode { - const { scene } = this._context; - const indent = " ".repeat(depth); - - this._logger.log(`${indent}Creating Group: ${vfxGroup.name} (without transformations)`, options); - const groupNode = new TransformNode(vfxGroup.name, scene); - - // Initialize with identity transform (will be applied in phase 2) - groupNode.position.setAll(0); - if (!groupNode.rotationQuaternion) { - groupNode.rotationQuaternion = Quaternion.Identity(); - } else { - groupNode.rotationQuaternion.set(0, 0, 0, 1); - } - groupNode.scaling.setAll(1); - - // Set visibility - groupNode.isVisible = false; - - // Set parent FIRST (before applying transformations) - if (parentGroup) { - groupNode.setParent(parentGroup); - this._logger.log(`${indent}Group parent set: ${parentGroup.name}`, options); - } - - // Store in map for reference (needed for phase 2) - this._context.groupNodesMap.set(vfxGroup.uuid, groupNode); - this._logger.log(`${indent}Group stored in map: ${vfxGroup.uuid}`, options); - - return groupNode; - } - - /** - * Apply VFX transform to a Group node - * Phase 2: Applies pre-converted transformations (already in left-handed system) - */ - private _applyVFXTransformToGroup(groupNode: TransformNode, transform: VFXTransform, depth: number, options?: VFXLoaderOptions): void { - const indent = " ".repeat(depth); - this._logger.log(`${indent}Applying converted transform to group: ${groupNode.name}`, options); - - // Transform is already converted to left-handed, apply directly - groupNode.position.copyFrom(transform.position); - groupNode.rotationQuaternion = transform.rotation.clone(); - groupNode.scaling.copyFrom(transform.scale); - - const pos = groupNode.position; - const rot = groupNode.rotationQuaternion; - const scl = groupNode.scaling; - this._logger.log(`${indent}Group position: (${pos.x.toFixed(2)}, ${pos.y.toFixed(2)}, ${pos.z.toFixed(2)})`, options); - if (rot) { - this._logger.log(`${indent}Group rotation quaternion: (${rot.x.toFixed(4)}, ${rot.y.toFixed(4)}, ${rot.z.toFixed(4)}, ${rot.w.toFixed(4)})`, options); - } - this._logger.log(`${indent}Group scale: (${scl.x.toFixed(2)}, ${scl.y.toFixed(2)}, ${scl.z.toFixed(2)})`, options); - } - - /** - * Process a VFX ParticleEmitter - * @param applyTransformations If false, creates system without transformations. If true, applies transformations. - */ - private _processVFXEmitter( - vfxEmitter: VFXEmitter, - currentGroup: Nullable, - depth: number, - options?: VFXLoaderOptions, - applyTransformations: boolean = false - ): Nullable { - const indent = " ".repeat(depth); - const emitterName = vfxEmitter.name; - - this._logger.log(`${indent}=== Processing ParticleEmitter: ${emitterName} ===`, options); - this._logger.log(`${indent}Current parent group: ${currentGroup ? currentGroup.name : "none"}`, options); - - // Log emitter configuration - if (options?.verbose) { - this._logger.log( - `${indent}Emitter config: ${JSON.stringify( - { - renderMode: vfxEmitter.config.renderMode, - duration: vfxEmitter.config.duration, - looping: vfxEmitter.config.looping, - prewarm: vfxEmitter.config.prewarm, - emissionOverTime: vfxEmitter.config.emissionOverTime, - startLife: vfxEmitter.config.startLife, - startSpeed: vfxEmitter.config.startSpeed, - startSize: vfxEmitter.config.startSize, - behaviorsCount: vfxEmitter.config.behaviors?.length || 0, - worldSpace: vfxEmitter.config.worldSpace, - }, - null, - 2 - )}`, - options - ); - } - - // Calculate cumulative scale from parent groups - const cumulativeScale = this._calculateCumulativeScale(currentGroup); - - const emitterData: VFXEmitterData = { - name: emitterName, - config: vfxEmitter.config, - materialId: vfxEmitter.materialId, - // Transform is already converted, will be passed through emitterData - matrix: undefined, - position: undefined, - parentGroup: currentGroup, - cumulativeScale, - }; - - // Store VFX emitter data (including transform) in emitterData for use in factory - emitterData.vfxEmitter = vfxEmitter; - - if (options?.verbose && (cumulativeScale.x !== 1 || cumulativeScale.y !== 1 || cumulativeScale.z !== 1)) { - this._logger.log( - `${indent}Cumulative scale from parent groups: (${cumulativeScale.x.toFixed(2)}, ${cumulativeScale.y.toFixed(2)}, ${cumulativeScale.z.toFixed(2)})`, - options - ); - } - - if (!applyTransformations) { - // Phase 1: Create particle system without transformations - const particleSystem = this._emitterFactory.createEmitter(emitterData); - - if (particleSystem) { - this._logger.log(`${indent}Particle system created successfully (without transformations)`, options); - - // VFX emitter data is already stored in emitterData, no need to store in particle system - - // Handle prewarm - if (vfxEmitter.config.prewarm) { - particleSystem.start(); - } - - return particleSystem as VFXParticleSystem; - } else { - this._logger.warn(`${indent}Failed to create particle system for ${emitterName}`, options); - return null; - } - } else { - // Phase 2: Apply transformations (this will be handled separately) - return null; - } - } - - /** - * Apply VFX transform to emitter (Phase 2) - * For SPS, transformations are applied in initParticles (after buildMesh) - * For ParticleSystem, we need to find and update the emitter mesh - */ - private _applyVFXTransformToEmitter(vfxEmitter: VFXEmitter, _currentGroup: Nullable, depth: number, options?: VFXLoaderOptions): void { - const indent = " ".repeat(depth); - const emitterName = vfxEmitter.name; - - // For SPS: transformations are applied in initParticles (called after buildMesh) - // Transform is already stored in _vfxEmitter and will be applied there - // For ParticleSystem: emitter is set during creation, but we need to apply transform if it's a mesh - // Note: ParticleSystem emitter transformations are handled during creation phase - // because emitter needs to be set before particle system starts - this._logger.log(`${indent}Transformations for emitter ${emitterName} (will be applied in initParticles for SPS)`, options); - } - - /** - * Calculate cumulative scale from parent groups - */ - private _calculateCumulativeScale(parent: Nullable): Vector3 { - const cumulativeScale = new Vector3(1, 1, 1); - let current = parent; - - while (current) { - cumulativeScale.x *= current.scaling.x; - cumulativeScale.y *= current.scaling.y; - cumulativeScale.z *= current.scaling.z; - current = current.parent as TransformNode; - } - - return cumulativeScale; - } + private _logger: VFXLogger; + private _context: VFXParseContext; + private _emitterFactory: IVFXEmitterFactory; + + constructor(context: VFXParseContext, emitterFactory: IVFXEmitterFactory) { + this._context = context; + this._logger = new VFXLogger("[VFXHierarchyProcessor]"); + this._emitterFactory = emitterFactory; + } + + /** + * Process the VFX hierarchy and create particle systems + * Uses pre-converted data (already in left-handed coordinate system) + */ + public processHierarchy(vfxData: VFXHierarchy): (VFXParticleSystem | VFXSolidParticleSystem)[] { + const { options } = this._context; + const particleSystems: (VFXParticleSystem | VFXSolidParticleSystem)[] = []; + + if (!vfxData.root) { + this._logger.warn("No root object found in VFX data", options); + return particleSystems; + } + + this._logger.log("Phase 1: Creating nodes and building hierarchy", options); + // Phase 1: Create all nodes without transformations, build hierarchy + this._processVFXObject(vfxData.root, null, 0, particleSystems, false, vfxData); + + this._logger.log("Phase 2: Applying transformations", options); + // Phase 2: Apply transformations after hierarchy is established + this._processVFXObject(vfxData.root, null, 0, particleSystems, true, vfxData); + + return particleSystems; + } + + /** + * Recursively process VFX object hierarchy + * @param applyTransformations If true, applies transformations. If false, creates nodes and builds hierarchy. + */ + private _processVFXObject( + vfxObj: VFXGroup | VFXEmitter, + parentGroup: Nullable, + depth: number, + particleSystems: (VFXParticleSystem | VFXSolidParticleSystem)[], + applyTransformations: boolean, + vfxData: VFXHierarchy + ): void { + const { options } = this._context; + const indent = " ".repeat(depth); + + if (!applyTransformations) { + this._logger.log(`${indent}Creating object: ${vfxObj.name}`, options); + } else { + this._logger.log(`${indent}Applying transformations to: ${vfxObj.name}`, options); + } + + let currentGroup: Nullable = parentGroup; + + // Handle Group objects + if ("children" in vfxObj) { + const vfxGroup = vfxObj as VFXGroup; + if (!applyTransformations) { + // Phase 1: Create group without transformations, set parent + currentGroup = this._createGroupFromVFX(vfxGroup, parentGroup, depth, options); + } else { + // Phase 2: Apply transformations to group + currentGroup = this._context.groupNodesMap.get(vfxGroup.uuid) || null; + if (currentGroup) { + this._applyVFXTransformToGroup(currentGroup, vfxGroup.transform, depth, options); + } + } + + // Process children recursively + if (vfxGroup.children && vfxGroup.children.length > 0) { + if (!applyTransformations) { + this._logger.log(`${indent}Processing ${vfxGroup.children.length} children`, options); + } + for (const child of vfxGroup.children) { + this._processVFXObject(child, currentGroup, depth + 1, particleSystems, applyTransformations, vfxData); + } + } + } else { + // Handle Emitter objects + const vfxEmitter = vfxObj as VFXEmitter; + if (!applyTransformations) { + // Phase 1: Create particle system without transformations + const particleSystem = this._processVFXEmitter(vfxEmitter, currentGroup, depth, options, false); + if (particleSystem) { + particleSystems.push(particleSystem); + } + } else { + // Phase 2: Apply transformations to particle system + this._applyVFXTransformToEmitter(vfxEmitter, currentGroup, depth, options); + } + } + } + + /** + * Create a Group (TransformNode) from VFX Group data + * Phase 1: Creates node without transformations, sets parent + */ + private _createGroupFromVFX(vfxGroup: VFXGroup, parentGroup: Nullable, depth: number, options?: VFXLoaderOptions): TransformNode { + const { scene } = this._context; + const indent = " ".repeat(depth); + + this._logger.log(`${indent}Creating Group: ${vfxGroup.name} (without transformations)`, options); + const groupNode = new TransformNode(vfxGroup.name, scene); + + // Initialize with identity transform (will be applied in phase 2) + groupNode.position.setAll(0); + if (!groupNode.rotationQuaternion) { + groupNode.rotationQuaternion = Quaternion.Identity(); + } else { + groupNode.rotationQuaternion.set(0, 0, 0, 1); + } + groupNode.scaling.setAll(1); + + // Set visibility + groupNode.isVisible = false; + + // Set parent FIRST (before applying transformations) + if (parentGroup) { + groupNode.setParent(parentGroup); + this._logger.log(`${indent}Group parent set: ${parentGroup.name}`, options); + } + + // Store in map for reference (needed for phase 2) + this._context.groupNodesMap.set(vfxGroup.uuid, groupNode); + this._logger.log(`${indent}Group stored in map: ${vfxGroup.uuid}`, options); + + return groupNode; + } + + /** + * Apply VFX transform to a Group node + * Phase 2: Applies pre-converted transformations (already in left-handed system) + */ + private _applyVFXTransformToGroup(groupNode: TransformNode, transform: VFXTransform, depth: number, options?: VFXLoaderOptions): void { + const indent = " ".repeat(depth); + this._logger.log(`${indent}Applying converted transform to group: ${groupNode.name}`, options); + + // Transform is already converted to left-handed, apply directly + groupNode.position.copyFrom(transform.position); + groupNode.rotationQuaternion = transform.rotation.clone(); + groupNode.scaling.copyFrom(transform.scale); + + const pos = groupNode.position; + const rot = groupNode.rotationQuaternion; + const scl = groupNode.scaling; + this._logger.log(`${indent}Group position: (${pos.x.toFixed(2)}, ${pos.y.toFixed(2)}, ${pos.z.toFixed(2)})`, options); + if (rot) { + this._logger.log(`${indent}Group rotation quaternion: (${rot.x.toFixed(4)}, ${rot.y.toFixed(4)}, ${rot.z.toFixed(4)}, ${rot.w.toFixed(4)})`, options); + } + this._logger.log(`${indent}Group scale: (${scl.x.toFixed(2)}, ${scl.y.toFixed(2)}, ${scl.z.toFixed(2)})`, options); + } + + /** + * Process a VFX ParticleEmitter + * @param applyTransformations If false, creates system without transformations. If true, applies transformations. + */ + private _processVFXEmitter( + vfxEmitter: VFXEmitter, + currentGroup: Nullable, + depth: number, + options?: VFXLoaderOptions, + applyTransformations: boolean = false + ): Nullable { + const indent = " ".repeat(depth); + const emitterName = vfxEmitter.name; + + this._logger.log(`${indent}=== Processing ParticleEmitter: ${emitterName} ===`, options); + this._logger.log(`${indent}Current parent group: ${currentGroup ? currentGroup.name : "none"}`, options); + + // Log emitter configuration + if (options?.verbose) { + this._logger.log( + `${indent}Emitter config: ${JSON.stringify( + { + renderMode: vfxEmitter.config.renderMode, + duration: vfxEmitter.config.duration, + looping: vfxEmitter.config.looping, + prewarm: vfxEmitter.config.prewarm, + emissionOverTime: vfxEmitter.config.emissionOverTime, + startLife: vfxEmitter.config.startLife, + startSpeed: vfxEmitter.config.startSpeed, + startSize: vfxEmitter.config.startSize, + behaviorsCount: vfxEmitter.config.behaviors?.length || 0, + worldSpace: vfxEmitter.config.worldSpace, + }, + null, + 2 + )}`, + options + ); + } + + // Calculate cumulative scale from parent groups + const cumulativeScale = this._calculateCumulativeScale(currentGroup); + + const emitterData: VFXEmitterData = { + name: emitterName, + config: vfxEmitter.config, + materialId: vfxEmitter.materialId, + // Transform is already converted, will be passed through emitterData + matrix: undefined, + position: undefined, + parentGroup: currentGroup, + cumulativeScale, + }; + + // Store VFX emitter data (including transform) in emitterData for use in factory + emitterData.vfxEmitter = vfxEmitter; + + if (options?.verbose && (cumulativeScale.x !== 1 || cumulativeScale.y !== 1 || cumulativeScale.z !== 1)) { + this._logger.log( + `${indent}Cumulative scale from parent groups: (${cumulativeScale.x.toFixed(2)}, ${cumulativeScale.y.toFixed(2)}, ${cumulativeScale.z.toFixed(2)})`, + options + ); + } + + if (!applyTransformations) { + // Phase 1: Create particle system without transformations + const particleSystem = this._emitterFactory.createEmitter(emitterData); + + if (particleSystem) { + this._logger.log(`${indent}Particle system created successfully (without transformations)`, options); + + // VFX emitter data is already stored in emitterData, no need to store in particle system + + // Handle prewarm + if (vfxEmitter.config.prewarm) { + particleSystem.start(); + } + + return particleSystem as VFXParticleSystem; + } else { + this._logger.warn(`${indent}Failed to create particle system for ${emitterName}`, options); + return null; + } + } else { + // Phase 2: Apply transformations (this will be handled separately) + return null; + } + } + + /** + * Apply VFX transform to emitter (Phase 2) + * For SPS, transformations are applied in initParticles (after buildMesh) + * For ParticleSystem, we need to find and update the emitter mesh + */ + private _applyVFXTransformToEmitter(vfxEmitter: VFXEmitter, _currentGroup: Nullable, depth: number, options?: VFXLoaderOptions): void { + const indent = " ".repeat(depth); + const emitterName = vfxEmitter.name; + + // For SPS: transformations are applied in initParticles (called after buildMesh) + // Transform is already stored in _vfxEmitter and will be applied there + // For ParticleSystem: emitter is set during creation, but we need to apply transform if it's a mesh + // Note: ParticleSystem emitter transformations are handled during creation phase + // because emitter needs to be set before particle system starts + this._logger.log(`${indent}Transformations for emitter ${emitterName} (will be applied in initParticles for SPS)`, options); + } + + /** + * Calculate cumulative scale from parent groups + */ + private _calculateCumulativeScale(parent: Nullable): Vector3 { + const cumulativeScale = new Vector3(1, 1, 1); + let current = parent; + + while (current) { + cumulativeScale.x *= current.scaling.x; + cumulativeScale.y *= current.scaling.y; + cumulativeScale.z *= current.scaling.z; + current = current.parent as TransformNode; + } + + return cumulativeScale; + } } diff --git a/editor/src/editor/windows/fx-editor/VFX/systems/VFXParticleSystem.ts b/editor/src/editor/windows/fx-editor/VFX/systems/VFXParticleSystem.ts index 6c1ff0fcd..47b8e6ead 100644 --- a/editor/src/editor/windows/fx-editor/VFX/systems/VFXParticleSystem.ts +++ b/editor/src/editor/windows/fx-editor/VFX/systems/VFXParticleSystem.ts @@ -3,19 +3,17 @@ import { ParticleSystem, Scene } from "@babylonjs/core"; import type { VFXValueParser } from "../parsers/VFXValueParser"; import type { VFXPerParticleBehaviorFunction } from "../types/VFXBehaviorFunction"; - /** * Extended ParticleSystem with VFX behaviors support * (logic intentionally minimal, behaviors handled elsewhere) */ export class VFXParticleSystem extends ParticleSystem { - constructor(name: string, capacity: number, scene: Scene, _valueParser: VFXValueParser, _avgStartSpeed: number, _avgStartSize: number, _startColor: Color4) { - super(name, capacity, scene); - // behavior wiring omitted by design (see VFXEmitterFactory) - } + constructor(name: string, capacity: number, scene: Scene, _valueParser: VFXValueParser, _avgStartSpeed: number, _avgStartSize: number, _startColor: Color4) { + super(name, capacity, scene); + // behavior wiring omitted by design (see VFXEmitterFactory) + } - public setPerParticleBehaviors(_functions: VFXPerParticleBehaviorFunction[]): void { - // intentionally no-op (kept for API parity) - } + public setPerParticleBehaviors(_functions: VFXPerParticleBehaviorFunction[]): void { + // intentionally no-op (kept for API parity) + } } - diff --git a/editor/src/editor/windows/fx-editor/VFX/systems/VFXSolidParticleSystem.ts b/editor/src/editor/windows/fx-editor/VFX/systems/VFXSolidParticleSystem.ts index 608e32b6c..cc8fd7520 100644 --- a/editor/src/editor/windows/fx-editor/VFX/systems/VFXSolidParticleSystem.ts +++ b/editor/src/editor/windows/fx-editor/VFX/systems/VFXSolidParticleSystem.ts @@ -13,15 +13,15 @@ import type { VFXPerSolidParticleBehaviorFunction, VFXPerParticleContext } from * Emission state matching three.quarks EmissionState structure */ interface EmissionState { - time: number; - waitEmiting: number; - travelDistance: number; - previousWorldPos?: Vector3; - burstIndex: number; - burstWaveIndex: number; - burstParticleIndex: number; - burstParticleCount: number; - isBursting: boolean; + time: number; + waitEmiting: number; + travelDistance: number; + previousWorldPos?: Vector3; + burstIndex: number; + burstWaveIndex: number; + burstParticleIndex: number; + burstParticleCount: number; + isBursting: boolean; } /** @@ -29,511 +29,511 @@ interface EmissionState { * This class replicates the exact behavior of three.quarks ParticleSystem with renderMode = Mesh */ export class VFXSolidParticleSystem extends SolidParticleSystem { - private _emissionState: EmissionState; - private _config: VFXParticleEmitterConfig; - private _valueParser: VFXValueParser; - private _perParticleBehaviors: VFXPerSolidParticleBehaviorFunction[]; - private _parentGroup: TransformNode | null; - private _vfxTransform: { position: Vector3; rotation: Quaternion; scale: Vector3 } | null; - private _logger: VFXLogger | null; - private _options: VFXLoaderOptions | undefined; - private _name: string; - private _duration: number; - private _looping: boolean; - private _emitEnded: boolean; - private _normalMatrix: Matrix; - private _tempVec: Vector3; - private _tempVec2: Vector3; - private _tempQuat: Quaternion; - - constructor( - name: string, - scene: any, - config: VFXParticleEmitterConfig, - valueParser: VFXValueParser, - options?: { - updatable?: boolean; - isPickable?: boolean; - enableDepthSort?: boolean; - particleIntersection?: boolean; - useModelMaterial?: boolean; - parentGroup?: TransformNode | null; - vfxTransform?: { position: Vector3; rotation: Quaternion; scale: Vector3 } | null; - logger?: VFXLogger | null; - loaderOptions?: VFXLoaderOptions; - } - ) { - super(name, scene, options); - - this._name = name; - this._config = config; - this._valueParser = valueParser; - this._perParticleBehaviors = []; - this._parentGroup = options?.parentGroup ?? null; - this._vfxTransform = options?.vfxTransform ?? null; - this._logger = options?.logger ?? null; - this._options = options?.loaderOptions; - this._duration = config.duration || 5; - this._looping = config.looping !== false; - this._emitEnded = false; - this._normalMatrix = new Matrix(); - this._tempVec = Vector3.Zero(); - this._tempVec2 = Vector3.Zero(); - this._tempQuat = Quaternion.Identity(); - - this.updateParticle = this._updateParticle.bind(this); - - this._emissionState = { - time: 0, - waitEmiting: 0, - travelDistance: 0, - burstIndex: 0, - burstWaveIndex: 0, - burstParticleIndex: 0, - burstParticleCount: 0, - isBursting: false, - }; - } - - private _findDeadParticle(): SolidParticle | null { - for (let j = 0; j < this.nbParticles; j++) { - if (!this.particles[j].alive) { - return this.particles[j]; - } - } - return null; - } - - private _resetParticle(particle: SolidParticle): void { - particle.age = 0; - particle.alive = true; - particle.isVisible = true; - particle.position.setAll(0); - particle.velocity.setAll(0); - particle.rotation.setAll(0); - particle.scaling.setAll(1); - if (particle.color) { - particle.color.set(1, 1, 1, 1); - } else { - particle.color = new Color4(1, 1, 1, 1); - } - - if (!particle.props) { - particle.props = {}; - } - particle.props.speedModifier = 1.0; - } - - private _initializeParticleColor(particle: SolidParticle): void { - const config = this._config; - const valueParser = this._valueParser; - - if (!particle.color) { - particle.color = new Color4(1, 1, 1, 1); - } - - if (config.startColor !== undefined) { - const startColor = valueParser.parseConstantColor(config.startColor); - particle.props!.startColor = startColor.clone(); - particle.color.copyFrom(startColor); - } else { - const defaultColor = new Color4(1, 1, 1, 1); - particle.props!.startColor = defaultColor.clone(); - particle.color.copyFrom(defaultColor); - } - } - - private _initializeParticleSpeed(particle: SolidParticle): void { - const config = this._config; - const valueParser = this._valueParser; - - if (config.startSpeed !== undefined) { - const normalizedTime = this._emissionState.time / this._duration; - particle.props!.startSpeed = valueParser.parseValue(config.startSpeed, normalizedTime); - } else { - particle.props!.startSpeed = 0; - } - } - - private _initializeParticleLife(particle: SolidParticle): void { - const config = this._config; - const valueParser = this._valueParser; - - if (config.startLife !== undefined) { - const normalizedTime = this._emissionState.time / this._duration; - particle.lifeTime = valueParser.parseValue(config.startLife, normalizedTime); - } else { - particle.lifeTime = 1; - } - } - - private _initializeParticleSize(particle: SolidParticle): void { - const config = this._config; - const valueParser = this._valueParser; - - if (config.startSize !== undefined) { - const normalizedTime = this._emissionState.time / this._duration; - const sizeValue = valueParser.parseValue(config.startSize, normalizedTime); - particle.props!.startSize = sizeValue; - particle.scaling.setAll(sizeValue); - } else { - particle.props!.startSize = 1; - particle.scaling.setAll(1); - } - } - - private _spawn(count: number): void { - const emissionState = this._emissionState; - - const emitterMatrix = this._getEmitterMatrix(); - const translation = this._tempVec; - const quaternion = this._tempQuat; - const scale = this._tempVec2; - emitterMatrix.decompose(scale, quaternion, translation); - emitterMatrix.toNormalMatrix(this._normalMatrix); - - for (let i = 0; i < count; i++) { - emissionState.burstParticleIndex = i; - - const particle = this._findDeadParticle(); - if (!particle) { - continue; - } - - this._resetParticle(particle); - this._initializeParticleColor(particle); - this._initializeParticleSpeed(particle); - this._initializeParticleLife(particle); - this._initializeParticleSize(particle); - - this._initializeEmitterShape(particle, emissionState); - } - } - - private _initializeSphereShape(particle: SolidParticle, radius: number, arc: number, thickness: number, startSpeed: number): void { - const u = Math.random(); - const v = Math.random(); - const rand = 1 - thickness + Math.random() * thickness; - const theta = u * arc; - const phi = Math.acos(2.0 * v - 1.0); - const sinTheta = Math.sin(theta); - const cosTheta = Math.cos(theta); - const sinPhi = Math.sin(phi); - const cosPhi = Math.cos(phi); - - particle.position.set(sinPhi * cosTheta, sinPhi * sinTheta, cosPhi); - particle.velocity.copyFrom(particle.position); - particle.velocity.scaleInPlace(startSpeed); - particle.position.scaleInPlace(radius * rand); - } - - private _initializeConeShape(particle: SolidParticle, radius: number, arc: number, thickness: number, angle: number, startSpeed: number): void { - const u = Math.random(); - const rand = 1 - thickness + Math.random() * thickness; - const theta = u * arc; - const r = Math.sqrt(rand); - const sinTheta = Math.sin(theta); - const cosTheta = Math.cos(theta); - - particle.position.set(r * cosTheta, r * sinTheta, 0); - const coneAngle = angle * r; - particle.velocity.set(0, 0, Math.cos(coneAngle)); - particle.velocity.addInPlace(particle.position.scale(Math.sin(coneAngle))); - particle.velocity.scaleInPlace(startSpeed); - particle.position.scaleInPlace(radius); - } - - private _initializePointShape(particle: SolidParticle, startSpeed: number): void { - const theta = Math.random() * Math.PI * 2; - const phi = Math.acos(2.0 * Math.random() - 1.0); - const direction = new Vector3(Math.sin(phi) * Math.cos(theta), Math.sin(phi) * Math.sin(theta), Math.cos(phi)); - particle.position.setAll(0); - particle.velocity.copyFrom(direction); - particle.velocity.scaleInPlace(startSpeed); - } - - private _initializeDefaultShape(particle: SolidParticle, startSpeed: number): void { - particle.position.setAll(0); - particle.velocity.set(0, 1, 0); - particle.velocity.scaleInPlace(startSpeed); - } - - private _initializeEmitterShape(particle: SolidParticle, emissionState: EmissionState): void { - console.log("initializeEmitterShape", particle, emissionState); - const config = this._config; - const startSpeed = particle.props?.startSpeed ?? 0; - - if (!config.shape) { - this._initializeDefaultShape(particle, startSpeed); - return; - } - - const shapeType = config.shape.type?.toLowerCase(); - const radius = config.shape.radius ?? 1; - const arc = config.shape.arc ?? Math.PI * 2; - const thickness = config.shape.thickness ?? 1; - const angle = config.shape.angle ?? Math.PI / 6; - - if (shapeType === "sphere") { - this._initializeSphereShape(particle, radius, arc, thickness, startSpeed); - } else if (shapeType === "cone") { - this._initializeConeShape(particle, radius, arc, thickness, angle, startSpeed); - } else if (shapeType === "point") { - this._initializePointShape(particle, startSpeed); - } else { - this._initializeDefaultShape(particle, startSpeed); - } - } - - private _getEmitterMatrix(): Matrix { - const matrix = Matrix.Identity(); - if (this.mesh) { - this.mesh.computeWorldMatrix(true); - matrix.copyFrom(this.mesh.getWorldMatrix()); - } - return matrix; - } - - private _handleEmissionLooping(): void { - const emissionState = this._emissionState; - - if (emissionState.time > this._duration) { - if (this._looping) { - emissionState.time -= this._duration; - emissionState.burstIndex = 0; - } else if (!this._emitEnded) { - this._emitEnded = true; - } - } - } - - private _spawnFromWaitEmiting(): void { - const emissionState = this._emissionState; - const totalSpawn = Math.ceil(emissionState.waitEmiting); - if (totalSpawn > 0) { - this._spawn(totalSpawn); - emissionState.waitEmiting -= totalSpawn; - } - } - - private _spawnBursts(): void { - const emissionState = this._emissionState; - const config = this._config; - const valueParser = this._valueParser; - - if (!config.emissionBursts || !Array.isArray(config.emissionBursts)) { - return; - } - - while (emissionState.burstIndex < config.emissionBursts.length && this._getBurstTime(config.emissionBursts[emissionState.burstIndex]) <= emissionState.time) { - const burst = config.emissionBursts[emissionState.burstIndex]; - const burstCount = valueParser.parseConstantValue(burst.count); - emissionState.isBursting = true; - emissionState.burstParticleCount = burstCount; - this._spawn(burstCount); - emissionState.isBursting = false; - emissionState.burstIndex++; - } - } - - private _accumulateEmission(delta: number): void { - const emissionState = this._emissionState; - const config = this._config; - const valueParser = this._valueParser; - - if (this._emitEnded) { - return; - } - - const emissionRate = config.emissionOverTime !== undefined ? valueParser.parseConstantValue(config.emissionOverTime) : 10; - emissionState.waitEmiting += delta * emissionRate; - - if (config.emissionOverDistance !== undefined && this.mesh && this.mesh.position) { - const emitPerMeter = valueParser.parseConstantValue(config.emissionOverDistance); - if (emitPerMeter > 0 && emissionState.previousWorldPos) { - const distance = Vector3.Distance(emissionState.previousWorldPos, this.mesh.position); - emissionState.travelDistance += distance; - if (emissionState.travelDistance * emitPerMeter > 0) { - const count = Math.floor(emissionState.travelDistance * emitPerMeter); - emissionState.travelDistance -= count / emitPerMeter; - emissionState.waitEmiting += count; - } - } - if (!emissionState.previousWorldPos) { - emissionState.previousWorldPos = Vector3.Zero(); - } - emissionState.previousWorldPos.copyFrom(this.mesh.position); - } - } - - private _emit(delta: number): void { - this._handleEmissionLooping(); - this._spawnFromWaitEmiting(); - this._spawnBursts(); - this._accumulateEmission(delta); - - this._emissionState.time += delta; - } - - private _getBurstTime(burst: VFXEmissionBurst): number { - return this._valueParser.parseConstantValue(burst.time); - } - - private _setupMeshProperties(): void { - const config = this._config; - - if (!this.mesh) { - if (this._logger) { - this._logger.warn(` SPS mesh is null in initParticles!`, this._options); - } - return; - } - - if (this._logger) { - this._logger.log(` initParticles called for SPS: ${this._name}`, this._options); - this._logger.log(` SPS mesh exists: ${this.mesh.name}`, this._options); - } - - if (config.renderOrder !== undefined) { - this.mesh.renderingGroupId = config.renderOrder; - if (this._logger) { - this._logger.log(` Set SPS mesh renderingGroupId: ${config.renderOrder}`, this._options); - } - } - - if (config.layers !== undefined) { - this.mesh.layerMask = config.layers; - if (this._logger) { - this._logger.log(` Set SPS mesh layerMask: ${config.layers}`, this._options); - } - } - - if (this._parentGroup) { - this.mesh.setParent(this._parentGroup, false, true); - if (this._logger) { - this._logger.log(` Set SPS mesh parent to: ${this._parentGroup.name}`, this._options); - } - } else if (this._logger) { - this._logger.log(` No parent group to set for SPS mesh`, this._options); - } - - if (this._vfxTransform) { - this.mesh.position.copyFrom(this._vfxTransform.position); - this.mesh.rotationQuaternion = this._vfxTransform.rotation.clone(); - this.mesh.scaling.copyFrom(this._vfxTransform.scale); - - if (this._logger) { - const rot = this.mesh.rotationQuaternion; - this._logger.log( - ` Applied VFX transform to SPS mesh: pos=(${this._vfxTransform.position.x.toFixed(2)}, ${this._vfxTransform.position.y.toFixed(2)}, ${this._vfxTransform.position.z.toFixed(2)}), rot=(${rot ? rot.x.toFixed(4) : 0}, ${rot ? rot.y.toFixed(4) : 0}, ${rot ? rot.z.toFixed(4) : 0}, ${rot ? rot.w.toFixed(4) : 1}), scale=(${this._vfxTransform.scale.x.toFixed(2)}, ${this._vfxTransform.scale.y.toFixed(2)}, ${this._vfxTransform.scale.z.toFixed(2)})`, - this._options - ); - } - } else if (this._logger) { - this._logger.log(` No VFX transform to apply to SPS mesh`, this._options); - } - } - - private _initializeDeadParticles(): void { - for (let i = 0; i < this.nbParticles; i++) { - const particle = this.particles[i]; - particle.alive = false; - particle.isVisible = false; - particle.age = 0; - particle.lifeTime = Infinity; - particle.position.setAll(0); - particle.velocity.setAll(0); - particle.rotation.setAll(0); - particle.scaling.setAll(1); - if (particle.color) { - particle.color.set(1, 1, 1, 1); - } else { - particle.color = new Color4(1, 1, 1, 1); - } - } - } - - private _resetEmissionState(): void { - this._emissionState.time = 0; - this._emissionState.waitEmiting = 0; - this._emissionState.travelDistance = 0; - this._emissionState.burstIndex = 0; - this._emissionState.burstWaveIndex = 0; - this._emissionState.burstParticleIndex = 0; - this._emissionState.burstParticleCount = 0; - this._emissionState.isBursting = false; - if (this.mesh && this.mesh.position) { - this._emissionState.previousWorldPos = this.mesh.position.clone(); - } - this._emitEnded = false; - } - - public override initParticles(): void { - this._setupMeshProperties(); - this._initializeDeadParticles(); - this._resetEmissionState(); - } - - public setPerParticleBehaviors(functions: VFXPerSolidParticleBehaviorFunction[]): void { - this._perParticleBehaviors = functions; - } - - public override beforeUpdateParticles(start?: number, stop?: number, update?: boolean): void { - super.beforeUpdateParticles(start, stop, update); - - if (!this._started || this._stopped) { - return; - } - - const deltaTime = this._scaledUpdateSpeed || 0.016; - - this._emit(deltaTime); - this._emissionState.time += deltaTime; - } - - private _updateParticle(particle: SolidParticle): SolidParticle { - if (!particle.alive) { - particle.isVisible = false; - - if (this._positions32 && particle._model) { - const shape = particle._model._shape; - const startIdx = particle._pos; - for (let pt = 0; pt < shape.length; pt++) { - const idx = startIdx + pt * 3; - this._positions32[idx] = 0; - this._positions32[idx + 1] = 0; - this._positions32[idx + 2] = 0; - } - } - - return particle; - } - - if (particle.age < 0) { - return particle; - } - - const lifeRatio = particle.age / particle.lifeTime; - const startSpeed = particle.props?.startSpeed ?? 0; - const startSize = particle.props?.startSize ?? 1; - const startColor = particle.props?.startColor ?? new Color4(1, 1, 1, 1); - - const context: VFXPerParticleContext = { - lifeRatio, - startSpeed, - startSize, - startColor: { r: startColor.r, g: startColor.g, b: startColor.b, a: startColor.a }, - updateSpeed: this.updateSpeed, - valueParser: this._valueParser, - }; - - for (const behaviorFn of this._perParticleBehaviors) { - behaviorFn(particle, context); - } - - const speedModifier = particle.props?.speedModifier ?? 1.0; - particle.position.addInPlace(particle.velocity.scale(this.updateSpeed * speedModifier)); - - return particle; - } + private _emissionState: EmissionState; + private _config: VFXParticleEmitterConfig; + private _valueParser: VFXValueParser; + private _perParticleBehaviors: VFXPerSolidParticleBehaviorFunction[]; + private _parentGroup: TransformNode | null; + private _vfxTransform: { position: Vector3; rotation: Quaternion; scale: Vector3 } | null; + private _logger: VFXLogger | null; + private _options: VFXLoaderOptions | undefined; + private _name: string; + private _duration: number; + private _looping: boolean; + private _emitEnded: boolean; + private _normalMatrix: Matrix; + private _tempVec: Vector3; + private _tempVec2: Vector3; + private _tempQuat: Quaternion; + + constructor( + name: string, + scene: any, + config: VFXParticleEmitterConfig, + valueParser: VFXValueParser, + options?: { + updatable?: boolean; + isPickable?: boolean; + enableDepthSort?: boolean; + particleIntersection?: boolean; + useModelMaterial?: boolean; + parentGroup?: TransformNode | null; + vfxTransform?: { position: Vector3; rotation: Quaternion; scale: Vector3 } | null; + logger?: VFXLogger | null; + loaderOptions?: VFXLoaderOptions; + } + ) { + super(name, scene, options); + + this._name = name; + this._config = config; + this._valueParser = valueParser; + this._perParticleBehaviors = []; + this._parentGroup = options?.parentGroup ?? null; + this._vfxTransform = options?.vfxTransform ?? null; + this._logger = options?.logger ?? null; + this._options = options?.loaderOptions; + this._duration = config.duration || 5; + this._looping = config.looping !== false; + this._emitEnded = false; + this._normalMatrix = new Matrix(); + this._tempVec = Vector3.Zero(); + this._tempVec2 = Vector3.Zero(); + this._tempQuat = Quaternion.Identity(); + + this.updateParticle = this._updateParticle.bind(this); + + this._emissionState = { + time: 0, + waitEmiting: 0, + travelDistance: 0, + burstIndex: 0, + burstWaveIndex: 0, + burstParticleIndex: 0, + burstParticleCount: 0, + isBursting: false, + }; + } + + private _findDeadParticle(): SolidParticle | null { + for (let j = 0; j < this.nbParticles; j++) { + if (!this.particles[j].alive) { + return this.particles[j]; + } + } + return null; + } + + private _resetParticle(particle: SolidParticle): void { + particle.age = 0; + particle.alive = true; + particle.isVisible = true; + particle.position.setAll(0); + particle.velocity.setAll(0); + particle.rotation.setAll(0); + particle.scaling.setAll(1); + if (particle.color) { + particle.color.set(1, 1, 1, 1); + } else { + particle.color = new Color4(1, 1, 1, 1); + } + + if (!particle.props) { + particle.props = {}; + } + particle.props.speedModifier = 1.0; + } + + private _initializeParticleColor(particle: SolidParticle): void { + const config = this._config; + const valueParser = this._valueParser; + + if (!particle.color) { + particle.color = new Color4(1, 1, 1, 1); + } + + if (config.startColor !== undefined) { + const startColor = valueParser.parseConstantColor(config.startColor); + particle.props!.startColor = startColor.clone(); + particle.color.copyFrom(startColor); + } else { + const defaultColor = new Color4(1, 1, 1, 1); + particle.props!.startColor = defaultColor.clone(); + particle.color.copyFrom(defaultColor); + } + } + + private _initializeParticleSpeed(particle: SolidParticle): void { + const config = this._config; + const valueParser = this._valueParser; + + if (config.startSpeed !== undefined) { + const normalizedTime = this._emissionState.time / this._duration; + particle.props!.startSpeed = valueParser.parseValue(config.startSpeed, normalizedTime); + } else { + particle.props!.startSpeed = 0; + } + } + + private _initializeParticleLife(particle: SolidParticle): void { + const config = this._config; + const valueParser = this._valueParser; + + if (config.startLife !== undefined) { + const normalizedTime = this._emissionState.time / this._duration; + particle.lifeTime = valueParser.parseValue(config.startLife, normalizedTime); + } else { + particle.lifeTime = 1; + } + } + + private _initializeParticleSize(particle: SolidParticle): void { + const config = this._config; + const valueParser = this._valueParser; + + if (config.startSize !== undefined) { + const normalizedTime = this._emissionState.time / this._duration; + const sizeValue = valueParser.parseValue(config.startSize, normalizedTime); + particle.props!.startSize = sizeValue; + particle.scaling.setAll(sizeValue); + } else { + particle.props!.startSize = 1; + particle.scaling.setAll(1); + } + } + + private _spawn(count: number): void { + const emissionState = this._emissionState; + + const emitterMatrix = this._getEmitterMatrix(); + const translation = this._tempVec; + const quaternion = this._tempQuat; + const scale = this._tempVec2; + emitterMatrix.decompose(scale, quaternion, translation); + emitterMatrix.toNormalMatrix(this._normalMatrix); + + for (let i = 0; i < count; i++) { + emissionState.burstParticleIndex = i; + + const particle = this._findDeadParticle(); + if (!particle) { + continue; + } + + this._resetParticle(particle); + this._initializeParticleColor(particle); + this._initializeParticleSpeed(particle); + this._initializeParticleLife(particle); + this._initializeParticleSize(particle); + + this._initializeEmitterShape(particle, emissionState); + } + } + + private _initializeSphereShape(particle: SolidParticle, radius: number, arc: number, thickness: number, startSpeed: number): void { + const u = Math.random(); + const v = Math.random(); + const rand = 1 - thickness + Math.random() * thickness; + const theta = u * arc; + const phi = Math.acos(2.0 * v - 1.0); + const sinTheta = Math.sin(theta); + const cosTheta = Math.cos(theta); + const sinPhi = Math.sin(phi); + const cosPhi = Math.cos(phi); + + particle.position.set(sinPhi * cosTheta, sinPhi * sinTheta, cosPhi); + particle.velocity.copyFrom(particle.position); + particle.velocity.scaleInPlace(startSpeed); + particle.position.scaleInPlace(radius * rand); + } + + private _initializeConeShape(particle: SolidParticle, radius: number, arc: number, thickness: number, angle: number, startSpeed: number): void { + const u = Math.random(); + const rand = 1 - thickness + Math.random() * thickness; + const theta = u * arc; + const r = Math.sqrt(rand); + const sinTheta = Math.sin(theta); + const cosTheta = Math.cos(theta); + + particle.position.set(r * cosTheta, r * sinTheta, 0); + const coneAngle = angle * r; + particle.velocity.set(0, 0, Math.cos(coneAngle)); + particle.velocity.addInPlace(particle.position.scale(Math.sin(coneAngle))); + particle.velocity.scaleInPlace(startSpeed); + particle.position.scaleInPlace(radius); + } + + private _initializePointShape(particle: SolidParticle, startSpeed: number): void { + const theta = Math.random() * Math.PI * 2; + const phi = Math.acos(2.0 * Math.random() - 1.0); + const direction = new Vector3(Math.sin(phi) * Math.cos(theta), Math.sin(phi) * Math.sin(theta), Math.cos(phi)); + particle.position.setAll(0); + particle.velocity.copyFrom(direction); + particle.velocity.scaleInPlace(startSpeed); + } + + private _initializeDefaultShape(particle: SolidParticle, startSpeed: number): void { + particle.position.setAll(0); + particle.velocity.set(0, 1, 0); + particle.velocity.scaleInPlace(startSpeed); + } + + private _initializeEmitterShape(particle: SolidParticle, emissionState: EmissionState): void { + console.log("initializeEmitterShape", particle, emissionState); + const config = this._config; + const startSpeed = particle.props?.startSpeed ?? 0; + + if (!config.shape) { + this._initializeDefaultShape(particle, startSpeed); + return; + } + + const shapeType = config.shape.type?.toLowerCase(); + const radius = config.shape.radius ?? 1; + const arc = config.shape.arc ?? Math.PI * 2; + const thickness = config.shape.thickness ?? 1; + const angle = config.shape.angle ?? Math.PI / 6; + + if (shapeType === "sphere") { + this._initializeSphereShape(particle, radius, arc, thickness, startSpeed); + } else if (shapeType === "cone") { + this._initializeConeShape(particle, radius, arc, thickness, angle, startSpeed); + } else if (shapeType === "point") { + this._initializePointShape(particle, startSpeed); + } else { + this._initializeDefaultShape(particle, startSpeed); + } + } + + private _getEmitterMatrix(): Matrix { + const matrix = Matrix.Identity(); + if (this.mesh) { + this.mesh.computeWorldMatrix(true); + matrix.copyFrom(this.mesh.getWorldMatrix()); + } + return matrix; + } + + private _handleEmissionLooping(): void { + const emissionState = this._emissionState; + + if (emissionState.time > this._duration) { + if (this._looping) { + emissionState.time -= this._duration; + emissionState.burstIndex = 0; + } else if (!this._emitEnded) { + this._emitEnded = true; + } + } + } + + private _spawnFromWaitEmiting(): void { + const emissionState = this._emissionState; + const totalSpawn = Math.ceil(emissionState.waitEmiting); + if (totalSpawn > 0) { + this._spawn(totalSpawn); + emissionState.waitEmiting -= totalSpawn; + } + } + + private _spawnBursts(): void { + const emissionState = this._emissionState; + const config = this._config; + const valueParser = this._valueParser; + + if (!config.emissionBursts || !Array.isArray(config.emissionBursts)) { + return; + } + + while (emissionState.burstIndex < config.emissionBursts.length && this._getBurstTime(config.emissionBursts[emissionState.burstIndex]) <= emissionState.time) { + const burst = config.emissionBursts[emissionState.burstIndex]; + const burstCount = valueParser.parseConstantValue(burst.count); + emissionState.isBursting = true; + emissionState.burstParticleCount = burstCount; + this._spawn(burstCount); + emissionState.isBursting = false; + emissionState.burstIndex++; + } + } + + private _accumulateEmission(delta: number): void { + const emissionState = this._emissionState; + const config = this._config; + const valueParser = this._valueParser; + + if (this._emitEnded) { + return; + } + + const emissionRate = config.emissionOverTime !== undefined ? valueParser.parseConstantValue(config.emissionOverTime) : 10; + emissionState.waitEmiting += delta * emissionRate; + + if (config.emissionOverDistance !== undefined && this.mesh && this.mesh.position) { + const emitPerMeter = valueParser.parseConstantValue(config.emissionOverDistance); + if (emitPerMeter > 0 && emissionState.previousWorldPos) { + const distance = Vector3.Distance(emissionState.previousWorldPos, this.mesh.position); + emissionState.travelDistance += distance; + if (emissionState.travelDistance * emitPerMeter > 0) { + const count = Math.floor(emissionState.travelDistance * emitPerMeter); + emissionState.travelDistance -= count / emitPerMeter; + emissionState.waitEmiting += count; + } + } + if (!emissionState.previousWorldPos) { + emissionState.previousWorldPos = Vector3.Zero(); + } + emissionState.previousWorldPos.copyFrom(this.mesh.position); + } + } + + private _emit(delta: number): void { + this._handleEmissionLooping(); + this._spawnFromWaitEmiting(); + this._spawnBursts(); + this._accumulateEmission(delta); + + this._emissionState.time += delta; + } + + private _getBurstTime(burst: VFXEmissionBurst): number { + return this._valueParser.parseConstantValue(burst.time); + } + + private _setupMeshProperties(): void { + const config = this._config; + + if (!this.mesh) { + if (this._logger) { + this._logger.warn(` SPS mesh is null in initParticles!`, this._options); + } + return; + } + + if (this._logger) { + this._logger.log(` initParticles called for SPS: ${this._name}`, this._options); + this._logger.log(` SPS mesh exists: ${this.mesh.name}`, this._options); + } + + if (config.renderOrder !== undefined) { + this.mesh.renderingGroupId = config.renderOrder; + if (this._logger) { + this._logger.log(` Set SPS mesh renderingGroupId: ${config.renderOrder}`, this._options); + } + } + + if (config.layers !== undefined) { + this.mesh.layerMask = config.layers; + if (this._logger) { + this._logger.log(` Set SPS mesh layerMask: ${config.layers}`, this._options); + } + } + + if (this._parentGroup) { + this.mesh.setParent(this._parentGroup, false, true); + if (this._logger) { + this._logger.log(` Set SPS mesh parent to: ${this._parentGroup.name}`, this._options); + } + } else if (this._logger) { + this._logger.log(` No parent group to set for SPS mesh`, this._options); + } + + if (this._vfxTransform) { + this.mesh.position.copyFrom(this._vfxTransform.position); + this.mesh.rotationQuaternion = this._vfxTransform.rotation.clone(); + this.mesh.scaling.copyFrom(this._vfxTransform.scale); + + if (this._logger) { + const rot = this.mesh.rotationQuaternion; + this._logger.log( + ` Applied VFX transform to SPS mesh: pos=(${this._vfxTransform.position.x.toFixed(2)}, ${this._vfxTransform.position.y.toFixed(2)}, ${this._vfxTransform.position.z.toFixed(2)}), rot=(${rot ? rot.x.toFixed(4) : 0}, ${rot ? rot.y.toFixed(4) : 0}, ${rot ? rot.z.toFixed(4) : 0}, ${rot ? rot.w.toFixed(4) : 1}), scale=(${this._vfxTransform.scale.x.toFixed(2)}, ${this._vfxTransform.scale.y.toFixed(2)}, ${this._vfxTransform.scale.z.toFixed(2)})`, + this._options + ); + } + } else if (this._logger) { + this._logger.log(` No VFX transform to apply to SPS mesh`, this._options); + } + } + + private _initializeDeadParticles(): void { + for (let i = 0; i < this.nbParticles; i++) { + const particle = this.particles[i]; + particle.alive = false; + particle.isVisible = false; + particle.age = 0; + particle.lifeTime = Infinity; + particle.position.setAll(0); + particle.velocity.setAll(0); + particle.rotation.setAll(0); + particle.scaling.setAll(1); + if (particle.color) { + particle.color.set(1, 1, 1, 1); + } else { + particle.color = new Color4(1, 1, 1, 1); + } + } + } + + private _resetEmissionState(): void { + this._emissionState.time = 0; + this._emissionState.waitEmiting = 0; + this._emissionState.travelDistance = 0; + this._emissionState.burstIndex = 0; + this._emissionState.burstWaveIndex = 0; + this._emissionState.burstParticleIndex = 0; + this._emissionState.burstParticleCount = 0; + this._emissionState.isBursting = false; + if (this.mesh && this.mesh.position) { + this._emissionState.previousWorldPos = this.mesh.position.clone(); + } + this._emitEnded = false; + } + + public override initParticles(): void { + this._setupMeshProperties(); + this._initializeDeadParticles(); + this._resetEmissionState(); + } + + public setPerParticleBehaviors(functions: VFXPerSolidParticleBehaviorFunction[]): void { + this._perParticleBehaviors = functions; + } + + public override beforeUpdateParticles(start?: number, stop?: number, update?: boolean): void { + super.beforeUpdateParticles(start, stop, update); + + if (!this._started || this._stopped) { + return; + } + + const deltaTime = this._scaledUpdateSpeed || 0.016; + + this._emit(deltaTime); + this._emissionState.time += deltaTime; + } + + private _updateParticle(particle: SolidParticle): SolidParticle { + if (!particle.alive) { + particle.isVisible = false; + + if (this._positions32 && particle._model) { + const shape = particle._model._shape; + const startIdx = particle._pos; + for (let pt = 0; pt < shape.length; pt++) { + const idx = startIdx + pt * 3; + this._positions32[idx] = 0; + this._positions32[idx + 1] = 0; + this._positions32[idx + 2] = 0; + } + } + + return particle; + } + + if (particle.age < 0) { + return particle; + } + + const lifeRatio = particle.age / particle.lifeTime; + const startSpeed = particle.props?.startSpeed ?? 0; + const startSize = particle.props?.startSize ?? 1; + const startColor = particle.props?.startColor ?? new Color4(1, 1, 1, 1); + + const context: VFXPerParticleContext = { + lifeRatio, + startSpeed, + startSize, + startColor: { r: startColor.r, g: startColor.g, b: startColor.b, a: startColor.a }, + updateSpeed: this.updateSpeed, + valueParser: this._valueParser, + }; + + for (const behaviorFn of this._perParticleBehaviors) { + behaviorFn(particle, context); + } + + const speedModifier = particle.props?.speedModifier ?? 1.0; + particle.position.addInPlace(particle.velocity.scale(this.updateSpeed * speedModifier)); + + return particle; + } } diff --git a/editor/src/editor/windows/fx-editor/VFX/treejs3dobject.particle.json b/editor/src/editor/windows/fx-editor/VFX/treejs3dobject.particle.json index f8fb4d962..bd7d7488c 100644 --- a/editor/src/editor/windows/fx-editor/VFX/treejs3dobject.particle.json +++ b/editor/src/editor/windows/fx-editor/VFX/treejs3dobject.particle.json @@ -1,870 +1,870 @@ { - "metadata": { "version": 4.6, "type": "Object", "generator": "Object3D.toJSON" }, - "geometries": [ - { "uuid": "780917d8-bd1b-4d63-8aca-f79e3211f964", "type": "PlaneGeometry", "name": "PlaneGeometry", "width": 1, "height": 1, "widthSegments": 1, "heightSegments": 1 }, - { - "uuid": "f40b6ee0-aa01-46e0-b05a-d938b54eec83", - "type": "BufferGeometry", - "name": "GlowCircleEmitter_geometry", - "data": { - "attributes": { - "position": { - "itemSize": 3, - "type": "Float32Array", - "array": [ - 0.41758671402931213, 0.08306316286325455, 0.10689251124858856, 0.42576777935028076, -1.2535783966427516e-8, 0.10689251124858856, 0.3199999928474426, 0, - 0, 0.3199999928474426, 0, 0, 0.3138512670993805, 0.062428902834653854, 0, 0.41758671402931213, 0.08306316286325455, 0.10689251124858856, - 0.39335811138153076, 0.16293425858020782, 0.10689251124858856, 0.41758671402931213, 0.08306316286325455, 0.10689251124858856, 0.3138512670993805, - 0.062428902834653854, 0, 0.3138512670993805, 0.062428902834653854, 0, 0.2956414520740509, 0.12245870381593704, 0, 0.39335811138153076, - 0.16293425858020782, 0.10689251124858856, 0.35401293635368347, 0.23654387891292572, 0.10689251124858856, 0.39335811138153076, 0.16293425858020782, - 0.10689251124858856, 0.2956414520740509, 0.12245870381593704, 0, 0.2956414520740509, 0.12245870381593704, 0, 0.26607027649879456, 0.17778247594833374, - 0, 0.35401293635368347, 0.23654387891292572, 0.10689251124858856, 0.3010632395744324, 0.30106326937675476, 0.10689251124858856, 0.35401293635368347, - 0.23654387891292572, 0.10689251124858856, 0.26607027649879456, 0.17778247594833374, 0, 0.26607027649879456, 0.17778247594833374, 0, 0.22627416253089905, - 0.22627416253089905, 0, 0.3010632395744324, 0.30106326937675476, 0.10689251124858856, 0.23654384911060333, 0.35401299595832825, 0.10689251124858856, - 0.3010632395744324, 0.30106326937675476, 0.10689251124858856, 0.22627416253089905, 0.22627416253089905, 0, 0.22627416253089905, 0.22627416253089905, 0, - 0.17778246104717255, 0.26607027649879456, 0, 0.23654384911060333, 0.35401299595832825, 0.10689251124858856, 0.16293422877788544, 0.39335811138153076, - 0.10689251124858856, 0.23654384911060333, 0.35401299595832825, 0.10689251124858856, 0.17778246104717255, 0.26607027649879456, 0, 0.17778246104717255, - 0.26607027649879456, 0, 0.12245869636535645, 0.2956414520740509, 0, 0.16293422877788544, 0.39335811138153076, 0.10689251124858856, 0.08306317031383514, - 0.41758671402931213, 0.10689251124858856, 0.16293422877788544, 0.39335811138153076, 0.10689251124858856, 0.12245869636535645, 0.2956414520740509, 0, - 0.12245869636535645, 0.2956414520740509, 0, 0.06242891401052475, 0.3138512670993805, 0, 0.08306317031383514, 0.41758671402931213, 0.10689251124858856, - 2.0868840877596995e-8, 0.42576777935028076, 0.10689251124858856, 0.08306317031383514, 0.41758671402931213, 0.10689251124858856, 0.06242891401052475, - 0.3138512670993805, 0, 0.06242891401052475, 0.3138512670993805, 0, 2.415932875976523e-8, 0.3199999928474426, 0, 2.0868840877596995e-8, - 0.42576777935028076, 0.10689251124858856, -0.08306313306093216, 0.4175867438316345, 0.10689251124858856, 2.0868840877596995e-8, 0.42576777935028076, - 0.10689251124858856, 2.415932875976523e-8, 0.3199999928474426, 0, 2.415932875976523e-8, 0.3199999928474426, 0, -0.06242886558175087, 0.3138512969017029, - 0, -0.08306313306093216, 0.4175867438316345, 0.10689251124858856, -0.16293422877788544, 0.39335814118385315, 0.10689251124858856, -0.08306313306093216, - 0.4175867438316345, 0.10689251124858856, -0.06242886558175087, 0.3138512969017029, 0, -0.06242886558175087, 0.3138512969017029, 0, -0.12245865166187286, - 0.2956414520740509, 0, -0.16293422877788544, 0.39335814118385315, 0.10689251124858856, -0.23654387891292572, 0.35401299595832825, 0.10689251124858856, - -0.16293422877788544, 0.39335814118385315, 0.10689251124858856, -0.12245865166187286, 0.2956414520740509, 0, -0.12245865166187286, 0.2956414520740509, - 0, -0.17778246104717255, 0.26607027649879456, 0, -0.23654387891292572, 0.35401299595832825, 0.10689251124858856, -0.30106329917907715, - 0.30106326937675476, 0.10689251124858856, -0.23654387891292572, 0.35401299595832825, 0.10689251124858856, -0.17778246104717255, 0.26607027649879456, 0, - -0.17778246104717255, 0.26607027649879456, 0, -0.22627416253089905, 0.22627416253089905, 0, -0.30106329917907715, 0.30106326937675476, - 0.10689251124858856, -0.35401299595832825, 0.23654386401176453, 0.10689251124858856, -0.30106329917907715, 0.30106326937675476, 0.10689251124858856, - -0.22627416253089905, 0.22627416253089905, 0, -0.22627416253089905, 0.22627416253089905, 0, -0.26607027649879456, 0.17778246104717255, 0, - -0.35401299595832825, 0.23654386401176453, 0.10689251124858856, -0.39335814118385315, 0.16293418407440186, 0.10689251124858856, -0.35401299595832825, - 0.23654386401176453, 0.10689251124858856, -0.26607027649879456, 0.17778246104717255, 0, -0.26607027649879456, 0.17778246104717255, 0, - -0.2956414818763733, 0.12245865166187286, 0, -0.39335814118385315, 0.16293418407440186, 0.10689251124858856, -0.4175867438316345, 0.08306305855512619, - 0.10689251124858856, -0.39335814118385315, 0.16293418407440186, 0.10689251124858856, -0.2956414818763733, 0.12245865166187286, 0, -0.2956414818763733, - 0.12245865166187286, 0, -0.3138512969017029, 0.062428828328847885, 0, -0.4175867438316345, 0.08306305855512619, 0.10689251124858856, - -0.42576777935028076, -1.5126852304092608e-7, 0.10689251124858856, -0.4175867438316345, 0.08306305855512619, 0.10689251124858856, -0.3138512969017029, - 0.062428828328847885, 0, -0.3138512969017029, 0.062428828328847885, 0, -0.3199999928474426, -1.0426924035300544e-7, 0, -0.42576777935028076, - -1.5126852304092608e-7, 0.10689251124858856, -0.41758671402931213, -0.08306335657835007, 0.10689251124858856, -0.42576777935028076, - -1.5126852304092608e-7, 0.10689251124858856, -0.3199999928474426, -1.0426924035300544e-7, 0, -0.3199999928474426, -1.0426924035300544e-7, 0, - -0.3138512670993805, -0.0624290332198143, 0, -0.41758671402931213, -0.08306335657835007, 0.10689251124858856, -0.393358051776886, -0.16293445229530334, - 0.10689251124858856, -0.41758671402931213, -0.08306335657835007, 0.10689251124858856, -0.3138512670993805, -0.0624290332198143, 0, -0.3138512670993805, - -0.0624290332198143, 0, -0.29564139246940613, -0.12245883792638779, 0, -0.393358051776886, -0.16293445229530334, 0.10689251124858856, - -0.35401278734207153, -0.23654408752918243, 0.10689251124858856, -0.393358051776886, -0.16293445229530334, 0.10689251124858856, -0.29564139246940613, - -0.12245883792638779, 0, -0.29564139246940613, -0.12245883792638779, 0, -0.2660701870918274, -0.17778262495994568, 0, -0.35401278734207153, - -0.23654408752918243, 0.10689251124858856, -0.30106309056282043, -0.3010634481906891, 0.10689251124858856, -0.35401278734207153, -0.23654408752918243, - 0.10689251124858856, -0.2660701870918274, -0.17778262495994568, 0, -0.2660701870918274, -0.17778262495994568, 0, -0.2262740284204483, - -0.226274311542511, 0, -0.30106309056282043, -0.3010634481906891, 0.10689251124858856, -0.2365436553955078, -0.3540131449699402, 0.10689251124858856, - -0.30106309056282043, -0.3010634481906891, 0.10689251124858856, -0.2262740284204483, -0.226274311542511, 0, -0.2262740284204483, -0.226274311542511, 0, - -0.17778228223323822, -0.2660703957080841, 0, -0.2365436553955078, -0.3540131449699402, 0.10689251124858856, -0.16293397545814514, -0.3933582603931427, - 0.10689251124858856, -0.2365436553955078, -0.3540131449699402, 0.10689251124858856, -0.17778228223323822, -0.2660703957080841, 0, -0.17778228223323822, - -0.2660703957080841, 0, -0.12245845794677734, -0.29564154148101807, 0, -0.16293397545814514, -0.3933582603931427, 0.10689251124858856, - -0.08306281268596649, -0.4175868332386017, 0.10689251124858856, -0.16293397545814514, -0.3933582603931427, 0.10689251124858856, -0.12245845794677734, - -0.29564154148101807, 0, -0.12245845794677734, -0.29564154148101807, 0, -0.06242862716317177, -0.31385132670402527, 0, -0.08306281268596649, - -0.4175868332386017, 0.10689251124858856, 3.9984527688829985e-7, -0.42576777935028076, 0.10689251124858856, -0.08306281268596649, -0.4175868332386017, - 0.10689251124858856, -0.06242862716317177, -0.31385132670402527, 0, -0.06242862716317177, -0.31385132670402527, 0, 3.0899172998033464e-7, - -0.3199999928474426, 0, 3.9984527688829985e-7, -0.42576777935028076, 0.10689251124858856, 0.08306359499692917, -0.41758668422698975, - 0.10689251124858856, 3.9984527688829985e-7, -0.42576777935028076, 0.10689251124858856, 3.0899172998033464e-7, -0.3199999928474426, 0, - 3.0899172998033464e-7, -0.3199999928474426, 0, 0.06242923438549042, -0.3138512372970581, 0, 0.08306359499692917, -0.41758668422698975, - 0.10689251124858856, 0.16293466091156006, -0.39335793256759644, 0.10689251124858856, 0.08306359499692917, -0.41758668422698975, 0.10689251124858856, - 0.06242923438549042, -0.3138512372970581, 0, 0.06242923438549042, -0.3138512372970581, 0, 0.1224590316414833, -0.29564130306243896, 0, - 0.16293466091156006, -0.39335793256759644, 0.10689251124858856, 0.23654431104660034, -0.3540126383304596, 0.10689251124858856, 0.16293466091156006, - -0.39335793256759644, 0.10689251124858856, 0.1224590316414833, -0.29564130306243896, 0, 0.1224590316414833, -0.29564130306243896, 0, 0.17778280377388, - -0.26607006788253784, 0, 0.23654431104660034, -0.3540126383304596, 0.10689251124858856, 0.3010636270046234, -0.3010628819465637, 0.10689251124858856, - 0.23654431104660034, -0.3540126383304596, 0.10689251124858856, 0.17778280377388, -0.26607006788253784, 0, 0.17778280377388, -0.26607006788253784, 0, - 0.22627444565296173, -0.22627387940883636, 0, 0.3010636270046234, -0.3010628819465637, 0.10689251124858856, 0.3540132939815521, -0.23654340207576752, - 0.10689251124858856, 0.3010636270046234, -0.3010628819465637, 0.10689251124858856, 0.22627444565296173, -0.22627387940883636, 0, 0.22627444565296173, - -0.22627387940883636, 0, 0.26607051491737366, -0.1777821183204651, 0, 0.3540132939815521, -0.23654340207576752, 0.10689251124858856, - 0.39335834980010986, -0.16293370723724365, 0.10689251124858856, 0.3540132939815521, -0.23654340207576752, 0.10689251124858856, 0.26607051491737366, - -0.1777821183204651, 0, 0.26607051491737366, -0.1777821183204651, 0, 0.29564163088798523, -0.12245826423168182, 0, 0.39335834980010986, - -0.16293370723724365, 0.10689251124858856, 0.4175868630409241, -0.083062544465065, 0.10689251124858856, 0.39335834980010986, -0.16293370723724365, - 0.10689251124858856, 0.29564163088798523, -0.12245826423168182, 0, 0.29564163088798523, -0.12245826423168182, 0, 0.31385138630867004, - -0.06242842227220535, 0, 0.4175868630409241, -0.083062544465065, 0.10689251124858856, 0.42576777935028076, -1.2535783966427516e-8, 0.10689251124858856, - 0.4175868630409241, -0.083062544465065, 0.10689251124858856, 0.31385138630867004, -0.06242842227220535, 0, 0.31385138630867004, -0.06242842227220535, 0, - 0.3199999928474426, 0, 0, 0.42576777935028076, -1.2535783966427516e-8, 0.10689251124858856, 0.4175865948200226, 0.08306316286325455, - 0.10689251124858856, 0.31385117769241333, 0.062428902834653854, 0, 0.3199998736381531, 0, 0, 0.3199998736381531, 0, 0, 0.4257676303386688, - -1.2535783966427516e-8, 0.10689251124858856, 0.4175865948200226, 0.08306316286325455, 0.10689251124858856, 0.3933579623699188, 0.16293425858020782, - 0.10689251124858856, 0.29564133286476135, 0.12245870381593704, 0, 0.31385117769241333, 0.062428902834653854, 0, 0.31385117769241333, - 0.062428902834653854, 0, 0.4175865948200226, 0.08306316286325455, 0.10689251124858856, 0.3933579623699188, 0.16293425858020782, 0.10689251124858856, - 0.35401275753974915, 0.23654387891292572, 0.10689251124858856, 0.2660701274871826, 0.17778247594833374, 0, 0.29564133286476135, 0.12245870381593704, 0, - 0.29564133286476135, 0.12245870381593704, 0, 0.3933579623699188, 0.16293425858020782, 0.10689251124858856, 0.35401275753974915, 0.23654387891292572, - 0.10689251124858856, 0.30106306076049805, 0.30106326937675476, 0.10689251124858856, 0.2262740284204483, 0.22627416253089905, 0, 0.2660701274871826, - 0.17778247594833374, 0, 0.2660701274871826, 0.17778247594833374, 0, 0.35401275753974915, 0.23654387891292572, 0.10689251124858856, 0.30106306076049805, - 0.30106326937675476, 0.10689251124858856, 0.2365437150001526, 0.35401299595832825, 0.10689251124858856, 0.177782341837883, 0.26607027649879456, 0, - 0.2262740284204483, 0.22627416253089905, 0, 0.2262740284204483, 0.22627416253089905, 0, 0.30106306076049805, 0.30106326937675476, 0.10689251124858856, - 0.2365437150001526, 0.35401299595832825, 0.10689251124858856, 0.1629340499639511, 0.39335811138153076, 0.10689251124858856, 0.1224585697054863, - 0.2956414520740509, 0, 0.177782341837883, 0.26607027649879456, 0, 0.177782341837883, 0.26607027649879456, 0, 0.2365437150001526, 0.35401299595832825, - 0.10689251124858856, 0.1629340499639511, 0.39335811138153076, 0.10689251124858856, 0.08306301385164261, 0.41758671402931213, 0.10689251124858856, - 0.0624287948012352, 0.3138512670993805, 0, 0.1224585697054863, 0.2956414520740509, 0, 0.1224585697054863, 0.2956414520740509, 0, 0.1629340499639511, - 0.39335811138153076, 0.10689251124858856, 0.08306301385164261, 0.41758671402931213, 0.10689251124858856, -1.3816443811265344e-7, 0.42576777935028076, - 0.10689251124858856, -9.536743306171047e-8, 0.3199999928474426, 0, 0.0624287948012352, 0.3138512670993805, 0, 0.0624287948012352, 0.3138512670993805, 0, - 0.08306301385164261, 0.41758671402931213, 0.10689251124858856, -1.3816443811265344e-7, 0.42576777935028076, 0.10689251124858856, -0.0830632895231247, - 0.4175867438316345, 0.10689251124858856, -0.06242898479104042, 0.3138512969017029, 0, -9.536743306171047e-8, 0.3199999928474426, 0, - -9.536743306171047e-8, 0.3199999928474426, 0, -1.3816443811265344e-7, 0.42576777935028076, 0.10689251124858856, -0.0830632895231247, 0.4175867438316345, - 0.10689251124858856, -0.16293437778949738, 0.39335814118385315, 0.10689251124858856, -0.12245876342058182, 0.2956414520740509, 0, -0.06242898479104042, - 0.3138512969017029, 0, -0.06242898479104042, 0.3138512969017029, 0, -0.0830632895231247, 0.4175867438316345, 0.10689251124858856, -0.16293437778949738, - 0.39335814118385315, 0.10689251124858856, -0.23654407262802124, 0.35401299595832825, 0.10689251124858856, -0.1777825951576233, 0.26607027649879456, 0, - -0.12245876342058182, 0.2956414520740509, 0, -0.12245876342058182, 0.2956414520740509, 0, -0.16293437778949738, 0.39335814118385315, - 0.10689251124858856, -0.23654407262802124, 0.35401299595832825, 0.10689251124858856, -0.3010634481906891, 0.30106326937675476, 0.10689251124858856, - -0.2262742966413498, 0.22627416253089905, 0, -0.1777825951576233, 0.26607027649879456, 0, -0.1777825951576233, 0.26607027649879456, 0, - -0.23654407262802124, 0.35401299595832825, 0.10689251124858856, -0.3010634481906891, 0.30106326937675476, 0.10689251124858856, -0.3540131449699402, - 0.23654386401176453, 0.10689251124858856, -0.2660703957080841, 0.17778246104717255, 0, -0.2262742966413498, 0.22627416253089905, 0, -0.2262742966413498, - 0.22627416253089905, 0, -0.3010634481906891, 0.30106326937675476, 0.10689251124858856, -0.3540131449699402, 0.23654386401176453, 0.10689251124858856, - -0.3933583199977875, 0.16293418407440186, 0.10689251124858856, -0.29564160108566284, 0.12245865166187286, 0, -0.2660703957080841, 0.17778246104717255, - 0, -0.2660703957080841, 0.17778246104717255, 0, -0.3540131449699402, 0.23654386401176453, 0.10689251124858856, -0.3933583199977875, 0.16293418407440186, - 0.10689251124858856, -0.41758692264556885, 0.08306305855512619, 0.10689251124858856, -0.3138514459133148, 0.062428828328847885, 0, -0.29564160108566284, - 0.12245865166187286, 0, -0.29564160108566284, 0.12245865166187286, 0, -0.3933583199977875, 0.16293418407440186, 0.10689251124858856, - -0.41758692264556885, 0.08306305855512619, 0.10689251124858856, -0.4257678985595703, -1.5126852304092608e-7, 0.10689251124858856, -0.3200001120567322, - -1.0426924035300544e-7, 0, -0.3138514459133148, 0.062428828328847885, 0, -0.3138514459133148, 0.062428828328847885, 0, -0.41758692264556885, - 0.08306305855512619, 0.10689251124858856, -0.4257678985595703, -1.5126852304092608e-7, 0.10689251124858856, -0.41758689284324646, -0.08306335657835007, - 0.10689251124858856, -0.31385138630867004, -0.0624290332198143, 0, -0.3200001120567322, -1.0426924035300544e-7, 0, -0.3200001120567322, - -1.0426924035300544e-7, 0, -0.4257678985595703, -1.5126852304092608e-7, 0.10689251124858856, -0.41758689284324646, -0.08306335657835007, - 0.10689251124858856, -0.3933582305908203, -0.16293445229530334, 0.10689251124858856, -0.2956415116786957, -0.12245883792638779, 0, -0.31385138630867004, - -0.0624290332198143, 0, -0.31385138630867004, -0.0624290332198143, 0, -0.41758689284324646, -0.08306335657835007, 0.10689251124858856, - -0.3933582305908203, -0.16293445229530334, 0.10689251124858856, -0.35401299595832825, -0.23654408752918243, 0.10689251124858856, -0.26607027649879456, - -0.17778262495994568, 0, -0.2956415116786957, -0.12245883792638779, 0, -0.2956415116786957, -0.12245883792638779, 0, -0.3933582305908203, - -0.16293445229530334, 0.10689251124858856, -0.35401299595832825, -0.23654408752918243, 0.10689251124858856, -0.3010632395744324, -0.3010634481906891, - 0.10689251124858856, -0.22627414762973785, -0.226274311542511, 0, -0.26607027649879456, -0.17778262495994568, 0, -0.26607027649879456, - -0.17778262495994568, 0, -0.35401299595832825, -0.23654408752918243, 0.10689251124858856, -0.3010632395744324, -0.3010634481906891, 0.10689251124858856, - -0.23654380440711975, -0.3540131449699402, 0.10689251124858856, -0.17778240144252777, -0.2660703957080841, 0, -0.22627414762973785, -0.226274311542511, - 0, -0.22627414762973785, -0.226274311542511, 0, -0.3010632395744324, -0.3010634481906891, 0.10689251124858856, -0.23654380440711975, - -0.3540131449699402, 0.10689251124858856, -0.16293412446975708, -0.3933582603931427, 0.10689251124858856, -0.1224585697054863, -0.29564154148101807, 0, - -0.17778240144252777, -0.2660703957080841, 0, -0.17778240144252777, -0.2660703957080841, 0, -0.23654380440711975, -0.3540131449699402, - 0.10689251124858856, -0.16293412446975708, -0.3933582603931427, 0.10689251124858856, -0.08306297659873962, -0.4175868332386017, 0.10689251124858856, - -0.06242874637246132, -0.31385132670402527, 0, -0.1224585697054863, -0.29564154148101807, 0, -0.1224585697054863, -0.29564154148101807, 0, - -0.16293412446975708, -0.3933582603931427, 0.10689251124858856, -0.08306297659873962, -0.4175868332386017, 0.10689251124858856, 2.4250164187833434e-7, - -0.42576777935028076, 0.10689251124858856, 1.9073486612342094e-7, -0.3199999928474426, 0, -0.06242874637246132, -0.31385132670402527, 0, - -0.06242874637246132, -0.31385132670402527, 0, -0.08306297659873962, -0.4175868332386017, 0.10689251124858856, 2.4250164187833434e-7, - -0.42576777935028076, 0.10689251124858856, 0.08306345343589783, -0.41758668422698975, 0.10689251124858856, 0.062429118901491165, -0.3138512372970581, 0, - 1.9073486612342094e-7, -0.3199999928474426, 0, 1.9073486612342094e-7, -0.3199999928474426, 0, 2.4250164187833434e-7, -0.42576777935028076, - 0.10689251124858856, 0.08306345343589783, -0.41758668422698975, 0.10689251124858856, 0.16293452680110931, -0.39335793256759644, 0.10689251124858856, - 0.12245891243219376, -0.29564130306243896, 0, 0.062429118901491165, -0.3138512372970581, 0, 0.062429118901491165, -0.3138512372970581, 0, - 0.08306345343589783, -0.41758668422698975, 0.10689251124858856, 0.16293452680110931, -0.39335793256759644, 0.10689251124858856, 0.2365441471338272, - -0.3540126383304596, 0.10689251124858856, 0.17778268456459045, -0.26607006788253784, 0, 0.12245891243219376, -0.29564130306243896, 0, - 0.12245891243219376, -0.29564130306243896, 0, 0.16293452680110931, -0.39335793256759644, 0.10689251124858856, 0.2365441471338272, -0.3540126383304596, - 0.10689251124858856, 0.3010634779930115, -0.3010628819465637, 0.10689251124858856, 0.22627434134483337, -0.22627387940883636, 0, 0.17778268456459045, - -0.26607006788253784, 0, 0.17778268456459045, -0.26607006788253784, 0, 0.2365441471338272, -0.3540126383304596, 0.10689251124858856, 0.3010634779930115, - -0.3010628819465637, 0.10689251124858856, 0.3540131449699402, -0.23654340207576752, 0.10689251124858856, 0.2660703957080841, -0.1777821183204651, 0, - 0.22627434134483337, -0.22627387940883636, 0, 0.22627434134483337, -0.22627387940883636, 0, 0.3010634779930115, -0.3010628819465637, - 0.10689251124858856, 0.3540131449699402, -0.23654340207576752, 0.10689251124858856, 0.3933582305908203, -0.16293370723724365, 0.10689251124858856, - 0.2956415116786957, -0.12245826423168182, 0, 0.2660703957080841, -0.1777821183204651, 0, 0.2660703957080841, -0.1777821183204651, 0, 0.3540131449699402, - -0.23654340207576752, 0.10689251124858856, 0.3933582305908203, -0.16293370723724365, 0.10689251124858856, 0.41758668422698975, -0.083062544465065, - 0.10689251124858856, 0.3138512372970581, -0.06242842227220535, 0, 0.2956415116786957, -0.12245826423168182, 0, 0.2956415116786957, -0.12245826423168182, - 0, 0.3933582305908203, -0.16293370723724365, 0.10689251124858856, 0.41758668422698975, -0.083062544465065, 0.10689251124858856, 0.4257676303386688, - -1.2535783966427516e-8, 0.10689251124858856, 0.3199998736381531, 0, 0, 0.3138512372970581, -0.06242842227220535, 0, 0.3138512372970581, - -0.06242842227220535, 0, 0.41758668422698975, -0.083062544465065, 0.10689251124858856, 0.4257676303386688, -1.2535783966427516e-8, 0.10689251124858856 - ], - "normalized": false - }, - "normal": { - "itemSize": 3, - "type": "Float32Array", - "array": [ - 0.6971781849861145, 0.1386774480342865, -0.7033570408821106, 0.7108367085456848, 3.257474361362256e-7, -0.7033571004867554, 0.71083664894104, - 3.6210147413839877e-7, -0.7033571600914001, 0.71083664894104, 3.6210147413839877e-7, -0.7033571600914001, 0.6971781849861145, 0.1386774480342865, - -0.7033570408821106, 0.6971781849861145, 0.1386774480342865, -0.7033570408821106, 0.6567274928092957, 0.27202534675598145, -0.7033570408821106, - 0.6971781849861145, 0.1386774480342865, -0.7033570408821106, 0.6971781849861145, 0.1386774480342865, -0.7033570408821106, 0.6971781849861145, - 0.1386774480342865, -0.7033570408821106, 0.6567275524139404, 0.27202534675598145, -0.7033570408821106, 0.6567274928092957, 0.27202534675598145, - -0.7033570408821106, 0.5910391211509705, 0.394919753074646, -0.7033570408821106, 0.6567274928092957, 0.27202534675598145, -0.7033570408821106, - 0.6567275524139404, 0.27202534675598145, -0.7033570408821106, 0.6567275524139404, 0.27202534675598145, -0.7033570408821106, 0.5910391211509705, - 0.3949197828769684, -0.7033570408821106, 0.5910391211509705, 0.394919753074646, -0.7033570408821106, 0.5026374459266663, 0.5026374459266663, - -0.7033571004867554, 0.5910391211509705, 0.394919753074646, -0.7033570408821106, 0.5910391211509705, 0.3949197828769684, -0.7033570408821106, - 0.5910391211509705, 0.3949197828769684, -0.7033570408821106, 0.5026374459266663, 0.5026374459266663, -0.7033570408821106, 0.5026374459266663, - 0.5026374459266663, -0.7033571004867554, 0.39491966366767883, 0.5910391807556152, -0.7033571004867554, 0.5026374459266663, 0.5026374459266663, - -0.7033571004867554, 0.5026374459266663, 0.5026374459266663, -0.7033570408821106, 0.5026374459266663, 0.5026374459266663, -0.7033570408821106, - 0.39491966366767883, 0.5910391211509705, -0.7033571004867554, 0.39491966366767883, 0.5910391807556152, -0.7033571004867554, 0.27202531695365906, - 0.6567276120185852, -0.7033570408821106, 0.39491966366767883, 0.5910391807556152, -0.7033571004867554, 0.39491966366767883, 0.5910391211509705, - -0.7033571004867554, 0.39491966366767883, 0.5910391211509705, -0.7033571004867554, 0.27202528715133667, 0.6567275524139404, -0.7033570408821106, - 0.27202531695365906, 0.6567276120185852, -0.7033570408821106, 0.13867750763893127, 0.6971781849861145, -0.7033570408821106, 0.27202531695365906, - 0.6567276120185852, -0.7033570408821106, 0.27202528715133667, 0.6567275524139404, -0.7033570408821106, 0.27202528715133667, 0.6567275524139404, - -0.7033570408821106, 0.13867750763893127, 0.6971781849861145, -0.7033570408821106, 0.13867750763893127, 0.6971781849861145, -0.7033570408821106, - 1.3676421417585516e-7, 0.71083664894104, -0.7033571004867554, 0.13867750763893127, 0.6971781849861145, -0.7033570408821106, 0.13867750763893127, - 0.6971781849861145, -0.7033570408821106, 0.13867750763893127, 0.6971781849861145, -0.7033570408821106, 1.525836097471256e-7, 0.7108367085456848, - -0.7033571004867554, 1.3676421417585516e-7, 0.71083664894104, -0.7033571004867554, -0.13867735862731934, 0.6971781253814697, -0.7033571004867554, - 1.3676421417585516e-7, 0.71083664894104, -0.7033571004867554, 1.525836097471256e-7, 0.7108367085456848, -0.7033571004867554, 1.525836097471256e-7, - 0.7108367085456848, -0.7033571004867554, -0.13867734372615814, 0.6971781253814697, -0.7033571004867554, -0.13867735862731934, 0.6971781253814697, - -0.7033571004867554, -0.2720252573490143, 0.6567274332046509, -0.7033571600914001, -0.13867735862731934, 0.6971781253814697, -0.7033571004867554, - -0.13867734372615814, 0.6971781253814697, -0.7033571004867554, -0.13867734372615814, 0.6971781253814697, -0.7033571004867554, -0.2720252573490143, - 0.6567274928092957, -0.7033571600914001, -0.2720252573490143, 0.6567274332046509, -0.7033571600914001, -0.39491966366767883, 0.5910390019416809, - -0.7033572196960449, -0.2720252573490143, 0.6567274332046509, -0.7033571600914001, -0.2720252573490143, 0.6567274928092957, -0.7033571600914001, - -0.2720252573490143, 0.6567274928092957, -0.7033571600914001, -0.39491963386535645, 0.5910390615463257, -0.7033571600914001, -0.39491966366767883, - 0.5910390019416809, -0.7033572196960449, -0.502637505531311, 0.5026373863220215, -0.7033571004867554, -0.39491966366767883, 0.5910390019416809, - -0.7033572196960449, -0.39491963386535645, 0.5910390615463257, -0.7033571600914001, -0.39491963386535645, 0.5910390615463257, -0.7033571600914001, - -0.5026374459266663, 0.5026373863220215, -0.7033571600914001, -0.502637505531311, 0.5026373863220215, -0.7033571004867554, -0.5910391211509705, - 0.39491963386535645, -0.7033571600914001, -0.502637505531311, 0.5026373863220215, -0.7033571004867554, -0.5026374459266663, 0.5026373863220215, - -0.7033571600914001, -0.5026374459266663, 0.5026373863220215, -0.7033571600914001, -0.5910391211509705, 0.39491963386535645, -0.7033571600914001, - -0.5910391211509705, 0.39491963386535645, -0.7033571600914001, -0.6567275524139404, 0.2720252275466919, -0.7033571004867554, -0.5910391211509705, - 0.39491963386535645, -0.7033571600914001, -0.5910391211509705, 0.39491963386535645, -0.7033571600914001, -0.5910391211509705, 0.39491963386535645, - -0.7033571600914001, -0.6567275524139404, 0.2720251977443695, -0.7033571004867554, -0.6567275524139404, 0.2720252275466919, -0.7033571004867554, - -0.6971781849861145, 0.1386772245168686, -0.7033570408821106, -0.6567275524139404, 0.2720252275466919, -0.7033571004867554, -0.6567275524139404, - 0.2720251977443695, -0.7033571004867554, -0.6567275524139404, 0.2720251977443695, -0.7033571004867554, -0.6971781849861145, 0.1386772245168686, - -0.7033570408821106, -0.6971781849861145, 0.1386772245168686, -0.7033570408821106, -0.71083664894104, -1.9644313908884214e-7, -0.7033571004867554, - -0.6971781849861145, 0.1386772245168686, -0.7033570408821106, -0.6971781849861145, 0.1386772245168686, -0.7033570408821106, -0.6971781849861145, - 0.1386772245168686, -0.7033570408821106, -0.71083664894104, -1.8446674232563964e-7, -0.7033571004867554, -0.71083664894104, -1.9644313908884214e-7, - -0.7033571004867554, -0.697178065776825, -0.13867776095867157, -0.7033571004867554, -0.71083664894104, -1.9644313908884214e-7, -0.7033571004867554, - -0.71083664894104, -1.8446674232563964e-7, -0.7033571004867554, -0.71083664894104, -1.8446674232563964e-7, -0.7033571004867554, -0.697178065776825, - -0.13867774605751038, -0.7033571004867554, -0.697178065776825, -0.13867776095867157, -0.7033571004867554, -0.6567273139953613, -0.27202582359313965, - -0.7033571004867554, -0.697178065776825, -0.13867776095867157, -0.7033571004867554, -0.697178065776825, -0.13867774605751038, -0.7033571004867554, - -0.697178065776825, -0.13867774605751038, -0.7033571004867554, -0.6567272543907166, -0.27202582359313965, -0.7033571004867554, -0.6567273139953613, - -0.27202582359313965, -0.7033571004867554, -0.5910389423370361, -0.3949200212955475, -0.7033571004867554, -0.6567273139953613, -0.27202582359313965, - -0.7033571004867554, -0.6567272543907166, -0.27202582359313965, -0.7033571004867554, -0.6567272543907166, -0.27202582359313965, -0.7033571004867554, - -0.5910389423370361, -0.3949199914932251, -0.7033570408821106, -0.5910389423370361, -0.3949200212955475, -0.7033571004867554, -0.5026372671127319, - -0.5026376843452454, -0.7033571004867554, -0.5910389423370361, -0.3949200212955475, -0.7033571004867554, -0.5910389423370361, -0.3949199914932251, - -0.7033570408821106, -0.5910389423370361, -0.3949199914932251, -0.7033570408821106, -0.5026372075080872, -0.5026376843452454, -0.7033571004867554, - -0.5026372671127319, -0.5026376843452454, -0.7033571004867554, -0.3949193060398102, -0.5910392999649048, -0.7033571600914001, -0.5026372671127319, - -0.5026376843452454, -0.7033571004867554, -0.5026372075080872, -0.5026376843452454, -0.7033571004867554, -0.5026372075080872, -0.5026376843452454, - -0.7033571004867554, -0.39491933584213257, -0.5910392999649048, -0.7033571600914001, -0.3949193060398102, -0.5910392999649048, -0.7033571600914001, - -0.27202484011650085, -0.6567276120185852, -0.7033571600914001, -0.3949193060398102, -0.5910392999649048, -0.7033571600914001, -0.39491933584213257, - -0.5910392999649048, -0.7033571600914001, -0.39491933584213257, -0.5910392999649048, -0.7033571600914001, -0.27202484011650085, -0.65672767162323, - -0.7033571600914001, -0.27202484011650085, -0.6567276120185852, -0.7033571600914001, -0.1386767476797104, -0.697178304195404, -0.7033571004867554, - -0.27202484011650085, -0.6567276120185852, -0.7033571600914001, -0.27202484011650085, -0.65672767162323, -0.7033571600914001, -0.27202484011650085, - -0.65672767162323, -0.7033571600914001, -0.1386767327785492, -0.6971782445907593, -0.7033571004867554, -0.1386767476797104, -0.697178304195404, - -0.7033571004867554, 6.514948722724512e-7, -0.7108367085456848, -0.7033571004867554, -0.1386767476797104, -0.697178304195404, -0.7033571004867554, - -0.1386767327785492, -0.6971782445907593, -0.7033571004867554, -0.1386767327785492, -0.6971782445907593, -0.7033571004867554, 6.536043883897946e-7, - -0.71083664894104, -0.7033571004867554, 6.514948722724512e-7, -0.7108367085456848, -0.7033571004867554, 0.1386781930923462, -0.6971779465675354, - -0.7033571004867554, 6.514948722724512e-7, -0.7108367085456848, -0.7033571004867554, 6.536043883897946e-7, -0.71083664894104, -0.7033571004867554, - 6.536043883897946e-7, -0.71083664894104, -0.7033571004867554, 0.1386781930923462, -0.6971779465675354, -0.7033571004867554, 0.1386781930923462, - -0.6971779465675354, -0.7033571004867554, 0.2720262110233307, -0.6567271947860718, -0.7033570408821106, 0.1386781930923462, -0.6971779465675354, - -0.7033571004867554, 0.1386781930923462, -0.6971779465675354, -0.7033571004867554, 0.1386781930923462, -0.6971779465675354, -0.7033571004867554, - 0.2720262408256531, -0.6567271947860718, -0.7033570408821106, 0.2720262110233307, -0.6567271947860718, -0.7033570408821106, 0.3949204683303833, - -0.591038703918457, -0.7033569812774658, 0.2720262110233307, -0.6567271947860718, -0.7033570408821106, 0.2720262408256531, -0.6567271947860718, - -0.7033570408821106, 0.2720262408256531, -0.6567271947860718, -0.7033570408821106, 0.3949204683303833, -0.591038703918457, -0.7033569812774658, - 0.3949204683303833, -0.591038703918457, -0.7033569812774658, 0.5026381015777588, -0.5026369094848633, -0.7033569812774658, 0.3949204683303833, - -0.591038703918457, -0.7033569812774658, 0.3949204683303833, -0.591038703918457, -0.7033569812774658, 0.3949204683303833, -0.591038703918457, - -0.7033569812774658, 0.502638041973114, -0.5026369094848633, -0.7033570408821106, 0.5026381015777588, -0.5026369094848633, -0.7033569812774658, - 0.5910396575927734, -0.39491894841194153, -0.7033571004867554, 0.5026381015777588, -0.5026369094848633, -0.7033569812774658, 0.502638041973114, - -0.5026369094848633, -0.7033570408821106, 0.502638041973114, -0.5026369094848633, -0.7033570408821106, 0.5910395979881287, -0.3949189782142639, - -0.7033571004867554, 0.5910396575927734, -0.39491894841194153, -0.7033571004867554, 0.6567279100418091, -0.2720244228839874, -0.7033571004867554, - 0.5910396575927734, -0.39491894841194153, -0.7033571004867554, 0.5910395979881287, -0.3949189782142639, -0.7033571004867554, 0.5910395979881287, - -0.3949189782142639, -0.7033571004867554, 0.6567278504371643, -0.27202436327934265, -0.7033571004867554, 0.6567279100418091, -0.2720244228839874, - -0.7033571004867554, 0.697178304195404, -0.13867662847042084, -0.7033571004867554, 0.6567279100418091, -0.2720244228839874, -0.7033571004867554, - 0.6567278504371643, -0.27202436327934265, -0.7033571004867554, 0.6567278504371643, -0.27202436327934265, -0.7033571004867554, 0.697178304195404, - -0.13867664337158203, -0.7033571004867554, 0.697178304195404, -0.13867662847042084, -0.7033571004867554, 0.7108367085456848, 3.257474361362256e-7, - -0.7033571004867554, 0.697178304195404, -0.13867662847042084, -0.7033571004867554, 0.697178304195404, -0.13867664337158203, -0.7033571004867554, - 0.697178304195404, -0.13867664337158203, -0.7033571004867554, 0.71083664894104, 3.6210147413839877e-7, -0.7033571600914001, 0.7108367085456848, - 3.257474361362256e-7, -0.7033571004867554, -0.6971782445907593, -0.1386774629354477, 0.7033569812774658, -0.6971781849861145, -0.1386774629354477, - 0.7033570408821106, -0.7108367085456848, -1.1159099955193597e-7, 0.7033570408821106, -0.7108367085456848, -1.1159099955193597e-7, 0.7033570408821106, - -0.7108367085456848, -9.200498851669181e-8, 0.7033570408821106, -0.6971782445907593, -0.1386774629354477, 0.7033569812774658, -0.6567275524139404, - -0.27202561497688293, 0.7033569812774658, -0.6567274928092957, -0.27202558517456055, 0.7033569812774658, -0.6971781849861145, -0.1386774629354477, - 0.7033570408821106, -0.6971781849861145, -0.1386774629354477, 0.7033570408821106, -0.6971782445907593, -0.1386774629354477, 0.7033569812774658, - -0.6567275524139404, -0.27202561497688293, 0.7033569812774658, -0.5910391807556152, -0.3949199616909027, 0.703356921672821, -0.5910391211509705, - -0.3949199616909027, 0.703356921672821, -0.6567274928092957, -0.27202558517456055, 0.7033569812774658, -0.6567274928092957, -0.27202558517456055, - 0.7033569812774658, -0.6567275524139404, -0.27202561497688293, 0.7033569812774658, -0.5910391807556152, -0.3949199616909027, 0.703356921672821, - -0.5026376247406006, -0.502637505531311, 0.703356921672821, -0.5026376247406006, -0.5026374459266663, 0.703356921672821, -0.5910391211509705, - -0.3949199616909027, 0.703356921672821, -0.5910391211509705, -0.3949199616909027, 0.703356921672821, -0.5910391807556152, -0.3949199616909027, - 0.703356921672821, -0.5026376247406006, -0.502637505531311, 0.703356921672821, -0.3949197232723236, -0.5910391807556152, 0.7033570408821106, - -0.394919753074646, -0.5910391807556152, 0.7033569812774658, -0.5026376247406006, -0.5026374459266663, 0.703356921672821, -0.5026376247406006, - -0.5026374459266663, 0.703356921672821, -0.5026376247406006, -0.502637505531311, 0.703356921672821, -0.3949197232723236, -0.5910391807556152, - 0.7033570408821106, -0.27202528715133667, -0.65672767162323, 0.7033569812774658, -0.27202528715133667, -0.65672767162323, 0.703356921672821, - -0.394919753074646, -0.5910391807556152, 0.7033569812774658, -0.394919753074646, -0.5910391807556152, 0.7033569812774658, -0.3949197232723236, - -0.5910391807556152, 0.7033570408821106, -0.27202528715133667, -0.65672767162323, 0.7033569812774658, -0.13867752254009247, -0.6971781849861145, - 0.7033569812774658, -0.13867753744125366, -0.6971782445907593, 0.7033569812774658, -0.27202528715133667, -0.65672767162323, 0.703356921672821, - -0.27202528715133667, -0.65672767162323, 0.703356921672821, -0.27202528715133667, -0.65672767162323, 0.7033569812774658, -0.13867752254009247, - -0.6971781849861145, 0.7033569812774658, -1.2184446518404002e-7, -0.7108367085456848, 0.7033571004867554, -1.594157055251344e-7, -0.71083664894104, - 0.7033571004867554, -0.13867753744125366, -0.6971782445907593, 0.7033569812774658, -0.13867753744125366, -0.6971782445907593, 0.7033569812774658, - -0.13867752254009247, -0.6971781849861145, 0.7033569812774658, -1.2184446518404002e-7, -0.7108367085456848, 0.7033571004867554, 0.13867734372615814, - -0.6971781253814697, 0.7033571600914001, 0.13867731392383575, -0.6971781253814697, 0.7033571004867554, -1.594157055251344e-7, -0.71083664894104, - 0.7033571004867554, -1.594157055251344e-7, -0.71083664894104, 0.7033571004867554, -1.2184446518404002e-7, -0.7108367085456848, 0.7033571004867554, - 0.13867734372615814, -0.6971781253814697, 0.7033571600914001, 0.2720251977443695, -0.6567274332046509, 0.7033571600914001, 0.27202513813972473, - -0.6567274928092957, 0.7033571600914001, 0.13867731392383575, -0.6971781253814697, 0.7033571004867554, 0.13867731392383575, -0.6971781253814697, - 0.7033571004867554, 0.13867734372615814, -0.6971781253814697, 0.7033571600914001, 0.2720251977443695, -0.6567274332046509, 0.7033571600914001, - 0.3949195146560669, -0.5910390615463257, 0.7033572793006897, 0.3949195146560669, -0.5910390615463257, 0.7033572793006897, 0.27202513813972473, - -0.6567274928092957, 0.7033571600914001, 0.27202513813972473, -0.6567274928092957, 0.7033571600914001, 0.2720251977443695, -0.6567274332046509, - 0.7033571600914001, 0.3949195146560669, -0.5910390615463257, 0.7033572793006897, 0.5026373863220215, -0.5026372671127319, 0.7033572196960449, - 0.5026374459266663, -0.5026372671127319, 0.7033572196960449, 0.3949195146560669, -0.5910390615463257, 0.7033572793006897, 0.3949195146560669, - -0.5910390615463257, 0.7033572793006897, 0.3949195146560669, -0.5910390615463257, 0.7033572793006897, 0.5026373863220215, -0.5026372671127319, - 0.7033572196960449, 0.5910391211509705, -0.3949195444583893, 0.7033572196960449, 0.5910390615463257, -0.39491957426071167, 0.7033572196960449, - 0.5026374459266663, -0.5026372671127319, 0.7033572196960449, 0.5026374459266663, -0.5026372671127319, 0.7033572196960449, 0.5026373863220215, - -0.5026372671127319, 0.7033572196960449, 0.5910391211509705, -0.3949195444583893, 0.7033572196960449, 0.6567274332046509, -0.27202528715133667, - 0.7033571600914001, 0.6567274928092957, -0.27202534675598145, 0.7033571600914001, 0.5910390615463257, -0.39491957426071167, 0.7033572196960449, - 0.5910390615463257, -0.39491957426071167, 0.7033572196960449, 0.5910391211509705, -0.3949195444583893, 0.7033572196960449, 0.6567274332046509, - -0.27202528715133667, 0.7033571600914001, 0.6971781253814697, -0.13867710530757904, 0.7033572196960449, 0.6971781253814697, -0.13867712020874023, - 0.7033572196960449, 0.6567274928092957, -0.27202534675598145, 0.7033571600914001, 0.6567274928092957, -0.27202534675598145, 0.7033571600914001, - 0.6567274332046509, -0.27202528715133667, 0.7033571600914001, 0.6971781253814697, -0.13867710530757904, 0.7033572196960449, 0.7108365893363953, - 1.9644303961285914e-7, 0.7033571600914001, 0.7108365893363953, 1.8674414548058849e-7, 0.7033572196960449, 0.6971781253814697, -0.13867712020874023, - 0.7033572196960449, 0.6971781253814697, -0.13867712020874023, 0.7033572196960449, 0.6971781253814697, -0.13867710530757904, 0.7033572196960449, - 0.7108365893363953, 1.9644303961285914e-7, 0.7033571600914001, 0.6971780061721802, 0.1386774778366089, 0.7033572793006897, 0.6971780061721802, - 0.13867749273777008, 0.7033572793006897, 0.7108365893363953, 1.8674414548058849e-7, 0.7033572196960449, 0.7108365893363953, 1.8674414548058849e-7, - 0.7033572196960449, 0.7108365893363953, 1.9644303961285914e-7, 0.7033571600914001, 0.6971780061721802, 0.1386774778366089, 0.7033572793006897, - 0.656727135181427, 0.2720257043838501, 0.7033573389053345, 0.6567271947860718, 0.2720257341861725, 0.7033572793006897, 0.6971780061721802, - 0.13867749273777008, 0.7033572793006897, 0.6971780061721802, 0.13867749273777008, 0.7033572793006897, 0.6971780061721802, 0.1386774778366089, - 0.7033572793006897, 0.656727135181427, 0.2720257043838501, 0.7033573389053345, 0.5910387635231018, 0.3949199616909027, 0.7033572196960449, - 0.5910387635231018, 0.3949199616909027, 0.7033572793006897, 0.6567271947860718, 0.2720257341861725, 0.7033572793006897, 0.6567271947860718, - 0.2720257341861725, 0.7033572793006897, 0.656727135181427, 0.2720257043838501, 0.7033573389053345, 0.5910387635231018, 0.3949199616909027, - 0.7033572196960449, 0.5026370286941528, 0.5026376247406006, 0.7033572793006897, 0.5026370882987976, 0.5026376843452454, 0.7033572196960449, - 0.5910387635231018, 0.3949199616909027, 0.7033572793006897, 0.5910387635231018, 0.3949199616909027, 0.7033572793006897, 0.5910387635231018, - 0.3949199616909027, 0.7033572196960449, 0.5026370286941528, 0.5026376247406006, 0.7033572793006897, 0.39491918683052063, 0.59103924036026, - 0.7033572196960449, 0.394919216632843, 0.5910392999649048, 0.7033572196960449, 0.5026370882987976, 0.5026376843452454, 0.7033572196960449, - 0.5026370882987976, 0.5026376843452454, 0.7033572196960449, 0.5026370286941528, 0.5026376247406006, 0.7033572793006897, 0.39491918683052063, - 0.59103924036026, 0.7033572196960449, 0.27202481031417847, 0.6567276120185852, 0.7033572196960449, 0.2720247805118561, 0.6567276120185852, - 0.7033572196960449, 0.394919216632843, 0.5910392999649048, 0.7033572196960449, 0.394919216632843, 0.5910392999649048, 0.7033572196960449, - 0.39491918683052063, 0.59103924036026, 0.7033572196960449, 0.27202481031417847, 0.6567276120185852, 0.7033572196960449, 0.1386767327785492, - 0.6971782445907593, 0.7033571600914001, 0.138676717877388, 0.6971781849861145, 0.7033571600914001, 0.2720247805118561, 0.6567276120185852, - 0.7033572196960449, 0.2720247805118561, 0.6567276120185852, 0.7033572196960449, 0.27202481031417847, 0.6567276120185852, 0.7033572196960449, - 0.1386767327785492, 0.6971782445907593, 0.7033571600914001, -6.365750095937983e-7, 0.7108365893363953, 0.7033571600914001, -6.627138304793334e-7, - 0.71083664894104, 0.7033571600914001, 0.138676717877388, 0.6971781849861145, 0.7033571600914001, 0.138676717877388, 0.6971781849861145, - 0.7033571600914001, 0.1386767327785492, 0.6971782445907593, 0.7033571600914001, -6.365750095937983e-7, 0.7108365893363953, 0.7033571600914001, - -0.138678178191185, 0.697178065776825, 0.7033570408821106, -0.13867820799350739, 0.6971779465675354, 0.7033571004867554, -6.627138304793334e-7, - 0.71083664894104, 0.7033571600914001, -6.627138304793334e-7, 0.71083664894104, 0.7033571600914001, -6.365750095937983e-7, 0.7108365893363953, - 0.7033571600914001, -0.138678178191185, 0.697178065776825, 0.7033570408821106, -0.27202627062797546, 0.6567271947860718, 0.7033569812774658, - -0.27202630043029785, 0.6567271947860718, 0.7033570408821106, -0.13867820799350739, 0.6971779465675354, 0.7033571004867554, -0.13867820799350739, - 0.6971779465675354, 0.7033571004867554, -0.138678178191185, 0.697178065776825, 0.7033570408821106, -0.27202627062797546, 0.6567271947860718, - 0.7033569812774658, -0.3949204385280609, 0.5910387635231018, 0.703356921672821, -0.3949204683303833, 0.591038703918457, 0.7033569812774658, - -0.27202630043029785, 0.6567271947860718, 0.7033570408821106, -0.27202630043029785, 0.6567271947860718, 0.7033570408821106, -0.27202627062797546, - 0.6567271947860718, 0.7033569812774658, -0.3949204385280609, 0.5910387635231018, 0.703356921672821, -0.5026381015777588, 0.5026369094848633, - 0.7033569812774658, -0.5026381015777588, 0.5026369094848633, 0.7033569812774658, -0.3949204683303833, 0.591038703918457, 0.7033569812774658, - -0.3949204683303833, 0.591038703918457, 0.7033569812774658, -0.3949204385280609, 0.5910387635231018, 0.703356921672821, -0.5026381015777588, - 0.5026369094848633, 0.7033569812774658, -0.5910396575927734, 0.3949190378189087, 0.7033569812774658, -0.5910396575927734, 0.3949190676212311, - 0.7033569812774658, -0.5026381015777588, 0.5026369094848633, 0.7033569812774658, -0.5026381015777588, 0.5026369094848633, 0.7033569812774658, - -0.5026381015777588, 0.5026369094848633, 0.7033569812774658, -0.5910396575927734, 0.3949190378189087, 0.7033569812774658, -0.6567279696464539, - 0.27202433347702026, 0.7033570408821106, -0.6567279696464539, 0.2720243036746979, 0.7033570408821106, -0.5910396575927734, 0.3949190676212311, - 0.7033569812774658, -0.5910396575927734, 0.3949190676212311, 0.7033569812774658, -0.5910396575927734, 0.3949190378189087, 0.7033569812774658, - -0.6567279696464539, 0.27202433347702026, 0.7033570408821106, -0.6971784234046936, 0.13867659866809845, 0.7033569812774658, -0.6971784234046936, - 0.13867662847042084, 0.703356921672821, -0.6567279696464539, 0.2720243036746979, 0.7033570408821106, -0.6567279696464539, 0.2720243036746979, - 0.7033570408821106, -0.6567279696464539, 0.27202433347702026, 0.7033570408821106, -0.6971784234046936, 0.13867659866809845, 0.7033569812774658, - -0.7108367085456848, -9.200498851669181e-8, 0.7033570408821106, -0.7108367085456848, -1.1159099955193597e-7, 0.7033570408821106, -0.6971784234046936, - 0.13867662847042084, 0.703356921672821, -0.6971784234046936, 0.13867662847042084, 0.703356921672821, -0.6971784234046936, 0.13867659866809845, - 0.7033569812774658, -0.7108367085456848, -9.200498851669181e-8, 0.7033570408821106 - ], - "normalized": false - }, - "uv": { - "itemSize": 2, - "type": "Float32Array", - "array": [ - 0.8906737565994263, 0.9400254487991333, 0.4999995231628418, 0.9400254487991333, 0.4999995231628418, 0.0599745512008667, 0.4999995231628418, - 0.0599745512008667, 0.8906737565994263, 0.0599745512008667, 0.8906737565994263, 0.9400254487991333, 1.2663346529006958, 0.9400254487991333, - 0.8906737565994263, 0.9400254487991333, 0.8906737565994263, 0.0599745512008667, 0.8906737565994263, 0.0599745512008667, 1.2663346529006958, - 0.0599745512008667, 1.2663346529006958, 0.9400254487991333, 1.6125456094741821, 0.9400254487991333, 1.2663346529006958, 0.9400254487991333, - 1.2663346529006958, 0.0599745512008667, 1.2663346529006958, 0.0599745512008667, 1.6125456094741821, 0.0599745512008667, 1.6125456094741821, - 0.9400254487991333, 1.9160020351409912, 0.9400254487991333, 1.6125456094741821, 0.9400254487991333, 1.6125456094741821, 0.0599745512008667, - 1.6125456094741821, 0.0599745512008667, 1.9160020351409912, 0.0599745512008667, 1.9160020351409912, 0.9400254487991333, -0.6125462055206299, - 0.9400254487991333, -0.9160027503967285, 0.9400254487991333, -0.9160027503967285, 0.0599745512008667, -0.9160027503967285, 0.0599745512008667, - -0.6125462055206299, 0.0599745512008667, -0.6125462055206299, 0.9400254487991333, -0.26633548736572266, 0.9400254487991333, -0.6125462055206299, - 0.9400254487991333, -0.6125462055206299, 0.0599745512008667, -0.6125462055206299, 0.0599745512008667, -0.26633548736572266, 0.0599745512008667, - -0.26633548736572266, 0.9400254487991333, 0.10932528972625732, 0.9400254487991333, -0.26633548736572266, 0.9400254487991333, -0.26633548736572266, - 0.0599745512008667, -0.26633548736572266, 0.0599745512008667, 0.10932528972625732, 0.0599745512008667, 0.10932528972625732, 0.9400254487991333, - 0.49999934434890747, 0.9400254487991333, 0.10932528972625732, 0.9400254487991333, 0.10932528972625732, 0.0599745512008667, 0.10932528972625732, - 0.0599745512008667, 0.49999934434890747, 0.0599745512008667, 0.49999934434890747, 0.9400254487991333, 0.8906735181808472, 0.9400254487991333, - 0.49999934434890747, 0.9400254487991333, 0.49999934434890747, 0.0599745512008667, 0.49999934434890747, 0.0599745512008667, 0.8906735181808472, - 0.0599745512008667, 0.8906735181808472, 0.9400254487991333, 1.2663342952728271, 0.9400254487991333, 0.8906735181808472, 0.9400254487991333, - 0.8906735181808472, 0.0599745512008667, 0.8906735181808472, 0.0599745512008667, 1.2663342952728271, 0.0599745512008667, 1.2663342952728271, - 0.9400254487991333, 1.6125454902648926, 0.9400254487991333, 1.2663342952728271, 0.9400254487991333, 1.2663342952728271, 0.0599745512008667, - 1.2663342952728271, 0.0599745512008667, 1.6125454902648926, 0.0599745512008667, 1.6125454902648926, 0.9400254487991333, 1.9160020351409912, - 0.9400254487991333, 1.6125454902648926, 0.9400254487991333, 1.6125454902648926, 0.0599745512008667, 1.6125454902648926, 0.0599745512008667, - 1.9160020351409912, 0.0599745512008667, 1.9160020351409912, 0.9400254487991333, -0.6125462055206299, 0.9400254487991333, -0.9160027503967285, - 0.9400254487991333, -0.9160027503967285, 0.0599745512008667, -0.9160027503967285, 0.0599745512008667, -0.6125462055206299, 0.0599745512008667, - -0.6125462055206299, 0.9400254487991333, -0.266335129737854, 0.9400254487991333, -0.6125462055206299, 0.9400254487991333, -0.6125462055206299, - 0.0599745512008667, -0.6125462055206299, 0.0599745512008667, -0.266335129737854, 0.0599745512008667, -0.266335129737854, 0.9400254487991333, - 0.10932576656341553, 0.9400254487991333, -0.266335129737854, 0.9400254487991333, -0.266335129737854, 0.0599745512008667, -0.266335129737854, - 0.0599745512008667, 0.10932576656341553, 0.0599745512008667, 0.10932576656341553, 0.9400254487991333, 0.5000001788139343, 0.9400254487991333, - 0.10932576656341553, 0.9400254487991333, 0.10932576656341553, 0.0599745512008667, 0.10932576656341553, 0.0599745512008667, 0.5000001788139343, - 0.0599745512008667, 0.5000001788139343, 0.9400254487991333, 0.8906745314598083, 0.9400254487991333, 0.5000001788139343, 0.9400254487991333, - 0.5000001788139343, 0.0599745512008667, 0.5000001788139343, 0.0599745512008667, 0.8906745314598083, 0.0599745512008667, 0.8906745314598083, - 0.9400254487991333, 1.2663354873657227, 0.9400254487991333, 0.8906745314598083, 0.9400254487991333, 0.8906745314598083, 0.0599745512008667, - 0.8906745314598083, 0.0599745512008667, 1.2663354873657227, 0.0599745512008667, 1.2663354873657227, 0.9400254487991333, 1.6125465631484985, - 0.9400254487991333, 1.2663354873657227, 0.9400254487991333, 1.2663354873657227, 0.0599745512008667, 1.2663354873657227, 0.0599745512008667, - 1.6125465631484985, 0.0599745512008667, 1.6125465631484985, 0.9400254487991333, 1.9160029888153076, 0.9400254487991333, 1.6125465631484985, - 0.9400254487991333, 1.6125465631484985, 0.0599745512008667, 1.6125465631484985, 0.0599745512008667, 1.9160029888153076, 0.0599745512008667, - 1.9160029888153076, 0.9400254487991333, -0.6125451326370239, 0.9400254487991333, -0.9160019159317017, 0.9400254487991333, -0.9160019159317017, - 0.0599745512008667, -0.9160019159317017, 0.0599745512008667, -0.6125451326370239, 0.0599745512008667, -0.6125451326370239, 0.9400254487991333, - -0.2663339376449585, 0.9400254487991333, -0.6125451326370239, 0.9400254487991333, -0.6125451326370239, 0.0599745512008667, -0.6125451326370239, - 0.0599745512008667, -0.2663339376449585, 0.0599745512008667, -0.2663339376449585, 0.9400254487991333, 0.1093270480632782, 0.9400254487991333, - -0.2663339376449585, 0.9400254487991333, -0.2663339376449585, 0.0599745512008667, -0.2663339376449585, 0.0599745512008667, 0.1093270480632782, - 0.0599745512008667, 0.1093270480632782, 0.9400254487991333, 0.5000014305114746, 0.9400254487991333, 0.1093270480632782, 0.9400254487991333, - 0.1093270480632782, 0.0599745512008667, 0.1093270480632782, 0.0599745512008667, 0.5000014305114746, 0.0599745512008667, 0.5000014305114746, - 0.9400254487991333, 0.8906757831573486, 0.9400254487991333, 0.5000014305114746, 0.9400254487991333, 0.5000014305114746, 0.0599745512008667, - 0.5000014305114746, 0.0599745512008667, 0.8906757831573486, 0.0599745512008667, 0.8906757831573486, 0.9400254487991333, 1.2663366794586182, - 0.9400254487991333, 0.8906757831573486, 0.9400254487991333, 0.8906757831573486, 0.0599745512008667, 0.8906757831573486, 0.0599745512008667, - 1.2663366794586182, 0.0599745512008667, 1.2663366794586182, 0.9400254487991333, 1.6125476360321045, 0.9400254487991333, 1.2663366794586182, - 0.9400254487991333, 1.2663366794586182, 0.0599745512008667, 1.2663366794586182, 0.0599745512008667, 1.6125476360321045, 0.0599745512008667, - 1.6125476360321045, 0.9400254487991333, 1.9160038232803345, 0.9400254487991333, 1.6125476360321045, 0.9400254487991333, 1.6125476360321045, - 0.0599745512008667, 1.6125476360321045, 0.0599745512008667, 1.9160038232803345, 0.0599745512008667, 1.9160038232803345, 0.9400254487991333, - -0.612544059753418, 0.9400254487991333, -0.9160009622573853, 0.9400254487991333, -0.9160009622573853, 0.0599745512008667, -0.9160009622573853, - 0.0599745512008667, -0.612544059753418, 0.0599745512008667, -0.612544059753418, 0.9400254487991333, -0.266332745552063, 0.9400254487991333, - -0.612544059753418, 0.9400254487991333, -0.612544059753418, 0.0599745512008667, -0.612544059753418, 0.0599745512008667, -0.266332745552063, - 0.0599745512008667, -0.266332745552063, 0.9400254487991333, 0.10932832956314087, 0.9400254487991333, -0.266332745552063, 0.9400254487991333, - -0.266332745552063, 0.0599745512008667, -0.266332745552063, 0.0599745512008667, 0.10932832956314087, 0.0599745512008667, 0.10932832956314087, - 0.9400254487991333, 0.4999995231628418, 0.9400254487991333, 0.10932832956314087, 0.9400254487991333, 0.10932832956314087, 0.0599745512008667, - 0.10932832956314087, 0.0599745512008667, 0.4999995231628418, 0.0599745512008667, 0.4999995231628418, 0.9400254487991333, 0.8906737565994263, - 0.9400254487991333, 0.8906737565994263, 0.0599745512008667, 0.4999995231628418, 0.0599745512008667, 0.4999995231628418, 0.0599745512008667, - 0.4999995231628418, 0.9400254487991333, 0.8906737565994263, 0.9400254487991333, 1.2663346529006958, 0.9400254487991333, 1.2663346529006958, - 0.0599745512008667, 0.8906737565994263, 0.0599745512008667, 0.8906737565994263, 0.0599745512008667, 0.8906737565994263, 0.9400254487991333, - 1.2663346529006958, 0.9400254487991333, 1.6125456094741821, 0.9400254487991333, 1.6125456094741821, 0.0599745512008667, 1.2663346529006958, - 0.0599745512008667, 1.2663346529006958, 0.0599745512008667, 1.2663346529006958, 0.9400254487991333, 1.6125456094741821, 0.9400254487991333, - 1.9160020351409912, 0.9400254487991333, 1.9160020351409912, 0.0599745512008667, 1.6125456094741821, 0.0599745512008667, 1.6125456094741821, - 0.0599745512008667, 1.6125456094741821, 0.9400254487991333, 1.9160020351409912, 0.9400254487991333, -0.6125462055206299, 0.9400254487991333, - -0.6125462055206299, 0.0599745512008667, -0.9160027503967285, 0.0599745512008667, -0.9160027503967285, 0.0599745512008667, -0.9160027503967285, - 0.9400254487991333, -0.6125462055206299, 0.9400254487991333, -0.26633548736572266, 0.9400254487991333, -0.26633548736572266, 0.0599745512008667, - -0.6125462055206299, 0.0599745512008667, -0.6125462055206299, 0.0599745512008667, -0.6125462055206299, 0.9400254487991333, -0.26633548736572266, - 0.9400254487991333, 0.10932528972625732, 0.9400254487991333, 0.10932528972625732, 0.0599745512008667, -0.26633548736572266, 0.0599745512008667, - -0.26633548736572266, 0.0599745512008667, -0.26633548736572266, 0.9400254487991333, 0.10932528972625732, 0.9400254487991333, 0.49999934434890747, - 0.9400254487991333, 0.49999934434890747, 0.0599745512008667, 0.10932528972625732, 0.0599745512008667, 0.10932528972625732, 0.0599745512008667, - 0.10932528972625732, 0.9400254487991333, 0.49999934434890747, 0.9400254487991333, 0.8906735181808472, 0.9400254487991333, 0.8906735181808472, - 0.0599745512008667, 0.49999934434890747, 0.0599745512008667, 0.49999934434890747, 0.0599745512008667, 0.49999934434890747, 0.9400254487991333, - 0.8906735181808472, 0.9400254487991333, 1.2663342952728271, 0.9400254487991333, 1.2663342952728271, 0.0599745512008667, 0.8906735181808472, - 0.0599745512008667, 0.8906735181808472, 0.0599745512008667, 0.8906735181808472, 0.9400254487991333, 1.2663342952728271, 0.9400254487991333, - 1.6125454902648926, 0.9400254487991333, 1.6125454902648926, 0.0599745512008667, 1.2663342952728271, 0.0599745512008667, 1.2663342952728271, - 0.0599745512008667, 1.2663342952728271, 0.9400254487991333, 1.6125454902648926, 0.9400254487991333, 1.9160020351409912, 0.9400254487991333, - 1.9160020351409912, 0.0599745512008667, 1.6125454902648926, 0.0599745512008667, 1.6125454902648926, 0.0599745512008667, 1.6125454902648926, - 0.9400254487991333, 1.9160020351409912, 0.9400254487991333, -0.6125462055206299, 0.9400254487991333, -0.6125462055206299, 0.0599745512008667, - -0.9160027503967285, 0.0599745512008667, -0.9160027503967285, 0.0599745512008667, -0.9160027503967285, 0.9400254487991333, -0.6125462055206299, - 0.9400254487991333, -0.266335129737854, 0.9400254487991333, -0.266335129737854, 0.0599745512008667, -0.6125462055206299, 0.0599745512008667, - -0.6125462055206299, 0.0599745512008667, -0.6125462055206299, 0.9400254487991333, -0.266335129737854, 0.9400254487991333, 0.10932576656341553, - 0.9400254487991333, 0.10932576656341553, 0.0599745512008667, -0.266335129737854, 0.0599745512008667, -0.266335129737854, 0.0599745512008667, - -0.266335129737854, 0.9400254487991333, 0.10932576656341553, 0.9400254487991333, 0.5000001788139343, 0.9400254487991333, 0.5000001788139343, - 0.0599745512008667, 0.10932576656341553, 0.0599745512008667, 0.10932576656341553, 0.0599745512008667, 0.10932576656341553, 0.9400254487991333, - 0.5000001788139343, 0.9400254487991333, 0.8906745314598083, 0.9400254487991333, 0.8906745314598083, 0.0599745512008667, 0.5000001788139343, - 0.0599745512008667, 0.5000001788139343, 0.0599745512008667, 0.5000001788139343, 0.9400254487991333, 0.8906745314598083, 0.9400254487991333, - 1.2663354873657227, 0.9400254487991333, 1.2663354873657227, 0.0599745512008667, 0.8906745314598083, 0.0599745512008667, 0.8906745314598083, - 0.0599745512008667, 0.8906745314598083, 0.9400254487991333, 1.2663354873657227, 0.9400254487991333, 1.6125465631484985, 0.9400254487991333, - 1.6125465631484985, 0.0599745512008667, 1.2663354873657227, 0.0599745512008667, 1.2663354873657227, 0.0599745512008667, 1.2663354873657227, - 0.9400254487991333, 1.6125465631484985, 0.9400254487991333, 1.9160029888153076, 0.9400254487991333, 1.9160029888153076, 0.0599745512008667, - 1.6125465631484985, 0.0599745512008667, 1.6125465631484985, 0.0599745512008667, 1.6125465631484985, 0.9400254487991333, 1.9160029888153076, - 0.9400254487991333, -0.6125451326370239, 0.9400254487991333, -0.6125451326370239, 0.0599745512008667, -0.9160019159317017, 0.0599745512008667, - -0.9160019159317017, 0.0599745512008667, -0.9160019159317017, 0.9400254487991333, -0.6125451326370239, 0.9400254487991333, -0.2663339376449585, - 0.9400254487991333, -0.2663339376449585, 0.0599745512008667, -0.6125451326370239, 0.0599745512008667, -0.6125451326370239, 0.0599745512008667, - -0.6125451326370239, 0.9400254487991333, -0.2663339376449585, 0.9400254487991333, 0.1093270480632782, 0.9400254487991333, 0.1093270480632782, - 0.0599745512008667, -0.2663339376449585, 0.0599745512008667, -0.2663339376449585, 0.0599745512008667, -0.2663339376449585, 0.9400254487991333, - 0.1093270480632782, 0.9400254487991333, 0.5000014305114746, 0.9400254487991333, 0.5000014305114746, 0.0599745512008667, 0.1093270480632782, - 0.0599745512008667, 0.1093270480632782, 0.0599745512008667, 0.1093270480632782, 0.9400254487991333, 0.5000014305114746, 0.9400254487991333, - 0.8906757831573486, 0.9400254487991333, 0.8906757831573486, 0.0599745512008667, 0.5000014305114746, 0.0599745512008667, 0.5000014305114746, - 0.0599745512008667, 0.5000014305114746, 0.9400254487991333, 0.8906757831573486, 0.9400254487991333, 1.2663366794586182, 0.9400254487991333, - 1.2663366794586182, 0.0599745512008667, 0.8906757831573486, 0.0599745512008667, 0.8906757831573486, 0.0599745512008667, 0.8906757831573486, - 0.9400254487991333, 1.2663366794586182, 0.9400254487991333, 1.6125476360321045, 0.9400254487991333, 1.6125476360321045, 0.0599745512008667, - 1.2663366794586182, 0.0599745512008667, 1.2663366794586182, 0.0599745512008667, 1.2663366794586182, 0.9400254487991333, 1.6125476360321045, - 0.9400254487991333, 1.9160038232803345, 0.9400254487991333, 1.9160038232803345, 0.0599745512008667, 1.6125476360321045, 0.0599745512008667, - 1.6125476360321045, 0.0599745512008667, 1.6125476360321045, 0.9400254487991333, 1.9160038232803345, 0.9400254487991333, -0.612544059753418, - 0.9400254487991333, -0.612544059753418, 0.0599745512008667, -0.9160009622573853, 0.0599745512008667, -0.9160009622573853, 0.0599745512008667, - -0.9160009622573853, 0.9400254487991333, -0.612544059753418, 0.9400254487991333, -0.266332745552063, 0.9400254487991333, -0.266332745552063, - 0.0599745512008667, -0.612544059753418, 0.0599745512008667, -0.612544059753418, 0.0599745512008667, -0.612544059753418, 0.9400254487991333, - -0.266332745552063, 0.9400254487991333, 0.10932832956314087, 0.9400254487991333, 0.10932832956314087, 0.0599745512008667, -0.266332745552063, - 0.0599745512008667, -0.266332745552063, 0.0599745512008667, -0.266332745552063, 0.9400254487991333, 0.10932832956314087, 0.9400254487991333, - 0.4999995231628418, 0.9400254487991333, 0.4999995231628418, 0.0599745512008667, 0.10932832956314087, 0.0599745512008667, 0.10932832956314087, - 0.0599745512008667, 0.10932832956314087, 0.9400254487991333, 0.4999995231628418, 0.9400254487991333 - ], - "normalized": false - } - } - } - } - ], - "materials": [ - { - "uuid": "769df3ee-4567-40b7-8da4-473fb149f350", - "type": "MeshBasicMaterial", - "color": 16777215, - "map": "3874a02e-6d61-4cbb-8379-9c1436361bb4", - "envMapRotation": [0, 0, 0, "XYZ"], - "reflectivity": 1, - "refractionRatio": 0.98, - "blending": 2, - "side": 2, - "transparent": true, - "blendColor": 0, - "depthWrite": false - }, - { - "uuid": "6d9283b7-81c2-4063-84cc-f696054ce6f6", - "type": "MeshBasicMaterial", - "color": 16777215, - "map": "93d77365-4fc6-43a7-b19a-e2fb18ab38d0", - "envMapRotation": [0, 0, 0, "XYZ"], - "reflectivity": 1, - "refractionRatio": 0.98, - "blending": 2, - "side": 2, - "transparent": true, - "blendColor": 0, - "depthWrite": false - }, - { - "uuid": "7442c205-fb42-4fb9-baec-82a192b81351", - "type": "MeshBasicMaterial", - "color": 16777215, - "map": "65e3d0e9-bf33-48b4-a8a1-5bc966d46e4e", - "envMapRotation": [0, 0, 0, "XYZ"], - "reflectivity": 1, - "refractionRatio": 0.98, - "blending": 2, - "side": 2, - "transparent": true, - "blendColor": 0, - "depthWrite": false - } - ], - "textures": [ - { - "uuid": "3874a02e-6d61-4cbb-8379-9c1436361bb4", - "name": "GroundGlowEmitter_texture", - "image": "396bc86c-4059-45f7-b34f-f6228436b397", - "mapping": 300, - "channel": 0, - "repeat": [1, 1], - "offset": [0, 0], - "center": [0, 0], - "rotation": 0, - "wrap": [1001, 1001], - "format": 1023, - "internalFormat": null, - "type": 1009, - "colorSpace": "", - "minFilter": 1008, - "magFilter": 1006, - "anisotropy": 1, - "flipY": true, - "generateMipmaps": true, - "premultiplyAlpha": false, - "unpackAlignment": 4 - }, - { - "uuid": "93d77365-4fc6-43a7-b19a-e2fb18ab38d0", - "name": "GlowCircleEmitter_texture", - "image": "55a3bc1f-853b-4d4a-bbe1-77abe1ccb390", - "mapping": 300, - "channel": 0, - "repeat": [1, 1], - "offset": [0, 0], - "center": [0, 0], - "rotation": 0, - "wrap": [1001, 1001], - "format": 1023, - "internalFormat": null, - "type": 1009, - "colorSpace": "", - "minFilter": 1008, - "magFilter": 1006, - "anisotropy": 1, - "flipY": true, - "generateMipmaps": true, - "premultiplyAlpha": false, - "unpackAlignment": 4 - }, - { - "uuid": "65e3d0e9-bf33-48b4-a8a1-5bc966d46e4e", - "name": "BasicZoneBlueEmitter_texture", - "image": "a44aaf69-213b-4f68-96fc-304a19e9cdae", - "mapping": 300, - "channel": 0, - "repeat": [1, 1], - "offset": [0, 0], - "center": [0, 0], - "rotation": 0, - "wrap": [1001, 1001], - "format": 1023, - "internalFormat": null, - "type": 1009, - "colorSpace": "", - "minFilter": 1008, - "magFilter": 1006, - "anisotropy": 1, - "flipY": true, - "generateMipmaps": true, - "premultiplyAlpha": false, - "unpackAlignment": 4 - } - ], - "images": [ - { - "uuid": "396bc86c-4059-45f7-b34f-f6228436b397", - "url": "data:image/jpeg;base64,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" - }, - { - "uuid": "55a3bc1f-853b-4d4a-bbe1-77abe1ccb390", - "url": "data:image/jpeg;base64,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" - }, - { - "uuid": "a44aaf69-213b-4f68-96fc-304a19e9cdae", - "url": "data:image/jpeg;base64,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" - } - ], - "object": { - "uuid": "da66c047-c0da-4a53-90dd-589c4e53e868", - "type": "Group", - "name": "MagicZoneGreen", - "visible": true, - "layers": 1, - "matrix": [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1], - "up": [0, 1, 0], - "children": [ - { - "uuid": "1921b779-fac1-42ec-b40a-a1af729bf6fa", - "type": "Group", - "name": "PortalDust", - "layers": 1, - "matrix": [ - -1.0000003044148624, 7.619931978698374e-8, 1.2979021821838232e-8, 0, -1.2979033855407715e-8, -1.8384778810981241e-7, -1.0000001522074553, 0, - -7.619930580271816e-8, -1.0000001522073967, 1.8384778922002538e-7, 0, 0, 0, 0, 1 - ], - "up": [0, 1, 0], - "children": [ - { - "uuid": "9ec4a1d9-3f3d-49a6-85fc-577cef6084a2", - "type": "ParticleEmitter", - "name": "PortalDustEmitter", - "layers": 1, - "matrix": [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1], - "up": [0, 1, 0], - "ps": { - "version": "3.0", - "autoDestroy": false, - "looping": true, - "prewarm": false, - "duration": 5, - "shape": { - "type": "cone", - "radius": 1.21, - "arc": 6.283185307179586, - "thickness": 0, - "angle": 0, - "mode": 0, - "spread": 0, - "speed": { "type": "ConstantValue", "value": 1 } - }, - "startLife": { "type": "IntervalValue", "a": 1, "b": 1.5 }, - "startSpeed": { "type": "ConstantValue", "value": -1 }, - "startRotation": { "type": "IntervalValue", "a": 0, "b": 6.283185 }, - "startSize": { "type": "IntervalValue", "a": 0.15, "b": 0.2 }, - "startColor": { "type": "ConstantColor", "color": { "r": 1, "g": 1, "b": 1, "a": 1 } }, - "emissionOverTime": { "type": "ConstantValue", "value": 25 }, - "emissionOverDistance": { "type": "ConstantValue", "value": 0 }, - "emissionBursts": [], - "onlyUsedByOther": false, - "instancingGeometry": "780917d8-bd1b-4d63-8aca-f79e3211f964", - "renderOrder": 0, - "renderMode": 0, - "rendererEmitterSettings": {}, - "material": "769df3ee-4567-40b7-8da4-473fb149f350", - "layers": 1, - "startTileIndex": { "type": "ConstantValue", "value": 0 }, - "uTileCount": 1, - "vTileCount": 1, - "blendTiles": false, - "softParticles": false, - "softFarFade": 0, - "softNearFade": 0, - "behaviors": [ - { - "type": "ForceOverLife", - "x": { "type": "ConstantValue", "value": 0 }, - "y": { "type": "ConstantValue", "value": 0 }, - "z": { "type": "ConstantValue", "value": 0 } - }, - { - "type": "SizeOverLife", - "size": { - "type": "PiecewiseBezier", - "functions": [ - { "function": { "p0": 0.8495575, "p1": 0.8495575, "p2": 1, "p3": 1 }, "start": 0 }, - { "function": { "p0": 1, "p1": 1, "p2": 0, "p3": 0 }, "start": 0.49871457 } - ] - } - }, - { "type": "RotationOverLife", "angularVelocity": { "type": "IntervalValue", "a": -3.1415925, "b": 3.1415925 } }, - { - "type": "ColorOverLife", - "color": { - "type": "Gradient", - "color": { - "type": "CLinearFunction", - "subType": "Color", - "keys": [ - { "value": { "r": 1, "g": 1, "b": 1 }, "pos": 0 }, - { "value": { "r": 0.59607846, "g": 1, "b": 0.050980393 }, "pos": 0.4587167162584878 }, - { "value": { "r": 0, "g": 1, "b": 0.047058824 }, "pos": 0.9518272678721293 } - ] - }, - "alpha": { - "type": "CLinearFunction", - "subType": "Number", - "keys": [ - { "value": 0, "pos": 0 }, - { "value": 1, "pos": 0.41690699626153965 }, - { "value": 1, "pos": 0.7580224307621881 }, - { "value": 0, "pos": 1 } - ] - } - } - } - ], - "worldSpace": true - } - } - ] - }, - { - "uuid": "cfe42db9-925f-4bd2-bc92-3d15a4e2b795", - "type": "Group", - "name": "GlowCircle", - "layers": 1, - "matrix": [1, 0, 0, 0, 0, -5.321248014494817e-8, -1.0000000532124802, 0, 0, 1.0000000532124802, -5.321248014494817e-8, 0, 0, 0, 0, 1], - "up": [0, 1, 0], - "children": [ - { - "uuid": "94cac5fe-52a9-431d-ba8a-19fe44a2cdc1", - "type": "ParticleEmitter", - "name": "GlowCircleEmitter", - "layers": 1, - "matrix": [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1], - "up": [0, 1, 0], - "ps": { - "version": "3.0", - "autoDestroy": false, - "looping": true, - "prewarm": false, - "duration": 2, - "shape": { - "type": "cone", - "radius": 0.01, - "arc": 6.283185307179586, - "thickness": 1, - "angle": 0.06981317007977318, - "mode": 0, - "spread": 0, - "speed": { "type": "ConstantValue", "value": 1 } - }, - "startLife": { "type": "ConstantValue", "value": 2 }, - "startSpeed": { "type": "ConstantValue", "value": 0 }, - "startRotation": { - "type": "Euler", - "angleX": { "type": "IntervalValue", "a": 0, "b": 0 }, - "angleY": { "type": "IntervalValue", "a": 0, "b": 0 }, - "angleZ": { "type": "IntervalValue", "a": 0, "b": 6.283185 }, - "eulerOrder": "XYZ" - }, - "startSize": { "type": "ConstantValue", "value": 4.1 }, - "startColor": { "type": "ConstantColor", "color": { "r": 0.45882353, "g": 1, "b": 0.28627452, "a": 0.4509804 } }, - "emissionOverTime": { "type": "ConstantValue", "value": 1 }, - "emissionOverDistance": { "type": "ConstantValue", "value": 0 }, - "emissionBursts": [], - "onlyUsedByOther": false, - "instancingGeometry": "f40b6ee0-aa01-46e0-b05a-d938b54eec83", - "renderOrder": 0, - "renderMode": 2, - "rendererEmitterSettings": {}, - "material": "6d9283b7-81c2-4063-84cc-f696054ce6f6", - "layers": 1, - "startTileIndex": { "type": "ConstantValue", "value": 0 }, - "uTileCount": 1, - "vTileCount": 1, - "blendTiles": false, - "softParticles": false, - "softFarFade": 0, - "softNearFade": 0, - "behaviors": [ - { - "type": "ForceOverLife", - "x": { "type": "ConstantValue", "value": 0 }, - "y": { "type": "ConstantValue", "value": 0 }, - "z": { "type": "ConstantValue", "value": 0 } - }, - { - "type": "ColorOverLife", - "color": { - "type": "Gradient", - "color": { - "type": "CLinearFunction", - "subType": "Color", - "keys": [ - { "value": { "r": 1, "g": 1, "b": 1 }, "pos": 0 }, - { "value": { "r": 1, "g": 1, "b": 1 }, "pos": 1 } - ] - }, - "alpha": { - "type": "CLinearFunction", - "subType": "Number", - "keys": [ - { "value": 0, "pos": 0 }, - { "value": 1, "pos": 0.5014572365911345 }, - { "value": 0, "pos": 1 } - ] - } - } - } - ], - "worldSpace": true - } - } - ] - }, - { - "uuid": "c86a5eb7-2571-4e87-b5fd-e68a0f965b0a", - "type": "ParticleEmitter", - "name": "MagicZoneGreenEmitter", - "layers": 1, - "matrix": [1, 0, 0, 0, 0, -2.220446049250313e-16, -1, 0, 0, 1, -2.220446049250313e-16, 0, 0, 0, 0, 1], - "up": [0, 1, 0], - "ps": { - "version": "3.0", - "autoDestroy": false, - "looping": true, - "prewarm": false, - "duration": 5, - "shape": { "type": "point" }, - "startLife": { "type": "ConstantValue", "value": 1 }, - "startSpeed": { "type": "ConstantValue", "value": 0 }, - "startRotation": { - "type": "Euler", - "angleX": { "type": "IntervalValue", "a": 1.5707963, "b": 1.5707963 }, - "angleY": { "type": "IntervalValue", "a": 0, "b": 6.283185 }, - "angleZ": { "type": "IntervalValue", "a": 0, "b": 0 }, - "eulerOrder": "XYZ" - }, - "startSize": { "type": "ConstantValue", "value": 2.8 }, - "startColor": { "type": "ConstantColor", "color": { "r": 1, "g": 1, "b": 1, "a": 1 } }, - "emissionOverTime": { "type": "ConstantValue", "value": 2.5 }, - "emissionOverDistance": { "type": "ConstantValue", "value": 0 }, - "emissionBursts": [], - "onlyUsedByOther": false, - "instancingGeometry": "780917d8-bd1b-4d63-8aca-f79e3211f964", - "renderOrder": 0, - "renderMode": 2, - "rendererEmitterSettings": {}, - "material": "7442c205-fb42-4fb9-baec-82a192b81351", - "layers": 1, - "startTileIndex": { "type": "ConstantValue", "value": 0 }, - "uTileCount": 1, - "vTileCount": 1, - "blendTiles": false, - "softParticles": false, - "softFarFade": 0, - "softNearFade": 0, - "behaviors": [ - { - "type": "ForceOverLife", - "x": { "type": "ConstantValue", "value": 0 }, - "y": { "type": "ConstantValue", "value": 0 }, - "z": { "type": "ConstantValue", "value": 0 } - }, - { - "type": "ColorOverLife", - "color": { - "type": "Gradient", - "color": { - "type": "CLinearFunction", - "subType": "Color", - "keys": [ - { "value": { "r": 0.6156863, "g": 1, "b": 0 }, "pos": 0 }, - { "value": { "r": 0.101960786, "g": 1, "b": 0.10980392 }, "pos": 1 } - ] - }, - "alpha": { - "type": "CLinearFunction", - "subType": "Number", - "keys": [ - { "value": 0, "pos": 0.004592965590905623 }, - { "value": 1, "pos": 0.5014572365911345 }, - { "value": 0, "pos": 1 } - ] - } - } - } - ], - "worldSpace": true - } - } - ] - } + "metadata": { "version": 4.6, "type": "Object", "generator": "Object3D.toJSON" }, + "geometries": [ + { "uuid": "780917d8-bd1b-4d63-8aca-f79e3211f964", "type": "PlaneGeometry", "name": "PlaneGeometry", "width": 1, "height": 1, "widthSegments": 1, "heightSegments": 1 }, + { + "uuid": "f40b6ee0-aa01-46e0-b05a-d938b54eec83", + "type": "BufferGeometry", + "name": "GlowCircleEmitter_geometry", + "data": { + "attributes": { + "position": { + "itemSize": 3, + "type": "Float32Array", + "array": [ + 0.41758671402931213, 0.08306316286325455, 0.10689251124858856, 0.42576777935028076, -1.2535783966427516e-8, 0.10689251124858856, 0.3199999928474426, 0, + 0, 0.3199999928474426, 0, 0, 0.3138512670993805, 0.062428902834653854, 0, 0.41758671402931213, 0.08306316286325455, 0.10689251124858856, + 0.39335811138153076, 0.16293425858020782, 0.10689251124858856, 0.41758671402931213, 0.08306316286325455, 0.10689251124858856, 0.3138512670993805, + 0.062428902834653854, 0, 0.3138512670993805, 0.062428902834653854, 0, 0.2956414520740509, 0.12245870381593704, 0, 0.39335811138153076, + 0.16293425858020782, 0.10689251124858856, 0.35401293635368347, 0.23654387891292572, 0.10689251124858856, 0.39335811138153076, 0.16293425858020782, + 0.10689251124858856, 0.2956414520740509, 0.12245870381593704, 0, 0.2956414520740509, 0.12245870381593704, 0, 0.26607027649879456, 0.17778247594833374, + 0, 0.35401293635368347, 0.23654387891292572, 0.10689251124858856, 0.3010632395744324, 0.30106326937675476, 0.10689251124858856, 0.35401293635368347, + 0.23654387891292572, 0.10689251124858856, 0.26607027649879456, 0.17778247594833374, 0, 0.26607027649879456, 0.17778247594833374, 0, 0.22627416253089905, + 0.22627416253089905, 0, 0.3010632395744324, 0.30106326937675476, 0.10689251124858856, 0.23654384911060333, 0.35401299595832825, 0.10689251124858856, + 0.3010632395744324, 0.30106326937675476, 0.10689251124858856, 0.22627416253089905, 0.22627416253089905, 0, 0.22627416253089905, 0.22627416253089905, 0, + 0.17778246104717255, 0.26607027649879456, 0, 0.23654384911060333, 0.35401299595832825, 0.10689251124858856, 0.16293422877788544, 0.39335811138153076, + 0.10689251124858856, 0.23654384911060333, 0.35401299595832825, 0.10689251124858856, 0.17778246104717255, 0.26607027649879456, 0, 0.17778246104717255, + 0.26607027649879456, 0, 0.12245869636535645, 0.2956414520740509, 0, 0.16293422877788544, 0.39335811138153076, 0.10689251124858856, 0.08306317031383514, + 0.41758671402931213, 0.10689251124858856, 0.16293422877788544, 0.39335811138153076, 0.10689251124858856, 0.12245869636535645, 0.2956414520740509, 0, + 0.12245869636535645, 0.2956414520740509, 0, 0.06242891401052475, 0.3138512670993805, 0, 0.08306317031383514, 0.41758671402931213, 0.10689251124858856, + 2.0868840877596995e-8, 0.42576777935028076, 0.10689251124858856, 0.08306317031383514, 0.41758671402931213, 0.10689251124858856, 0.06242891401052475, + 0.3138512670993805, 0, 0.06242891401052475, 0.3138512670993805, 0, 2.415932875976523e-8, 0.3199999928474426, 0, 2.0868840877596995e-8, + 0.42576777935028076, 0.10689251124858856, -0.08306313306093216, 0.4175867438316345, 0.10689251124858856, 2.0868840877596995e-8, 0.42576777935028076, + 0.10689251124858856, 2.415932875976523e-8, 0.3199999928474426, 0, 2.415932875976523e-8, 0.3199999928474426, 0, -0.06242886558175087, 0.3138512969017029, + 0, -0.08306313306093216, 0.4175867438316345, 0.10689251124858856, -0.16293422877788544, 0.39335814118385315, 0.10689251124858856, -0.08306313306093216, + 0.4175867438316345, 0.10689251124858856, -0.06242886558175087, 0.3138512969017029, 0, -0.06242886558175087, 0.3138512969017029, 0, -0.12245865166187286, + 0.2956414520740509, 0, -0.16293422877788544, 0.39335814118385315, 0.10689251124858856, -0.23654387891292572, 0.35401299595832825, 0.10689251124858856, + -0.16293422877788544, 0.39335814118385315, 0.10689251124858856, -0.12245865166187286, 0.2956414520740509, 0, -0.12245865166187286, 0.2956414520740509, + 0, -0.17778246104717255, 0.26607027649879456, 0, -0.23654387891292572, 0.35401299595832825, 0.10689251124858856, -0.30106329917907715, + 0.30106326937675476, 0.10689251124858856, -0.23654387891292572, 0.35401299595832825, 0.10689251124858856, -0.17778246104717255, 0.26607027649879456, 0, + -0.17778246104717255, 0.26607027649879456, 0, -0.22627416253089905, 0.22627416253089905, 0, -0.30106329917907715, 0.30106326937675476, + 0.10689251124858856, -0.35401299595832825, 0.23654386401176453, 0.10689251124858856, -0.30106329917907715, 0.30106326937675476, 0.10689251124858856, + -0.22627416253089905, 0.22627416253089905, 0, -0.22627416253089905, 0.22627416253089905, 0, -0.26607027649879456, 0.17778246104717255, 0, + -0.35401299595832825, 0.23654386401176453, 0.10689251124858856, -0.39335814118385315, 0.16293418407440186, 0.10689251124858856, -0.35401299595832825, + 0.23654386401176453, 0.10689251124858856, -0.26607027649879456, 0.17778246104717255, 0, -0.26607027649879456, 0.17778246104717255, 0, + -0.2956414818763733, 0.12245865166187286, 0, -0.39335814118385315, 0.16293418407440186, 0.10689251124858856, -0.4175867438316345, 0.08306305855512619, + 0.10689251124858856, -0.39335814118385315, 0.16293418407440186, 0.10689251124858856, -0.2956414818763733, 0.12245865166187286, 0, -0.2956414818763733, + 0.12245865166187286, 0, -0.3138512969017029, 0.062428828328847885, 0, -0.4175867438316345, 0.08306305855512619, 0.10689251124858856, + -0.42576777935028076, -1.5126852304092608e-7, 0.10689251124858856, -0.4175867438316345, 0.08306305855512619, 0.10689251124858856, -0.3138512969017029, + 0.062428828328847885, 0, -0.3138512969017029, 0.062428828328847885, 0, -0.3199999928474426, -1.0426924035300544e-7, 0, -0.42576777935028076, + -1.5126852304092608e-7, 0.10689251124858856, -0.41758671402931213, -0.08306335657835007, 0.10689251124858856, -0.42576777935028076, + -1.5126852304092608e-7, 0.10689251124858856, -0.3199999928474426, -1.0426924035300544e-7, 0, -0.3199999928474426, -1.0426924035300544e-7, 0, + -0.3138512670993805, -0.0624290332198143, 0, -0.41758671402931213, -0.08306335657835007, 0.10689251124858856, -0.393358051776886, -0.16293445229530334, + 0.10689251124858856, -0.41758671402931213, -0.08306335657835007, 0.10689251124858856, -0.3138512670993805, -0.0624290332198143, 0, -0.3138512670993805, + -0.0624290332198143, 0, -0.29564139246940613, -0.12245883792638779, 0, -0.393358051776886, -0.16293445229530334, 0.10689251124858856, + -0.35401278734207153, -0.23654408752918243, 0.10689251124858856, -0.393358051776886, -0.16293445229530334, 0.10689251124858856, -0.29564139246940613, + -0.12245883792638779, 0, -0.29564139246940613, -0.12245883792638779, 0, -0.2660701870918274, -0.17778262495994568, 0, -0.35401278734207153, + -0.23654408752918243, 0.10689251124858856, -0.30106309056282043, -0.3010634481906891, 0.10689251124858856, -0.35401278734207153, -0.23654408752918243, + 0.10689251124858856, -0.2660701870918274, -0.17778262495994568, 0, -0.2660701870918274, -0.17778262495994568, 0, -0.2262740284204483, + -0.226274311542511, 0, -0.30106309056282043, -0.3010634481906891, 0.10689251124858856, -0.2365436553955078, -0.3540131449699402, 0.10689251124858856, + -0.30106309056282043, -0.3010634481906891, 0.10689251124858856, -0.2262740284204483, -0.226274311542511, 0, -0.2262740284204483, -0.226274311542511, 0, + -0.17778228223323822, -0.2660703957080841, 0, -0.2365436553955078, -0.3540131449699402, 0.10689251124858856, -0.16293397545814514, -0.3933582603931427, + 0.10689251124858856, -0.2365436553955078, -0.3540131449699402, 0.10689251124858856, -0.17778228223323822, -0.2660703957080841, 0, -0.17778228223323822, + -0.2660703957080841, 0, -0.12245845794677734, -0.29564154148101807, 0, -0.16293397545814514, -0.3933582603931427, 0.10689251124858856, + -0.08306281268596649, -0.4175868332386017, 0.10689251124858856, -0.16293397545814514, -0.3933582603931427, 0.10689251124858856, -0.12245845794677734, + -0.29564154148101807, 0, -0.12245845794677734, -0.29564154148101807, 0, -0.06242862716317177, -0.31385132670402527, 0, -0.08306281268596649, + -0.4175868332386017, 0.10689251124858856, 3.9984527688829985e-7, -0.42576777935028076, 0.10689251124858856, -0.08306281268596649, -0.4175868332386017, + 0.10689251124858856, -0.06242862716317177, -0.31385132670402527, 0, -0.06242862716317177, -0.31385132670402527, 0, 3.0899172998033464e-7, + -0.3199999928474426, 0, 3.9984527688829985e-7, -0.42576777935028076, 0.10689251124858856, 0.08306359499692917, -0.41758668422698975, + 0.10689251124858856, 3.9984527688829985e-7, -0.42576777935028076, 0.10689251124858856, 3.0899172998033464e-7, -0.3199999928474426, 0, + 3.0899172998033464e-7, -0.3199999928474426, 0, 0.06242923438549042, -0.3138512372970581, 0, 0.08306359499692917, -0.41758668422698975, + 0.10689251124858856, 0.16293466091156006, -0.39335793256759644, 0.10689251124858856, 0.08306359499692917, -0.41758668422698975, 0.10689251124858856, + 0.06242923438549042, -0.3138512372970581, 0, 0.06242923438549042, -0.3138512372970581, 0, 0.1224590316414833, -0.29564130306243896, 0, + 0.16293466091156006, -0.39335793256759644, 0.10689251124858856, 0.23654431104660034, -0.3540126383304596, 0.10689251124858856, 0.16293466091156006, + -0.39335793256759644, 0.10689251124858856, 0.1224590316414833, -0.29564130306243896, 0, 0.1224590316414833, -0.29564130306243896, 0, 0.17778280377388, + -0.26607006788253784, 0, 0.23654431104660034, -0.3540126383304596, 0.10689251124858856, 0.3010636270046234, -0.3010628819465637, 0.10689251124858856, + 0.23654431104660034, -0.3540126383304596, 0.10689251124858856, 0.17778280377388, -0.26607006788253784, 0, 0.17778280377388, -0.26607006788253784, 0, + 0.22627444565296173, -0.22627387940883636, 0, 0.3010636270046234, -0.3010628819465637, 0.10689251124858856, 0.3540132939815521, -0.23654340207576752, + 0.10689251124858856, 0.3010636270046234, -0.3010628819465637, 0.10689251124858856, 0.22627444565296173, -0.22627387940883636, 0, 0.22627444565296173, + -0.22627387940883636, 0, 0.26607051491737366, -0.1777821183204651, 0, 0.3540132939815521, -0.23654340207576752, 0.10689251124858856, + 0.39335834980010986, -0.16293370723724365, 0.10689251124858856, 0.3540132939815521, -0.23654340207576752, 0.10689251124858856, 0.26607051491737366, + -0.1777821183204651, 0, 0.26607051491737366, -0.1777821183204651, 0, 0.29564163088798523, -0.12245826423168182, 0, 0.39335834980010986, + -0.16293370723724365, 0.10689251124858856, 0.4175868630409241, -0.083062544465065, 0.10689251124858856, 0.39335834980010986, -0.16293370723724365, + 0.10689251124858856, 0.29564163088798523, -0.12245826423168182, 0, 0.29564163088798523, -0.12245826423168182, 0, 0.31385138630867004, + -0.06242842227220535, 0, 0.4175868630409241, -0.083062544465065, 0.10689251124858856, 0.42576777935028076, -1.2535783966427516e-8, 0.10689251124858856, + 0.4175868630409241, -0.083062544465065, 0.10689251124858856, 0.31385138630867004, -0.06242842227220535, 0, 0.31385138630867004, -0.06242842227220535, 0, + 0.3199999928474426, 0, 0, 0.42576777935028076, -1.2535783966427516e-8, 0.10689251124858856, 0.4175865948200226, 0.08306316286325455, + 0.10689251124858856, 0.31385117769241333, 0.062428902834653854, 0, 0.3199998736381531, 0, 0, 0.3199998736381531, 0, 0, 0.4257676303386688, + -1.2535783966427516e-8, 0.10689251124858856, 0.4175865948200226, 0.08306316286325455, 0.10689251124858856, 0.3933579623699188, 0.16293425858020782, + 0.10689251124858856, 0.29564133286476135, 0.12245870381593704, 0, 0.31385117769241333, 0.062428902834653854, 0, 0.31385117769241333, + 0.062428902834653854, 0, 0.4175865948200226, 0.08306316286325455, 0.10689251124858856, 0.3933579623699188, 0.16293425858020782, 0.10689251124858856, + 0.35401275753974915, 0.23654387891292572, 0.10689251124858856, 0.2660701274871826, 0.17778247594833374, 0, 0.29564133286476135, 0.12245870381593704, 0, + 0.29564133286476135, 0.12245870381593704, 0, 0.3933579623699188, 0.16293425858020782, 0.10689251124858856, 0.35401275753974915, 0.23654387891292572, + 0.10689251124858856, 0.30106306076049805, 0.30106326937675476, 0.10689251124858856, 0.2262740284204483, 0.22627416253089905, 0, 0.2660701274871826, + 0.17778247594833374, 0, 0.2660701274871826, 0.17778247594833374, 0, 0.35401275753974915, 0.23654387891292572, 0.10689251124858856, 0.30106306076049805, + 0.30106326937675476, 0.10689251124858856, 0.2365437150001526, 0.35401299595832825, 0.10689251124858856, 0.177782341837883, 0.26607027649879456, 0, + 0.2262740284204483, 0.22627416253089905, 0, 0.2262740284204483, 0.22627416253089905, 0, 0.30106306076049805, 0.30106326937675476, 0.10689251124858856, + 0.2365437150001526, 0.35401299595832825, 0.10689251124858856, 0.1629340499639511, 0.39335811138153076, 0.10689251124858856, 0.1224585697054863, + 0.2956414520740509, 0, 0.177782341837883, 0.26607027649879456, 0, 0.177782341837883, 0.26607027649879456, 0, 0.2365437150001526, 0.35401299595832825, + 0.10689251124858856, 0.1629340499639511, 0.39335811138153076, 0.10689251124858856, 0.08306301385164261, 0.41758671402931213, 0.10689251124858856, + 0.0624287948012352, 0.3138512670993805, 0, 0.1224585697054863, 0.2956414520740509, 0, 0.1224585697054863, 0.2956414520740509, 0, 0.1629340499639511, + 0.39335811138153076, 0.10689251124858856, 0.08306301385164261, 0.41758671402931213, 0.10689251124858856, -1.3816443811265344e-7, 0.42576777935028076, + 0.10689251124858856, -9.536743306171047e-8, 0.3199999928474426, 0, 0.0624287948012352, 0.3138512670993805, 0, 0.0624287948012352, 0.3138512670993805, 0, + 0.08306301385164261, 0.41758671402931213, 0.10689251124858856, -1.3816443811265344e-7, 0.42576777935028076, 0.10689251124858856, -0.0830632895231247, + 0.4175867438316345, 0.10689251124858856, -0.06242898479104042, 0.3138512969017029, 0, -9.536743306171047e-8, 0.3199999928474426, 0, + -9.536743306171047e-8, 0.3199999928474426, 0, -1.3816443811265344e-7, 0.42576777935028076, 0.10689251124858856, -0.0830632895231247, 0.4175867438316345, + 0.10689251124858856, -0.16293437778949738, 0.39335814118385315, 0.10689251124858856, -0.12245876342058182, 0.2956414520740509, 0, -0.06242898479104042, + 0.3138512969017029, 0, -0.06242898479104042, 0.3138512969017029, 0, -0.0830632895231247, 0.4175867438316345, 0.10689251124858856, -0.16293437778949738, + 0.39335814118385315, 0.10689251124858856, -0.23654407262802124, 0.35401299595832825, 0.10689251124858856, -0.1777825951576233, 0.26607027649879456, 0, + -0.12245876342058182, 0.2956414520740509, 0, -0.12245876342058182, 0.2956414520740509, 0, -0.16293437778949738, 0.39335814118385315, + 0.10689251124858856, -0.23654407262802124, 0.35401299595832825, 0.10689251124858856, -0.3010634481906891, 0.30106326937675476, 0.10689251124858856, + -0.2262742966413498, 0.22627416253089905, 0, -0.1777825951576233, 0.26607027649879456, 0, -0.1777825951576233, 0.26607027649879456, 0, + -0.23654407262802124, 0.35401299595832825, 0.10689251124858856, -0.3010634481906891, 0.30106326937675476, 0.10689251124858856, -0.3540131449699402, + 0.23654386401176453, 0.10689251124858856, -0.2660703957080841, 0.17778246104717255, 0, -0.2262742966413498, 0.22627416253089905, 0, -0.2262742966413498, + 0.22627416253089905, 0, -0.3010634481906891, 0.30106326937675476, 0.10689251124858856, -0.3540131449699402, 0.23654386401176453, 0.10689251124858856, + -0.3933583199977875, 0.16293418407440186, 0.10689251124858856, -0.29564160108566284, 0.12245865166187286, 0, -0.2660703957080841, 0.17778246104717255, + 0, -0.2660703957080841, 0.17778246104717255, 0, -0.3540131449699402, 0.23654386401176453, 0.10689251124858856, -0.3933583199977875, 0.16293418407440186, + 0.10689251124858856, -0.41758692264556885, 0.08306305855512619, 0.10689251124858856, -0.3138514459133148, 0.062428828328847885, 0, -0.29564160108566284, + 0.12245865166187286, 0, -0.29564160108566284, 0.12245865166187286, 0, -0.3933583199977875, 0.16293418407440186, 0.10689251124858856, + -0.41758692264556885, 0.08306305855512619, 0.10689251124858856, -0.4257678985595703, -1.5126852304092608e-7, 0.10689251124858856, -0.3200001120567322, + -1.0426924035300544e-7, 0, -0.3138514459133148, 0.062428828328847885, 0, -0.3138514459133148, 0.062428828328847885, 0, -0.41758692264556885, + 0.08306305855512619, 0.10689251124858856, -0.4257678985595703, -1.5126852304092608e-7, 0.10689251124858856, -0.41758689284324646, -0.08306335657835007, + 0.10689251124858856, -0.31385138630867004, -0.0624290332198143, 0, -0.3200001120567322, -1.0426924035300544e-7, 0, -0.3200001120567322, + -1.0426924035300544e-7, 0, -0.4257678985595703, -1.5126852304092608e-7, 0.10689251124858856, -0.41758689284324646, -0.08306335657835007, + 0.10689251124858856, -0.3933582305908203, -0.16293445229530334, 0.10689251124858856, -0.2956415116786957, -0.12245883792638779, 0, -0.31385138630867004, + -0.0624290332198143, 0, -0.31385138630867004, -0.0624290332198143, 0, -0.41758689284324646, -0.08306335657835007, 0.10689251124858856, + -0.3933582305908203, -0.16293445229530334, 0.10689251124858856, -0.35401299595832825, -0.23654408752918243, 0.10689251124858856, -0.26607027649879456, + -0.17778262495994568, 0, -0.2956415116786957, -0.12245883792638779, 0, -0.2956415116786957, -0.12245883792638779, 0, -0.3933582305908203, + -0.16293445229530334, 0.10689251124858856, -0.35401299595832825, -0.23654408752918243, 0.10689251124858856, -0.3010632395744324, -0.3010634481906891, + 0.10689251124858856, -0.22627414762973785, -0.226274311542511, 0, -0.26607027649879456, -0.17778262495994568, 0, -0.26607027649879456, + -0.17778262495994568, 0, -0.35401299595832825, -0.23654408752918243, 0.10689251124858856, -0.3010632395744324, -0.3010634481906891, 0.10689251124858856, + -0.23654380440711975, -0.3540131449699402, 0.10689251124858856, -0.17778240144252777, -0.2660703957080841, 0, -0.22627414762973785, -0.226274311542511, + 0, -0.22627414762973785, -0.226274311542511, 0, -0.3010632395744324, -0.3010634481906891, 0.10689251124858856, -0.23654380440711975, + -0.3540131449699402, 0.10689251124858856, -0.16293412446975708, -0.3933582603931427, 0.10689251124858856, -0.1224585697054863, -0.29564154148101807, 0, + -0.17778240144252777, -0.2660703957080841, 0, -0.17778240144252777, -0.2660703957080841, 0, -0.23654380440711975, -0.3540131449699402, + 0.10689251124858856, -0.16293412446975708, -0.3933582603931427, 0.10689251124858856, -0.08306297659873962, -0.4175868332386017, 0.10689251124858856, + -0.06242874637246132, -0.31385132670402527, 0, -0.1224585697054863, -0.29564154148101807, 0, -0.1224585697054863, -0.29564154148101807, 0, + -0.16293412446975708, -0.3933582603931427, 0.10689251124858856, -0.08306297659873962, -0.4175868332386017, 0.10689251124858856, 2.4250164187833434e-7, + -0.42576777935028076, 0.10689251124858856, 1.9073486612342094e-7, -0.3199999928474426, 0, -0.06242874637246132, -0.31385132670402527, 0, + -0.06242874637246132, -0.31385132670402527, 0, -0.08306297659873962, -0.4175868332386017, 0.10689251124858856, 2.4250164187833434e-7, + -0.42576777935028076, 0.10689251124858856, 0.08306345343589783, -0.41758668422698975, 0.10689251124858856, 0.062429118901491165, -0.3138512372970581, 0, + 1.9073486612342094e-7, -0.3199999928474426, 0, 1.9073486612342094e-7, -0.3199999928474426, 0, 2.4250164187833434e-7, -0.42576777935028076, + 0.10689251124858856, 0.08306345343589783, -0.41758668422698975, 0.10689251124858856, 0.16293452680110931, -0.39335793256759644, 0.10689251124858856, + 0.12245891243219376, -0.29564130306243896, 0, 0.062429118901491165, -0.3138512372970581, 0, 0.062429118901491165, -0.3138512372970581, 0, + 0.08306345343589783, -0.41758668422698975, 0.10689251124858856, 0.16293452680110931, -0.39335793256759644, 0.10689251124858856, 0.2365441471338272, + -0.3540126383304596, 0.10689251124858856, 0.17778268456459045, -0.26607006788253784, 0, 0.12245891243219376, -0.29564130306243896, 0, + 0.12245891243219376, -0.29564130306243896, 0, 0.16293452680110931, -0.39335793256759644, 0.10689251124858856, 0.2365441471338272, -0.3540126383304596, + 0.10689251124858856, 0.3010634779930115, -0.3010628819465637, 0.10689251124858856, 0.22627434134483337, -0.22627387940883636, 0, 0.17778268456459045, + -0.26607006788253784, 0, 0.17778268456459045, -0.26607006788253784, 0, 0.2365441471338272, -0.3540126383304596, 0.10689251124858856, 0.3010634779930115, + -0.3010628819465637, 0.10689251124858856, 0.3540131449699402, -0.23654340207576752, 0.10689251124858856, 0.2660703957080841, -0.1777821183204651, 0, + 0.22627434134483337, -0.22627387940883636, 0, 0.22627434134483337, -0.22627387940883636, 0, 0.3010634779930115, -0.3010628819465637, + 0.10689251124858856, 0.3540131449699402, -0.23654340207576752, 0.10689251124858856, 0.3933582305908203, -0.16293370723724365, 0.10689251124858856, + 0.2956415116786957, -0.12245826423168182, 0, 0.2660703957080841, -0.1777821183204651, 0, 0.2660703957080841, -0.1777821183204651, 0, 0.3540131449699402, + -0.23654340207576752, 0.10689251124858856, 0.3933582305908203, -0.16293370723724365, 0.10689251124858856, 0.41758668422698975, -0.083062544465065, + 0.10689251124858856, 0.3138512372970581, -0.06242842227220535, 0, 0.2956415116786957, -0.12245826423168182, 0, 0.2956415116786957, -0.12245826423168182, + 0, 0.3933582305908203, -0.16293370723724365, 0.10689251124858856, 0.41758668422698975, -0.083062544465065, 0.10689251124858856, 0.4257676303386688, + -1.2535783966427516e-8, 0.10689251124858856, 0.3199998736381531, 0, 0, 0.3138512372970581, -0.06242842227220535, 0, 0.3138512372970581, + -0.06242842227220535, 0, 0.41758668422698975, -0.083062544465065, 0.10689251124858856, 0.4257676303386688, -1.2535783966427516e-8, 0.10689251124858856 + ], + "normalized": false + }, + "normal": { + "itemSize": 3, + "type": "Float32Array", + "array": [ + 0.6971781849861145, 0.1386774480342865, -0.7033570408821106, 0.7108367085456848, 3.257474361362256e-7, -0.7033571004867554, 0.71083664894104, + 3.6210147413839877e-7, -0.7033571600914001, 0.71083664894104, 3.6210147413839877e-7, -0.7033571600914001, 0.6971781849861145, 0.1386774480342865, + -0.7033570408821106, 0.6971781849861145, 0.1386774480342865, -0.7033570408821106, 0.6567274928092957, 0.27202534675598145, -0.7033570408821106, + 0.6971781849861145, 0.1386774480342865, -0.7033570408821106, 0.6971781849861145, 0.1386774480342865, -0.7033570408821106, 0.6971781849861145, + 0.1386774480342865, -0.7033570408821106, 0.6567275524139404, 0.27202534675598145, -0.7033570408821106, 0.6567274928092957, 0.27202534675598145, + -0.7033570408821106, 0.5910391211509705, 0.394919753074646, -0.7033570408821106, 0.6567274928092957, 0.27202534675598145, -0.7033570408821106, + 0.6567275524139404, 0.27202534675598145, -0.7033570408821106, 0.6567275524139404, 0.27202534675598145, -0.7033570408821106, 0.5910391211509705, + 0.3949197828769684, -0.7033570408821106, 0.5910391211509705, 0.394919753074646, -0.7033570408821106, 0.5026374459266663, 0.5026374459266663, + -0.7033571004867554, 0.5910391211509705, 0.394919753074646, -0.7033570408821106, 0.5910391211509705, 0.3949197828769684, -0.7033570408821106, + 0.5910391211509705, 0.3949197828769684, -0.7033570408821106, 0.5026374459266663, 0.5026374459266663, -0.7033570408821106, 0.5026374459266663, + 0.5026374459266663, -0.7033571004867554, 0.39491966366767883, 0.5910391807556152, -0.7033571004867554, 0.5026374459266663, 0.5026374459266663, + -0.7033571004867554, 0.5026374459266663, 0.5026374459266663, -0.7033570408821106, 0.5026374459266663, 0.5026374459266663, -0.7033570408821106, + 0.39491966366767883, 0.5910391211509705, -0.7033571004867554, 0.39491966366767883, 0.5910391807556152, -0.7033571004867554, 0.27202531695365906, + 0.6567276120185852, -0.7033570408821106, 0.39491966366767883, 0.5910391807556152, -0.7033571004867554, 0.39491966366767883, 0.5910391211509705, + -0.7033571004867554, 0.39491966366767883, 0.5910391211509705, -0.7033571004867554, 0.27202528715133667, 0.6567275524139404, -0.7033570408821106, + 0.27202531695365906, 0.6567276120185852, -0.7033570408821106, 0.13867750763893127, 0.6971781849861145, -0.7033570408821106, 0.27202531695365906, + 0.6567276120185852, -0.7033570408821106, 0.27202528715133667, 0.6567275524139404, -0.7033570408821106, 0.27202528715133667, 0.6567275524139404, + -0.7033570408821106, 0.13867750763893127, 0.6971781849861145, -0.7033570408821106, 0.13867750763893127, 0.6971781849861145, -0.7033570408821106, + 1.3676421417585516e-7, 0.71083664894104, -0.7033571004867554, 0.13867750763893127, 0.6971781849861145, -0.7033570408821106, 0.13867750763893127, + 0.6971781849861145, -0.7033570408821106, 0.13867750763893127, 0.6971781849861145, -0.7033570408821106, 1.525836097471256e-7, 0.7108367085456848, + -0.7033571004867554, 1.3676421417585516e-7, 0.71083664894104, -0.7033571004867554, -0.13867735862731934, 0.6971781253814697, -0.7033571004867554, + 1.3676421417585516e-7, 0.71083664894104, -0.7033571004867554, 1.525836097471256e-7, 0.7108367085456848, -0.7033571004867554, 1.525836097471256e-7, + 0.7108367085456848, -0.7033571004867554, -0.13867734372615814, 0.6971781253814697, -0.7033571004867554, -0.13867735862731934, 0.6971781253814697, + -0.7033571004867554, -0.2720252573490143, 0.6567274332046509, -0.7033571600914001, -0.13867735862731934, 0.6971781253814697, -0.7033571004867554, + -0.13867734372615814, 0.6971781253814697, -0.7033571004867554, -0.13867734372615814, 0.6971781253814697, -0.7033571004867554, -0.2720252573490143, + 0.6567274928092957, -0.7033571600914001, -0.2720252573490143, 0.6567274332046509, -0.7033571600914001, -0.39491966366767883, 0.5910390019416809, + -0.7033572196960449, -0.2720252573490143, 0.6567274332046509, -0.7033571600914001, -0.2720252573490143, 0.6567274928092957, -0.7033571600914001, + -0.2720252573490143, 0.6567274928092957, -0.7033571600914001, -0.39491963386535645, 0.5910390615463257, -0.7033571600914001, -0.39491966366767883, + 0.5910390019416809, -0.7033572196960449, -0.502637505531311, 0.5026373863220215, -0.7033571004867554, -0.39491966366767883, 0.5910390019416809, + -0.7033572196960449, -0.39491963386535645, 0.5910390615463257, -0.7033571600914001, -0.39491963386535645, 0.5910390615463257, -0.7033571600914001, + -0.5026374459266663, 0.5026373863220215, -0.7033571600914001, -0.502637505531311, 0.5026373863220215, -0.7033571004867554, -0.5910391211509705, + 0.39491963386535645, -0.7033571600914001, -0.502637505531311, 0.5026373863220215, -0.7033571004867554, -0.5026374459266663, 0.5026373863220215, + -0.7033571600914001, -0.5026374459266663, 0.5026373863220215, -0.7033571600914001, -0.5910391211509705, 0.39491963386535645, -0.7033571600914001, + -0.5910391211509705, 0.39491963386535645, -0.7033571600914001, -0.6567275524139404, 0.2720252275466919, -0.7033571004867554, -0.5910391211509705, + 0.39491963386535645, -0.7033571600914001, -0.5910391211509705, 0.39491963386535645, -0.7033571600914001, -0.5910391211509705, 0.39491963386535645, + -0.7033571600914001, -0.6567275524139404, 0.2720251977443695, -0.7033571004867554, -0.6567275524139404, 0.2720252275466919, -0.7033571004867554, + -0.6971781849861145, 0.1386772245168686, -0.7033570408821106, -0.6567275524139404, 0.2720252275466919, -0.7033571004867554, -0.6567275524139404, + 0.2720251977443695, -0.7033571004867554, -0.6567275524139404, 0.2720251977443695, -0.7033571004867554, -0.6971781849861145, 0.1386772245168686, + -0.7033570408821106, -0.6971781849861145, 0.1386772245168686, -0.7033570408821106, -0.71083664894104, -1.9644313908884214e-7, -0.7033571004867554, + -0.6971781849861145, 0.1386772245168686, -0.7033570408821106, -0.6971781849861145, 0.1386772245168686, -0.7033570408821106, -0.6971781849861145, + 0.1386772245168686, -0.7033570408821106, -0.71083664894104, -1.8446674232563964e-7, -0.7033571004867554, -0.71083664894104, -1.9644313908884214e-7, + -0.7033571004867554, -0.697178065776825, -0.13867776095867157, -0.7033571004867554, -0.71083664894104, -1.9644313908884214e-7, -0.7033571004867554, + -0.71083664894104, -1.8446674232563964e-7, -0.7033571004867554, -0.71083664894104, -1.8446674232563964e-7, -0.7033571004867554, -0.697178065776825, + -0.13867774605751038, -0.7033571004867554, -0.697178065776825, -0.13867776095867157, -0.7033571004867554, -0.6567273139953613, -0.27202582359313965, + -0.7033571004867554, -0.697178065776825, -0.13867776095867157, -0.7033571004867554, -0.697178065776825, -0.13867774605751038, -0.7033571004867554, + -0.697178065776825, -0.13867774605751038, -0.7033571004867554, -0.6567272543907166, -0.27202582359313965, -0.7033571004867554, -0.6567273139953613, + -0.27202582359313965, -0.7033571004867554, -0.5910389423370361, -0.3949200212955475, -0.7033571004867554, -0.6567273139953613, -0.27202582359313965, + -0.7033571004867554, -0.6567272543907166, -0.27202582359313965, -0.7033571004867554, -0.6567272543907166, -0.27202582359313965, -0.7033571004867554, + -0.5910389423370361, -0.3949199914932251, -0.7033570408821106, -0.5910389423370361, -0.3949200212955475, -0.7033571004867554, -0.5026372671127319, + -0.5026376843452454, -0.7033571004867554, -0.5910389423370361, -0.3949200212955475, -0.7033571004867554, -0.5910389423370361, -0.3949199914932251, + -0.7033570408821106, -0.5910389423370361, -0.3949199914932251, -0.7033570408821106, -0.5026372075080872, -0.5026376843452454, -0.7033571004867554, + -0.5026372671127319, -0.5026376843452454, -0.7033571004867554, -0.3949193060398102, -0.5910392999649048, -0.7033571600914001, -0.5026372671127319, + -0.5026376843452454, -0.7033571004867554, -0.5026372075080872, -0.5026376843452454, -0.7033571004867554, -0.5026372075080872, -0.5026376843452454, + -0.7033571004867554, -0.39491933584213257, -0.5910392999649048, -0.7033571600914001, -0.3949193060398102, -0.5910392999649048, -0.7033571600914001, + -0.27202484011650085, -0.6567276120185852, -0.7033571600914001, -0.3949193060398102, -0.5910392999649048, -0.7033571600914001, -0.39491933584213257, + -0.5910392999649048, -0.7033571600914001, -0.39491933584213257, -0.5910392999649048, -0.7033571600914001, -0.27202484011650085, -0.65672767162323, + -0.7033571600914001, -0.27202484011650085, -0.6567276120185852, -0.7033571600914001, -0.1386767476797104, -0.697178304195404, -0.7033571004867554, + -0.27202484011650085, -0.6567276120185852, -0.7033571600914001, -0.27202484011650085, -0.65672767162323, -0.7033571600914001, -0.27202484011650085, + -0.65672767162323, -0.7033571600914001, -0.1386767327785492, -0.6971782445907593, -0.7033571004867554, -0.1386767476797104, -0.697178304195404, + -0.7033571004867554, 6.514948722724512e-7, -0.7108367085456848, -0.7033571004867554, -0.1386767476797104, -0.697178304195404, -0.7033571004867554, + -0.1386767327785492, -0.6971782445907593, -0.7033571004867554, -0.1386767327785492, -0.6971782445907593, -0.7033571004867554, 6.536043883897946e-7, + -0.71083664894104, -0.7033571004867554, 6.514948722724512e-7, -0.7108367085456848, -0.7033571004867554, 0.1386781930923462, -0.6971779465675354, + -0.7033571004867554, 6.514948722724512e-7, -0.7108367085456848, -0.7033571004867554, 6.536043883897946e-7, -0.71083664894104, -0.7033571004867554, + 6.536043883897946e-7, -0.71083664894104, -0.7033571004867554, 0.1386781930923462, -0.6971779465675354, -0.7033571004867554, 0.1386781930923462, + -0.6971779465675354, -0.7033571004867554, 0.2720262110233307, -0.6567271947860718, -0.7033570408821106, 0.1386781930923462, -0.6971779465675354, + -0.7033571004867554, 0.1386781930923462, -0.6971779465675354, -0.7033571004867554, 0.1386781930923462, -0.6971779465675354, -0.7033571004867554, + 0.2720262408256531, -0.6567271947860718, -0.7033570408821106, 0.2720262110233307, -0.6567271947860718, -0.7033570408821106, 0.3949204683303833, + -0.591038703918457, -0.7033569812774658, 0.2720262110233307, -0.6567271947860718, -0.7033570408821106, 0.2720262408256531, -0.6567271947860718, + -0.7033570408821106, 0.2720262408256531, -0.6567271947860718, -0.7033570408821106, 0.3949204683303833, -0.591038703918457, -0.7033569812774658, + 0.3949204683303833, -0.591038703918457, -0.7033569812774658, 0.5026381015777588, -0.5026369094848633, -0.7033569812774658, 0.3949204683303833, + -0.591038703918457, -0.7033569812774658, 0.3949204683303833, -0.591038703918457, -0.7033569812774658, 0.3949204683303833, -0.591038703918457, + -0.7033569812774658, 0.502638041973114, -0.5026369094848633, -0.7033570408821106, 0.5026381015777588, -0.5026369094848633, -0.7033569812774658, + 0.5910396575927734, -0.39491894841194153, -0.7033571004867554, 0.5026381015777588, -0.5026369094848633, -0.7033569812774658, 0.502638041973114, + -0.5026369094848633, -0.7033570408821106, 0.502638041973114, -0.5026369094848633, -0.7033570408821106, 0.5910395979881287, -0.3949189782142639, + -0.7033571004867554, 0.5910396575927734, -0.39491894841194153, -0.7033571004867554, 0.6567279100418091, -0.2720244228839874, -0.7033571004867554, + 0.5910396575927734, -0.39491894841194153, -0.7033571004867554, 0.5910395979881287, -0.3949189782142639, -0.7033571004867554, 0.5910395979881287, + -0.3949189782142639, -0.7033571004867554, 0.6567278504371643, -0.27202436327934265, -0.7033571004867554, 0.6567279100418091, -0.2720244228839874, + -0.7033571004867554, 0.697178304195404, -0.13867662847042084, -0.7033571004867554, 0.6567279100418091, -0.2720244228839874, -0.7033571004867554, + 0.6567278504371643, -0.27202436327934265, -0.7033571004867554, 0.6567278504371643, -0.27202436327934265, -0.7033571004867554, 0.697178304195404, + -0.13867664337158203, -0.7033571004867554, 0.697178304195404, -0.13867662847042084, -0.7033571004867554, 0.7108367085456848, 3.257474361362256e-7, + -0.7033571004867554, 0.697178304195404, -0.13867662847042084, -0.7033571004867554, 0.697178304195404, -0.13867664337158203, -0.7033571004867554, + 0.697178304195404, -0.13867664337158203, -0.7033571004867554, 0.71083664894104, 3.6210147413839877e-7, -0.7033571600914001, 0.7108367085456848, + 3.257474361362256e-7, -0.7033571004867554, -0.6971782445907593, -0.1386774629354477, 0.7033569812774658, -0.6971781849861145, -0.1386774629354477, + 0.7033570408821106, -0.7108367085456848, -1.1159099955193597e-7, 0.7033570408821106, -0.7108367085456848, -1.1159099955193597e-7, 0.7033570408821106, + -0.7108367085456848, -9.200498851669181e-8, 0.7033570408821106, -0.6971782445907593, -0.1386774629354477, 0.7033569812774658, -0.6567275524139404, + -0.27202561497688293, 0.7033569812774658, -0.6567274928092957, -0.27202558517456055, 0.7033569812774658, -0.6971781849861145, -0.1386774629354477, + 0.7033570408821106, -0.6971781849861145, -0.1386774629354477, 0.7033570408821106, -0.6971782445907593, -0.1386774629354477, 0.7033569812774658, + -0.6567275524139404, -0.27202561497688293, 0.7033569812774658, -0.5910391807556152, -0.3949199616909027, 0.703356921672821, -0.5910391211509705, + -0.3949199616909027, 0.703356921672821, -0.6567274928092957, -0.27202558517456055, 0.7033569812774658, -0.6567274928092957, -0.27202558517456055, + 0.7033569812774658, -0.6567275524139404, -0.27202561497688293, 0.7033569812774658, -0.5910391807556152, -0.3949199616909027, 0.703356921672821, + -0.5026376247406006, -0.502637505531311, 0.703356921672821, -0.5026376247406006, -0.5026374459266663, 0.703356921672821, -0.5910391211509705, + -0.3949199616909027, 0.703356921672821, -0.5910391211509705, -0.3949199616909027, 0.703356921672821, -0.5910391807556152, -0.3949199616909027, + 0.703356921672821, -0.5026376247406006, -0.502637505531311, 0.703356921672821, -0.3949197232723236, -0.5910391807556152, 0.7033570408821106, + -0.394919753074646, -0.5910391807556152, 0.7033569812774658, -0.5026376247406006, -0.5026374459266663, 0.703356921672821, -0.5026376247406006, + -0.5026374459266663, 0.703356921672821, -0.5026376247406006, -0.502637505531311, 0.703356921672821, -0.3949197232723236, -0.5910391807556152, + 0.7033570408821106, -0.27202528715133667, -0.65672767162323, 0.7033569812774658, -0.27202528715133667, -0.65672767162323, 0.703356921672821, + -0.394919753074646, -0.5910391807556152, 0.7033569812774658, -0.394919753074646, -0.5910391807556152, 0.7033569812774658, -0.3949197232723236, + -0.5910391807556152, 0.7033570408821106, -0.27202528715133667, -0.65672767162323, 0.7033569812774658, -0.13867752254009247, -0.6971781849861145, + 0.7033569812774658, -0.13867753744125366, -0.6971782445907593, 0.7033569812774658, -0.27202528715133667, -0.65672767162323, 0.703356921672821, + -0.27202528715133667, -0.65672767162323, 0.703356921672821, -0.27202528715133667, -0.65672767162323, 0.7033569812774658, -0.13867752254009247, + -0.6971781849861145, 0.7033569812774658, -1.2184446518404002e-7, -0.7108367085456848, 0.7033571004867554, -1.594157055251344e-7, -0.71083664894104, + 0.7033571004867554, -0.13867753744125366, -0.6971782445907593, 0.7033569812774658, -0.13867753744125366, -0.6971782445907593, 0.7033569812774658, + -0.13867752254009247, -0.6971781849861145, 0.7033569812774658, -1.2184446518404002e-7, -0.7108367085456848, 0.7033571004867554, 0.13867734372615814, + -0.6971781253814697, 0.7033571600914001, 0.13867731392383575, -0.6971781253814697, 0.7033571004867554, -1.594157055251344e-7, -0.71083664894104, + 0.7033571004867554, -1.594157055251344e-7, -0.71083664894104, 0.7033571004867554, -1.2184446518404002e-7, -0.7108367085456848, 0.7033571004867554, + 0.13867734372615814, -0.6971781253814697, 0.7033571600914001, 0.2720251977443695, -0.6567274332046509, 0.7033571600914001, 0.27202513813972473, + -0.6567274928092957, 0.7033571600914001, 0.13867731392383575, -0.6971781253814697, 0.7033571004867554, 0.13867731392383575, -0.6971781253814697, + 0.7033571004867554, 0.13867734372615814, -0.6971781253814697, 0.7033571600914001, 0.2720251977443695, -0.6567274332046509, 0.7033571600914001, + 0.3949195146560669, -0.5910390615463257, 0.7033572793006897, 0.3949195146560669, -0.5910390615463257, 0.7033572793006897, 0.27202513813972473, + -0.6567274928092957, 0.7033571600914001, 0.27202513813972473, -0.6567274928092957, 0.7033571600914001, 0.2720251977443695, -0.6567274332046509, + 0.7033571600914001, 0.3949195146560669, -0.5910390615463257, 0.7033572793006897, 0.5026373863220215, -0.5026372671127319, 0.7033572196960449, + 0.5026374459266663, -0.5026372671127319, 0.7033572196960449, 0.3949195146560669, -0.5910390615463257, 0.7033572793006897, 0.3949195146560669, + -0.5910390615463257, 0.7033572793006897, 0.3949195146560669, -0.5910390615463257, 0.7033572793006897, 0.5026373863220215, -0.5026372671127319, + 0.7033572196960449, 0.5910391211509705, -0.3949195444583893, 0.7033572196960449, 0.5910390615463257, -0.39491957426071167, 0.7033572196960449, + 0.5026374459266663, -0.5026372671127319, 0.7033572196960449, 0.5026374459266663, -0.5026372671127319, 0.7033572196960449, 0.5026373863220215, + -0.5026372671127319, 0.7033572196960449, 0.5910391211509705, -0.3949195444583893, 0.7033572196960449, 0.6567274332046509, -0.27202528715133667, + 0.7033571600914001, 0.6567274928092957, -0.27202534675598145, 0.7033571600914001, 0.5910390615463257, -0.39491957426071167, 0.7033572196960449, + 0.5910390615463257, -0.39491957426071167, 0.7033572196960449, 0.5910391211509705, -0.3949195444583893, 0.7033572196960449, 0.6567274332046509, + -0.27202528715133667, 0.7033571600914001, 0.6971781253814697, -0.13867710530757904, 0.7033572196960449, 0.6971781253814697, -0.13867712020874023, + 0.7033572196960449, 0.6567274928092957, -0.27202534675598145, 0.7033571600914001, 0.6567274928092957, -0.27202534675598145, 0.7033571600914001, + 0.6567274332046509, -0.27202528715133667, 0.7033571600914001, 0.6971781253814697, -0.13867710530757904, 0.7033572196960449, 0.7108365893363953, + 1.9644303961285914e-7, 0.7033571600914001, 0.7108365893363953, 1.8674414548058849e-7, 0.7033572196960449, 0.6971781253814697, -0.13867712020874023, + 0.7033572196960449, 0.6971781253814697, -0.13867712020874023, 0.7033572196960449, 0.6971781253814697, -0.13867710530757904, 0.7033572196960449, + 0.7108365893363953, 1.9644303961285914e-7, 0.7033571600914001, 0.6971780061721802, 0.1386774778366089, 0.7033572793006897, 0.6971780061721802, + 0.13867749273777008, 0.7033572793006897, 0.7108365893363953, 1.8674414548058849e-7, 0.7033572196960449, 0.7108365893363953, 1.8674414548058849e-7, + 0.7033572196960449, 0.7108365893363953, 1.9644303961285914e-7, 0.7033571600914001, 0.6971780061721802, 0.1386774778366089, 0.7033572793006897, + 0.656727135181427, 0.2720257043838501, 0.7033573389053345, 0.6567271947860718, 0.2720257341861725, 0.7033572793006897, 0.6971780061721802, + 0.13867749273777008, 0.7033572793006897, 0.6971780061721802, 0.13867749273777008, 0.7033572793006897, 0.6971780061721802, 0.1386774778366089, + 0.7033572793006897, 0.656727135181427, 0.2720257043838501, 0.7033573389053345, 0.5910387635231018, 0.3949199616909027, 0.7033572196960449, + 0.5910387635231018, 0.3949199616909027, 0.7033572793006897, 0.6567271947860718, 0.2720257341861725, 0.7033572793006897, 0.6567271947860718, + 0.2720257341861725, 0.7033572793006897, 0.656727135181427, 0.2720257043838501, 0.7033573389053345, 0.5910387635231018, 0.3949199616909027, + 0.7033572196960449, 0.5026370286941528, 0.5026376247406006, 0.7033572793006897, 0.5026370882987976, 0.5026376843452454, 0.7033572196960449, + 0.5910387635231018, 0.3949199616909027, 0.7033572793006897, 0.5910387635231018, 0.3949199616909027, 0.7033572793006897, 0.5910387635231018, + 0.3949199616909027, 0.7033572196960449, 0.5026370286941528, 0.5026376247406006, 0.7033572793006897, 0.39491918683052063, 0.59103924036026, + 0.7033572196960449, 0.394919216632843, 0.5910392999649048, 0.7033572196960449, 0.5026370882987976, 0.5026376843452454, 0.7033572196960449, + 0.5026370882987976, 0.5026376843452454, 0.7033572196960449, 0.5026370286941528, 0.5026376247406006, 0.7033572793006897, 0.39491918683052063, + 0.59103924036026, 0.7033572196960449, 0.27202481031417847, 0.6567276120185852, 0.7033572196960449, 0.2720247805118561, 0.6567276120185852, + 0.7033572196960449, 0.394919216632843, 0.5910392999649048, 0.7033572196960449, 0.394919216632843, 0.5910392999649048, 0.7033572196960449, + 0.39491918683052063, 0.59103924036026, 0.7033572196960449, 0.27202481031417847, 0.6567276120185852, 0.7033572196960449, 0.1386767327785492, + 0.6971782445907593, 0.7033571600914001, 0.138676717877388, 0.6971781849861145, 0.7033571600914001, 0.2720247805118561, 0.6567276120185852, + 0.7033572196960449, 0.2720247805118561, 0.6567276120185852, 0.7033572196960449, 0.27202481031417847, 0.6567276120185852, 0.7033572196960449, + 0.1386767327785492, 0.6971782445907593, 0.7033571600914001, -6.365750095937983e-7, 0.7108365893363953, 0.7033571600914001, -6.627138304793334e-7, + 0.71083664894104, 0.7033571600914001, 0.138676717877388, 0.6971781849861145, 0.7033571600914001, 0.138676717877388, 0.6971781849861145, + 0.7033571600914001, 0.1386767327785492, 0.6971782445907593, 0.7033571600914001, -6.365750095937983e-7, 0.7108365893363953, 0.7033571600914001, + -0.138678178191185, 0.697178065776825, 0.7033570408821106, -0.13867820799350739, 0.6971779465675354, 0.7033571004867554, -6.627138304793334e-7, + 0.71083664894104, 0.7033571600914001, -6.627138304793334e-7, 0.71083664894104, 0.7033571600914001, -6.365750095937983e-7, 0.7108365893363953, + 0.7033571600914001, -0.138678178191185, 0.697178065776825, 0.7033570408821106, -0.27202627062797546, 0.6567271947860718, 0.7033569812774658, + -0.27202630043029785, 0.6567271947860718, 0.7033570408821106, -0.13867820799350739, 0.6971779465675354, 0.7033571004867554, -0.13867820799350739, + 0.6971779465675354, 0.7033571004867554, -0.138678178191185, 0.697178065776825, 0.7033570408821106, -0.27202627062797546, 0.6567271947860718, + 0.7033569812774658, -0.3949204385280609, 0.5910387635231018, 0.703356921672821, -0.3949204683303833, 0.591038703918457, 0.7033569812774658, + -0.27202630043029785, 0.6567271947860718, 0.7033570408821106, -0.27202630043029785, 0.6567271947860718, 0.7033570408821106, -0.27202627062797546, + 0.6567271947860718, 0.7033569812774658, -0.3949204385280609, 0.5910387635231018, 0.703356921672821, -0.5026381015777588, 0.5026369094848633, + 0.7033569812774658, -0.5026381015777588, 0.5026369094848633, 0.7033569812774658, -0.3949204683303833, 0.591038703918457, 0.7033569812774658, + -0.3949204683303833, 0.591038703918457, 0.7033569812774658, -0.3949204385280609, 0.5910387635231018, 0.703356921672821, -0.5026381015777588, + 0.5026369094848633, 0.7033569812774658, -0.5910396575927734, 0.3949190378189087, 0.7033569812774658, -0.5910396575927734, 0.3949190676212311, + 0.7033569812774658, -0.5026381015777588, 0.5026369094848633, 0.7033569812774658, -0.5026381015777588, 0.5026369094848633, 0.7033569812774658, + -0.5026381015777588, 0.5026369094848633, 0.7033569812774658, -0.5910396575927734, 0.3949190378189087, 0.7033569812774658, -0.6567279696464539, + 0.27202433347702026, 0.7033570408821106, -0.6567279696464539, 0.2720243036746979, 0.7033570408821106, -0.5910396575927734, 0.3949190676212311, + 0.7033569812774658, -0.5910396575927734, 0.3949190676212311, 0.7033569812774658, -0.5910396575927734, 0.3949190378189087, 0.7033569812774658, + -0.6567279696464539, 0.27202433347702026, 0.7033570408821106, -0.6971784234046936, 0.13867659866809845, 0.7033569812774658, -0.6971784234046936, + 0.13867662847042084, 0.703356921672821, -0.6567279696464539, 0.2720243036746979, 0.7033570408821106, -0.6567279696464539, 0.2720243036746979, + 0.7033570408821106, -0.6567279696464539, 0.27202433347702026, 0.7033570408821106, -0.6971784234046936, 0.13867659866809845, 0.7033569812774658, + -0.7108367085456848, -9.200498851669181e-8, 0.7033570408821106, -0.7108367085456848, -1.1159099955193597e-7, 0.7033570408821106, -0.6971784234046936, + 0.13867662847042084, 0.703356921672821, -0.6971784234046936, 0.13867662847042084, 0.703356921672821, -0.6971784234046936, 0.13867659866809845, + 0.7033569812774658, -0.7108367085456848, -9.200498851669181e-8, 0.7033570408821106 + ], + "normalized": false + }, + "uv": { + "itemSize": 2, + "type": "Float32Array", + "array": [ + 0.8906737565994263, 0.9400254487991333, 0.4999995231628418, 0.9400254487991333, 0.4999995231628418, 0.0599745512008667, 0.4999995231628418, + 0.0599745512008667, 0.8906737565994263, 0.0599745512008667, 0.8906737565994263, 0.9400254487991333, 1.2663346529006958, 0.9400254487991333, + 0.8906737565994263, 0.9400254487991333, 0.8906737565994263, 0.0599745512008667, 0.8906737565994263, 0.0599745512008667, 1.2663346529006958, + 0.0599745512008667, 1.2663346529006958, 0.9400254487991333, 1.6125456094741821, 0.9400254487991333, 1.2663346529006958, 0.9400254487991333, + 1.2663346529006958, 0.0599745512008667, 1.2663346529006958, 0.0599745512008667, 1.6125456094741821, 0.0599745512008667, 1.6125456094741821, + 0.9400254487991333, 1.9160020351409912, 0.9400254487991333, 1.6125456094741821, 0.9400254487991333, 1.6125456094741821, 0.0599745512008667, + 1.6125456094741821, 0.0599745512008667, 1.9160020351409912, 0.0599745512008667, 1.9160020351409912, 0.9400254487991333, -0.6125462055206299, + 0.9400254487991333, -0.9160027503967285, 0.9400254487991333, -0.9160027503967285, 0.0599745512008667, -0.9160027503967285, 0.0599745512008667, + -0.6125462055206299, 0.0599745512008667, -0.6125462055206299, 0.9400254487991333, -0.26633548736572266, 0.9400254487991333, -0.6125462055206299, + 0.9400254487991333, -0.6125462055206299, 0.0599745512008667, -0.6125462055206299, 0.0599745512008667, -0.26633548736572266, 0.0599745512008667, + -0.26633548736572266, 0.9400254487991333, 0.10932528972625732, 0.9400254487991333, -0.26633548736572266, 0.9400254487991333, -0.26633548736572266, + 0.0599745512008667, -0.26633548736572266, 0.0599745512008667, 0.10932528972625732, 0.0599745512008667, 0.10932528972625732, 0.9400254487991333, + 0.49999934434890747, 0.9400254487991333, 0.10932528972625732, 0.9400254487991333, 0.10932528972625732, 0.0599745512008667, 0.10932528972625732, + 0.0599745512008667, 0.49999934434890747, 0.0599745512008667, 0.49999934434890747, 0.9400254487991333, 0.8906735181808472, 0.9400254487991333, + 0.49999934434890747, 0.9400254487991333, 0.49999934434890747, 0.0599745512008667, 0.49999934434890747, 0.0599745512008667, 0.8906735181808472, + 0.0599745512008667, 0.8906735181808472, 0.9400254487991333, 1.2663342952728271, 0.9400254487991333, 0.8906735181808472, 0.9400254487991333, + 0.8906735181808472, 0.0599745512008667, 0.8906735181808472, 0.0599745512008667, 1.2663342952728271, 0.0599745512008667, 1.2663342952728271, + 0.9400254487991333, 1.6125454902648926, 0.9400254487991333, 1.2663342952728271, 0.9400254487991333, 1.2663342952728271, 0.0599745512008667, + 1.2663342952728271, 0.0599745512008667, 1.6125454902648926, 0.0599745512008667, 1.6125454902648926, 0.9400254487991333, 1.9160020351409912, + 0.9400254487991333, 1.6125454902648926, 0.9400254487991333, 1.6125454902648926, 0.0599745512008667, 1.6125454902648926, 0.0599745512008667, + 1.9160020351409912, 0.0599745512008667, 1.9160020351409912, 0.9400254487991333, -0.6125462055206299, 0.9400254487991333, -0.9160027503967285, + 0.9400254487991333, -0.9160027503967285, 0.0599745512008667, -0.9160027503967285, 0.0599745512008667, -0.6125462055206299, 0.0599745512008667, + -0.6125462055206299, 0.9400254487991333, -0.266335129737854, 0.9400254487991333, -0.6125462055206299, 0.9400254487991333, -0.6125462055206299, + 0.0599745512008667, -0.6125462055206299, 0.0599745512008667, -0.266335129737854, 0.0599745512008667, -0.266335129737854, 0.9400254487991333, + 0.10932576656341553, 0.9400254487991333, -0.266335129737854, 0.9400254487991333, -0.266335129737854, 0.0599745512008667, -0.266335129737854, + 0.0599745512008667, 0.10932576656341553, 0.0599745512008667, 0.10932576656341553, 0.9400254487991333, 0.5000001788139343, 0.9400254487991333, + 0.10932576656341553, 0.9400254487991333, 0.10932576656341553, 0.0599745512008667, 0.10932576656341553, 0.0599745512008667, 0.5000001788139343, + 0.0599745512008667, 0.5000001788139343, 0.9400254487991333, 0.8906745314598083, 0.9400254487991333, 0.5000001788139343, 0.9400254487991333, + 0.5000001788139343, 0.0599745512008667, 0.5000001788139343, 0.0599745512008667, 0.8906745314598083, 0.0599745512008667, 0.8906745314598083, + 0.9400254487991333, 1.2663354873657227, 0.9400254487991333, 0.8906745314598083, 0.9400254487991333, 0.8906745314598083, 0.0599745512008667, + 0.8906745314598083, 0.0599745512008667, 1.2663354873657227, 0.0599745512008667, 1.2663354873657227, 0.9400254487991333, 1.6125465631484985, + 0.9400254487991333, 1.2663354873657227, 0.9400254487991333, 1.2663354873657227, 0.0599745512008667, 1.2663354873657227, 0.0599745512008667, + 1.6125465631484985, 0.0599745512008667, 1.6125465631484985, 0.9400254487991333, 1.9160029888153076, 0.9400254487991333, 1.6125465631484985, + 0.9400254487991333, 1.6125465631484985, 0.0599745512008667, 1.6125465631484985, 0.0599745512008667, 1.9160029888153076, 0.0599745512008667, + 1.9160029888153076, 0.9400254487991333, -0.6125451326370239, 0.9400254487991333, -0.9160019159317017, 0.9400254487991333, -0.9160019159317017, + 0.0599745512008667, -0.9160019159317017, 0.0599745512008667, -0.6125451326370239, 0.0599745512008667, -0.6125451326370239, 0.9400254487991333, + -0.2663339376449585, 0.9400254487991333, -0.6125451326370239, 0.9400254487991333, -0.6125451326370239, 0.0599745512008667, -0.6125451326370239, + 0.0599745512008667, -0.2663339376449585, 0.0599745512008667, -0.2663339376449585, 0.9400254487991333, 0.1093270480632782, 0.9400254487991333, + -0.2663339376449585, 0.9400254487991333, -0.2663339376449585, 0.0599745512008667, -0.2663339376449585, 0.0599745512008667, 0.1093270480632782, + 0.0599745512008667, 0.1093270480632782, 0.9400254487991333, 0.5000014305114746, 0.9400254487991333, 0.1093270480632782, 0.9400254487991333, + 0.1093270480632782, 0.0599745512008667, 0.1093270480632782, 0.0599745512008667, 0.5000014305114746, 0.0599745512008667, 0.5000014305114746, + 0.9400254487991333, 0.8906757831573486, 0.9400254487991333, 0.5000014305114746, 0.9400254487991333, 0.5000014305114746, 0.0599745512008667, + 0.5000014305114746, 0.0599745512008667, 0.8906757831573486, 0.0599745512008667, 0.8906757831573486, 0.9400254487991333, 1.2663366794586182, + 0.9400254487991333, 0.8906757831573486, 0.9400254487991333, 0.8906757831573486, 0.0599745512008667, 0.8906757831573486, 0.0599745512008667, + 1.2663366794586182, 0.0599745512008667, 1.2663366794586182, 0.9400254487991333, 1.6125476360321045, 0.9400254487991333, 1.2663366794586182, + 0.9400254487991333, 1.2663366794586182, 0.0599745512008667, 1.2663366794586182, 0.0599745512008667, 1.6125476360321045, 0.0599745512008667, + 1.6125476360321045, 0.9400254487991333, 1.9160038232803345, 0.9400254487991333, 1.6125476360321045, 0.9400254487991333, 1.6125476360321045, + 0.0599745512008667, 1.6125476360321045, 0.0599745512008667, 1.9160038232803345, 0.0599745512008667, 1.9160038232803345, 0.9400254487991333, + -0.612544059753418, 0.9400254487991333, -0.9160009622573853, 0.9400254487991333, -0.9160009622573853, 0.0599745512008667, -0.9160009622573853, + 0.0599745512008667, -0.612544059753418, 0.0599745512008667, -0.612544059753418, 0.9400254487991333, -0.266332745552063, 0.9400254487991333, + -0.612544059753418, 0.9400254487991333, -0.612544059753418, 0.0599745512008667, -0.612544059753418, 0.0599745512008667, -0.266332745552063, + 0.0599745512008667, -0.266332745552063, 0.9400254487991333, 0.10932832956314087, 0.9400254487991333, -0.266332745552063, 0.9400254487991333, + -0.266332745552063, 0.0599745512008667, -0.266332745552063, 0.0599745512008667, 0.10932832956314087, 0.0599745512008667, 0.10932832956314087, + 0.9400254487991333, 0.4999995231628418, 0.9400254487991333, 0.10932832956314087, 0.9400254487991333, 0.10932832956314087, 0.0599745512008667, + 0.10932832956314087, 0.0599745512008667, 0.4999995231628418, 0.0599745512008667, 0.4999995231628418, 0.9400254487991333, 0.8906737565994263, + 0.9400254487991333, 0.8906737565994263, 0.0599745512008667, 0.4999995231628418, 0.0599745512008667, 0.4999995231628418, 0.0599745512008667, + 0.4999995231628418, 0.9400254487991333, 0.8906737565994263, 0.9400254487991333, 1.2663346529006958, 0.9400254487991333, 1.2663346529006958, + 0.0599745512008667, 0.8906737565994263, 0.0599745512008667, 0.8906737565994263, 0.0599745512008667, 0.8906737565994263, 0.9400254487991333, + 1.2663346529006958, 0.9400254487991333, 1.6125456094741821, 0.9400254487991333, 1.6125456094741821, 0.0599745512008667, 1.2663346529006958, + 0.0599745512008667, 1.2663346529006958, 0.0599745512008667, 1.2663346529006958, 0.9400254487991333, 1.6125456094741821, 0.9400254487991333, + 1.9160020351409912, 0.9400254487991333, 1.9160020351409912, 0.0599745512008667, 1.6125456094741821, 0.0599745512008667, 1.6125456094741821, + 0.0599745512008667, 1.6125456094741821, 0.9400254487991333, 1.9160020351409912, 0.9400254487991333, -0.6125462055206299, 0.9400254487991333, + -0.6125462055206299, 0.0599745512008667, -0.9160027503967285, 0.0599745512008667, -0.9160027503967285, 0.0599745512008667, -0.9160027503967285, + 0.9400254487991333, -0.6125462055206299, 0.9400254487991333, -0.26633548736572266, 0.9400254487991333, -0.26633548736572266, 0.0599745512008667, + -0.6125462055206299, 0.0599745512008667, -0.6125462055206299, 0.0599745512008667, -0.6125462055206299, 0.9400254487991333, -0.26633548736572266, + 0.9400254487991333, 0.10932528972625732, 0.9400254487991333, 0.10932528972625732, 0.0599745512008667, -0.26633548736572266, 0.0599745512008667, + -0.26633548736572266, 0.0599745512008667, -0.26633548736572266, 0.9400254487991333, 0.10932528972625732, 0.9400254487991333, 0.49999934434890747, + 0.9400254487991333, 0.49999934434890747, 0.0599745512008667, 0.10932528972625732, 0.0599745512008667, 0.10932528972625732, 0.0599745512008667, + 0.10932528972625732, 0.9400254487991333, 0.49999934434890747, 0.9400254487991333, 0.8906735181808472, 0.9400254487991333, 0.8906735181808472, + 0.0599745512008667, 0.49999934434890747, 0.0599745512008667, 0.49999934434890747, 0.0599745512008667, 0.49999934434890747, 0.9400254487991333, + 0.8906735181808472, 0.9400254487991333, 1.2663342952728271, 0.9400254487991333, 1.2663342952728271, 0.0599745512008667, 0.8906735181808472, + 0.0599745512008667, 0.8906735181808472, 0.0599745512008667, 0.8906735181808472, 0.9400254487991333, 1.2663342952728271, 0.9400254487991333, + 1.6125454902648926, 0.9400254487991333, 1.6125454902648926, 0.0599745512008667, 1.2663342952728271, 0.0599745512008667, 1.2663342952728271, + 0.0599745512008667, 1.2663342952728271, 0.9400254487991333, 1.6125454902648926, 0.9400254487991333, 1.9160020351409912, 0.9400254487991333, + 1.9160020351409912, 0.0599745512008667, 1.6125454902648926, 0.0599745512008667, 1.6125454902648926, 0.0599745512008667, 1.6125454902648926, + 0.9400254487991333, 1.9160020351409912, 0.9400254487991333, -0.6125462055206299, 0.9400254487991333, -0.6125462055206299, 0.0599745512008667, + -0.9160027503967285, 0.0599745512008667, -0.9160027503967285, 0.0599745512008667, -0.9160027503967285, 0.9400254487991333, -0.6125462055206299, + 0.9400254487991333, -0.266335129737854, 0.9400254487991333, -0.266335129737854, 0.0599745512008667, -0.6125462055206299, 0.0599745512008667, + -0.6125462055206299, 0.0599745512008667, -0.6125462055206299, 0.9400254487991333, -0.266335129737854, 0.9400254487991333, 0.10932576656341553, + 0.9400254487991333, 0.10932576656341553, 0.0599745512008667, -0.266335129737854, 0.0599745512008667, -0.266335129737854, 0.0599745512008667, + -0.266335129737854, 0.9400254487991333, 0.10932576656341553, 0.9400254487991333, 0.5000001788139343, 0.9400254487991333, 0.5000001788139343, + 0.0599745512008667, 0.10932576656341553, 0.0599745512008667, 0.10932576656341553, 0.0599745512008667, 0.10932576656341553, 0.9400254487991333, + 0.5000001788139343, 0.9400254487991333, 0.8906745314598083, 0.9400254487991333, 0.8906745314598083, 0.0599745512008667, 0.5000001788139343, + 0.0599745512008667, 0.5000001788139343, 0.0599745512008667, 0.5000001788139343, 0.9400254487991333, 0.8906745314598083, 0.9400254487991333, + 1.2663354873657227, 0.9400254487991333, 1.2663354873657227, 0.0599745512008667, 0.8906745314598083, 0.0599745512008667, 0.8906745314598083, + 0.0599745512008667, 0.8906745314598083, 0.9400254487991333, 1.2663354873657227, 0.9400254487991333, 1.6125465631484985, 0.9400254487991333, + 1.6125465631484985, 0.0599745512008667, 1.2663354873657227, 0.0599745512008667, 1.2663354873657227, 0.0599745512008667, 1.2663354873657227, + 0.9400254487991333, 1.6125465631484985, 0.9400254487991333, 1.9160029888153076, 0.9400254487991333, 1.9160029888153076, 0.0599745512008667, + 1.6125465631484985, 0.0599745512008667, 1.6125465631484985, 0.0599745512008667, 1.6125465631484985, 0.9400254487991333, 1.9160029888153076, + 0.9400254487991333, -0.6125451326370239, 0.9400254487991333, -0.6125451326370239, 0.0599745512008667, -0.9160019159317017, 0.0599745512008667, + -0.9160019159317017, 0.0599745512008667, -0.9160019159317017, 0.9400254487991333, -0.6125451326370239, 0.9400254487991333, -0.2663339376449585, + 0.9400254487991333, -0.2663339376449585, 0.0599745512008667, -0.6125451326370239, 0.0599745512008667, -0.6125451326370239, 0.0599745512008667, + -0.6125451326370239, 0.9400254487991333, -0.2663339376449585, 0.9400254487991333, 0.1093270480632782, 0.9400254487991333, 0.1093270480632782, + 0.0599745512008667, -0.2663339376449585, 0.0599745512008667, -0.2663339376449585, 0.0599745512008667, -0.2663339376449585, 0.9400254487991333, + 0.1093270480632782, 0.9400254487991333, 0.5000014305114746, 0.9400254487991333, 0.5000014305114746, 0.0599745512008667, 0.1093270480632782, + 0.0599745512008667, 0.1093270480632782, 0.0599745512008667, 0.1093270480632782, 0.9400254487991333, 0.5000014305114746, 0.9400254487991333, + 0.8906757831573486, 0.9400254487991333, 0.8906757831573486, 0.0599745512008667, 0.5000014305114746, 0.0599745512008667, 0.5000014305114746, + 0.0599745512008667, 0.5000014305114746, 0.9400254487991333, 0.8906757831573486, 0.9400254487991333, 1.2663366794586182, 0.9400254487991333, + 1.2663366794586182, 0.0599745512008667, 0.8906757831573486, 0.0599745512008667, 0.8906757831573486, 0.0599745512008667, 0.8906757831573486, + 0.9400254487991333, 1.2663366794586182, 0.9400254487991333, 1.6125476360321045, 0.9400254487991333, 1.6125476360321045, 0.0599745512008667, + 1.2663366794586182, 0.0599745512008667, 1.2663366794586182, 0.0599745512008667, 1.2663366794586182, 0.9400254487991333, 1.6125476360321045, + 0.9400254487991333, 1.9160038232803345, 0.9400254487991333, 1.9160038232803345, 0.0599745512008667, 1.6125476360321045, 0.0599745512008667, + 1.6125476360321045, 0.0599745512008667, 1.6125476360321045, 0.9400254487991333, 1.9160038232803345, 0.9400254487991333, -0.612544059753418, + 0.9400254487991333, -0.612544059753418, 0.0599745512008667, -0.9160009622573853, 0.0599745512008667, -0.9160009622573853, 0.0599745512008667, + -0.9160009622573853, 0.9400254487991333, -0.612544059753418, 0.9400254487991333, -0.266332745552063, 0.9400254487991333, -0.266332745552063, + 0.0599745512008667, -0.612544059753418, 0.0599745512008667, -0.612544059753418, 0.0599745512008667, -0.612544059753418, 0.9400254487991333, + -0.266332745552063, 0.9400254487991333, 0.10932832956314087, 0.9400254487991333, 0.10932832956314087, 0.0599745512008667, -0.266332745552063, + 0.0599745512008667, -0.266332745552063, 0.0599745512008667, -0.266332745552063, 0.9400254487991333, 0.10932832956314087, 0.9400254487991333, + 0.4999995231628418, 0.9400254487991333, 0.4999995231628418, 0.0599745512008667, 0.10932832956314087, 0.0599745512008667, 0.10932832956314087, + 0.0599745512008667, 0.10932832956314087, 0.9400254487991333, 0.4999995231628418, 0.9400254487991333 + ], + "normalized": false + } + } + } + } + ], + "materials": [ + { + "uuid": "769df3ee-4567-40b7-8da4-473fb149f350", + "type": "MeshBasicMaterial", + "color": 16777215, + "map": "3874a02e-6d61-4cbb-8379-9c1436361bb4", + "envMapRotation": [0, 0, 0, "XYZ"], + "reflectivity": 1, + "refractionRatio": 0.98, + "blending": 2, + "side": 2, + "transparent": true, + "blendColor": 0, + "depthWrite": false + }, + { + "uuid": "6d9283b7-81c2-4063-84cc-f696054ce6f6", + "type": "MeshBasicMaterial", + "color": 16777215, + "map": "93d77365-4fc6-43a7-b19a-e2fb18ab38d0", + "envMapRotation": [0, 0, 0, "XYZ"], + "reflectivity": 1, + "refractionRatio": 0.98, + "blending": 2, + "side": 2, + "transparent": true, + "blendColor": 0, + "depthWrite": false + }, + { + "uuid": "7442c205-fb42-4fb9-baec-82a192b81351", + "type": "MeshBasicMaterial", + "color": 16777215, + "map": "65e3d0e9-bf33-48b4-a8a1-5bc966d46e4e", + "envMapRotation": [0, 0, 0, "XYZ"], + "reflectivity": 1, + "refractionRatio": 0.98, + "blending": 2, + "side": 2, + "transparent": true, + "blendColor": 0, + "depthWrite": false + } + ], + "textures": [ + { + "uuid": "3874a02e-6d61-4cbb-8379-9c1436361bb4", + "name": "GroundGlowEmitter_texture", + "image": "396bc86c-4059-45f7-b34f-f6228436b397", + "mapping": 300, + "channel": 0, + "repeat": [1, 1], + "offset": [0, 0], + "center": [0, 0], + "rotation": 0, + "wrap": [1001, 1001], + "format": 1023, + "internalFormat": null, + "type": 1009, + "colorSpace": "", + "minFilter": 1008, + "magFilter": 1006, + "anisotropy": 1, + "flipY": true, + "generateMipmaps": true, + "premultiplyAlpha": false, + "unpackAlignment": 4 + }, + { + "uuid": "93d77365-4fc6-43a7-b19a-e2fb18ab38d0", + "name": "GlowCircleEmitter_texture", + "image": "55a3bc1f-853b-4d4a-bbe1-77abe1ccb390", + "mapping": 300, + "channel": 0, + "repeat": [1, 1], + "offset": [0, 0], + "center": [0, 0], + "rotation": 0, + "wrap": [1001, 1001], + "format": 1023, + "internalFormat": null, + "type": 1009, + "colorSpace": "", + "minFilter": 1008, + "magFilter": 1006, + "anisotropy": 1, + "flipY": true, + "generateMipmaps": true, + "premultiplyAlpha": false, + "unpackAlignment": 4 + }, + { + "uuid": "65e3d0e9-bf33-48b4-a8a1-5bc966d46e4e", + "name": "BasicZoneBlueEmitter_texture", + "image": "a44aaf69-213b-4f68-96fc-304a19e9cdae", + "mapping": 300, + "channel": 0, + "repeat": [1, 1], + "offset": [0, 0], + "center": [0, 0], + "rotation": 0, + "wrap": [1001, 1001], + "format": 1023, + "internalFormat": null, + "type": 1009, + "colorSpace": "", + "minFilter": 1008, + "magFilter": 1006, + "anisotropy": 1, + "flipY": true, + "generateMipmaps": true, + "premultiplyAlpha": false, + "unpackAlignment": 4 + } + ], + "images": [ + { + "uuid": "396bc86c-4059-45f7-b34f-f6228436b397", + "url": "data:image/jpeg;base64,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" + }, + { + "uuid": "55a3bc1f-853b-4d4a-bbe1-77abe1ccb390", + "url": "data:image/jpeg;base64,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" + }, + { + "uuid": "a44aaf69-213b-4f68-96fc-304a19e9cdae", + "url": "data:image/jpeg;base64,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" + } + ], + "object": { + "uuid": "da66c047-c0da-4a53-90dd-589c4e53e868", + "type": "Group", + "name": "MagicZoneGreen", + "visible": true, + "layers": 1, + "matrix": [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1], + "up": [0, 1, 0], + "children": [ + { + "uuid": "1921b779-fac1-42ec-b40a-a1af729bf6fa", + "type": "Group", + "name": "PortalDust", + "layers": 1, + "matrix": [ + -1.0000003044148624, 7.619931978698374e-8, 1.2979021821838232e-8, 0, -1.2979033855407715e-8, -1.8384778810981241e-7, -1.0000001522074553, 0, + -7.619930580271816e-8, -1.0000001522073967, 1.8384778922002538e-7, 0, 0, 0, 0, 1 + ], + "up": [0, 1, 0], + "children": [ + { + "uuid": "9ec4a1d9-3f3d-49a6-85fc-577cef6084a2", + "type": "ParticleEmitter", + "name": "PortalDustEmitter", + "layers": 1, + "matrix": [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1], + "up": [0, 1, 0], + "ps": { + "version": "3.0", + "autoDestroy": false, + "looping": true, + "prewarm": false, + "duration": 5, + "shape": { + "type": "cone", + "radius": 1.21, + "arc": 6.283185307179586, + "thickness": 0, + "angle": 0, + "mode": 0, + "spread": 0, + "speed": { "type": "ConstantValue", "value": 1 } + }, + "startLife": { "type": "IntervalValue", "a": 1, "b": 1.5 }, + "startSpeed": { "type": "ConstantValue", "value": -1 }, + "startRotation": { "type": "IntervalValue", "a": 0, "b": 6.283185 }, + "startSize": { "type": "IntervalValue", "a": 0.15, "b": 0.2 }, + "startColor": { "type": "ConstantColor", "color": { "r": 1, "g": 1, "b": 1, "a": 1 } }, + "emissionOverTime": { "type": "ConstantValue", "value": 25 }, + "emissionOverDistance": { "type": "ConstantValue", "value": 0 }, + "emissionBursts": [], + "onlyUsedByOther": false, + "instancingGeometry": "780917d8-bd1b-4d63-8aca-f79e3211f964", + "renderOrder": 0, + "renderMode": 0, + "rendererEmitterSettings": {}, + "material": "769df3ee-4567-40b7-8da4-473fb149f350", + "layers": 1, + "startTileIndex": { "type": "ConstantValue", "value": 0 }, + "uTileCount": 1, + "vTileCount": 1, + "blendTiles": false, + "softParticles": false, + "softFarFade": 0, + "softNearFade": 0, + "behaviors": [ + { + "type": "ForceOverLife", + "x": { "type": "ConstantValue", "value": 0 }, + "y": { "type": "ConstantValue", "value": 0 }, + "z": { "type": "ConstantValue", "value": 0 } + }, + { + "type": "SizeOverLife", + "size": { + "type": "PiecewiseBezier", + "functions": [ + { "function": { "p0": 0.8495575, "p1": 0.8495575, "p2": 1, "p3": 1 }, "start": 0 }, + { "function": { "p0": 1, "p1": 1, "p2": 0, "p3": 0 }, "start": 0.49871457 } + ] + } + }, + { "type": "RotationOverLife", "angularVelocity": { "type": "IntervalValue", "a": -3.1415925, "b": 3.1415925 } }, + { + "type": "ColorOverLife", + "color": { + "type": "Gradient", + "color": { + "type": "CLinearFunction", + "subType": "Color", + "keys": [ + { "value": { "r": 1, "g": 1, "b": 1 }, "pos": 0 }, + { "value": { "r": 0.59607846, "g": 1, "b": 0.050980393 }, "pos": 0.4587167162584878 }, + { "value": { "r": 0, "g": 1, "b": 0.047058824 }, "pos": 0.9518272678721293 } + ] + }, + "alpha": { + "type": "CLinearFunction", + "subType": "Number", + "keys": [ + { "value": 0, "pos": 0 }, + { "value": 1, "pos": 0.41690699626153965 }, + { "value": 1, "pos": 0.7580224307621881 }, + { "value": 0, "pos": 1 } + ] + } + } + } + ], + "worldSpace": true + } + } + ] + }, + { + "uuid": "cfe42db9-925f-4bd2-bc92-3d15a4e2b795", + "type": "Group", + "name": "GlowCircle", + "layers": 1, + "matrix": [1, 0, 0, 0, 0, -5.321248014494817e-8, -1.0000000532124802, 0, 0, 1.0000000532124802, -5.321248014494817e-8, 0, 0, 0, 0, 1], + "up": [0, 1, 0], + "children": [ + { + "uuid": "94cac5fe-52a9-431d-ba8a-19fe44a2cdc1", + "type": "ParticleEmitter", + "name": "GlowCircleEmitter", + "layers": 1, + "matrix": [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1], + "up": [0, 1, 0], + "ps": { + "version": "3.0", + "autoDestroy": false, + "looping": true, + "prewarm": false, + "duration": 2, + "shape": { + "type": "cone", + "radius": 0.01, + "arc": 6.283185307179586, + "thickness": 1, + "angle": 0.06981317007977318, + "mode": 0, + "spread": 0, + "speed": { "type": "ConstantValue", "value": 1 } + }, + "startLife": { "type": "ConstantValue", "value": 2 }, + "startSpeed": { "type": "ConstantValue", "value": 0 }, + "startRotation": { + "type": "Euler", + "angleX": { "type": "IntervalValue", "a": 0, "b": 0 }, + "angleY": { "type": "IntervalValue", "a": 0, "b": 0 }, + "angleZ": { "type": "IntervalValue", "a": 0, "b": 6.283185 }, + "eulerOrder": "XYZ" + }, + "startSize": { "type": "ConstantValue", "value": 4.1 }, + "startColor": { "type": "ConstantColor", "color": { "r": 0.45882353, "g": 1, "b": 0.28627452, "a": 0.4509804 } }, + "emissionOverTime": { "type": "ConstantValue", "value": 1 }, + "emissionOverDistance": { "type": "ConstantValue", "value": 0 }, + "emissionBursts": [], + "onlyUsedByOther": false, + "instancingGeometry": "f40b6ee0-aa01-46e0-b05a-d938b54eec83", + "renderOrder": 0, + "renderMode": 2, + "rendererEmitterSettings": {}, + "material": "6d9283b7-81c2-4063-84cc-f696054ce6f6", + "layers": 1, + "startTileIndex": { "type": "ConstantValue", "value": 0 }, + "uTileCount": 1, + "vTileCount": 1, + "blendTiles": false, + "softParticles": false, + "softFarFade": 0, + "softNearFade": 0, + "behaviors": [ + { + "type": "ForceOverLife", + "x": { "type": "ConstantValue", "value": 0 }, + "y": { "type": "ConstantValue", "value": 0 }, + "z": { "type": "ConstantValue", "value": 0 } + }, + { + "type": "ColorOverLife", + "color": { + "type": "Gradient", + "color": { + "type": "CLinearFunction", + "subType": "Color", + "keys": [ + { "value": { "r": 1, "g": 1, "b": 1 }, "pos": 0 }, + { "value": { "r": 1, "g": 1, "b": 1 }, "pos": 1 } + ] + }, + "alpha": { + "type": "CLinearFunction", + "subType": "Number", + "keys": [ + { "value": 0, "pos": 0 }, + { "value": 1, "pos": 0.5014572365911345 }, + { "value": 0, "pos": 1 } + ] + } + } + } + ], + "worldSpace": true + } + } + ] + }, + { + "uuid": "c86a5eb7-2571-4e87-b5fd-e68a0f965b0a", + "type": "ParticleEmitter", + "name": "MagicZoneGreenEmitter", + "layers": 1, + "matrix": [1, 0, 0, 0, 0, -2.220446049250313e-16, -1, 0, 0, 1, -2.220446049250313e-16, 0, 0, 0, 0, 1], + "up": [0, 1, 0], + "ps": { + "version": "3.0", + "autoDestroy": false, + "looping": true, + "prewarm": false, + "duration": 5, + "shape": { "type": "point" }, + "startLife": { "type": "ConstantValue", "value": 1 }, + "startSpeed": { "type": "ConstantValue", "value": 0 }, + "startRotation": { + "type": "Euler", + "angleX": { "type": "IntervalValue", "a": 1.5707963, "b": 1.5707963 }, + "angleY": { "type": "IntervalValue", "a": 0, "b": 6.283185 }, + "angleZ": { "type": "IntervalValue", "a": 0, "b": 0 }, + "eulerOrder": "XYZ" + }, + "startSize": { "type": "ConstantValue", "value": 2.8 }, + "startColor": { "type": "ConstantColor", "color": { "r": 1, "g": 1, "b": 1, "a": 1 } }, + "emissionOverTime": { "type": "ConstantValue", "value": 2.5 }, + "emissionOverDistance": { "type": "ConstantValue", "value": 0 }, + "emissionBursts": [], + "onlyUsedByOther": false, + "instancingGeometry": "780917d8-bd1b-4d63-8aca-f79e3211f964", + "renderOrder": 0, + "renderMode": 2, + "rendererEmitterSettings": {}, + "material": "7442c205-fb42-4fb9-baec-82a192b81351", + "layers": 1, + "startTileIndex": { "type": "ConstantValue", "value": 0 }, + "uTileCount": 1, + "vTileCount": 1, + "blendTiles": false, + "softParticles": false, + "softFarFade": 0, + "softNearFade": 0, + "behaviors": [ + { + "type": "ForceOverLife", + "x": { "type": "ConstantValue", "value": 0 }, + "y": { "type": "ConstantValue", "value": 0 }, + "z": { "type": "ConstantValue", "value": 0 } + }, + { + "type": "ColorOverLife", + "color": { + "type": "Gradient", + "color": { + "type": "CLinearFunction", + "subType": "Color", + "keys": [ + { "value": { "r": 0.6156863, "g": 1, "b": 0 }, "pos": 0 }, + { "value": { "r": 0.101960786, "g": 1, "b": 0.10980392 }, "pos": 1 } + ] + }, + "alpha": { + "type": "CLinearFunction", + "subType": "Number", + "keys": [ + { "value": 0, "pos": 0.004592965590905623 }, + { "value": 1, "pos": 0.5014572365911345 }, + { "value": 0, "pos": 1 } + ] + } + } + } + ], + "worldSpace": true + } + } + ] + } } diff --git a/editor/src/editor/windows/fx-editor/VFX/types/VFXBehaviorFunction.ts b/editor/src/editor/windows/fx-editor/VFX/types/VFXBehaviorFunction.ts index a894a2bb8..add9957bc 100644 --- a/editor/src/editor/windows/fx-editor/VFX/types/VFXBehaviorFunction.ts +++ b/editor/src/editor/windows/fx-editor/VFX/types/VFXBehaviorFunction.ts @@ -7,12 +7,12 @@ import type { VFXValueParser } from "../parsers/VFXValueParser"; * Context for per-particle behavior functions */ export interface VFXPerParticleContext { - lifeRatio: number; - startSpeed: number; - startSize: number; - startColor: { r: number; g: number; b: number; a: number }; - updateSpeed: number; - valueParser: VFXValueParser; + lifeRatio: number; + startSpeed: number; + startSize: number; + startColor: { r: number; g: number; b: number; a: number }; + updateSpeed: number; + valueParser: VFXValueParser; } /** @@ -29,4 +29,3 @@ export type VFXPerSolidParticleBehaviorFunction = (particle: SolidParticle, cont * System-level behavior function (applied once during initialization) */ export type VFXSystemBehaviorFunction = (particleSystem: ParticleSystem, valueParser: VFXValueParser) => void; - diff --git a/editor/src/editor/windows/fx-editor/VFX/types/behaviors.ts b/editor/src/editor/windows/fx-editor/VFX/types/behaviors.ts index 227d08b90..23c589a98 100644 --- a/editor/src/editor/windows/fx-editor/VFX/types/behaviors.ts +++ b/editor/src/editor/windows/fx-editor/VFX/types/behaviors.ts @@ -5,134 +5,133 @@ import type { VFXGradientKey } from "./gradients"; * VFX behavior types (converted from Quarks) */ export interface VFXColorOverLifeBehavior { - type: "ColorOverLife"; - color?: { - color?: { - keys: VFXGradientKey[]; - }; - alpha?: { - keys: VFXGradientKey[]; - }; - keys?: VFXGradientKey[]; - }; + type: "ColorOverLife"; + color?: { + color?: { + keys: VFXGradientKey[]; + }; + alpha?: { + keys: VFXGradientKey[]; + }; + keys?: VFXGradientKey[]; + }; } export interface VFXSizeOverLifeBehavior { - type: "SizeOverLife"; - size?: { - keys?: VFXGradientKey[]; - functions?: Array<{ - start: number; - function: { - p0?: number; - p3?: number; - }; - }>; - }; + type: "SizeOverLife"; + size?: { + keys?: VFXGradientKey[]; + functions?: Array<{ + start: number; + function: { + p0?: number; + p3?: number; + }; + }>; + }; } export interface VFXRotationOverLifeBehavior { - type: "RotationOverLife" | "Rotation3DOverLife"; - angularVelocity?: VFXValue; + type: "RotationOverLife" | "Rotation3DOverLife"; + angularVelocity?: VFXValue; } export interface VFXForceOverLifeBehavior { - type: "ForceOverLife" | "ApplyForce"; - force?: { - x?: VFXValue; - y?: VFXValue; - z?: VFXValue; - }; - x?: VFXValue; - y?: VFXValue; - z?: VFXValue; + type: "ForceOverLife" | "ApplyForce"; + force?: { + x?: VFXValue; + y?: VFXValue; + z?: VFXValue; + }; + x?: VFXValue; + y?: VFXValue; + z?: VFXValue; } export interface VFXGravityForceBehavior { - type: "GravityForce"; - gravity?: VFXValue; + type: "GravityForce"; + gravity?: VFXValue; } export interface VFXSpeedOverLifeBehavior { - type: "SpeedOverLife"; - speed?: - | { - keys?: VFXGradientKey[]; - functions?: Array<{ - start: number; - function: { - p0?: number; - p3?: number; - }; - }>; - } - | VFXValue; + type: "SpeedOverLife"; + speed?: + | { + keys?: VFXGradientKey[]; + functions?: Array<{ + start: number; + function: { + p0?: number; + p3?: number; + }; + }>; + } + | VFXValue; } export interface VFXFrameOverLifeBehavior { - type: "FrameOverLife"; - frame?: - | { - keys?: VFXGradientKey[]; - } - | VFXValue; + type: "FrameOverLife"; + frame?: + | { + keys?: VFXGradientKey[]; + } + | VFXValue; } export interface VFXLimitSpeedOverLifeBehavior { - type: "LimitSpeedOverLife"; - maxSpeed?: VFXValue; - speed?: VFXValue | { keys?: VFXGradientKey[] }; - dampen?: VFXValue; + type: "LimitSpeedOverLife"; + maxSpeed?: VFXValue; + speed?: VFXValue | { keys?: VFXGradientKey[] }; + dampen?: VFXValue; } export interface VFXColorBySpeedBehavior { - type: "ColorBySpeed"; - color?: { - keys: VFXGradientKey[]; - }; - minSpeed?: VFXValue; - maxSpeed?: VFXValue; + type: "ColorBySpeed"; + color?: { + keys: VFXGradientKey[]; + }; + minSpeed?: VFXValue; + maxSpeed?: VFXValue; } export interface VFXSizeBySpeedBehavior { - type: "SizeBySpeed"; - size?: { - keys: VFXGradientKey[]; - }; - minSpeed?: VFXValue; - maxSpeed?: VFXValue; + type: "SizeBySpeed"; + size?: { + keys: VFXGradientKey[]; + }; + minSpeed?: VFXValue; + maxSpeed?: VFXValue; } export interface VFXRotationBySpeedBehavior { - type: "RotationBySpeed"; - angularVelocity?: VFXValue; - minSpeed?: VFXValue; - maxSpeed?: VFXValue; + type: "RotationBySpeed"; + angularVelocity?: VFXValue; + minSpeed?: VFXValue; + maxSpeed?: VFXValue; } export interface VFXOrbitOverLifeBehavior { - type: "OrbitOverLife"; - center?: { - x?: number; - y?: number; - z?: number; - }; - radius?: VFXValue | { keys?: VFXGradientKey[] }; - speed?: VFXValue; + type: "OrbitOverLife"; + center?: { + x?: number; + y?: number; + z?: number; + }; + radius?: VFXValue | { keys?: VFXGradientKey[] }; + speed?: VFXValue; } export type VFXBehavior = - | VFXColorOverLifeBehavior - | VFXSizeOverLifeBehavior - | VFXRotationOverLifeBehavior - | VFXForceOverLifeBehavior - | VFXGravityForceBehavior - | VFXSpeedOverLifeBehavior - | VFXFrameOverLifeBehavior - | VFXLimitSpeedOverLifeBehavior - | VFXColorBySpeedBehavior - | VFXSizeBySpeedBehavior - | VFXRotationBySpeedBehavior - | VFXOrbitOverLifeBehavior - | { type: string; [key: string]: unknown }; // Fallback for unknown behaviors - + | VFXColorOverLifeBehavior + | VFXSizeOverLifeBehavior + | VFXRotationOverLifeBehavior + | VFXForceOverLifeBehavior + | VFXGravityForceBehavior + | VFXSpeedOverLifeBehavior + | VFXFrameOverLifeBehavior + | VFXLimitSpeedOverLifeBehavior + | VFXColorBySpeedBehavior + | VFXSizeBySpeedBehavior + | VFXRotationBySpeedBehavior + | VFXOrbitOverLifeBehavior + | { type: string; [key: string]: unknown }; // Fallback for unknown behaviors diff --git a/editor/src/editor/windows/fx-editor/VFX/types/colors.ts b/editor/src/editor/windows/fx-editor/VFX/types/colors.ts index 4ff7aae20..0e15e144e 100644 --- a/editor/src/editor/windows/fx-editor/VFX/types/colors.ts +++ b/editor/src/editor/windows/fx-editor/VFX/types/colors.ts @@ -2,9 +2,8 @@ * VFX color types (converted from Quarks) */ export interface VFXConstantColor { - type: "ConstantColor"; - value: [number, number, number, number]; // RGBA + type: "ConstantColor"; + value: [number, number, number, number]; // RGBA } export type VFXColor = VFXConstantColor | [number, number, number, number] | string; - diff --git a/editor/src/editor/windows/fx-editor/VFX/types/context.ts b/editor/src/editor/windows/fx-editor/VFX/types/context.ts index 3d6a04fde..a8cf2842a 100644 --- a/editor/src/editor/windows/fx-editor/VFX/types/context.ts +++ b/editor/src/editor/windows/fx-editor/VFX/types/context.ts @@ -8,10 +8,10 @@ import type { VFXLoaderOptions } from "./loader"; * Context for VFX parsing operations */ export interface VFXParseContext { - scene: Scene; - rootUrl: string; - jsonData: QuarksVFXJSON; - options: VFXLoaderOptions; - groupNodesMap: Map; - vfxData?: VFXHierarchy; + scene: Scene; + rootUrl: string; + jsonData: QuarksVFXJSON; + options: VFXLoaderOptions; + groupNodesMap: Map; + vfxData?: VFXHierarchy; } diff --git a/editor/src/editor/windows/fx-editor/VFX/types/emitter.ts b/editor/src/editor/windows/fx-editor/VFX/types/emitter.ts index 8c6c913da..3633ae91a 100644 --- a/editor/src/editor/windows/fx-editor/VFX/types/emitter.ts +++ b/editor/src/editor/windows/fx-editor/VFX/types/emitter.ts @@ -8,13 +8,12 @@ import type { VFXEmitter } from "./hierarchy"; * Data structure for emitter creation */ export interface VFXEmitterData { - name: string; - config: VFXParticleEmitterConfig; - materialId?: string; - matrix?: number[]; - position?: number[]; - parentGroup: Nullable; - cumulativeScale: Vector3; - vfxEmitter?: VFXEmitter; + name: string; + config: VFXParticleEmitterConfig; + materialId?: string; + matrix?: number[]; + position?: number[]; + parentGroup: Nullable; + cumulativeScale: Vector3; + vfxEmitter?: VFXEmitter; } - diff --git a/editor/src/editor/windows/fx-editor/VFX/types/emitterConfig.ts b/editor/src/editor/windows/fx-editor/VFX/types/emitterConfig.ts index 67609db8e..a6ad882db 100644 --- a/editor/src/editor/windows/fx-editor/VFX/types/emitterConfig.ts +++ b/editor/src/editor/windows/fx-editor/VFX/types/emitterConfig.ts @@ -8,43 +8,42 @@ import type { VFXBehavior } from "./behaviors"; * VFX emission burst (converted from Quarks) */ export interface VFXEmissionBurst { - time: VFXValue; - count: VFXValue; + time: VFXValue; + count: VFXValue; } /** * VFX particle emitter configuration (converted from Quarks) */ export interface VFXParticleEmitterConfig { - version?: string; - autoDestroy?: boolean; - looping?: boolean; - prewarm?: boolean; - duration?: number; - shape?: VFXShape; - startLife?: VFXValue; - startSpeed?: VFXValue; - startRotation?: VFXRotation; - startSize?: VFXValue; - startColor?: VFXColor; - emissionOverTime?: VFXValue; - emissionOverDistance?: VFXValue; - emissionBursts?: VFXEmissionBurst[]; - onlyUsedByOther?: boolean; - instancingGeometry?: string; - renderOrder?: number; - renderMode?: number; - rendererEmitterSettings?: Record; - material?: string; - layers?: number; - startTileIndex?: VFXValue; - uTileCount?: number; - vTileCount?: number; - blendTiles?: boolean; - softParticles?: boolean; - softFarFade?: number; - softNearFade?: number; - behaviors?: VFXBehavior[]; - worldSpace?: boolean; + version?: string; + autoDestroy?: boolean; + looping?: boolean; + prewarm?: boolean; + duration?: number; + shape?: VFXShape; + startLife?: VFXValue; + startSpeed?: VFXValue; + startRotation?: VFXRotation; + startSize?: VFXValue; + startColor?: VFXColor; + emissionOverTime?: VFXValue; + emissionOverDistance?: VFXValue; + emissionBursts?: VFXEmissionBurst[]; + onlyUsedByOther?: boolean; + instancingGeometry?: string; + renderOrder?: number; + renderMode?: number; + rendererEmitterSettings?: Record; + material?: string; + layers?: number; + startTileIndex?: VFXValue; + uTileCount?: number; + vTileCount?: number; + blendTiles?: boolean; + softParticles?: boolean; + softFarFade?: number; + softNearFade?: number; + behaviors?: VFXBehavior[]; + worldSpace?: boolean; } - diff --git a/editor/src/editor/windows/fx-editor/VFX/types/factories.ts b/editor/src/editor/windows/fx-editor/VFX/types/factories.ts index 99af4a4cb..8e79338ad 100644 --- a/editor/src/editor/windows/fx-editor/VFX/types/factories.ts +++ b/editor/src/editor/windows/fx-editor/VFX/types/factories.ts @@ -15,23 +15,22 @@ import type { VFXEmitterData } from "./emitter"; * Factory interfaces for dependency injection */ export interface IVFXMaterialFactory { - createMaterial(materialId: string, name: string): Nullable; - createTexture(materialId: string): Nullable; + createMaterial(materialId: string, name: string): Nullable; + createTexture(materialId: string): Nullable; } export interface IVFXGeometryFactory { - createMesh(geometryId: string, materialId: string | undefined, name: string): Nullable; + createMesh(geometryId: string, materialId: string | undefined, name: string): Nullable; } export interface IVFXEmitterFactory { - createEmitter(emitterData: VFXEmitterData): Nullable; + createEmitter(emitterData: VFXEmitterData): Nullable; } export interface IVFXValueParser { - parseConstantValue(value: VFXValue): number; - parseIntervalValue(value: VFXValue): { min: number; max: number }; - parseConstantColor(value: VFXColor): Color4; - parseGradientColorKeys(keys: VFXGradientKey[]): ColorGradient[]; - parseGradientAlphaKeys(keys: VFXGradientKey[]): { gradient: number; factor: number }[]; + parseConstantValue(value: VFXValue): number; + parseIntervalValue(value: VFXValue): { min: number; max: number }; + parseConstantColor(value: VFXColor): Color4; + parseGradientColorKeys(keys: VFXGradientKey[]): ColorGradient[]; + parseGradientAlphaKeys(keys: VFXGradientKey[]): { gradient: number; factor: number }[]; } - diff --git a/editor/src/editor/windows/fx-editor/VFX/types/gradients.ts b/editor/src/editor/windows/fx-editor/VFX/types/gradients.ts index 77066d797..cb12d80d5 100644 --- a/editor/src/editor/windows/fx-editor/VFX/types/gradients.ts +++ b/editor/src/editor/windows/fx-editor/VFX/types/gradients.ts @@ -2,8 +2,7 @@ * VFX gradient key (converted from Quarks) */ export interface VFXGradientKey { - time?: number; - value: number | [number, number, number, number] | { r: number; g: number; b: number; a?: number }; - pos?: number; + time?: number; + value: number | [number, number, number, number] | { r: number; g: number; b: number; a?: number }; + pos?: number; } - diff --git a/editor/src/editor/windows/fx-editor/VFX/types/hierarchy.ts b/editor/src/editor/windows/fx-editor/VFX/types/hierarchy.ts index 8618ecd53..61d2646e9 100644 --- a/editor/src/editor/windows/fx-editor/VFX/types/hierarchy.ts +++ b/editor/src/editor/windows/fx-editor/VFX/types/hierarchy.ts @@ -5,39 +5,38 @@ import type { VFXParticleEmitterConfig } from "./emitterConfig"; * VFX transform (converted from Quarks, left-handed coordinate system) */ export interface VFXTransform { - position: Vector3; - rotation: Quaternion; - scale: Vector3; + position: Vector3; + rotation: Quaternion; + scale: Vector3; } /** * VFX group (converted from Quarks) */ export interface VFXGroup { - uuid: string; - name: string; - transform: VFXTransform; - children: (VFXGroup | VFXEmitter)[]; + uuid: string; + name: string; + transform: VFXTransform; + children: (VFXGroup | VFXEmitter)[]; } /** * VFX emitter (converted from Quarks) */ export interface VFXEmitter { - uuid: string; - name: string; - transform: VFXTransform; - config: VFXParticleEmitterConfig; - materialId?: string; - parentUuid?: string; + uuid: string; + name: string; + transform: VFXTransform; + config: VFXParticleEmitterConfig; + materialId?: string; + parentUuid?: string; } /** * VFX hierarchy (converted from Quarks) */ export interface VFXHierarchy { - root: VFXGroup | VFXEmitter | null; - groups: Map; - emitters: Map; + root: VFXGroup | VFXEmitter | null; + groups: Map; + emitters: Map; } - diff --git a/editor/src/editor/windows/fx-editor/VFX/types/index.ts b/editor/src/editor/windows/fx-editor/VFX/types/index.ts index 26f12a8cc..5048e59cd 100644 --- a/editor/src/editor/windows/fx-editor/VFX/types/index.ts +++ b/editor/src/editor/windows/fx-editor/VFX/types/index.ts @@ -22,19 +22,19 @@ export type { VFXEulerRotation, VFXRotation } from "./rotations"; export type { VFXGradientKey } from "./gradients"; export type { VFXShape } from "./shapes"; export type { - VFXColorOverLifeBehavior, - VFXSizeOverLifeBehavior, - VFXRotationOverLifeBehavior, - VFXForceOverLifeBehavior, - VFXGravityForceBehavior, - VFXSpeedOverLifeBehavior, - VFXFrameOverLifeBehavior, - VFXLimitSpeedOverLifeBehavior, - VFXColorBySpeedBehavior, - VFXSizeBySpeedBehavior, - VFXRotationBySpeedBehavior, - VFXOrbitOverLifeBehavior, - VFXBehavior, + VFXColorOverLifeBehavior, + VFXSizeOverLifeBehavior, + VFXRotationOverLifeBehavior, + VFXForceOverLifeBehavior, + VFXGravityForceBehavior, + VFXSpeedOverLifeBehavior, + VFXFrameOverLifeBehavior, + VFXLimitSpeedOverLifeBehavior, + VFXColorBySpeedBehavior, + VFXSizeBySpeedBehavior, + VFXRotationBySpeedBehavior, + VFXOrbitOverLifeBehavior, + VFXBehavior, } from "./behaviors"; export type { VFXEmissionBurst, VFXParticleEmitterConfig } from "./emitterConfig"; export type { VFXTransform, VFXGroup, VFXEmitter, VFXHierarchy } from "./hierarchy"; diff --git a/editor/src/editor/windows/fx-editor/VFX/types/loader.ts b/editor/src/editor/windows/fx-editor/VFX/types/loader.ts index bff7133b5..85db9d4a7 100644 --- a/editor/src/editor/windows/fx-editor/VFX/types/loader.ts +++ b/editor/src/editor/windows/fx-editor/VFX/types/loader.ts @@ -2,13 +2,12 @@ * Options for parsing Quarks/Three.js particle JSON */ export interface VFXLoaderOptions { - /** - * Enable verbose logging for debugging - */ - verbose?: boolean; - /** - * Validate parsed data and log warnings - */ - validate?: boolean; + /** + * Enable verbose logging for debugging + */ + verbose?: boolean; + /** + * Validate parsed data and log warnings + */ + validate?: boolean; } - diff --git a/editor/src/editor/windows/fx-editor/VFX/types/quarksTypes.ts b/editor/src/editor/windows/fx-editor/VFX/types/quarksTypes.ts index 73b69c151..834baf98b 100644 --- a/editor/src/editor/windows/fx-editor/VFX/types/quarksTypes.ts +++ b/editor/src/editor/windows/fx-editor/VFX/types/quarksTypes.ts @@ -7,27 +7,27 @@ * Quarks/Three.js value types */ export interface QuarksConstantValue { - type: "ConstantValue"; - value: number; + type: "ConstantValue"; + value: number; } export interface QuarksIntervalValue { - type: "IntervalValue"; - a: number; // min - b: number; // max + type: "IntervalValue"; + a: number; // min + b: number; // max } export interface QuarksPiecewiseBezier { - type: "PiecewiseBezier"; - functions: Array<{ - function: { - p0: number; - p1: number; - p2: number; - p3: number; - }; - start: number; - }>; + type: "PiecewiseBezier"; + functions: Array<{ + function: { + p0: number; + p1: number; + p2: number; + p3: number; + }; + start: number; + }>; } export type QuarksValue = QuarksConstantValue | QuarksIntervalValue | QuarksPiecewiseBezier | number; @@ -36,14 +36,14 @@ export type QuarksValue = QuarksConstantValue | QuarksIntervalValue | QuarksPiec * Quarks/Three.js color types */ export interface QuarksConstantColor { - type: "ConstantColor"; - color?: { - r: number; - g: number; - b: number; - a?: number; - }; - value?: [number, number, number, number]; // RGBA array alternative + type: "ConstantColor"; + color?: { + r: number; + g: number; + b: number; + a?: number; + }; + value?: [number, number, number, number]; // RGBA array alternative } export type QuarksColor = QuarksConstantColor | [number, number, number, number] | string; @@ -52,10 +52,10 @@ export type QuarksColor = QuarksConstantColor | [number, number, number, number] * Quarks/Three.js rotation types */ export interface QuarksEulerRotation { - type: "Euler"; - angleX?: QuarksValue; - angleY?: QuarksValue; - angleZ?: QuarksValue; + type: "Euler"; + angleX?: QuarksValue; + angleY?: QuarksValue; + angleZ?: QuarksValue; } export type QuarksRotation = QuarksEulerRotation | QuarksValue; @@ -64,230 +64,230 @@ export type QuarksRotation = QuarksEulerRotation | QuarksValue; * Quarks/Three.js gradient key */ export interface QuarksGradientKey { - time?: number; - value: number | [number, number, number, number] | { r: number; g: number; b: number; a?: number }; - pos?: number; + time?: number; + value: number | [number, number, number, number] | { r: number; g: number; b: number; a?: number }; + pos?: number; } /** * Quarks/Three.js shape configuration */ export interface QuarksShape { - type: string; - radius?: number; - arc?: number; - thickness?: number; - angle?: number; - mode?: number; - spread?: number; - speed?: QuarksValue; - size?: number[]; - height?: number; + type: string; + radius?: number; + arc?: number; + thickness?: number; + angle?: number; + mode?: number; + spread?: number; + speed?: QuarksValue; + size?: number[]; + height?: number; } /** * Quarks/Three.js emission burst */ export interface QuarksEmissionBurst { - time: QuarksValue; - count: QuarksValue; + time: QuarksValue; + count: QuarksValue; } /** * Quarks/Three.js behavior types */ export interface QuarksColorOverLifeBehavior { - type: "ColorOverLife"; - color?: { - color?: { - keys: QuarksGradientKey[]; - }; - alpha?: { - keys: QuarksGradientKey[]; - }; - keys?: QuarksGradientKey[]; - }; + type: "ColorOverLife"; + color?: { + color?: { + keys: QuarksGradientKey[]; + }; + alpha?: { + keys: QuarksGradientKey[]; + }; + keys?: QuarksGradientKey[]; + }; } export interface QuarksSizeOverLifeBehavior { - type: "SizeOverLife"; - size?: { - keys?: QuarksGradientKey[]; - functions?: Array<{ - start: number; - function: { - p0?: number; - p3?: number; - }; - }>; - }; + type: "SizeOverLife"; + size?: { + keys?: QuarksGradientKey[]; + functions?: Array<{ + start: number; + function: { + p0?: number; + p3?: number; + }; + }>; + }; } export interface QuarksRotationOverLifeBehavior { - type: "RotationOverLife" | "Rotation3DOverLife"; - angularVelocity?: QuarksValue; + type: "RotationOverLife" | "Rotation3DOverLife"; + angularVelocity?: QuarksValue; } export interface QuarksForceOverLifeBehavior { - type: "ForceOverLife" | "ApplyForce"; - force?: { - x?: QuarksValue; - y?: QuarksValue; - z?: QuarksValue; - }; - x?: QuarksValue; - y?: QuarksValue; - z?: QuarksValue; + type: "ForceOverLife" | "ApplyForce"; + force?: { + x?: QuarksValue; + y?: QuarksValue; + z?: QuarksValue; + }; + x?: QuarksValue; + y?: QuarksValue; + z?: QuarksValue; } export interface QuarksGravityForceBehavior { - type: "GravityForce"; - gravity?: QuarksValue; + type: "GravityForce"; + gravity?: QuarksValue; } export interface QuarksSpeedOverLifeBehavior { - type: "SpeedOverLife"; - speed?: - | { - keys?: QuarksGradientKey[]; - functions?: Array<{ - start: number; - function: { - p0?: number; - p3?: number; - }; - }>; - } - | QuarksValue; + type: "SpeedOverLife"; + speed?: + | { + keys?: QuarksGradientKey[]; + functions?: Array<{ + start: number; + function: { + p0?: number; + p3?: number; + }; + }>; + } + | QuarksValue; } export interface QuarksFrameOverLifeBehavior { - type: "FrameOverLife"; - frame?: - | { - keys?: QuarksGradientKey[]; - } - | QuarksValue; + type: "FrameOverLife"; + frame?: + | { + keys?: QuarksGradientKey[]; + } + | QuarksValue; } export interface QuarksLimitSpeedOverLifeBehavior { - type: "LimitSpeedOverLife"; - maxSpeed?: QuarksValue; - speed?: QuarksValue | { keys?: QuarksGradientKey[] }; - dampen?: QuarksValue; + type: "LimitSpeedOverLife"; + maxSpeed?: QuarksValue; + speed?: QuarksValue | { keys?: QuarksGradientKey[] }; + dampen?: QuarksValue; } export interface QuarksColorBySpeedBehavior { - type: "ColorBySpeed"; - color?: { - keys: QuarksGradientKey[]; - }; - minSpeed?: QuarksValue; - maxSpeed?: QuarksValue; + type: "ColorBySpeed"; + color?: { + keys: QuarksGradientKey[]; + }; + minSpeed?: QuarksValue; + maxSpeed?: QuarksValue; } export interface QuarksSizeBySpeedBehavior { - type: "SizeBySpeed"; - size?: { - keys: QuarksGradientKey[]; - }; - minSpeed?: QuarksValue; - maxSpeed?: QuarksValue; + type: "SizeBySpeed"; + size?: { + keys: QuarksGradientKey[]; + }; + minSpeed?: QuarksValue; + maxSpeed?: QuarksValue; } export interface QuarksRotationBySpeedBehavior { - type: "RotationBySpeed"; - angularVelocity?: QuarksValue; - minSpeed?: QuarksValue; - maxSpeed?: QuarksValue; + type: "RotationBySpeed"; + angularVelocity?: QuarksValue; + minSpeed?: QuarksValue; + maxSpeed?: QuarksValue; } export interface QuarksOrbitOverLifeBehavior { - type: "OrbitOverLife"; - center?: { - x?: number; - y?: number; - z?: number; - }; - radius?: QuarksValue; - speed?: QuarksValue; + type: "OrbitOverLife"; + center?: { + x?: number; + y?: number; + z?: number; + }; + radius?: QuarksValue; + speed?: QuarksValue; } export type QuarksBehavior = - | QuarksColorOverLifeBehavior - | QuarksSizeOverLifeBehavior - | QuarksRotationOverLifeBehavior - | QuarksForceOverLifeBehavior - | QuarksGravityForceBehavior - | QuarksSpeedOverLifeBehavior - | QuarksFrameOverLifeBehavior - | QuarksLimitSpeedOverLifeBehavior - | QuarksColorBySpeedBehavior - | QuarksSizeBySpeedBehavior - | QuarksRotationBySpeedBehavior - | QuarksOrbitOverLifeBehavior - | { type: string; [key: string]: unknown }; // Fallback for unknown behaviors + | QuarksColorOverLifeBehavior + | QuarksSizeOverLifeBehavior + | QuarksRotationOverLifeBehavior + | QuarksForceOverLifeBehavior + | QuarksGravityForceBehavior + | QuarksSpeedOverLifeBehavior + | QuarksFrameOverLifeBehavior + | QuarksLimitSpeedOverLifeBehavior + | QuarksColorBySpeedBehavior + | QuarksSizeBySpeedBehavior + | QuarksRotationBySpeedBehavior + | QuarksOrbitOverLifeBehavior + | { type: string; [key: string]: unknown }; // Fallback for unknown behaviors /** * Quarks/Three.js particle emitter configuration */ export interface QuarksParticleEmitterConfig { - version?: string; - autoDestroy?: boolean; - looping?: boolean; - prewarm?: boolean; - duration?: number; - shape?: QuarksShape; - startLife?: QuarksValue; - startSpeed?: QuarksValue; - startRotation?: QuarksRotation; - startSize?: QuarksValue; - startColor?: QuarksColor; - emissionOverTime?: QuarksValue; - emissionOverDistance?: QuarksValue; - emissionBursts?: QuarksEmissionBurst[]; - onlyUsedByOther?: boolean; - instancingGeometry?: string; - renderOrder?: number; - renderMode?: number; - rendererEmitterSettings?: Record; - material?: string; - layers?: number; - startTileIndex?: QuarksValue; - uTileCount?: number; - vTileCount?: number; - blendTiles?: boolean; - softParticles?: boolean; - softFarFade?: number; - softNearFade?: number; - behaviors?: QuarksBehavior[]; - worldSpace?: boolean; + version?: string; + autoDestroy?: boolean; + looping?: boolean; + prewarm?: boolean; + duration?: number; + shape?: QuarksShape; + startLife?: QuarksValue; + startSpeed?: QuarksValue; + startRotation?: QuarksRotation; + startSize?: QuarksValue; + startColor?: QuarksColor; + emissionOverTime?: QuarksValue; + emissionOverDistance?: QuarksValue; + emissionBursts?: QuarksEmissionBurst[]; + onlyUsedByOther?: boolean; + instancingGeometry?: string; + renderOrder?: number; + renderMode?: number; + rendererEmitterSettings?: Record; + material?: string; + layers?: number; + startTileIndex?: QuarksValue; + uTileCount?: number; + vTileCount?: number; + blendTiles?: boolean; + softParticles?: boolean; + softFarFade?: number; + softNearFade?: number; + behaviors?: QuarksBehavior[]; + worldSpace?: boolean; } /** * Quarks/Three.js object types */ export interface QuarksGroup { - uuid: string; - type: "Group"; - name: string; - matrix?: number[]; - position?: number[]; - rotation?: number[]; - scale?: number[]; - children?: QuarksObject[]; + uuid: string; + type: "Group"; + name: string; + matrix?: number[]; + position?: number[]; + rotation?: number[]; + scale?: number[]; + children?: QuarksObject[]; } export interface QuarksParticleEmitter { - uuid: string; - type: "ParticleEmitter"; - name: string; - matrix?: number[]; - position?: number[]; - rotation?: number[]; - scale?: number[]; - ps: QuarksParticleEmitterConfig; - children?: QuarksObject[]; + uuid: string; + type: "ParticleEmitter"; + name: string; + matrix?: number[]; + position?: number[]; + rotation?: number[]; + scale?: number[]; + ps: QuarksParticleEmitterConfig; + children?: QuarksObject[]; } export type QuarksObject = QuarksGroup | QuarksParticleEmitter; @@ -296,84 +296,84 @@ export type QuarksObject = QuarksGroup | QuarksParticleEmitter; * Quarks/Three.js material */ export interface QuarksMaterial { - uuid: string; - type: string; - name?: string; - color?: number; - map?: string; - blending?: number; - side?: number; - transparent?: boolean; - depthWrite?: boolean; - [key: string]: unknown; + uuid: string; + type: string; + name?: string; + color?: number; + map?: string; + blending?: number; + side?: number; + transparent?: boolean; + depthWrite?: boolean; + [key: string]: unknown; } /** * Quarks/Three.js texture */ export interface QuarksTexture { - uuid: string; - name?: string; - image?: string; - mapping?: number; - wrap?: number[]; - repeat?: number[]; - offset?: number[]; - rotation?: number; - minFilter?: number; - magFilter?: number; - flipY?: boolean; - generateMipmaps?: boolean; - format?: number; - [key: string]: unknown; + uuid: string; + name?: string; + image?: string; + mapping?: number; + wrap?: number[]; + repeat?: number[]; + offset?: number[]; + rotation?: number; + minFilter?: number; + magFilter?: number; + flipY?: boolean; + generateMipmaps?: boolean; + format?: number; + [key: string]: unknown; } /** * Quarks/Three.js image */ export interface QuarksImage { - uuid: string; - url?: string; - data?: string; - [key: string]: unknown; + uuid: string; + url?: string; + data?: string; + [key: string]: unknown; } /** * Quarks/Three.js geometry */ export interface QuarksGeometry { - uuid: string; - type: string; - data?: { - attributes?: Record< - string, - { - itemSize: number; - type: string; - array: number[]; - } - >; - index?: { - type: string; - array: number[]; - }; - }; - [key: string]: unknown; + uuid: string; + type: string; + data?: { + attributes?: Record< + string, + { + itemSize: number; + type: string; + array: number[]; + } + >; + index?: { + type: string; + array: number[]; + }; + }; + [key: string]: unknown; } /** * Quarks/Three.js JSON structure */ export interface QuarksVFXJSON { - metadata?: { - version?: number; - type?: string; - generator?: string; - [key: string]: unknown; - }; - geometries?: QuarksGeometry[]; - materials?: QuarksMaterial[]; - textures?: QuarksTexture[]; - images?: QuarksImage[]; - object?: QuarksObject; + metadata?: { + version?: number; + type?: string; + generator?: string; + [key: string]: unknown; + }; + geometries?: QuarksGeometry[]; + materials?: QuarksMaterial[]; + textures?: QuarksTexture[]; + images?: QuarksImage[]; + object?: QuarksObject; } diff --git a/editor/src/editor/windows/fx-editor/VFX/types/rotations.ts b/editor/src/editor/windows/fx-editor/VFX/types/rotations.ts index b9ff54210..703c3b2f8 100644 --- a/editor/src/editor/windows/fx-editor/VFX/types/rotations.ts +++ b/editor/src/editor/windows/fx-editor/VFX/types/rotations.ts @@ -4,11 +4,10 @@ import type { VFXValue } from "./values"; * VFX rotation types (converted from Quarks) */ export interface VFXEulerRotation { - type: "Euler"; - angleX?: VFXValue; - angleY?: VFXValue; - angleZ?: VFXValue; + type: "Euler"; + angleX?: VFXValue; + angleY?: VFXValue; + angleZ?: VFXValue; } export type VFXRotation = VFXEulerRotation | VFXValue; - diff --git a/editor/src/editor/windows/fx-editor/VFX/types/shapes.ts b/editor/src/editor/windows/fx-editor/VFX/types/shapes.ts index f280206cb..88d8d8b2b 100644 --- a/editor/src/editor/windows/fx-editor/VFX/types/shapes.ts +++ b/editor/src/editor/windows/fx-editor/VFX/types/shapes.ts @@ -4,15 +4,14 @@ import type { VFXValue } from "./values"; * VFX shape configuration (converted from Quarks) */ export interface VFXShape { - type: string; - radius?: number; - arc?: number; - thickness?: number; - angle?: number; - mode?: number; - spread?: number; - speed?: VFXValue; - size?: number[]; - height?: number; + type: string; + radius?: number; + arc?: number; + thickness?: number; + angle?: number; + mode?: number; + spread?: number; + speed?: VFXValue; + size?: number[]; + height?: number; } - diff --git a/editor/src/editor/windows/fx-editor/VFX/types/values.ts b/editor/src/editor/windows/fx-editor/VFX/types/values.ts index b9d9a8414..40de8f5ea 100644 --- a/editor/src/editor/windows/fx-editor/VFX/types/values.ts +++ b/editor/src/editor/windows/fx-editor/VFX/types/values.ts @@ -2,26 +2,26 @@ * VFX value types (converted from Quarks) */ export interface VFXConstantValue { - type: "ConstantValue"; - value: number; + type: "ConstantValue"; + value: number; } export interface VFXIntervalValue { - type: "IntervalValue"; - min: number; - max: number; + type: "IntervalValue"; + min: number; + max: number; } export interface VFXPiecewiseBezier { - type: "PiecewiseBezier"; - functions: Array<{ - function: { - p0: number; - p1: number; - p2: number; - p3: number; - }; - start: number; - }>; + type: "PiecewiseBezier"; + functions: Array<{ + function: { + p0: number; + p1: number; + p2: number; + p3: number; + }; + start: number; + }>; } export type VFXValue = VFXConstantValue | VFXIntervalValue | VFXPiecewiseBezier | number; diff --git a/editor/src/editor/windows/fx-editor/graph.tsx b/editor/src/editor/windows/fx-editor/graph.tsx index d385b2b06..d9f341e12 100644 --- a/editor/src/editor/windows/fx-editor/graph.tsx +++ b/editor/src/editor/windows/fx-editor/graph.tsx @@ -1,9 +1,12 @@ import { Component, ReactNode } from "react"; import { Tree, TreeNodeInfo } from "@blueprintjs/core"; -import { Scene, - +import { + Scene, + // AbstractMesh, - Vector3, Color4 } from "@babylonjs/core"; + Vector3, + Color4, +} from "@babylonjs/core"; import { IFXParticleData, IFXGroupData, IFXNodeData, isGroupData, isParticleData } from "./properties/types"; import { AiOutlinePlus, AiOutlineClose } from "react-icons/ai"; @@ -242,13 +245,9 @@ export class FXEditorGraph extends Component @@ -480,7 +477,6 @@ export class FXEditorGraph extends Component { - private _model: Model = Model.fromJson(layoutModel as any); private _components: Record = {}; @@ -182,12 +181,7 @@ export class FXEditorLayout extends Component ), - resources: ( - (this.props.editor.resources = r!)} - resources={this.state.resources} - /> - ), + resources: (this.props.editor.resources = r!)} resources={this.state.resources} />, animation: (this.props.editor.animation = r!)} filePath={this.props.filePath} editor={this.props.editor} />, properties: ( @@ -76,8 +76,8 @@ export class FXEditorProperties extends Component - { this.forceUpdate(); this.props.onNameChanged?.(); @@ -94,8 +94,8 @@ export class FXEditorProperties extends Component - { this.forceUpdate(); this.props.onNameChanged?.(); diff --git a/editor/src/editor/windows/fx-editor/properties/behaviors.tsx b/editor/src/editor/windows/fx-editor/properties/behaviors.tsx index 5638b1a39..4adce7088 100644 --- a/editor/src/editor/windows/fx-editor/properties/behaviors.tsx +++ b/editor/src/editor/windows/fx-editor/properties/behaviors.tsx @@ -75,4 +75,3 @@ export function FXEditorBehaviorsProperties(props: IFXEditorBehaviorsPropertiesP ); } - diff --git a/editor/src/editor/windows/fx-editor/properties/behaviors/bezier-editor.tsx b/editor/src/editor/windows/fx-editor/properties/behaviors/bezier-editor.tsx index 467b6853d..f1ae02948 100644 --- a/editor/src/editor/windows/fx-editor/properties/behaviors/bezier-editor.tsx +++ b/editor/src/editor/windows/fx-editor/properties/behaviors/bezier-editor.tsx @@ -280,8 +280,8 @@ export class BezierEditor extends Component { if (point === "p0" || point === "p3") { - return isHovered(point) ? "#3b82f6" : "#2563eb" - }; + return isHovered(point) ? "#3b82f6" : "#2563eb"; + } return isHovered(point) ? "#8b5cf6" : "#7c3aed"; }; diff --git a/editor/src/editor/windows/fx-editor/properties/object.tsx b/editor/src/editor/windows/fx-editor/properties/object.tsx index 7478dc341..d4c4a032c 100644 --- a/editor/src/editor/windows/fx-editor/properties/object.tsx +++ b/editor/src/editor/windows/fx-editor/properties/object.tsx @@ -16,12 +16,7 @@ export function FXEditorObjectProperties(props: IFXEditorObjectPropertiesProps): return ( <> - + diff --git a/editor/src/editor/windows/fx-editor/properties/particle-initialization.tsx b/editor/src/editor/windows/fx-editor/properties/particle-initialization.tsx index af602d6a0..772664c9b 100644 --- a/editor/src/editor/windows/fx-editor/properties/particle-initialization.tsx +++ b/editor/src/editor/windows/fx-editor/properties/particle-initialization.tsx @@ -54,35 +54,11 @@ export function FXEditorParticleInitializationProperties(props: IFXEditorParticl return ( <> - - - - - + + + + + ); } diff --git a/editor/src/editor/windows/fx-editor/properties/particle-renderer.tsx b/editor/src/editor/windows/fx-editor/properties/particle-renderer.tsx index c0a0d2b79..ce1586ab9 100644 --- a/editor/src/editor/windows/fx-editor/properties/particle-renderer.tsx +++ b/editor/src/editor/windows/fx-editor/properties/particle-renderer.tsx @@ -115,7 +115,15 @@ export class FXEditorParticleRendererProperties extends Component this.props.onChange()} />; + return ( + this.props.onChange()} + /> + ); } private _getRenderModeSpecificProperties(renderMode: string): ReactNode { diff --git a/editor/src/editor/windows/fx-editor/resources.tsx b/editor/src/editor/windows/fx-editor/resources.tsx index 8d3644d68..925ba2143 100644 --- a/editor/src/editor/windows/fx-editor/resources.tsx +++ b/editor/src/editor/windows/fx-editor/resources.tsx @@ -3,12 +3,7 @@ import { Tree, TreeNodeInfo } from "@blueprintjs/core"; import { IoImageOutline, IoCubeOutline } from "react-icons/io5"; -import { - ContextMenu, - ContextMenuContent, - ContextMenuItem, - ContextMenuTrigger, -} from "../../../ui/shadcn/ui/context-menu"; +import { ContextMenu, ContextMenuContent, ContextMenuItem, ContextMenuTrigger } from "../../../ui/shadcn/ui/context-menu"; export interface IFXEditorResourcesProps { resources: any[]; @@ -37,11 +32,7 @@ export class FXEditorResources extends Component { - const icon = resource.type === "texture" ? ( - - ) : ( - - ); + const icon = resource.type === "texture" ? : ; const label = ( @@ -85,4 +76,3 @@ export class FXEditorResources extends Component Date: Fri, 12 Dec 2025 11:34:30 +0300 Subject: [PATCH 12/62] refactor: update Babylon.js imports across FX Editor components for consistency and clarity --- editor/src/editor/windows/fx-editor/VFX/VFXEffect.ts | 4 +--- .../windows/fx-editor/VFX/behaviors/colorBySpeed.ts | 3 +-- .../windows/fx-editor/VFX/behaviors/colorOverLife.ts | 4 +--- .../windows/fx-editor/VFX/behaviors/forceOverLife.ts | 3 +-- .../windows/fx-editor/VFX/behaviors/frameOverLife.ts | 2 +- .../fx-editor/VFX/behaviors/limitSpeedOverLife.ts | 2 +- .../windows/fx-editor/VFX/behaviors/orbitOverLife.ts | 3 +-- .../fx-editor/VFX/behaviors/rotationBySpeed.ts | 4 +--- .../fx-editor/VFX/behaviors/rotationOverLife.ts | 3 +-- .../windows/fx-editor/VFX/behaviors/sizeBySpeed.ts | 3 +-- .../windows/fx-editor/VFX/behaviors/sizeOverLife.ts | 3 +-- .../windows/fx-editor/VFX/behaviors/speedOverLife.ts | 3 +-- .../VFX/factories/VFXBehaviorFunctionFactory.ts | 4 +--- .../fx-editor/VFX/factories/VFXEmitterFactory.ts | 9 +-------- .../fx-editor/VFX/factories/VFXGeometryFactory.ts | 6 +----- .../fx-editor/VFX/factories/VFXMaterialFactory.ts | 9 +-------- .../editor/windows/fx-editor/VFX/loggers/VFXLogger.ts | 2 +- .../windows/fx-editor/VFX/parsers/VFXDataConverter.ts | 2 +- .../editor/windows/fx-editor/VFX/parsers/VFXParser.ts | 3 +-- .../windows/fx-editor/VFX/parsers/VFXValueParser.ts | 3 +-- .../fx-editor/VFX/processors/VFXHierarchyProcessor.ts | 4 +--- .../fx-editor/VFX/systems/VFXParticleSystem.ts | 11 +++++++---- .../fx-editor/VFX/systems/VFXSolidParticleSystem.ts | 6 +----- .../fx-editor/VFX/types/VFXBehaviorFunction.ts | 4 +--- .../src/editor/windows/fx-editor/VFX/types/context.ts | 3 +-- .../src/editor/windows/fx-editor/VFX/types/emitter.ts | 4 +--- .../editor/windows/fx-editor/VFX/types/factories.ts | 9 +-------- .../editor/windows/fx-editor/VFX/types/hierarchy.ts | 2 +- editor/src/editor/windows/fx-editor/graph.tsx | 10 ++-------- editor/src/editor/windows/fx-editor/preview.tsx | 4 ++-- 30 files changed, 38 insertions(+), 94 deletions(-) diff --git a/editor/src/editor/windows/fx-editor/VFX/VFXEffect.ts b/editor/src/editor/windows/fx-editor/VFX/VFXEffect.ts index 56b5ba74c..bec836137 100644 --- a/editor/src/editor/windows/fx-editor/VFX/VFXEffect.ts +++ b/editor/src/editor/windows/fx-editor/VFX/VFXEffect.ts @@ -1,6 +1,4 @@ -import type { Scene } from "@babylonjs/core"; -import { Tools } from "@babylonjs/core/Misc/tools"; -import type { IDisposable } from "@babylonjs/core/scene"; +import { Scene, Tools, IDisposable } from "babylonjs"; import type { QuarksVFXJSON } from "./types/quarksTypes"; import type { VFXLoaderOptions } from "./types/loader"; import { VFXParser } from "./parsers/VFXParser"; diff --git a/editor/src/editor/windows/fx-editor/VFX/behaviors/colorBySpeed.ts b/editor/src/editor/windows/fx-editor/VFX/behaviors/colorBySpeed.ts index e090041d3..c9cc89a3f 100644 --- a/editor/src/editor/windows/fx-editor/VFX/behaviors/colorBySpeed.ts +++ b/editor/src/editor/windows/fx-editor/VFX/behaviors/colorBySpeed.ts @@ -1,5 +1,4 @@ -import type { SolidParticle } from "@babylonjs/core/Particles/solidParticle"; -import type { Particle } from "@babylonjs/core/Particles/particle"; +import { SolidParticle, Particle } from "babylonjs"; import type { VFXColorBySpeedBehavior } from "../types/behaviors"; import { interpolateColorKeys } from "./utils"; import { VFXValueParser } from "../parsers/VFXValueParser"; diff --git a/editor/src/editor/windows/fx-editor/VFX/behaviors/colorOverLife.ts b/editor/src/editor/windows/fx-editor/VFX/behaviors/colorOverLife.ts index bbeef65b6..d0d373956 100644 --- a/editor/src/editor/windows/fx-editor/VFX/behaviors/colorOverLife.ts +++ b/editor/src/editor/windows/fx-editor/VFX/behaviors/colorOverLife.ts @@ -1,8 +1,6 @@ -import { Color4 } from "@babylonjs/core/Maths/math.color"; -import type { SolidParticle } from "@babylonjs/core/Particles/solidParticle"; +import { Color4, ParticleSystem, SolidParticle } from "babylonjs"; import type { VFXColorOverLifeBehavior } from "../types/behaviors"; import { extractColorFromValue, extractAlphaFromValue, interpolateColorKeys, interpolateGradientKeys } from "./utils"; -import { ParticleSystem } from "@babylonjs/core/Particles/particleSystem"; /** * Apply ColorOverLife behavior to ParticleSystem diff --git a/editor/src/editor/windows/fx-editor/VFX/behaviors/forceOverLife.ts b/editor/src/editor/windows/fx-editor/VFX/behaviors/forceOverLife.ts index ca623b9fe..e4e1be26b 100644 --- a/editor/src/editor/windows/fx-editor/VFX/behaviors/forceOverLife.ts +++ b/editor/src/editor/windows/fx-editor/VFX/behaviors/forceOverLife.ts @@ -1,5 +1,4 @@ -import { Vector3 } from "@babylonjs/core/Maths/math.vector"; -import type { ParticleSystem } from "@babylonjs/core/Particles/particleSystem"; +import { Vector3, ParticleSystem } from "babylonjs"; import type { VFXForceOverLifeBehavior, VFXGravityForceBehavior } from "../types/behaviors"; import { VFXValueParser } from "../parsers/VFXValueParser"; diff --git a/editor/src/editor/windows/fx-editor/VFX/behaviors/frameOverLife.ts b/editor/src/editor/windows/fx-editor/VFX/behaviors/frameOverLife.ts index 59669ccd4..79d3fd610 100644 --- a/editor/src/editor/windows/fx-editor/VFX/behaviors/frameOverLife.ts +++ b/editor/src/editor/windows/fx-editor/VFX/behaviors/frameOverLife.ts @@ -1,4 +1,4 @@ -import type { ParticleSystem } from "@babylonjs/core/Particles/particleSystem"; +import { ParticleSystem } from "babylonjs"; import type { VFXFrameOverLifeBehavior } from "../types/behaviors"; import { VFXValueParser } from "../parsers/VFXValueParser"; diff --git a/editor/src/editor/windows/fx-editor/VFX/behaviors/limitSpeedOverLife.ts b/editor/src/editor/windows/fx-editor/VFX/behaviors/limitSpeedOverLife.ts index f159a9efa..2b11b7a42 100644 --- a/editor/src/editor/windows/fx-editor/VFX/behaviors/limitSpeedOverLife.ts +++ b/editor/src/editor/windows/fx-editor/VFX/behaviors/limitSpeedOverLife.ts @@ -1,4 +1,4 @@ -import type { ParticleSystem } from "@babylonjs/core/Particles/particleSystem"; +import { ParticleSystem } from "babylonjs"; import type { VFXLimitSpeedOverLifeBehavior } from "../types/behaviors"; import { extractNumberFromValue } from "./utils"; import { VFXValueParser } from "../parsers/VFXValueParser"; diff --git a/editor/src/editor/windows/fx-editor/VFX/behaviors/orbitOverLife.ts b/editor/src/editor/windows/fx-editor/VFX/behaviors/orbitOverLife.ts index fb1a66c2e..1dddabfec 100644 --- a/editor/src/editor/windows/fx-editor/VFX/behaviors/orbitOverLife.ts +++ b/editor/src/editor/windows/fx-editor/VFX/behaviors/orbitOverLife.ts @@ -1,5 +1,4 @@ -import type { Particle } from "@babylonjs/core/Particles/particle"; -import type { SolidParticle } from "@babylonjs/core/Particles/solidParticle"; +import { Particle, SolidParticle } from "babylonjs"; import type { VFXOrbitOverLifeBehavior } from "../types/behaviors"; import { extractNumberFromValue, interpolateGradientKeys } from "./utils"; import { VFXValueParser } from "../parsers/VFXValueParser"; diff --git a/editor/src/editor/windows/fx-editor/VFX/behaviors/rotationBySpeed.ts b/editor/src/editor/windows/fx-editor/VFX/behaviors/rotationBySpeed.ts index dda7a996e..e98210f3c 100644 --- a/editor/src/editor/windows/fx-editor/VFX/behaviors/rotationBySpeed.ts +++ b/editor/src/editor/windows/fx-editor/VFX/behaviors/rotationBySpeed.ts @@ -1,6 +1,4 @@ -import type { Particle } from "@babylonjs/core/Particles/particle"; -import type { SolidParticle } from "@babylonjs/core/Particles/solidParticle"; -import type { ParticleSystem } from "@babylonjs/core/Particles/particleSystem"; +import { Particle, ParticleSystem, SolidParticle } from "babylonjs"; import type { VFXRotationBySpeedBehavior } from "../types/behaviors"; import { extractNumberFromValue, interpolateGradientKeys } from "./utils"; import { VFXValueParser } from "../parsers/VFXValueParser"; diff --git a/editor/src/editor/windows/fx-editor/VFX/behaviors/rotationOverLife.ts b/editor/src/editor/windows/fx-editor/VFX/behaviors/rotationOverLife.ts index f1be1c2cb..5d28c4211 100644 --- a/editor/src/editor/windows/fx-editor/VFX/behaviors/rotationOverLife.ts +++ b/editor/src/editor/windows/fx-editor/VFX/behaviors/rotationOverLife.ts @@ -1,5 +1,4 @@ -import type { ParticleSystem } from "@babylonjs/core/Particles/particleSystem"; -import type { SolidParticle } from "@babylonjs/core/Particles/solidParticle"; +import type { ParticleSystem, SolidParticle } from "babylonjs"; import type { VFXRotationOverLifeBehavior } from "../types/behaviors"; import { VFXValueParser } from "../parsers/VFXValueParser"; diff --git a/editor/src/editor/windows/fx-editor/VFX/behaviors/sizeBySpeed.ts b/editor/src/editor/windows/fx-editor/VFX/behaviors/sizeBySpeed.ts index f825bf4d9..ad73a848d 100644 --- a/editor/src/editor/windows/fx-editor/VFX/behaviors/sizeBySpeed.ts +++ b/editor/src/editor/windows/fx-editor/VFX/behaviors/sizeBySpeed.ts @@ -1,5 +1,4 @@ -import type { Particle } from "@babylonjs/core/Particles/particle"; -import type { SolidParticle } from "@babylonjs/core/Particles/solidParticle"; +import type { Particle, SolidParticle } from "babylonjs"; import type { VFXSizeBySpeedBehavior } from "../types/behaviors"; import { extractNumberFromValue, interpolateGradientKeys } from "./utils"; import { VFXValueParser } from "../parsers/VFXValueParser"; diff --git a/editor/src/editor/windows/fx-editor/VFX/behaviors/sizeOverLife.ts b/editor/src/editor/windows/fx-editor/VFX/behaviors/sizeOverLife.ts index 01fe06071..3fca02b73 100644 --- a/editor/src/editor/windows/fx-editor/VFX/behaviors/sizeOverLife.ts +++ b/editor/src/editor/windows/fx-editor/VFX/behaviors/sizeOverLife.ts @@ -1,5 +1,4 @@ -import type { ParticleSystem } from "@babylonjs/core/Particles/particleSystem"; -import type { SolidParticle } from "@babylonjs/core/Particles/solidParticle"; +import { ParticleSystem, SolidParticle } from "babylonjs"; import type { VFXSizeOverLifeBehavior } from "../types/behaviors"; import { extractNumberFromValue, interpolateGradientKeys } from "./utils"; diff --git a/editor/src/editor/windows/fx-editor/VFX/behaviors/speedOverLife.ts b/editor/src/editor/windows/fx-editor/VFX/behaviors/speedOverLife.ts index a4ba2ac02..6189689e3 100644 --- a/editor/src/editor/windows/fx-editor/VFX/behaviors/speedOverLife.ts +++ b/editor/src/editor/windows/fx-editor/VFX/behaviors/speedOverLife.ts @@ -1,5 +1,4 @@ -import type { ParticleSystem } from "@babylonjs/core/Particles/particleSystem"; -import type { SolidParticle } from "@babylonjs/core/Particles/solidParticle"; +import { SolidParticle, ParticleSystem } from "babylonjs"; import type { VFXSpeedOverLifeBehavior } from "../types/behaviors"; import { extractNumberFromValue, interpolateGradientKeys } from "./utils"; import { VFXValueParser } from "../parsers/VFXValueParser"; diff --git a/editor/src/editor/windows/fx-editor/VFX/factories/VFXBehaviorFunctionFactory.ts b/editor/src/editor/windows/fx-editor/VFX/factories/VFXBehaviorFunctionFactory.ts index e436d7e96..e09525882 100644 --- a/editor/src/editor/windows/fx-editor/VFX/factories/VFXBehaviorFunctionFactory.ts +++ b/editor/src/editor/windows/fx-editor/VFX/factories/VFXBehaviorFunctionFactory.ts @@ -1,6 +1,4 @@ -import type { Particle } from "@babylonjs/core/Particles/particle"; -import type { SolidParticle } from "@babylonjs/core/Particles/solidParticle"; -import type { ParticleSystem } from "@babylonjs/core/Particles/particleSystem"; +import { Particle, SolidParticle, ParticleSystem } from "babylonjs"; import type { VFXBehavior, VFXColorBySpeedBehavior, diff --git a/editor/src/editor/windows/fx-editor/VFX/factories/VFXEmitterFactory.ts b/editor/src/editor/windows/fx-editor/VFX/factories/VFXEmitterFactory.ts index 8773da440..80b00ac3b 100644 --- a/editor/src/editor/windows/fx-editor/VFX/factories/VFXEmitterFactory.ts +++ b/editor/src/editor/windows/fx-editor/VFX/factories/VFXEmitterFactory.ts @@ -1,11 +1,4 @@ -import type { Nullable } from "@babylonjs/core/types"; -import { Vector3, Matrix, Quaternion } from "@babylonjs/core/Maths/math.vector"; -import { Color4 } from "@babylonjs/core/Maths/math.color"; -import { ParticleSystem } from "@babylonjs/core/Particles/particleSystem"; -import { SolidParticleSystem } from "@babylonjs/core/Particles/solidParticleSystem"; -import { CreatePlane } from "@babylonjs/core/Meshes/Builders/planeBuilder"; -import { Mesh } from "@babylonjs/core/Meshes/mesh"; -import { Constants } from "@babylonjs/core/Engines/constants"; +import { Mesh, CreatePlane, Nullable, Color4, Matrix, ParticleSystem, SolidParticleSystem, Constants, Vector3, Quaternion } from "babylonjs"; import type { VFXEmitterData } from "../types/emitter"; import type { VFXParseContext } from "../types/context"; import type { VFXLoaderOptions } from "../types/loader"; diff --git a/editor/src/editor/windows/fx-editor/VFX/factories/VFXGeometryFactory.ts b/editor/src/editor/windows/fx-editor/VFX/factories/VFXGeometryFactory.ts index c98c0bf6b..8551f6503 100644 --- a/editor/src/editor/windows/fx-editor/VFX/factories/VFXGeometryFactory.ts +++ b/editor/src/editor/windows/fx-editor/VFX/factories/VFXGeometryFactory.ts @@ -1,8 +1,4 @@ -import type { Nullable } from "@babylonjs/core/types"; -import { Scene } from "@babylonjs/core/scene"; -import { Mesh } from "@babylonjs/core/Meshes/mesh"; -import { VertexData } from "@babylonjs/core/Meshes/mesh.vertexData"; -import { CreatePlane } from "@babylonjs/core/Meshes/Builders/planeBuilder"; +import { Scene, Mesh, VertexData, CreatePlane, Nullable } from "babylonjs"; import type { IVFXGeometryFactory } from "../types/factories"; import type { VFXParseContext } from "../types/context"; import type { VFXLoaderOptions } from "../types/loader"; diff --git a/editor/src/editor/windows/fx-editor/VFX/factories/VFXMaterialFactory.ts b/editor/src/editor/windows/fx-editor/VFX/factories/VFXMaterialFactory.ts index 2e655dcb9..8d75f9325 100644 --- a/editor/src/editor/windows/fx-editor/VFX/factories/VFXMaterialFactory.ts +++ b/editor/src/editor/windows/fx-editor/VFX/factories/VFXMaterialFactory.ts @@ -1,16 +1,9 @@ -import type { Nullable } from "@babylonjs/core/types"; -import { Color3 } from "@babylonjs/core/Maths/math.color"; -import { Texture } from "@babylonjs/core/Materials/Textures/texture"; -import { PBRMaterial } from "@babylonjs/core/Materials/PBR/pbrMaterial"; -import { Material } from "@babylonjs/core/Materials/material"; -import { Constants } from "@babylonjs/core/Engines/constants"; -import { Tools } from "@babylonjs/core/Misc/tools"; +import { Nullable, Color3, Texture, PBRMaterial, Material, Constants, Tools, Scene } from "babylonjs"; import type { IVFXMaterialFactory } from "../types/factories"; import type { VFXParseContext } from "../types/context"; import type { VFXLoaderOptions } from "../types/loader"; import { VFXLogger } from "../loggers/VFXLogger"; import type { QuarksTexture } from "../types/quarksTypes"; -import type { Scene } from "@babylonjs/core/scene"; /** * Factory for creating materials and textures from Three.js JSON data diff --git a/editor/src/editor/windows/fx-editor/VFX/loggers/VFXLogger.ts b/editor/src/editor/windows/fx-editor/VFX/loggers/VFXLogger.ts index 4d51628d1..6e1c48405 100644 --- a/editor/src/editor/windows/fx-editor/VFX/loggers/VFXLogger.ts +++ b/editor/src/editor/windows/fx-editor/VFX/loggers/VFXLogger.ts @@ -1,4 +1,4 @@ -import { Logger } from "@babylonjs/core/Misc/logger"; +import { Logger } from "babylonjs"; import type { VFXLoaderOptions } from "../types"; /** diff --git a/editor/src/editor/windows/fx-editor/VFX/parsers/VFXDataConverter.ts b/editor/src/editor/windows/fx-editor/VFX/parsers/VFXDataConverter.ts index 6252d153a..2e940503d 100644 --- a/editor/src/editor/windows/fx-editor/VFX/parsers/VFXDataConverter.ts +++ b/editor/src/editor/windows/fx-editor/VFX/parsers/VFXDataConverter.ts @@ -1,4 +1,4 @@ -import { Vector3, Matrix, Quaternion } from "@babylonjs/core/Maths/math.vector"; +import { Vector3, Matrix, Quaternion } from "babylonjs"; import type { VFXLoaderOptions } from "../types/loader"; import type { QuarksVFXJSON } from "../types/quarksTypes"; import type { diff --git a/editor/src/editor/windows/fx-editor/VFX/parsers/VFXParser.ts b/editor/src/editor/windows/fx-editor/VFX/parsers/VFXParser.ts index e52186651..a2b83e7ae 100644 --- a/editor/src/editor/windows/fx-editor/VFX/parsers/VFXParser.ts +++ b/editor/src/editor/windows/fx-editor/VFX/parsers/VFXParser.ts @@ -1,4 +1,4 @@ -import type { Scene } from "@babylonjs/core/scene"; +import { Scene, TransformNode } from "babylonjs"; import type { QuarksVFXJSON } from "../types/quarksTypes"; import type { VFXLoaderOptions } from "../types/loader"; import type { VFXParseContext } from "../types/context"; @@ -9,7 +9,6 @@ import { VFXGeometryFactory } from "../factories/VFXGeometryFactory"; import { VFXEmitterFactory } from "../factories/VFXEmitterFactory"; import { VFXHierarchyProcessor } from "../processors/VFXHierarchyProcessor"; import { VFXDataConverter } from "./VFXDataConverter"; -import { TransformNode } from "@babylonjs/core/Meshes/transformNode"; import { VFXParticleSystem } from "../systems/VFXParticleSystem"; import { VFXSolidParticleSystem } from "../systems/VFXSolidParticleSystem"; diff --git a/editor/src/editor/windows/fx-editor/VFX/parsers/VFXValueParser.ts b/editor/src/editor/windows/fx-editor/VFX/parsers/VFXValueParser.ts index 0b25206f4..ecc34b96f 100644 --- a/editor/src/editor/windows/fx-editor/VFX/parsers/VFXValueParser.ts +++ b/editor/src/editor/windows/fx-editor/VFX/parsers/VFXValueParser.ts @@ -1,5 +1,4 @@ -import { Color4 } from "@babylonjs/core/Maths/math.color"; -import { ColorGradient } from "@babylonjs/core/Misc/gradients"; +import { Color4, ColorGradient } from "babylonjs"; import type { IVFXValueParser } from "../types/factories"; import type { VFXValue } from "../types/values"; import type { VFXColor } from "../types/colors"; diff --git a/editor/src/editor/windows/fx-editor/VFX/processors/VFXHierarchyProcessor.ts b/editor/src/editor/windows/fx-editor/VFX/processors/VFXHierarchyProcessor.ts index 3f6e221ef..de4e17166 100644 --- a/editor/src/editor/windows/fx-editor/VFX/processors/VFXHierarchyProcessor.ts +++ b/editor/src/editor/windows/fx-editor/VFX/processors/VFXHierarchyProcessor.ts @@ -1,6 +1,4 @@ -import type { Nullable } from "@babylonjs/core/types"; -import { Vector3, Quaternion } from "@babylonjs/core/Maths/math.vector"; -import { TransformNode } from "@babylonjs/core/Meshes/transformNode"; +import { Nullable, Vector3, Quaternion, TransformNode } from "babylonjs"; import { VFXParticleSystem } from "../systems/VFXParticleSystem"; import { VFXSolidParticleSystem } from "../systems/VFXSolidParticleSystem"; import type { VFXParseContext } from "../types/context"; diff --git a/editor/src/editor/windows/fx-editor/VFX/systems/VFXParticleSystem.ts b/editor/src/editor/windows/fx-editor/VFX/systems/VFXParticleSystem.ts index 47b8e6ead..798806c70 100644 --- a/editor/src/editor/windows/fx-editor/VFX/systems/VFXParticleSystem.ts +++ b/editor/src/editor/windows/fx-editor/VFX/systems/VFXParticleSystem.ts @@ -1,5 +1,4 @@ -import { Color4 } from "@babylonjs/core/Maths/math.color"; -import { ParticleSystem, Scene } from "@babylonjs/core"; +import { Color4, ParticleSystem, Scene } from "babylonjs"; import type { VFXValueParser } from "../parsers/VFXValueParser"; import type { VFXPerParticleBehaviorFunction } from "../types/VFXBehaviorFunction"; @@ -8,12 +7,16 @@ import type { VFXPerParticleBehaviorFunction } from "../types/VFXBehaviorFunctio * (logic intentionally minimal, behaviors handled elsewhere) */ export class VFXParticleSystem extends ParticleSystem { + public startSize: number; + public startSpeed: number; + public startColor: Color4; + public behaviors: VFXPerParticleBehaviorFunction[]; constructor(name: string, capacity: number, scene: Scene, _valueParser: VFXValueParser, _avgStartSpeed: number, _avgStartSize: number, _startColor: Color4) { super(name, capacity, scene); // behavior wiring omitted by design (see VFXEmitterFactory) } - public setPerParticleBehaviors(_functions: VFXPerParticleBehaviorFunction[]): void { - // intentionally no-op (kept for API parity) + public setPerParticleBehaviors(functions: VFXPerParticleBehaviorFunction[]): void { + this.behaviors = functions; } } diff --git a/editor/src/editor/windows/fx-editor/VFX/systems/VFXSolidParticleSystem.ts b/editor/src/editor/windows/fx-editor/VFX/systems/VFXSolidParticleSystem.ts index cc8fd7520..f8258f512 100644 --- a/editor/src/editor/windows/fx-editor/VFX/systems/VFXSolidParticleSystem.ts +++ b/editor/src/editor/windows/fx-editor/VFX/systems/VFXSolidParticleSystem.ts @@ -1,8 +1,4 @@ -import { Vector3, Quaternion, Matrix } from "@babylonjs/core/Maths/math.vector"; -import { Color4 } from "@babylonjs/core/Maths/math.color"; -import { SolidParticleSystem } from "@babylonjs/core/Particles/solidParticleSystem"; -import { SolidParticle } from "@babylonjs/core/Particles/solidParticle"; -import type { TransformNode } from "@babylonjs/core/Meshes/transformNode"; +import { Vector3, Quaternion, Matrix, Color4, SolidParticleSystem, SolidParticle, TransformNode } from "babylonjs"; import type { VFXParticleEmitterConfig, VFXEmissionBurst } from "../types/emitterConfig"; import type { VFXValueParser } from "../parsers/VFXValueParser"; import { VFXLogger } from "../loggers/VFXLogger"; diff --git a/editor/src/editor/windows/fx-editor/VFX/types/VFXBehaviorFunction.ts b/editor/src/editor/windows/fx-editor/VFX/types/VFXBehaviorFunction.ts index add9957bc..80c16a13e 100644 --- a/editor/src/editor/windows/fx-editor/VFX/types/VFXBehaviorFunction.ts +++ b/editor/src/editor/windows/fx-editor/VFX/types/VFXBehaviorFunction.ts @@ -1,6 +1,4 @@ -import type { Particle } from "@babylonjs/core/Particles/particle"; -import type { SolidParticle } from "@babylonjs/core/Particles/solidParticle"; -import type { ParticleSystem } from "@babylonjs/core/Particles/particleSystem"; +import { Particle, SolidParticle, ParticleSystem } from "babylonjs"; import type { VFXValueParser } from "../parsers/VFXValueParser"; /** diff --git a/editor/src/editor/windows/fx-editor/VFX/types/context.ts b/editor/src/editor/windows/fx-editor/VFX/types/context.ts index a8cf2842a..3fcc8feaf 100644 --- a/editor/src/editor/windows/fx-editor/VFX/types/context.ts +++ b/editor/src/editor/windows/fx-editor/VFX/types/context.ts @@ -1,5 +1,4 @@ -import type { Scene } from "@babylonjs/core/scene"; -import type { TransformNode } from "@babylonjs/core/Meshes/transformNode"; +import { Scene, TransformNode } from "babylonjs"; import type { QuarksVFXJSON } from "./quarksTypes"; import type { VFXHierarchy } from "./hierarchy"; import type { VFXLoaderOptions } from "./loader"; diff --git a/editor/src/editor/windows/fx-editor/VFX/types/emitter.ts b/editor/src/editor/windows/fx-editor/VFX/types/emitter.ts index 3633ae91a..5a187d9f2 100644 --- a/editor/src/editor/windows/fx-editor/VFX/types/emitter.ts +++ b/editor/src/editor/windows/fx-editor/VFX/types/emitter.ts @@ -1,6 +1,4 @@ -import type { Nullable } from "@babylonjs/core/types"; -import type { TransformNode } from "@babylonjs/core/Meshes/transformNode"; -import type { Vector3 } from "@babylonjs/core/Maths/math.vector"; +import { Nullable, TransformNode, Vector3 } from "babylonjs"; import type { VFXParticleEmitterConfig } from "./emitterConfig"; import type { VFXEmitter } from "./hierarchy"; diff --git a/editor/src/editor/windows/fx-editor/VFX/types/factories.ts b/editor/src/editor/windows/fx-editor/VFX/types/factories.ts index 8e79338ad..d0099cc48 100644 --- a/editor/src/editor/windows/fx-editor/VFX/types/factories.ts +++ b/editor/src/editor/windows/fx-editor/VFX/types/factories.ts @@ -1,11 +1,4 @@ -import type { Nullable } from "@babylonjs/core/types"; -import type { Mesh } from "@babylonjs/core/Meshes/mesh"; -import type { ParticleSystem } from "@babylonjs/core/Particles/particleSystem"; -import type { SolidParticleSystem } from "@babylonjs/core/Particles/solidParticleSystem"; -import { PBRMaterial } from "@babylonjs/core/Materials/PBR/pbrMaterial"; -import type { Color4 } from "@babylonjs/core/Maths/math.color"; -import type { Texture } from "@babylonjs/core/Materials/Textures/texture"; -import type { ColorGradient } from "@babylonjs/core/Misc/gradients"; +import { Nullable, Mesh, ParticleSystem, SolidParticleSystem, PBRMaterial, Color4, Texture, ColorGradient } from "babylonjs"; import type { VFXValue } from "./values"; import type { VFXColor } from "./colors"; import type { VFXGradientKey } from "./gradients"; diff --git a/editor/src/editor/windows/fx-editor/VFX/types/hierarchy.ts b/editor/src/editor/windows/fx-editor/VFX/types/hierarchy.ts index 61d2646e9..bf7f5b771 100644 --- a/editor/src/editor/windows/fx-editor/VFX/types/hierarchy.ts +++ b/editor/src/editor/windows/fx-editor/VFX/types/hierarchy.ts @@ -1,4 +1,4 @@ -import { Vector3, Quaternion } from "@babylonjs/core/Maths/math.vector"; +import { Vector3, Quaternion } from "babylonjs"; import type { VFXParticleEmitterConfig } from "./emitterConfig"; /** diff --git a/editor/src/editor/windows/fx-editor/graph.tsx b/editor/src/editor/windows/fx-editor/graph.tsx index d9f341e12..e2347681e 100644 --- a/editor/src/editor/windows/fx-editor/graph.tsx +++ b/editor/src/editor/windows/fx-editor/graph.tsx @@ -1,12 +1,6 @@ import { Component, ReactNode } from "react"; import { Tree, TreeNodeInfo } from "@blueprintjs/core"; -import { - Scene, - - // AbstractMesh, - Vector3, - Color4, -} from "@babylonjs/core"; +import { Scene, Vector3, Color4 } from "babylonjs"; import { IFXParticleData, IFXGroupData, IFXNodeData, isGroupData, isParticleData } from "./properties/types"; import { AiOutlinePlus, AiOutlineClose } from "react-icons/ai"; @@ -261,7 +255,7 @@ export class FXEditorGraph extends Component { system.start(); diff --git a/editor/src/editor/windows/fx-editor/preview.tsx b/editor/src/editor/windows/fx-editor/preview.tsx index 42c198bae..194c775c1 100644 --- a/editor/src/editor/windows/fx-editor/preview.tsx +++ b/editor/src/editor/windows/fx-editor/preview.tsx @@ -1,6 +1,6 @@ import { Component, ReactNode } from "react"; -import { Scene, Engine, ArcRotateCamera, DirectionalLight, Vector3, Color3, Color4, MeshBuilder } from "@babylonjs/core"; -import { GridMaterial } from "@babylonjs/materials"; +import { Scene, Engine, ArcRotateCamera, DirectionalLight, Vector3, Color3, Color4, MeshBuilder } from "babylonjs"; +import { GridMaterial } from "babylonjs-materials"; import { Button } from "../../../ui/shadcn/ui/button"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "../../../ui/shadcn/ui/tooltip"; From 8ae0288395a055343c1ad6596ebb73a323741572 Mon Sep 17 00:00:00 2001 From: Mikalai Lazitski Date: Fri, 12 Dec 2025 13:55:14 +0300 Subject: [PATCH 13/62] refactor: remove unused particle JSON file and enhance VFX component imports for improved clarity and consistency --- .../fx-editor/VFX/behaviors/orbitOverLife.ts | 5 +- .../VFX/factories/VFXEmitterFactory.ts | 956 +++++++++++------- .../VFX/factories/VFXGeometryFactory.ts | 260 +++-- .../VFX/factories/VFXMaterialFactory.ts | 511 +++++----- .../VFX/systems/VFXSolidParticleSystem.ts | 1 - .../VFX/treejs3dobject.particle.json | 870 ---------------- .../fx-editor/VFX/types/quarksTypes.ts | 7 +- 7 files changed, 1031 insertions(+), 1579 deletions(-) delete mode 100644 editor/src/editor/windows/fx-editor/VFX/treejs3dobject.particle.json diff --git a/editor/src/editor/windows/fx-editor/VFX/behaviors/orbitOverLife.ts b/editor/src/editor/windows/fx-editor/VFX/behaviors/orbitOverLife.ts index 1dddabfec..67ca08114 100644 --- a/editor/src/editor/windows/fx-editor/VFX/behaviors/orbitOverLife.ts +++ b/editor/src/editor/windows/fx-editor/VFX/behaviors/orbitOverLife.ts @@ -2,6 +2,7 @@ import { Particle, SolidParticle } from "babylonjs"; import type { VFXOrbitOverLifeBehavior } from "../types/behaviors"; import { extractNumberFromValue, interpolateGradientKeys } from "./utils"; import { VFXValueParser } from "../parsers/VFXValueParser"; +import { VFXValue } from "../types"; /** * Apply OrbitOverLife behavior to Particle @@ -27,7 +28,7 @@ export function applyOrbitOverLifePS(particle: Particle, behavior: VFXOrbitOverL radius = interpolateGradientKeys(radiusValue.keys, lifeRatio, extractNumberFromValue); } else if (radiusValue !== undefined && radiusValue !== null) { // Parse as VFXValue (number, VFXConstantValue, or VFXIntervalValue) - const parsedRadius = valueParser.parseIntervalValue(radiusValue as import("../types/values").VFXValue); + const parsedRadius = valueParser.parseIntervalValue(radiusValue as VFXValue); radius = parsedRadius.min + (parsedRadius.max - parsedRadius.min) * lifeRatio; } @@ -75,7 +76,7 @@ export function applyOrbitOverLifeSPS(particle: SolidParticle, behavior: VFXOrbi radius = interpolateGradientKeys(radiusValue.keys, lifeRatio, extractNumberFromValue); } else if (radiusValue !== undefined && radiusValue !== null) { // Parse as VFXValue (number, VFXConstantValue, or VFXIntervalValue) - const parsedRadius = valueParser.parseIntervalValue(radiusValue as import("../types/values").VFXValue); + const parsedRadius = valueParser.parseIntervalValue(radiusValue as VFXValue); radius = parsedRadius.min + (parsedRadius.max - parsedRadius.min) * lifeRatio; } diff --git a/editor/src/editor/windows/fx-editor/VFX/factories/VFXEmitterFactory.ts b/editor/src/editor/windows/fx-editor/VFX/factories/VFXEmitterFactory.ts index 80b00ac3b..c63e6d116 100644 --- a/editor/src/editor/windows/fx-editor/VFX/factories/VFXEmitterFactory.ts +++ b/editor/src/editor/windows/fx-editor/VFX/factories/VFXEmitterFactory.ts @@ -1,7 +1,6 @@ import { Mesh, CreatePlane, Nullable, Color4, Matrix, ParticleSystem, SolidParticleSystem, Constants, Vector3, Quaternion } from "babylonjs"; import type { VFXEmitterData } from "../types/emitter"; import type { VFXParseContext } from "../types/context"; -import type { VFXLoaderOptions } from "../types/loader"; import { VFXLogger } from "../loggers/VFXLogger"; import { VFXValueParser } from "../parsers/VFXValueParser"; import type { IVFXMaterialFactory, IVFXGeometryFactory } from "../types/factories"; @@ -18,6 +17,23 @@ import { applyFrameOverLifePS, applyLimitSpeedOverLifePS, } from "../behaviors"; +import type { VFXBehavior, VFXEmissionBurst } from "../types"; +import type { VFXParticleEmitterConfig } from "../types/emitterConfig"; + +/** + * Parsed values for particle system creation + */ +type ParsedParticleValues = { + emissionRate: number; + duration: number; + capacity: number; + lifeTime: { min: number; max: number }; + speed: { min: number; max: number }; + avgStartSpeed: number; + size: { min: number; max: number }; + avgStartSize: number; + startColor: Color4; +}; /** * Factory for creating particle emitters (ParticleSystem and SolidParticleSystem) @@ -60,165 +76,288 @@ export class VFXEmitterFactory { */ private _createParticleSystem(emitterData: VFXEmitterData): Nullable { const { name, config } = emitterData; - const { scene, options } = this._context; + const { options } = this._context; this._logger.log(`Creating ParticleSystem: ${name}`, options); - // Calculate capacity based on emission rate and duration + const parsedValues = this._parseParticleSystemValues(config); + const particleSystem = this._createParticleSystemInstance(name, parsedValues); + + this._configureBasicProperties(particleSystem, parsedValues); + this._configureRotation(particleSystem, config); + this._configureSpriteTiles(particleSystem, config); + this._configureRendering(particleSystem, config); + this._setEmitterShape(particleSystem, config.shape, emitterData.cumulativeScale, emitterData.matrix); + this._applyTextureAndBlendMode(particleSystem, emitterData.materialId); + this._applyEmissionBurstsIfNeeded(particleSystem, config, parsedValues.emissionRate, parsedValues.duration); + this._applyBehaviorsIfNeeded(particleSystem, config.behaviors); + this._configureWorldSpace(particleSystem, config); + this._configureLooping(particleSystem, config, parsedValues.duration); + this._configureRenderMode(particleSystem, config); + this._configureSoftParticlesAndAutoDestroy(particleSystem, config); + + this._logger.log(`ParticleSystem created: ${name}`, options); + return particleSystem; + } + + /** + * Parses all particle system values from config + */ + private _parseParticleSystemValues(config: VFXParticleEmitterConfig): ParsedParticleValues { const emissionRate = config.emissionOverTime !== undefined ? this._valueParser.parseConstantValue(config.emissionOverTime) : 10; const duration = config.duration || 5; - const capacity = Math.ceil(emissionRate * duration * 2); // Add some buffer - this._logger.log(` Emission rate: ${emissionRate}, Duration: ${duration}, Capacity: ${capacity}`, options); + const capacity = Math.ceil(emissionRate * duration * 2); - // Parse life time const lifeTime = config.startLife !== undefined ? this._valueParser.parseIntervalValue(config.startLife) : { min: 1, max: 1 }; - this._logger.log(` Life time: ${lifeTime.min} - ${lifeTime.max}`, options); - - // Parse speed const speed = config.startSpeed !== undefined ? this._valueParser.parseIntervalValue(config.startSpeed) : { min: 1, max: 1 }; - const avgStartSpeed = (speed.min + speed.max) / 2; - this._logger.log(` Speed: ${speed.min} - ${speed.max}`, options); - - // Parse size const size = config.startSize !== undefined ? this._valueParser.parseIntervalValue(config.startSize) : { min: 1, max: 1 }; - const avgStartSize = (size.min + size.max) / 2; - this._logger.log(` Size: ${size.min} - ${size.max}`, options); - - // Parse start color const startColor = config.startColor !== undefined ? this._valueParser.parseConstantColor(config.startColor) : new Color4(1, 1, 1, 1); + + this._logParsedValues(emissionRate, duration, capacity, lifeTime, speed, size, startColor); + + return { + emissionRate, + duration, + capacity, + lifeTime, + speed, + avgStartSpeed: (speed.min + speed.max) / 2, + size, + avgStartSize: (size.min + size.max) / 2, + startColor, + }; + } + + /** + * Logs parsed particle system values + */ + private _logParsedValues( + emissionRate: number, + duration: number, + capacity: number, + lifeTime: { min: number; max: number }, + speed: { min: number; max: number }, + size: { min: number; max: number }, + startColor: Color4 + ): void { + const { options } = this._context; + this._logger.log(` Emission rate: ${emissionRate}, Duration: ${duration}, Capacity: ${capacity}`, options); + this._logger.log(` Life time: ${lifeTime.min} - ${lifeTime.max}`, options); + this._logger.log(` Speed: ${speed.min} - ${speed.max}`, options); + this._logger.log(` Size: ${size.min} - ${size.max}`, options); this._logger.log(` Start color: R=${startColor.r}, G=${startColor.g}, B=${startColor.b}, A=${startColor.a}`, options); + } - // Create VFXParticleSystem instead of regular ParticleSystem - const particleSystem = new VFXParticleSystem(name, capacity, scene, this._valueParser, avgStartSpeed, avgStartSize, startColor); + /** + * Creates ParticleSystem instance + */ + private _createParticleSystemInstance(name: string, values: ParsedParticleValues): VFXParticleSystem { + const { scene } = this._context; + return new VFXParticleSystem(name, values.capacity, scene, this._valueParser, values.avgStartSpeed, values.avgStartSize, values.startColor); + } - // Set basic properties - particleSystem.targetStopDuration = duration; - particleSystem.emitRate = emissionRate; + /** + * Configures basic particle system properties + */ + private _configureBasicProperties(particleSystem: ParticleSystem, values: ParsedParticleValues): void { + particleSystem.targetStopDuration = values.duration; + particleSystem.emitRate = values.emissionRate; particleSystem.manualEmitCount = -1; - // Set life time - particleSystem.minLifeTime = lifeTime.min; - particleSystem.maxLifeTime = lifeTime.max; - - // Set speed and size - particleSystem.minEmitPower = speed.min; - particleSystem.maxEmitPower = speed.max; - particleSystem.minSize = size.min; - particleSystem.maxSize = size.max; - - // Set colors - particleSystem.color1 = startColor; - particleSystem.color2 = startColor; - particleSystem.colorDead = new Color4(startColor.r, startColor.g, startColor.b, 0); - - // Parse start rotation - if (config.startRotation) { - if (typeof config.startRotation === "object" && config.startRotation !== null && "type" in config.startRotation && config.startRotation.type === "Euler") { - const eulerRotation = config.startRotation; - if (eulerRotation.angleZ !== undefined) { - const angleZ = this._valueParser.parseIntervalValue(eulerRotation.angleZ); - particleSystem.minInitialRotation = angleZ.min; - particleSystem.maxInitialRotation = angleZ.max; - } - } else { - const rotation = this._valueParser.parseIntervalValue(config.startRotation); - particleSystem.minInitialRotation = rotation.min; - particleSystem.maxInitialRotation = rotation.max; + particleSystem.minLifeTime = values.lifeTime.min; + particleSystem.maxLifeTime = values.lifeTime.max; + + particleSystem.minEmitPower = values.speed.min; + particleSystem.maxEmitPower = values.speed.max; + particleSystem.minSize = values.size.min; + particleSystem.maxSize = values.size.max; + + particleSystem.color1 = values.startColor; + particleSystem.color2 = values.startColor; + particleSystem.colorDead = new Color4(values.startColor.r, values.startColor.g, values.startColor.b, 0); + } + + /** + * Configures rotation settings + */ + private _configureRotation(particleSystem: ParticleSystem, config: VFXParticleEmitterConfig): void { + if (!config.startRotation) { + return; + } + + if (this._isEulerRotation(config.startRotation)) { + if (config.startRotation.angleZ !== undefined) { + const angleZ = this._valueParser.parseIntervalValue(config.startRotation.angleZ); + particleSystem.minInitialRotation = angleZ.min; + particleSystem.maxInitialRotation = angleZ.max; } + } else { + const rotation = this._valueParser.parseIntervalValue(config.startRotation as any); + particleSystem.minInitialRotation = rotation.min; + particleSystem.maxInitialRotation = rotation.max; } + } + + /** + * Checks if rotation is Euler type + */ + private _isEulerRotation(rotation: any): rotation is { type: "Euler"; angleZ?: any } { + return typeof rotation === "object" && rotation !== null && "type" in rotation && rotation.type === "Euler"; + } - // Set sprite tiles if specified - if (config.uTileCount !== undefined && config.vTileCount !== undefined) { - if (config.uTileCount > 1 || config.vTileCount > 1) { - particleSystem.isAnimationSheetEnabled = true; - particleSystem.spriteCellWidth = config.uTileCount; - particleSystem.spriteCellHeight = config.vTileCount; - if (config.startTileIndex !== undefined) { - const startTile = this._valueParser.parseConstantValue(config.startTileIndex); - particleSystem.startSpriteCellID = Math.floor(startTile); - particleSystem.endSpriteCellID = Math.floor(startTile); - } + /** + * Configures sprite tiles for animation sheets + */ + private _configureSpriteTiles(particleSystem: ParticleSystem, config: VFXParticleEmitterConfig): void { + if (config.uTileCount === undefined || config.vTileCount === undefined) { + return; + } + + if (config.uTileCount > 1 || config.vTileCount > 1) { + particleSystem.isAnimationSheetEnabled = true; + particleSystem.spriteCellWidth = config.uTileCount; + particleSystem.spriteCellHeight = config.vTileCount; + + if (config.startTileIndex !== undefined) { + const startTile = this._valueParser.parseConstantValue(config.startTileIndex); + particleSystem.startSpriteCellID = Math.floor(startTile); + particleSystem.endSpriteCellID = Math.floor(startTile); } } + } - // Set render order and layers + /** + * Configures rendering properties (render order and layers) + */ + private _configureRendering(particleSystem: ParticleSystem, config: VFXParticleEmitterConfig): void { if (config.renderOrder !== undefined) { particleSystem.renderingGroupId = config.renderOrder; } if (config.layers !== undefined) { particleSystem.layerMask = config.layers; } + } - // Set emitter shape (pass matrix to extract rotation for emitter direction) - this._setEmitterShape(particleSystem, config.shape, emitterData.cumulativeScale, emitterData.matrix, options); - - // Load texture (ParticleSystem only needs texture, not material) - if (emitterData.materialId) { - const texture = this._materialFactory.createTexture(emitterData.materialId); - if (texture) { - particleSystem.particleTexture = texture; - // Get blend mode from material - const { jsonData } = this._context; - const material = jsonData.materials?.find((m: any) => m.uuid === emitterData.materialId); - if (material?.blending !== undefined) { - if (material.blending === 2) { - // Additive blending (Three.js AdditiveBlending) - particleSystem.blendMode = Constants.ALPHA_ADD; - } else if (material.blending === 1) { - // Normal blending (Three.js NormalBlending) - particleSystem.blendMode = Constants.ALPHA_COMBINE; - } else if (material.blending === 0) { - // No blending (Three.js NoBlending) - particleSystem.blendMode = Constants.ALPHA_DISABLE; - } - } - } + /** + * Applies texture and blend mode from material + */ + private _applyTextureAndBlendMode(particleSystem: ParticleSystem, materialId: string | undefined): void { + if (!materialId) { + return; + } + + const texture = this._materialFactory.createTexture(materialId); + if (!texture) { + return; } - // Handle emission bursts + particleSystem.particleTexture = texture; + const blendMode = this._getBlendModeFromMaterial(materialId); + if (blendMode !== undefined) { + particleSystem.blendMode = blendMode; + } + } + + /** + * Gets blend mode from material blending value + */ + private _getBlendModeFromMaterial(materialId: string): number | undefined { + const { jsonData } = this._context; + const material = jsonData.materials?.find((m: any) => m.uuid === materialId); + + if (material?.blending === undefined) { + return undefined; + } + + const blendModeMap: Record = { + 0: Constants.ALPHA_DISABLE, // NoBlending + 1: Constants.ALPHA_COMBINE, // NormalBlending + 2: Constants.ALPHA_ADD, // AdditiveBlending + }; + + return blendModeMap[material.blending]; + } + + /** + * Applies emission bursts if configured + */ + private _applyEmissionBurstsIfNeeded(particleSystem: ParticleSystem, config: VFXParticleEmitterConfig, emissionRate: number, duration: number): void { if (config.emissionBursts && Array.isArray(config.emissionBursts) && config.emissionBursts.length > 0) { - this._applyEmissionBursts(particleSystem, config.emissionBursts, emissionRate, duration, options); + this._applyEmissionBursts(particleSystem, config.emissionBursts, emissionRate, duration); } + } - // Apply behaviors - if (config.behaviors && Array.isArray(config.behaviors) && config.behaviors.length > 0) { - this._applyBehaviorsToPS(particleSystem, config.behaviors); + /** + * Applies behaviors if configured + */ + private _applyBehaviorsIfNeeded(particleSystem: ParticleSystem, behaviors: VFXBehavior[] | undefined): void { + if (behaviors && Array.isArray(behaviors) && behaviors.length > 0) { + this._applyBehaviorsToPS(particleSystem, behaviors); } + } - // Set world space + /** + * Configures world space setting + */ + private _configureWorldSpace(particleSystem: ParticleSystem, config: VFXParticleEmitterConfig): void { if (config.worldSpace !== undefined) { particleSystem.isLocal = !config.worldSpace; + const { options } = this._context; this._logger.log(` World space: ${config.worldSpace}`, options); } + } - // Set looping + /** + * Configures looping setting + */ + private _configureLooping(particleSystem: ParticleSystem, config: VFXParticleEmitterConfig, duration: number): void { if (config.looping !== undefined) { particleSystem.targetStopDuration = config.looping ? 0 : duration; + const { options } = this._context; this._logger.log(` Looping: ${config.looping}`, options); } + } - // Set render mode - if (config.renderMode !== undefined) { - if (config.renderMode === 0) { + /** + * Configures render mode + */ + private _configureRenderMode(particleSystem: ParticleSystem, config: VFXParticleEmitterConfig): void { + if (config.renderMode === undefined) { + return; + } + + const { options } = this._context; + const renderModeMap: Record void> = { + 0: () => { particleSystem.isBillboardBased = true; this._logger.log(` Render mode: Billboard`, options); - } else if (config.renderMode === 1) { + }, + 1: () => { particleSystem.billboardMode = ParticleSystem.BILLBOARDMODE_STRETCHED; this._logger.log(` Render mode: Stretched Billboard`, options); - } + }, + }; + + const handler = renderModeMap[config.renderMode]; + if (handler) { + handler(); } + } + + /** + * Configures soft particles and auto destroy settings + */ + private _configureSoftParticlesAndAutoDestroy(particleSystem: ParticleSystem, config: VFXParticleEmitterConfig): void { + const { options } = this._context; - // Set soft particles and auto destroy if (config.softParticles !== undefined) { this._logger.log(` Soft particles: ${config.softParticles} (not fully supported)`, options); } + if (config.autoDestroy !== undefined) { particleSystem.disposeOnStop = config.autoDestroy; this._logger.log(` Auto destroy: ${config.autoDestroy}`, options); } - - this._logger.log(`ParticleSystem created: ${name}`, options); - return particleSystem; } /** @@ -226,360 +365,449 @@ export class VFXEmitterFactory { */ private _createSolidParticleSystem(emitterData: VFXEmitterData): Nullable { const { name, config } = emitterData; - const { scene, options } = this._context; + const { options } = this._context; this._logger.log(`Creating SolidParticleSystem: ${name}`, options); - // Calculate capacity based on emission rate and particle lifetime - // duration = particle lifetime (how long each particle lives) - // startLife = when particle becomes "alive" (for behaviors that depend on age) - // emissionOverTime = particles per second (e.g., 2.5 means 2.5 particles per second) - const emissionRate = config.emissionOverTime !== undefined ? this._valueParser.parseConstantValue(config.emissionOverTime) : 10; // particles per second - const particleLifetime = config.duration || 5; // duration is the particle lifetime + const capacity = this._calculateSPSCapacity(config); + const vfxTransform = this._getVFXTransform(emitterData); + const sps = this._createSPSInstance(name, config, emitterData, vfxTransform); + + const particleMesh = this._createOrLoadParticleMesh(name, config, emitterData); + if (!particleMesh) { + return null; + } + + this._addShapeToSPS(sps, particleMesh, capacity); + this._configureSPSBillboard(sps, config); + this._applyBehaviorsIfNeededSPS(sps, config.behaviors); + + particleMesh.dispose(); + + this._logger.log(`SolidParticleSystem created: ${name}`, options); + return sps; + } + + /** + * Calculates capacity for SolidParticleSystem + */ + private _calculateSPSCapacity(config: VFXParticleEmitterConfig): number { + const { options } = this._context; + const emissionRate = config.emissionOverTime !== undefined ? this._valueParser.parseConstantValue(config.emissionOverTime) : 10; + const particleLifetime = config.duration || 5; const isLooping = config.looping !== false; - let capacity: number; if (isLooping) { - // For looping systems: capacity = emissionRate * particleLifetime - // This gives the steady-state number of particles needed for perfect looping - // Example: emissionRate=2.5 particles/sec, particleLifetime=5 sec - // -> capacity = 2.5 * 5 = 12.5 -> 13 particles - // This ensures we have enough particles to cover the lifetime at the emission rate - capacity = Math.ceil(emissionRate * particleLifetime); - // Ensure minimum capacity of at least 1 - capacity = Math.max(capacity, 1); + const capacity = Math.max(Math.ceil(emissionRate * particleLifetime), 1); this._logger.log(` Looping system: Emission rate: ${emissionRate} particles/sec, Particle lifetime: ${particleLifetime} sec, Capacity: ${capacity}`, options); + return capacity; } else { - // For non-looping: capacity = emissionRate * particleLifetime * 2 (buffer for particles still alive) - capacity = Math.ceil(emissionRate * particleLifetime * 2); + const capacity = Math.ceil(emissionRate * particleLifetime * 2); this._logger.log(` Non-looping system: Emission rate: ${emissionRate} particles/sec, Particle lifetime: ${particleLifetime} sec, Capacity: ${capacity}`, options); + return capacity; } + } - // Get VFX transform from emitter data (stored during conversion) - // This is the clean way - transform is already in left-handed coordinate system - let vfxTransform: { position: Vector3; rotation: Quaternion; scale: Vector3 } | null = null; + /** + * Gets VFX transform from emitter data + */ + private _getVFXTransform(emitterData: VFXEmitterData): { position: Vector3; rotation: Quaternion; scale: Vector3 } | null { const vfxEmitter = emitterData.vfxEmitter; - if (vfxEmitter && vfxEmitter.transform) { - vfxTransform = vfxEmitter.transform; - } + return vfxEmitter?.transform || null; + } - const sps = new VFXSolidParticleSystem(name, scene, config, this._valueParser, { + /** + * Creates SolidParticleSystem instance + */ + private _createSPSInstance( + name: string, + config: VFXParticleEmitterConfig, + emitterData: VFXEmitterData, + vfxTransform: { position: Vector3; rotation: Quaternion; scale: Vector3 } | null + ): VFXSolidParticleSystem { + const { scene, options } = this._context; + return new VFXSolidParticleSystem(name, scene, config, this._valueParser, { updatable: true, isPickable: false, enableDepthSort: false, particleIntersection: false, useModelMaterial: true, parentGroup: emitterData.parentGroup, - vfxTransform: vfxTransform, + vfxTransform, logger: this._logger, loaderOptions: options, }); + } - // Load geometry for particle shape - let particleMesh: Nullable = null; - if (config.instancingGeometry) { - this._logger.log(` Loading geometry: ${config.instancingGeometry}`, options); - particleMesh = this._geometryFactory.createMesh(config.instancingGeometry, emitterData.materialId, name + "_shape"); - if (!particleMesh) { - this._logger.warn(` Failed to load geometry ${config.instancingGeometry}, will create default plane`, options); - } - } + /** + * Creates or loads particle mesh for SPS + */ + private _createOrLoadParticleMesh(name: string, config: VFXParticleEmitterConfig, emitterData: VFXEmitterData): Nullable { + const { scene, options } = this._context; + let particleMesh = this._loadParticleGeometry(config, emitterData, name); - // Default to plane if no geometry found if (!particleMesh) { - this._logger.log(` Creating default plane geometry`, options); - particleMesh = CreatePlane(name + "_shape", { width: 1, height: 1 }, scene); - if (emitterData.materialId && particleMesh) { - const particleMaterial = this._materialFactory.createMaterial(emitterData.materialId, name); - if (particleMaterial) { - particleMesh.material = particleMaterial; - } - } + particleMesh = this._createDefaultPlaneMesh(name, scene); + this._applyMaterialToMesh(particleMesh, emitterData.materialId, name); } else { - // Ensure material is applied - if (emitterData.materialId && particleMesh && !particleMesh.material) { - const particleMaterial = this._materialFactory.createMaterial(emitterData.materialId, name); - if (particleMaterial) { - particleMesh.material = particleMaterial; - } - } + this._ensureMaterialApplied(particleMesh, emitterData.materialId, name); } if (!particleMesh) { this._logger.warn(` Cannot add shape to SPS: particleMesh is null`, options); + } + + return particleMesh; + } + + /** + * Loads particle geometry if specified + */ + private _loadParticleGeometry(config: VFXParticleEmitterConfig, emitterData: VFXEmitterData, name: string): Nullable { + const { options } = this._context; + + if (!config.instancingGeometry) { return null; } + this._logger.log(` Loading geometry: ${config.instancingGeometry}`, options); + const mesh = this._geometryFactory.createMesh(config.instancingGeometry, emitterData.materialId, name + "_shape"); + if (!mesh) { + this._logger.warn(` Failed to load geometry ${config.instancingGeometry}, will create default plane`, options); + } + + return mesh; + } + + /** + * Creates default plane mesh + */ + private _createDefaultPlaneMesh(name: string, scene: any): Mesh { + const { options } = this._context; + this._logger.log(` Creating default plane geometry`, options); + return CreatePlane(name + "_shape", { width: 1, height: 1 }, scene); + } + + /** + * Applies material to mesh + */ + private _applyMaterialToMesh(mesh: Mesh | null, materialId: string | undefined, name: string): void { + if (!mesh || !materialId) { + return; + } + + const material = this._materialFactory.createMaterial(materialId, name); + if (material) { + mesh.material = material; + } + } + + /** + * Ensures material is applied to mesh if missing + */ + private _ensureMaterialApplied(mesh: Mesh, materialId: string | undefined, name: string): void { + if (materialId && !mesh.material) { + this._applyMaterialToMesh(mesh, materialId, name); + } + } + + /** + * Adds shape to SPS + */ + private _addShapeToSPS(sps: SolidParticleSystem, particleMesh: Mesh, capacity: number): void { + const { options } = this._context; this._logger.log(` Adding shape to SPS: mesh name=${particleMesh.name}, hasMaterial=${!!particleMesh.material}`, options); sps.addShape(particleMesh, capacity); + } - // Set billboard mode if needed + /** + * Configures billboard mode for SPS + */ + private _configureSPSBillboard(sps: SolidParticleSystem, config: VFXParticleEmitterConfig): void { if (config.renderMode === 0 || config.renderMode === 1) { sps.billboard = true; } + } - // Apply behaviors to SPS - if (config.behaviors && Array.isArray(config.behaviors) && config.behaviors.length > 0) { - this._applyBehaviorsToSPS(sps, config.behaviors); - this._logger.log(` Set SPS behaviors (${config.behaviors.length})`, options); - } + /** + * Applies behaviors to SPS if configured + */ + private _applyBehaviorsIfNeededSPS(sps: SolidParticleSystem, behaviors: VFXBehavior[] | undefined): void { + const { options } = this._context; - // Cleanup temporary mesh - if (particleMesh) { - particleMesh.dispose(); + if (behaviors && Array.isArray(behaviors) && behaviors.length > 0) { + this._applyBehaviorsToSPS(sps, behaviors); + this._logger.log(` Set SPS behaviors (${behaviors.length})`, options); } - - this._logger.log(`SolidParticleSystem created: ${name}`, options); - return sps; } /** * Set the emitter shape based on Three.js shape configuration - * @param matrix Optional 4x4 matrix array from Three.js to extract rotation */ - private _setEmitterShape(particleSystem: ParticleSystem, shape: any, cumulativeScale: Vector3, matrix?: number[], options?: VFXLoaderOptions): void { + private _setEmitterShape(particleSystem: ParticleSystem, shape: any, cumulativeScale: Vector3, matrix?: number[]): void { if (!shape || !shape.type) { particleSystem.createPointEmitter(Vector3.Zero(), Vector3.Zero()); return; } - const scaleX = cumulativeScale.x; - const scaleY = cumulativeScale.y; - const scaleZ = cumulativeScale.z; + const rotationMatrix = this._extractRotationMatrix(matrix); + const shapeHandler = this._getShapeHandler(shape.type.toLowerCase()); - // Extract rotation from matrix if provided - let rotationMatrix: Matrix | null = null; - if (matrix && matrix.length >= 16) { - // Three.js uses column-major order, Babylon.js uses row-major - const mat = Matrix.FromArray(matrix); - mat.transpose(); + if (shapeHandler) { + shapeHandler(particleSystem, shape, cumulativeScale, rotationMatrix); + } else { + this._createDefaultPointEmitter(particleSystem, rotationMatrix); + } + } - // Extract rotation matrix (remove scale and translation) - rotationMatrix = mat.getRotationMatrix(); - this._logger.log(` Extracted rotation from matrix`, options); + /** + * Extracts rotation matrix from Three.js matrix array + */ + private _extractRotationMatrix(matrix: number[] | undefined): Matrix | null { + if (!matrix || matrix.length < 16) { + return null; } - // Helper function to apply rotation to default direction - const applyRotation = (defaultDir: Vector3): Vector3 => { - if (rotationMatrix) { - const rotatedDir = Vector3.Zero(); - Vector3.TransformNormalToRef(defaultDir, rotationMatrix, rotatedDir); - return rotatedDir; - } - return defaultDir; + const { options } = this._context; + const mat = Matrix.FromArray(matrix); + mat.transpose(); + const rotationMatrix = mat.getRotationMatrix(); + this._logger.log(` Extracted rotation from matrix`, options); + return rotationMatrix; + } + + /** + * Gets shape handler function for given shape type + */ + private _getShapeHandler(shapeType: string): ((ps: ParticleSystem, shape: any, scale: Vector3, rotation: Matrix | null) => void) | null { + const shapeHandlers: Record void> = { + cone: this._createConeEmitter.bind(this), + sphere: this._createSphereEmitter.bind(this), + point: this._createPointEmitter.bind(this), + box: this._createBoxEmitter.bind(this), + hemisphere: this._createHemisphereEmitter.bind(this), + cylinder: this._createCylinderEmitter.bind(this), }; - switch (shape.type.toLowerCase()) { - case "cone": { - let radius = shape.radius || 1; - const angle = shape.angle !== undefined ? shape.angle : Math.PI / 4; - const coneScale = (scaleX + scaleZ) / 2; - radius = radius * coneScale; - - // Default direction for cone is up (0, 1, 0) - const defaultDir = new Vector3(0, 1, 0); - const rotatedDir = applyRotation(defaultDir); - - if (rotationMatrix) { - // Use directed emitter with rotated direction - particleSystem.createDirectedConeEmitter(radius, angle, rotatedDir, rotatedDir); - this._logger.log( - ` Created directed cone emitter with rotated direction: (${rotatedDir.x.toFixed(2)}, ${rotatedDir.y.toFixed(2)}, ${rotatedDir.z.toFixed(2)})`, - options - ); - } else { - particleSystem.createConeEmitter(radius, angle); - } - break; - } + return shapeHandlers[shapeType] || null; + } - case "sphere": { - let sphereRadius = shape.radius || 1; - const sphereScale = (scaleX + scaleY + scaleZ) / 3; - sphereRadius = sphereRadius * sphereScale; - - // Default direction for sphere is up (0, 1, 0) - const defaultDir = new Vector3(0, 1, 0); - const rotatedDir = applyRotation(defaultDir); - - if (rotationMatrix) { - particleSystem.createDirectedSphereEmitter(sphereRadius, rotatedDir, rotatedDir); - this._logger.log( - ` Created directed sphere emitter with rotated direction: (${rotatedDir.x.toFixed(2)}, ${rotatedDir.y.toFixed(2)}, ${rotatedDir.z.toFixed(2)})`, - options - ); - } else { - particleSystem.createSphereEmitter(sphereRadius); - } - break; - } + /** + * Applies rotation to default direction vector + */ + private _applyRotationToDirection(defaultDir: Vector3, rotationMatrix: Matrix | null): Vector3 { + if (!rotationMatrix) { + return defaultDir; + } - case "point": { - const defaultDir = new Vector3(0, 1, 0); - const rotatedDir = applyRotation(defaultDir); - - if (rotationMatrix) { - particleSystem.createPointEmitter(rotatedDir, rotatedDir); - this._logger.log( - ` Created point emitter with rotated direction: (${rotatedDir.x.toFixed(2)}, ${rotatedDir.y.toFixed(2)}, ${rotatedDir.z.toFixed(2)})`, - options - ); - } else { - particleSystem.createPointEmitter(Vector3.Zero(), Vector3.Zero()); - } - break; - } + const rotatedDir = Vector3.Zero(); + Vector3.TransformNormalToRef(defaultDir, rotationMatrix, rotatedDir); + return rotatedDir; + } - case "box": { - let boxSize = shape.size || [1, 1, 1]; - boxSize = [boxSize[0] * scaleX, boxSize[1] * scaleY, boxSize[2] * scaleZ]; - const minBox = new Vector3(-boxSize[0] / 2, -boxSize[1] / 2, -boxSize[2] / 2); - const maxBox = new Vector3(boxSize[0] / 2, boxSize[1] / 2, boxSize[2] / 2); - - const defaultDir = new Vector3(0, 1, 0); - const rotatedDir = applyRotation(defaultDir); - - if (rotationMatrix) { - particleSystem.createBoxEmitter(rotatedDir, rotatedDir, minBox, maxBox); - this._logger.log(` Created box emitter with rotated direction: (${rotatedDir.x.toFixed(2)}, ${rotatedDir.y.toFixed(2)}, ${rotatedDir.z.toFixed(2)})`, options); - } else { - particleSystem.createBoxEmitter(Vector3.Zero(), Vector3.Zero(), minBox, maxBox); - } - break; - } + /** + * Creates cone emitter + */ + private _createConeEmitter(particleSystem: ParticleSystem, shape: any, scale: Vector3, rotationMatrix: Matrix | null): void { + const { options } = this._context; + const radius = (shape.radius || 1) * ((scale.x + scale.z) / 2); + const angle = shape.angle !== undefined ? shape.angle : Math.PI / 4; + const defaultDir = new Vector3(0, 1, 0); + const rotatedDir = this._applyRotationToDirection(defaultDir, rotationMatrix); + + if (rotationMatrix) { + particleSystem.createDirectedConeEmitter(radius, angle, rotatedDir, rotatedDir); + this._logger.log( + ` Created directed cone emitter with rotated direction: (${rotatedDir.x.toFixed(2)}, ${rotatedDir.y.toFixed(2)}, ${rotatedDir.z.toFixed(2)})`, + options + ); + } else { + particleSystem.createConeEmitter(radius, angle); + } + } - case "hemisphere": { - let hemRadius = shape.radius || 1; - const hemScale = (scaleX + scaleY + scaleZ) / 3; - hemRadius = hemRadius * hemScale; - particleSystem.createHemisphericEmitter(hemRadius); - break; - } + /** + * Creates sphere emitter + */ + private _createSphereEmitter(particleSystem: ParticleSystem, shape: any, scale: Vector3, rotationMatrix: Matrix | null): void { + const { options } = this._context; + const radius = (shape.radius || 1) * ((scale.x + scale.y + scale.z) / 3); + const defaultDir = new Vector3(0, 1, 0); + const rotatedDir = this._applyRotationToDirection(defaultDir, rotationMatrix); + + if (rotationMatrix) { + particleSystem.createDirectedSphereEmitter(radius, rotatedDir, rotatedDir); + this._logger.log( + ` Created directed sphere emitter with rotated direction: (${rotatedDir.x.toFixed(2)}, ${rotatedDir.y.toFixed(2)}, ${rotatedDir.z.toFixed(2)})`, + options + ); + } else { + particleSystem.createSphereEmitter(radius); + } + } - case "cylinder": { - let cylRadius = shape.radius || 1; - let height = shape.height || 1; - const cylRadiusScale = (scaleX + scaleZ) / 2; - cylRadius = cylRadius * cylRadiusScale; - height = height * scaleY; - - // Default direction for cylinder is up (0, 1, 0) - const defaultDir = new Vector3(0, 1, 0); - const rotatedDir = applyRotation(defaultDir); - - if (rotationMatrix) { - particleSystem.createDirectedCylinderEmitter(cylRadius, height, 1, rotatedDir, rotatedDir); - this._logger.log( - ` Created directed cylinder emitter with rotated direction: (${rotatedDir.x.toFixed(2)}, ${rotatedDir.y.toFixed(2)}, ${rotatedDir.z.toFixed(2)})`, - options - ); - } else { - particleSystem.createCylinderEmitter(cylRadius, height); - } - break; - } + /** + * Creates point emitter + */ + private _createPointEmitter(particleSystem: ParticleSystem, _shape: any, _scale: Vector3, rotationMatrix: Matrix | null): void { + const { options } = this._context; + const defaultDir = new Vector3(0, 1, 0); + const rotatedDir = this._applyRotationToDirection(defaultDir, rotationMatrix); - default: { - const defaultDir = new Vector3(0, 1, 0); - const rotatedDir = applyRotation(defaultDir); + if (rotationMatrix) { + particleSystem.createPointEmitter(rotatedDir, rotatedDir); + this._logger.log(` Created point emitter with rotated direction: (${rotatedDir.x.toFixed(2)}, ${rotatedDir.y.toFixed(2)}, ${rotatedDir.z.toFixed(2)})`, options); + } else { + particleSystem.createPointEmitter(Vector3.Zero(), Vector3.Zero()); + } + } - if (rotationMatrix) { - particleSystem.createPointEmitter(rotatedDir, rotatedDir); - } else { - particleSystem.createPointEmitter(Vector3.Zero(), Vector3.Zero()); - } - break; - } + /** + * Creates box emitter + */ + private _createBoxEmitter(particleSystem: ParticleSystem, shape: any, scale: Vector3, rotationMatrix: Matrix | null): void { + const { options } = this._context; + const boxSize = (shape.size || [1, 1, 1]).map((s: number, i: number) => s * [scale.x, scale.y, scale.z][i]); + const minBox = new Vector3(-boxSize[0] / 2, -boxSize[1] / 2, -boxSize[2] / 2); + const maxBox = new Vector3(boxSize[0] / 2, boxSize[1] / 2, boxSize[2] / 2); + const defaultDir = new Vector3(0, 1, 0); + const rotatedDir = this._applyRotationToDirection(defaultDir, rotationMatrix); + + if (rotationMatrix) { + particleSystem.createBoxEmitter(rotatedDir, rotatedDir, minBox, maxBox); + this._logger.log(` Created box emitter with rotated direction: (${rotatedDir.x.toFixed(2)}, ${rotatedDir.y.toFixed(2)}, ${rotatedDir.z.toFixed(2)})`, options); + } else { + particleSystem.createBoxEmitter(Vector3.Zero(), Vector3.Zero(), minBox, maxBox); + } + } + + /** + * Creates hemisphere emitter + */ + private _createHemisphereEmitter(particleSystem: ParticleSystem, shape: any, scale: Vector3, _rotationMatrix: Matrix | null): void { + const radius = (shape.radius || 1) * ((scale.x + scale.y + scale.z) / 3); + particleSystem.createHemisphericEmitter(radius); + } + + /** + * Creates cylinder emitter + */ + private _createCylinderEmitter(particleSystem: ParticleSystem, shape: any, scale: Vector3, rotationMatrix: Matrix | null): void { + const { options } = this._context; + const radius = (shape.radius || 1) * ((scale.x + scale.z) / 2); + const height = (shape.height || 1) * scale.y; + const defaultDir = new Vector3(0, 1, 0); + const rotatedDir = this._applyRotationToDirection(defaultDir, rotationMatrix); + + if (rotationMatrix) { + particleSystem.createDirectedCylinderEmitter(radius, height, 1, rotatedDir, rotatedDir); + this._logger.log( + ` Created directed cylinder emitter with rotated direction: (${rotatedDir.x.toFixed(2)}, ${rotatedDir.y.toFixed(2)}, ${rotatedDir.z.toFixed(2)})`, + options + ); + } else { + particleSystem.createCylinderEmitter(radius, height); + } + } + + /** + * Creates default point emitter + */ + private _createDefaultPointEmitter(particleSystem: ParticleSystem, rotationMatrix: Matrix | null): void { + const defaultDir = new Vector3(0, 1, 0); + const rotatedDir = this._applyRotationToDirection(defaultDir, rotationMatrix); + + if (rotationMatrix) { + particleSystem.createPointEmitter(rotatedDir, rotatedDir); + } else { + particleSystem.createPointEmitter(Vector3.Zero(), Vector3.Zero()); } } /** * Apply emission bursts via emit rate gradients */ - private _applyEmissionBursts( - particleSystem: ParticleSystem, - bursts: import("../types/emitterConfig").VFXEmissionBurst[], - baseEmitRate: number, - duration: number, - _options?: VFXLoaderOptions - ): void { + private _applyEmissionBursts(particleSystem: ParticleSystem, bursts: VFXEmissionBurst[], baseEmitRate: number, duration: number): void { for (const burst of bursts) { - if (burst.time !== undefined && burst.count !== undefined) { - const burstTime = this._valueParser.parseConstantValue(burst.time); - const burstCount = this._valueParser.parseConstantValue(burst.count); - const timeRatio = Math.min(Math.max(burstTime / duration, 0), 1); + if (burst.time === undefined || burst.count === undefined) { + continue; + } - const windowSize = 0.02; - const burstEmitRate = burstCount / windowSize; + const burstTime = this._valueParser.parseConstantValue(burst.time); + const burstCount = this._valueParser.parseConstantValue(burst.count); + const timeRatio = Math.min(Math.max(burstTime / duration, 0), 1); + const windowSize = 0.02; + const burstEmitRate = burstCount / windowSize; - const beforeTime = Math.max(0, timeRatio - windowSize); - const afterTime = Math.min(1, timeRatio + windowSize); + const beforeTime = Math.max(0, timeRatio - windowSize); + const afterTime = Math.min(1, timeRatio + windowSize); - particleSystem.addEmitRateGradient(beforeTime, baseEmitRate); - particleSystem.addEmitRateGradient(timeRatio, burstEmitRate); - particleSystem.addEmitRateGradient(afterTime, baseEmitRate); - } + particleSystem.addEmitRateGradient(beforeTime, baseEmitRate); + particleSystem.addEmitRateGradient(timeRatio, burstEmitRate); + particleSystem.addEmitRateGradient(afterTime, baseEmitRate); } } /** * Apply behaviors to ParticleSystem */ - private _applyBehaviorsToPS(particleSystem: ParticleSystem, behaviors: import("../types/behaviors").VFXBehavior[]): void { + private _applyBehaviorsToPS(particleSystem: ParticleSystem, behaviors: VFXBehavior[]): void { + const vfxPS = particleSystem as any as VFXParticleSystem; + if (!vfxPS || typeof vfxPS.setPerParticleBehaviors !== "function") { + return; + } + + this._applySystemLevelBehaviors(particleSystem, behaviors); + + const perParticleFunctions = VFXBehaviorFunctionFactory.createPerParticleFunctionsPS(behaviors, this._valueParser, particleSystem); + vfxPS.setPerParticleBehaviors(perParticleFunctions); + } + + /** + * Applies system-level behaviors (gradients, etc.) + */ + private _applySystemLevelBehaviors(particleSystem: ParticleSystem, behaviors: VFXBehavior[]): void { const { options } = this._context; const valueParser = this._valueParser; - const vfxPS = particleSystem as any as VFXParticleSystem; - if (vfxPS && typeof vfxPS.setPerParticleBehaviors === "function") { - // Apply system-level behaviors (gradients, etc.) - for (const behavior of behaviors) { - if (!behavior.type) { - this._logger.warn(`Behavior missing type: ${JSON.stringify(behavior)}`, options); - continue; - } - - this._logger.log(` Processing behavior: ${behavior.type}`, options); - - switch (behavior.type) { - case "ColorOverLife": - applyColorOverLifePS(particleSystem, behavior as any); - break; - case "SizeOverLife": - applySizeOverLifePS(particleSystem, behavior as any); - break; - case "RotationOverLife": - case "Rotation3DOverLife": - applyRotationOverLifePS(particleSystem, behavior as any, valueParser); - break; - case "ForceOverLife": - case "ApplyForce": - applyForceOverLifePS(particleSystem, behavior as any, valueParser); - break; - case "GravityForce": - applyGravityForcePS(particleSystem, behavior as any, valueParser); - break; - case "SpeedOverLife": - applySpeedOverLifePS(particleSystem, behavior as any, valueParser); - break; - case "FrameOverLife": - applyFrameOverLifePS(particleSystem, behavior as any, valueParser); - break; - case "LimitSpeedOverLife": - applyLimitSpeedOverLifePS(particleSystem, behavior as any, valueParser); - break; - } + for (const behavior of behaviors) { + if (!behavior.type) { + this._logger.warn(`Behavior missing type: ${JSON.stringify(behavior)}`, options); + continue; } - // Create and set per-particle behavior functions - const perParticleFunctions = VFXBehaviorFunctionFactory.createPerParticleFunctionsPS(behaviors, valueParser, particleSystem); - vfxPS.setPerParticleBehaviors(perParticleFunctions); + this._logger.log(` Processing behavior: ${behavior.type}`, options); + this._applyBehaviorToPS(particleSystem, behavior, valueParser); + } + } + + /** + * Applies a single behavior to ParticleSystem + */ + private _applyBehaviorToPS(particleSystem: ParticleSystem, behavior: VFXBehavior, valueParser: VFXValueParser): void { + const behaviorHandlers: Record void> = { + ColorOverLife: (ps, b) => applyColorOverLifePS(ps, b as any), + SizeOverLife: (ps, b) => applySizeOverLifePS(ps, b as any), + RotationOverLife: (ps, b, vp) => applyRotationOverLifePS(ps, b as any, vp), + Rotation3DOverLife: (ps, b, vp) => applyRotationOverLifePS(ps, b as any, vp), + ForceOverLife: (ps, b, vp) => applyForceOverLifePS(ps, b as any, vp), + ApplyForce: (ps, b, vp) => applyForceOverLifePS(ps, b as any, vp), + GravityForce: (ps, b, vp) => applyGravityForcePS(ps, b as any, vp), + SpeedOverLife: (ps, b, vp) => applySpeedOverLifePS(ps, b as any, vp), + FrameOverLife: (ps, b, vp) => applyFrameOverLifePS(ps, b as any, vp), + LimitSpeedOverLife: (ps, b, vp) => applyLimitSpeedOverLifePS(ps, b as any, vp), + }; + + const handler = behaviorHandlers[behavior.type]; + if (handler) { + handler(particleSystem, behavior, valueParser); } } /** * Apply behaviors to SolidParticleSystem */ - private _applyBehaviorsToSPS(sps: SolidParticleSystem, behaviors: import("../types/behaviors").VFXBehavior[]): void { + private _applyBehaviorsToSPS(sps: SolidParticleSystem, behaviors: VFXBehavior[]): void { const vfxSPS = sps as any as VFXSolidParticleSystem; if (vfxSPS && typeof vfxSPS.setPerParticleBehaviors === "function") { const perParticleFunctions = VFXBehaviorFunctionFactory.createPerParticleFunctionsSPS(behaviors, this._valueParser); diff --git a/editor/src/editor/windows/fx-editor/VFX/factories/VFXGeometryFactory.ts b/editor/src/editor/windows/fx-editor/VFX/factories/VFXGeometryFactory.ts index 8551f6503..7e8c68fb2 100644 --- a/editor/src/editor/windows/fx-editor/VFX/factories/VFXGeometryFactory.ts +++ b/editor/src/editor/windows/fx-editor/VFX/factories/VFXGeometryFactory.ts @@ -1,9 +1,9 @@ -import { Scene, Mesh, VertexData, CreatePlane, Nullable } from "babylonjs"; +import { Mesh, VertexData, CreatePlane, Nullable } from "babylonjs"; import type { IVFXGeometryFactory } from "../types/factories"; import type { VFXParseContext } from "../types/context"; -import type { VFXLoaderOptions } from "../types/loader"; import { VFXLogger } from "../loggers/VFXLogger"; import { VFXMaterialFactory } from "./VFXMaterialFactory"; +import type { QuarksGeometry } from "../types/quarksTypes"; /** * Factory for creating meshes from Three.js geometry data @@ -23,124 +23,200 @@ export class VFXGeometryFactory implements IVFXGeometryFactory { * Create a mesh from geometry ID with material applied */ public createMesh(geometryId: string, materialId: string | undefined, name: string): Nullable { - const { jsonData, scene, options } = this._context; - + const { options } = this._context; this._logger.log(`Creating mesh from geometry ID: ${geometryId}, name: ${name}`, options); + + const geometryData = this._findGeometry(geometryId); + if (!geometryData) { + return null; + } + + this._logGeometryInfo(geometryData, geometryId); + + const mesh = this._createMeshFromGeometry(geometryData, name); + if (!mesh) { + this._logger.warn(`Failed to create mesh from geometry ${geometryId}`, options); + return null; + } + + this._applyMaterial(mesh, materialId, name); + return mesh; + } + + /** + * Finds geometry by UUID + */ + private _findGeometry(geometryId: string): QuarksGeometry | null { + const { jsonData, options } = this._context; + if (!jsonData.geometries) { this._logger.warn("No geometries data available", options); return null; } - // Find geometry - const geometryData = jsonData.geometries.find((g) => g.uuid === geometryId); - if (!geometryData) { + const geometry = jsonData.geometries.find((g) => g.uuid === geometryId); + if (!geometry) { this._logger.warn(`Geometry not found: ${geometryId}`, options); return null; } - this._logger.log(`Found geometry: ${geometryData.name || geometryData.type || geometryId} (type: ${geometryData.type})`, options); + return geometry; + } - // Create mesh from geometry - const mesh = this._createMeshFromGeometry(geometryData, scene, name, options); - if (!mesh) { - this._logger.warn(`Failed to create mesh from geometry ${geometryId}`, options); + /** + * Logs geometry information + */ + private _logGeometryInfo(geometryData: QuarksGeometry, geometryId: string): void { + const { options } = this._context; + const geometryName = geometryData.type || geometryId; + this._logger.log(`Found geometry: ${geometryName} (type: ${geometryData.type})`, options); + } + + /** + * Applies material to mesh if provided + */ + private _applyMaterial(mesh: Mesh, materialId: string | undefined, name: string): void { + if (!materialId) { + return; + } + + const { options } = this._context; + const material = this._materialFactory.createMaterial(materialId, name); + if (material) { + mesh.material = material; + this._logger.log(`Applied material to mesh: ${name}`, options); + } + } + + /** + * Creates mesh from geometry data based on type + */ + private _createMeshFromGeometry(geometryData: QuarksGeometry, name: string): Nullable { + const { options } = this._context; + this._logger.log(`createMeshFromGeometry: type=${geometryData.type}, name=${name}`, options); + + const geometryTypeHandlers: Record Nullable> = { + PlaneGeometry: (data, meshName) => this._createPlaneGeometry(data, meshName), + BufferGeometry: (data, meshName) => this._createBufferGeometry(data, meshName), + }; + + const handler = geometryTypeHandlers[geometryData.type]; + if (!handler) { + this._logger.warn(`Unsupported geometry type: ${geometryData.type}`, options); + return null; + } + + return handler(geometryData, name); + } + + /** + * Creates plane geometry mesh + */ + private _createPlaneGeometry(geometryData: QuarksGeometry, name: string): Nullable { + const { scene, options } = this._context; + const width = this._getNumericProperty(geometryData, "width", 1); + const height = this._getNumericProperty(geometryData, "height", 1); + + this._logger.log(`Creating PlaneGeometry: width=${width}, height=${height}`, options); + + const mesh = CreatePlane(name, { width, height }, scene); + if (mesh) { + this._logger.log(`PlaneGeometry created successfully`, options); + } else { + this._logger.warn(`Failed to create PlaneGeometry`, options); + } + + return mesh; + } + + /** + * Creates buffer geometry mesh + */ + private _createBufferGeometry(geometryData: QuarksGeometry, name: string): Nullable { + const { scene, options } = this._context; + + if (!geometryData.data?.attributes) { + this._logger.warn("BufferGeometry missing data or attributes", options); return null; } - // Apply material if provided - if (materialId) { - const material = this._materialFactory.createMaterial(materialId, name); - if (material) { - mesh.material = material; - this._logger.log(`Applied material to mesh: ${name}`, options); - } + const vertexData = this._createVertexDataFromAttributes(geometryData); + if (!vertexData) { + return null; } + const mesh = new Mesh(name, scene); + vertexData.applyToMesh(mesh); + this._convertToLeftHanded(mesh); + return mesh; } /** - * Create a mesh from Three.js geometry data + * Creates VertexData from BufferGeometry attributes */ - private _createMeshFromGeometry( - geometryData: import("../types/quarksTypes").QuarksGeometry, - scene: Scene, - name: string = "ParticleMesh", - options?: VFXLoaderOptions - ): Nullable { - if (!geometryData) { - this._logger.warn(`createMeshFromGeometry: geometryData is null`, options); + private _createVertexDataFromAttributes(geometryData: QuarksGeometry): Nullable { + const { options } = this._context; + + if (!geometryData.data?.attributes) { return null; } - this._logger.log(`createMeshFromGeometry: type=${geometryData.type}, name=${name}`, options); + const attrs = geometryData.data.attributes; + const positions = attrs.position; + if (!positions?.array) { + this._logger.warn("BufferGeometry missing position attribute", options); + return null; + } + + const vertexData = new VertexData(); + vertexData.positions = Array.from(positions.array); + + this._applyAttribute(vertexData, attrs.normal, "normals"); + this._applyAttribute(vertexData, attrs.uv, "uvs"); + this._applyAttribute(vertexData, attrs.color, "colors"); - // Handle PlaneGeometry - if (geometryData.type === "PlaneGeometry") { - const width = typeof geometryData.width === "number" ? geometryData.width : 1; - const height = typeof geometryData.height === "number" ? geometryData.height : 1; - this._logger.log(` Creating PlaneGeometry: width=${width}, height=${height}`, options); - const mesh = CreatePlane(name, { width, height }, scene); - if (mesh) { - this._logger.log(` PlaneGeometry created successfully`, options); - } else { - this._logger.warn(` Failed to create PlaneGeometry`, options); - } - return mesh; + const indices = geometryData.data.index; + if (indices?.array) { + vertexData.indices = Array.from(indices.array); + } else { + vertexData.indices = this._generateIndices(vertexData.positions.length); } - // Handle BufferGeometry - if (geometryData.type === "BufferGeometry" && geometryData.data && geometryData.data.attributes) { - const attrs = geometryData.data.attributes; - const positions = attrs.position; - const normals = attrs.normal; - const uvs = attrs.uv; - const colors = attrs.color; - const indices = geometryData.data.index; - - if (!positions || !positions.array) { - return null; - } - - const vertexData = new VertexData(); - vertexData.positions = Array.from(positions.array); - - if (normals && normals.array) { - vertexData.normals = Array.from(normals.array); - } - - if (uvs && uvs.array) { - vertexData.uvs = Array.from(uvs.array); - } - - if (colors && colors.array) { - vertexData.colors = Array.from(colors.array); - } - - if (indices && indices.array) { - vertexData.indices = Array.from(indices.array); - } else { - // Generate indices if not provided - const vertexCount = vertexData.positions.length / 3; - const generatedIndices: number[] = []; - for (let i = 0; i < vertexCount; i++) { - generatedIndices.push(i); - } - vertexData.indices = generatedIndices; - } - - const mesh = new Mesh(name, scene); - vertexData.applyToMesh(mesh); - - // Convert from Three.js (right-handed) to Babylon.js (left-handed) coordinate system - // This inverts Z coordinates, flips face winding, and negates normal Z - if (mesh.geometry) { - mesh.geometry.toLeftHanded(); - } - - return mesh; + return vertexData; + } + + /** + * Applies attribute data to VertexData if available + */ + private _applyAttribute(vertexData: VertexData, attribute: { array?: number[] } | undefined, property: "normals" | "uvs" | "colors"): void { + if (attribute?.array) { + (vertexData as any)[property] = Array.from(attribute.array); } + } - return null; + /** + * Generates sequential indices for vertices + */ + private _generateIndices(positionsLength: number): number[] { + const vertexCount = positionsLength / 3; + return Array.from({ length: vertexCount }, (_, i) => i); + } + + /** + * Converts mesh geometry from right-handed (Three.js) to left-handed (Babylon.js) coordinate system + */ + private _convertToLeftHanded(mesh: Mesh): void { + if (mesh.geometry) { + mesh.geometry.toLeftHanded(); + } + } + + /** + * Gets numeric property from geometry data with fallback + */ + private _getNumericProperty(geometryData: QuarksGeometry, property: string, defaultValue: number): number { + const value = (geometryData as any)[property]; + return typeof value === "number" ? value : defaultValue; } } diff --git a/editor/src/editor/windows/fx-editor/VFX/factories/VFXMaterialFactory.ts b/editor/src/editor/windows/fx-editor/VFX/factories/VFXMaterialFactory.ts index 8d75f9325..92f780545 100644 --- a/editor/src/editor/windows/fx-editor/VFX/factories/VFXMaterialFactory.ts +++ b/editor/src/editor/windows/fx-editor/VFX/factories/VFXMaterialFactory.ts @@ -1,9 +1,8 @@ -import { Nullable, Color3, Texture, PBRMaterial, Material, Constants, Tools, Scene } from "babylonjs"; +import { Nullable, Color3, Texture, PBRMaterial, Material, Constants, Tools } from "babylonjs"; import type { IVFXMaterialFactory } from "../types/factories"; import type { VFXParseContext } from "../types/context"; -import type { VFXLoaderOptions } from "../types/loader"; import { VFXLogger } from "../loggers/VFXLogger"; -import type { QuarksTexture } from "../types/quarksTypes"; +import type { QuarksTexture, QuarksMaterial, QuarksImage } from "../types/quarksTypes"; /** * Factory for creating materials and textures from Three.js JSON data @@ -21,93 +20,144 @@ export class VFXMaterialFactory implements IVFXMaterialFactory { * Create a texture from material ID (for ParticleSystem - no material needed) */ public createTexture(materialId: string): Nullable { - const { jsonData, scene, rootUrl, options } = this._context; + const textureData = this._resolveTextureData(materialId); + if (!textureData) { + return null; + } - if (!jsonData.materials || !jsonData.textures || !jsonData.images) { + const { texture, image } = textureData; + const textureUrl = this._buildTextureUrl(image); + return this._createTextureFromData(textureUrl, texture); + } + + /** + * Resolves material, texture, and image data from material ID + */ + private _resolveTextureData(materialId: string): { material: QuarksMaterial; texture: QuarksTexture; image: QuarksImage } | null { + const { options } = this._context; + + if (!this._hasRequiredData()) { this._logger.warn(`Missing materials/textures/images data for material ${materialId}`, options); return null; } - // Find material - const material = jsonData.materials.find((m) => m.uuid === materialId); - if (!material) { - this._logger.warn(`Material not found: ${materialId}`, options); + const material = this._findMaterial(materialId); + if (!material || !material.map) { return null; } - if (!material.map) { - this._logger.warn(`Material ${materialId} has no texture map`, options); + + const texture = this._findTexture(material.map); + if (!texture || !texture.image) { return null; } - // Find texture - const texture = jsonData.textures.find((t) => t.uuid === material.map); - if (!texture) { - this._logger.warn(`Texture not found: ${material.map}`, options); + const image = this._findImage(texture.image); + if (!image || !image.url) { return null; } - if (!texture.image) { - this._logger.warn(`Texture ${material.map} has no image`, options); + + return { material, texture, image }; + } + + /** + * Checks if required JSON data is available + */ + private _hasRequiredData(): boolean { + const { jsonData } = this._context; + return !!(jsonData.materials && jsonData.textures && jsonData.images); + } + + /** + * Finds material by UUID + */ + private _findMaterial(materialId: string): QuarksMaterial | null { + const { jsonData, options } = this._context; + const material = jsonData.materials?.find((m) => m.uuid === materialId); + if (!material) { + this._logger.warn(`Material not found: ${materialId}`, options); return null; } + return material; + } - // Find image - const image = jsonData.images.find((img) => img.uuid === texture.image); - if (!image) { - this._logger.warn(`Image not found: ${texture.image}`, options); + /** + * Finds texture by UUID + */ + private _findTexture(textureId: string): QuarksTexture | null { + const { jsonData, options } = this._context; + const texture = jsonData.textures?.find((t) => t.uuid === textureId); + if (!texture) { + this._logger.warn(`Texture not found: ${textureId}`, options); return null; } + return texture; + } - // Create texture URL from image data - let textureUrl: string; - if (image.url) { - textureUrl = Tools.GetAssetUrl(rootUrl + image.url); - } else if (image.data) { - // Base64 embedded texture - textureUrl = `data:image/${image.format || "png"};base64,${image.data}`; - } else { - this._logger.warn(`Image ${texture.image} has neither URL nor data`, options); + /** + * Finds image by UUID + */ + private _findImage(imageId: string): QuarksImage | null { + const { jsonData, options } = this._context; + const image = jsonData.images?.find((img) => img.uuid === imageId); + if (!image) { + this._logger.warn(`Image not found: ${imageId}`, options); return null; } + return image; + } - // Create texture using helper method - return this._createTextureFromData(textureUrl, texture, scene, options); + /** + * Builds texture URL from image data + */ + private _buildTextureUrl(image: QuarksImage): string { + const { rootUrl } = this._context; + if (!image.url) { + return ""; + } + const isBase64 = image.url.startsWith("data:"); + return isBase64 ? image.url : Tools.GetAssetUrl(rootUrl + image.url); } /** - * Helper method to create texture from texture data + * Parses sampling mode from Three.js texture filters */ - private _createTextureFromData(textureUrl: string, texture: QuarksTexture, scene: Scene, _options?: VFXLoaderOptions): Texture { - // Determine sampling mode from texture filters - let samplingMode = Texture.TRILINEAR_SAMPLINGMODE; // Default + private _parseSamplingMode(texture: QuarksTexture): number { + // Three.js filter constants: + // 1006 = LinearFilter (BILINEAR) + // 1007 = NearestMipmapLinearFilter + // 1008 = LinearMipmapLinearFilter (TRILINEAR) + // 1009 = LinearMipmapNearestFilter + if (texture.minFilter !== undefined) { if (texture.minFilter === 1008 || texture.minFilter === 1009) { - samplingMode = Texture.TRILINEAR_SAMPLINGMODE; - } else if (texture.minFilter === 1007 || texture.minFilter === 1006) { - samplingMode = Texture.BILINEAR_SAMPLINGMODE; - } else { - samplingMode = Texture.NEAREST_SAMPLINGMODE; + return Texture.TRILINEAR_SAMPLINGMODE; } - } else if (texture.magFilter !== undefined) { - if (texture.magFilter === 1006) { - samplingMode = Texture.BILINEAR_SAMPLINGMODE; - } else { - samplingMode = Texture.NEAREST_SAMPLINGMODE; + if (texture.minFilter === 1007 || texture.minFilter === 1006) { + return Texture.BILINEAR_SAMPLINGMODE; } + return Texture.NEAREST_SAMPLINGMODE; } - // Create texture with proper settings - const babylonTexture = new Texture(textureUrl, scene, { - noMipmap: !texture.generateMipmaps, - invertY: texture.flipY !== false, // Three.js flipY defaults to true - samplingMode: samplingMode, - }); + if (texture.magFilter !== undefined) { + return texture.magFilter === 1006 ? Texture.BILINEAR_SAMPLINGMODE : Texture.NEAREST_SAMPLINGMODE; + } - // Configure texture properties from Three.js JSON + return Texture.TRILINEAR_SAMPLINGMODE; + } + + /** + * Applies texture properties from Three.js JSON to Babylon.js texture + */ + private _applyTextureProperties(babylonTexture: Texture, texture: QuarksTexture): void { + // Wrap mode: Three.js 1000=Repeat, 1001=Clamp, 1002=Mirror if (texture.wrap && Array.isArray(texture.wrap)) { - const wrapU = texture.wrap[0] === 1000 ? Texture.WRAP_ADDRESSMODE : texture.wrap[0] === 1001 ? Texture.CLAMP_ADDRESSMODE : Texture.MIRROR_ADDRESSMODE; - const wrapV = texture.wrap[1] === 1000 ? Texture.WRAP_ADDRESSMODE : texture.wrap[1] === 1001 ? Texture.CLAMP_ADDRESSMODE : Texture.MIRROR_ADDRESSMODE; - babylonTexture.wrapU = wrapU; - babylonTexture.wrapV = wrapV; + const wrapModeMap: Record = { + 1000: Texture.WRAP_ADDRESSMODE, + 1001: Texture.CLAMP_ADDRESSMODE, + 1002: Texture.MIRROR_ADDRESSMODE, + }; + babylonTexture.wrapU = wrapModeMap[texture.wrap[0]] ?? Texture.WRAP_ADDRESSMODE; + babylonTexture.wrapV = wrapModeMap[texture.wrap[1]] ?? Texture.WRAP_ADDRESSMODE; } if (texture.repeat && Array.isArray(texture.repeat)) { @@ -120,14 +170,29 @@ export class VFXMaterialFactory implements IVFXMaterialFactory { babylonTexture.vOffset = texture.offset[1] || 0; } - if (texture.channel !== undefined && typeof texture.channel === "number") { + if (typeof texture.channel === "number") { babylonTexture.coordinatesIndex = texture.channel; } if (texture.rotation !== undefined) { babylonTexture.uAng = texture.rotation; } + } + /** + * Creates Babylon.js texture from texture data + */ + private _createTextureFromData(textureUrl: string, texture: QuarksTexture): Texture { + const { scene } = this._context; + const samplingMode = this._parseSamplingMode(texture); + + const babylonTexture = new Texture(textureUrl, scene, { + noMipmap: !texture.generateMipmaps, + invertY: texture.flipY !== false, + samplingMode, + }); + + this._applyTextureProperties(babylonTexture, texture); return babylonTexture; } @@ -135,225 +200,183 @@ export class VFXMaterialFactory implements IVFXMaterialFactory { * Create a material with texture from material ID */ public createMaterial(materialId: string, name: string): Nullable { - const { jsonData, scene, rootUrl, options } = this._context; - + const { options } = this._context; this._logger.log(`Creating material for ID: ${materialId}, name: ${name}`, options); - if (!jsonData.materials || !jsonData.textures || !jsonData.images) { - this._logger.warn(`Missing materials/textures/images data for material ${materialId}`, options); - return null; - } - // Find material - const material = jsonData.materials.find((m) => m.uuid === materialId); - if (!material) { - this._logger.warn(`Material not found: ${materialId}`, options); - return null; - } - if (!material.map) { - this._logger.warn(`Material ${materialId} has no texture map`, options); + const textureData = this._resolveTextureData(materialId); + if (!textureData) { return null; } + const { material, texture, image } = textureData; const materialType = material.type || "MeshStandardMaterial"; - this._logger.log(`Found material: type=${materialType}, uuid=${material.uuid}, transparent=${material.transparent}, blending=${material.blending}`, options); - // Find texture - const texture = jsonData.textures.find((t) => t.uuid === material.map); - if (!texture) { - this._logger.warn(`Texture not found: ${material.map}`, options); - return null; - } - if (!texture.image) { - this._logger.warn(`Texture ${material.map} has no image`, options); - return null; + this._logMaterialInfo(material, texture, image, materialType); + + const textureUrl = this._buildTextureUrl(image); + const babylonTexture = this._createTextureFromData(textureUrl, texture); + const materialColor = this._parseMaterialColor(material); + + if (materialType === "MeshBasicMaterial") { + return this._createUnlitMaterial(name, material, babylonTexture, materialColor); } + return new PBRMaterial(name + "_material", this._context.scene); + } + + /** + * Logs material, texture, and image information + */ + private _logMaterialInfo(material: QuarksMaterial, texture: QuarksTexture, image: QuarksImage, materialType: string): void { + const { options } = this._context; + this._logger.log(`Found material: type=${materialType}, uuid=${material.uuid}, transparent=${material.transparent}, blending=${material.blending}`, options); this._logger.log(`Found texture: ${JSON.stringify({ uuid: texture.uuid, image: texture.image })}`, options); - // Find image - const image = jsonData.images.find((img) => img.uuid === texture.image); - if (!image) { - this._logger.warn(`Image not found: ${texture.image}`, options); - return null; + const imageInfo = this._formatImageInfo(image); + this._logger.log(`Found image: ${imageInfo}`, options); + } + + /** + * Formats image information for logging + */ + private _formatImageInfo(image: QuarksImage): string { + if (!image.url) { + return "unknown"; } - const imageInfo: string[] = []; - if (image.url) { - const urlParts = image.url.split("/"); - let filename = urlParts[urlParts.length - 1] || image.url; - // If filename looks like base64 data (very long), truncate it - if (filename.length > 50) { - filename = filename.substring(0, 20) + "..."; - } - imageInfo.push(`file: ${filename}`); + const urlParts = image.url.split("/"); + let filename = urlParts[urlParts.length - 1] || image.url; + if (filename.length > 50) { + filename = filename.substring(0, 20) + "..."; } - if (image.data) { - imageInfo.push("embedded"); + return `file: ${filename}`; + } + + /** + * Parses material color from Three.js format + */ + private _parseMaterialColor(material: QuarksMaterial): Color3 { + const { options } = this._context; + + if (material.color === undefined) { + return new Color3(1, 1, 1); } - if (image.format) { - imageInfo.push(`format: ${image.format}`); + + const colorHex = this._parseColorHex(material.color); + const r = ((colorHex >> 16) & 0xff) / 255; + const g = ((colorHex >> 8) & 0xff) / 255; + const b = (colorHex & 0xff) / 255; + + this._logger.log(`Parsed material color: R=${r.toFixed(2)}, G=${g.toFixed(2)}, B=${b.toFixed(2)}`, options); + return new Color3(r, g, b); + } + + /** + * Parses color hex value from various formats + */ + private _parseColorHex(color: number | string): number { + if (typeof color === "number") { + return color; } - this._logger.log(`Found image: ${imageInfo.join(", ") || "unknown"}`, options); - - // Create texture URL from image data - let textureUrl: string; - if (image.url) { - textureUrl = Tools.GetAssetUrl(rootUrl + image.url); - // Extract filename from URL for logging - const urlParts = image.url.split("/"); - let filename = urlParts[urlParts.length - 1] || image.url; - // If filename looks like base64 data (very long), truncate it - if (filename.length > 50) { - filename = filename.substring(0, 20) + "..."; - } - this._logger.log(`Using external texture: ${filename}`, options); - } else if (image.data) { - // Base64 embedded texture - textureUrl = `data:image/${image.format || "png"};base64,${image.data}`; - this._logger.log(`Using base64 embedded texture (format: ${image.format || "png"})`, options); - } else { - this._logger.warn(`Image ${texture.image} has neither URL nor data`, options); - return null; + if (typeof color === "string") { + return parseInt(color.replace("#", ""), 16); } + return 0xffffff; + } - // Determine sampling mode from texture filters - let samplingMode = Texture.TRILINEAR_SAMPLINGMODE; // Default - if (texture.minFilter !== undefined) { - if (texture.minFilter === 1008 || texture.minFilter === 1009) { - samplingMode = Texture.TRILINEAR_SAMPLINGMODE; - } else if (texture.minFilter === 1007 || texture.minFilter === 1006) { - samplingMode = Texture.BILINEAR_SAMPLINGMODE; - } else { - samplingMode = Texture.NEAREST_SAMPLINGMODE; - } - } else if (texture.magFilter !== undefined) { - if (texture.magFilter === 1006) { - samplingMode = Texture.BILINEAR_SAMPLINGMODE; - } else { - samplingMode = Texture.NEAREST_SAMPLINGMODE; - } - } + /** + * Creates unlit material (MeshBasicMaterial equivalent) + */ + private _createUnlitMaterial(name: string, material: QuarksMaterial, texture: Texture, color: Color3): PBRMaterial { + const { scene, options } = this._context; + const unlitMaterial = new PBRMaterial(name + "_material", scene); - // Create texture with proper settings - const babylonTexture = new Texture(textureUrl, scene, { - noMipmap: !texture.generateMipmaps, - invertY: texture.flipY !== false, // Three.js flipY defaults to true - samplingMode: samplingMode, - }); + unlitMaterial.unlit = true; + unlitMaterial.albedoColor = color; + unlitMaterial.albedoTexture = texture; - // Configure texture properties from Three.js JSON - // wrap: [1001, 1001] = WRAP_ADDRESSMODE (repeat) - if (texture.wrap && Array.isArray(texture.wrap)) { - // Three.js wrap: 1000 = RepeatWrapping, 1001 = ClampToEdgeWrapping, 1002 = MirroredRepeatWrapping - // Babylon.js: WRAP_ADDRESSMODE = 0, CLAMP_ADDRESSMODE = 1, MIRROR_ADDRESSMODE = 2 - const wrapU = texture.wrap[0] === 1000 ? Texture.WRAP_ADDRESSMODE : texture.wrap[0] === 1001 ? Texture.CLAMP_ADDRESSMODE : Texture.MIRROR_ADDRESSMODE; - const wrapV = texture.wrap[1] === 1000 ? Texture.WRAP_ADDRESSMODE : texture.wrap[1] === 1001 ? Texture.CLAMP_ADDRESSMODE : Texture.MIRROR_ADDRESSMODE; - babylonTexture.wrapU = wrapU; - babylonTexture.wrapV = wrapV; - } + this._applyTransparency(unlitMaterial, material, texture); + this._applyDepthWrite(unlitMaterial, material); + this._applySideSettings(unlitMaterial, material); + this._applyBlendMode(unlitMaterial, material); - // repeat: [1, 1] -> uScale, vScale - if (texture.repeat && Array.isArray(texture.repeat)) { - babylonTexture.uScale = texture.repeat[0] || 1; - babylonTexture.vScale = texture.repeat[1] || 1; - } + this._logger.log(`Using MeshBasicMaterial: PBRMaterial with unlit=true, albedoTexture`, options); + this._logger.log(`Material created successfully: ${name}_material`, options); - // offset: [0, 0] -> uOffset, vOffset - if (texture.offset && Array.isArray(texture.offset)) { - babylonTexture.uOffset = texture.offset[0] || 0; - babylonTexture.vOffset = texture.offset[1] || 0; - } + return unlitMaterial; + } - // channel: 0 -> coordinatesIndex - if (texture.channel !== undefined && typeof texture.channel === "number") { - babylonTexture.coordinatesIndex = texture.channel; + /** + * Applies transparency settings to material + */ + private _applyTransparency(material: PBRMaterial, quarksMaterial: QuarksMaterial, texture: Texture): void { + const { options } = this._context; + + if (quarksMaterial.transparent) { + material.transparencyMode = Material.MATERIAL_ALPHABLEND; + material.needDepthPrePass = false; + texture.hasAlpha = true; + material.useAlphaFromAlbedoTexture = true; + this._logger.log(`Material is transparent (transparencyMode: ALPHABLEND, alphaMode: COMBINE)`, options); + } else { + material.transparencyMode = Material.MATERIAL_OPAQUE; + material.alpha = 1.0; } + } - // rotation: 0 -> uAng (rotation in radians) - if (texture.rotation !== undefined) { - babylonTexture.uAng = texture.rotation; - } + /** + * Applies depth write settings to material + */ + private _applyDepthWrite(material: PBRMaterial, quarksMaterial: QuarksMaterial): void { + const { options } = this._context; - // Parse color from Three.js material (default is white 0xffffff) - let materialColor = new Color3(1, 1, 1); - if (material.color !== undefined) { - // Three.js color is stored as hex number (e.g., 16777215 = 0xffffff) or hex string - let colorHex: number; - if (typeof material.color === "number") { - colorHex = material.color; - } else if (typeof material.color === "string") { - colorHex = parseInt((material.color as string).replace("#", ""), 16); - } else { - colorHex = 0xffffff; - } - const r = ((colorHex >> 16) & 0xff) / 255; - const g = ((colorHex >> 8) & 0xff) / 255; - const b = (colorHex & 0xff) / 255; - materialColor = new Color3(r, g, b); - this._logger.log(`Parsed material color: R=${r.toFixed(2)}, G=${g.toFixed(2)}, B=${b.toFixed(2)}`, options); + if (quarksMaterial.depthWrite !== undefined) { + material.disableDepthWrite = !quarksMaterial.depthWrite; + this._logger.log(`Set disableDepthWrite: ${!quarksMaterial.depthWrite}`, options); + } else { + material.disableDepthWrite = true; } + } - // Handle different Three.js material types - if (materialType === "MeshBasicMaterial") { - // MeshBasicMaterial: Use PBRMaterial with unlit = true (equivalent to UnlitMaterial) - const unlitMaterial = new PBRMaterial(name + "_material", scene); - unlitMaterial.unlit = true; - unlitMaterial.albedoColor = materialColor; - unlitMaterial.albedoTexture = babylonTexture; - - // Transparency - if (material.transparent !== undefined && material.transparent) { - unlitMaterial.transparencyMode = Material.MATERIAL_ALPHABLEND; - unlitMaterial.needDepthPrePass = false; - babylonTexture.hasAlpha = true; - unlitMaterial.useAlphaFromAlbedoTexture = true; - this._logger.log(`Material is transparent (transparencyMode: ALPHABLEND, alphaMode: COMBINE)`, options); - } else { - unlitMaterial.transparencyMode = Material.MATERIAL_OPAQUE; - unlitMaterial.alpha = 1.0; - } + /** + * Applies side orientation settings to material + */ + private _applySideSettings(material: PBRMaterial, quarksMaterial: QuarksMaterial): void { + const { options } = this._context; - // Depth write - if (material.depthWrite !== undefined) { - unlitMaterial.disableDepthWrite = !material.depthWrite; - this._logger.log(`Set disableDepthWrite: ${!material.depthWrite}`, options); - } else { - unlitMaterial.disableDepthWrite = true; // Default to false depthWrite = true disableDepthWrite - } + material.backFaceCulling = false; - // Double sided - unlitMaterial.backFaceCulling = false; + if (quarksMaterial.side !== undefined) { + material.sideOrientation = quarksMaterial.side; + this._logger.log(`Set sideOrientation: ${quarksMaterial.side}`, options); + } + } - // Side orientation - if (material.side !== undefined) { - // Three.js: 0 = FrontSide, 1 = BackSide, 2 = DoubleSide - // Babylon.js: 0 = Front, 1 = Back, 2 = Double - unlitMaterial.sideOrientation = material.side; - this._logger.log(`Set sideOrientation: ${material.side}`, options); - } + /** + * Applies blend mode to material + */ + private _applyBlendMode(material: PBRMaterial, quarksMaterial: QuarksMaterial): void { + const { options } = this._context; - // Blend mode - if (material.blending !== undefined) { - if (material.blending === 2) { - // Additive blending (Three.js AdditiveBlending) - unlitMaterial.alphaMode = Constants.ALPHA_ADD; - this._logger.log("Set blend mode: ADDITIVE", options); - } else if (material.blending === 1) { - // Normal blending (Three.js NormalBlending) - unlitMaterial.alphaMode = Constants.ALPHA_COMBINE; - this._logger.log("Set blend mode: NORMAL", options); - } else if (material.blending === 0) { - // No blending (Three.js NoBlending) - unlitMaterial.alphaMode = Constants.ALPHA_DISABLE; - this._logger.log("Set blend mode: NO_BLENDING", options); - } - } + if (quarksMaterial.blending === undefined) { + return; + } - this._logger.log(`Using MeshBasicMaterial: PBRMaterial with unlit=true, albedoTexture`, options); - this._logger.log(`Material created successfully: ${name}_material`, options); - return unlitMaterial; - } else { - return new PBRMaterial(name + "_material", scene); + const blendModeMap: Record = { + 0: Constants.ALPHA_DISABLE, // NoBlending + 1: Constants.ALPHA_COMBINE, // NormalBlending + 2: Constants.ALPHA_ADD, // AdditiveBlending + }; + + const alphaMode = blendModeMap[quarksMaterial.blending]; + if (alphaMode !== undefined) { + material.alphaMode = alphaMode; + const modeNames: Record = { + 0: "NO_BLENDING", + 1: "NORMAL", + 2: "ADDITIVE", + }; + this._logger.log(`Set blend mode: ${modeNames[quarksMaterial.blending]}`, options); } } } diff --git a/editor/src/editor/windows/fx-editor/VFX/systems/VFXSolidParticleSystem.ts b/editor/src/editor/windows/fx-editor/VFX/systems/VFXSolidParticleSystem.ts index f8258f512..29f2ea110 100644 --- a/editor/src/editor/windows/fx-editor/VFX/systems/VFXSolidParticleSystem.ts +++ b/editor/src/editor/windows/fx-editor/VFX/systems/VFXSolidParticleSystem.ts @@ -255,7 +255,6 @@ export class VFXSolidParticleSystem extends SolidParticleSystem { } private _initializeEmitterShape(particle: SolidParticle, emissionState: EmissionState): void { - console.log("initializeEmitterShape", particle, emissionState); const config = this._config; const startSpeed = particle.props?.startSpeed ?? 0; diff --git a/editor/src/editor/windows/fx-editor/VFX/treejs3dobject.particle.json b/editor/src/editor/windows/fx-editor/VFX/treejs3dobject.particle.json deleted file mode 100644 index bd7d7488c..000000000 --- a/editor/src/editor/windows/fx-editor/VFX/treejs3dobject.particle.json +++ /dev/null @@ -1,870 +0,0 @@ -{ - "metadata": { "version": 4.6, "type": "Object", "generator": "Object3D.toJSON" }, - "geometries": [ - { "uuid": "780917d8-bd1b-4d63-8aca-f79e3211f964", "type": "PlaneGeometry", "name": "PlaneGeometry", "width": 1, "height": 1, "widthSegments": 1, "heightSegments": 1 }, - { - "uuid": "f40b6ee0-aa01-46e0-b05a-d938b54eec83", - "type": "BufferGeometry", - "name": "GlowCircleEmitter_geometry", - "data": { - "attributes": { - "position": { - "itemSize": 3, - "type": "Float32Array", - "array": [ - 0.41758671402931213, 0.08306316286325455, 0.10689251124858856, 0.42576777935028076, -1.2535783966427516e-8, 0.10689251124858856, 0.3199999928474426, 0, - 0, 0.3199999928474426, 0, 0, 0.3138512670993805, 0.062428902834653854, 0, 0.41758671402931213, 0.08306316286325455, 0.10689251124858856, - 0.39335811138153076, 0.16293425858020782, 0.10689251124858856, 0.41758671402931213, 0.08306316286325455, 0.10689251124858856, 0.3138512670993805, - 0.062428902834653854, 0, 0.3138512670993805, 0.062428902834653854, 0, 0.2956414520740509, 0.12245870381593704, 0, 0.39335811138153076, - 0.16293425858020782, 0.10689251124858856, 0.35401293635368347, 0.23654387891292572, 0.10689251124858856, 0.39335811138153076, 0.16293425858020782, - 0.10689251124858856, 0.2956414520740509, 0.12245870381593704, 0, 0.2956414520740509, 0.12245870381593704, 0, 0.26607027649879456, 0.17778247594833374, - 0, 0.35401293635368347, 0.23654387891292572, 0.10689251124858856, 0.3010632395744324, 0.30106326937675476, 0.10689251124858856, 0.35401293635368347, - 0.23654387891292572, 0.10689251124858856, 0.26607027649879456, 0.17778247594833374, 0, 0.26607027649879456, 0.17778247594833374, 0, 0.22627416253089905, - 0.22627416253089905, 0, 0.3010632395744324, 0.30106326937675476, 0.10689251124858856, 0.23654384911060333, 0.35401299595832825, 0.10689251124858856, - 0.3010632395744324, 0.30106326937675476, 0.10689251124858856, 0.22627416253089905, 0.22627416253089905, 0, 0.22627416253089905, 0.22627416253089905, 0, - 0.17778246104717255, 0.26607027649879456, 0, 0.23654384911060333, 0.35401299595832825, 0.10689251124858856, 0.16293422877788544, 0.39335811138153076, - 0.10689251124858856, 0.23654384911060333, 0.35401299595832825, 0.10689251124858856, 0.17778246104717255, 0.26607027649879456, 0, 0.17778246104717255, - 0.26607027649879456, 0, 0.12245869636535645, 0.2956414520740509, 0, 0.16293422877788544, 0.39335811138153076, 0.10689251124858856, 0.08306317031383514, - 0.41758671402931213, 0.10689251124858856, 0.16293422877788544, 0.39335811138153076, 0.10689251124858856, 0.12245869636535645, 0.2956414520740509, 0, - 0.12245869636535645, 0.2956414520740509, 0, 0.06242891401052475, 0.3138512670993805, 0, 0.08306317031383514, 0.41758671402931213, 0.10689251124858856, - 2.0868840877596995e-8, 0.42576777935028076, 0.10689251124858856, 0.08306317031383514, 0.41758671402931213, 0.10689251124858856, 0.06242891401052475, - 0.3138512670993805, 0, 0.06242891401052475, 0.3138512670993805, 0, 2.415932875976523e-8, 0.3199999928474426, 0, 2.0868840877596995e-8, - 0.42576777935028076, 0.10689251124858856, -0.08306313306093216, 0.4175867438316345, 0.10689251124858856, 2.0868840877596995e-8, 0.42576777935028076, - 0.10689251124858856, 2.415932875976523e-8, 0.3199999928474426, 0, 2.415932875976523e-8, 0.3199999928474426, 0, -0.06242886558175087, 0.3138512969017029, - 0, -0.08306313306093216, 0.4175867438316345, 0.10689251124858856, -0.16293422877788544, 0.39335814118385315, 0.10689251124858856, -0.08306313306093216, - 0.4175867438316345, 0.10689251124858856, -0.06242886558175087, 0.3138512969017029, 0, -0.06242886558175087, 0.3138512969017029, 0, -0.12245865166187286, - 0.2956414520740509, 0, -0.16293422877788544, 0.39335814118385315, 0.10689251124858856, -0.23654387891292572, 0.35401299595832825, 0.10689251124858856, - -0.16293422877788544, 0.39335814118385315, 0.10689251124858856, -0.12245865166187286, 0.2956414520740509, 0, -0.12245865166187286, 0.2956414520740509, - 0, -0.17778246104717255, 0.26607027649879456, 0, -0.23654387891292572, 0.35401299595832825, 0.10689251124858856, -0.30106329917907715, - 0.30106326937675476, 0.10689251124858856, -0.23654387891292572, 0.35401299595832825, 0.10689251124858856, -0.17778246104717255, 0.26607027649879456, 0, - -0.17778246104717255, 0.26607027649879456, 0, -0.22627416253089905, 0.22627416253089905, 0, -0.30106329917907715, 0.30106326937675476, - 0.10689251124858856, -0.35401299595832825, 0.23654386401176453, 0.10689251124858856, -0.30106329917907715, 0.30106326937675476, 0.10689251124858856, - -0.22627416253089905, 0.22627416253089905, 0, -0.22627416253089905, 0.22627416253089905, 0, -0.26607027649879456, 0.17778246104717255, 0, - -0.35401299595832825, 0.23654386401176453, 0.10689251124858856, -0.39335814118385315, 0.16293418407440186, 0.10689251124858856, -0.35401299595832825, - 0.23654386401176453, 0.10689251124858856, -0.26607027649879456, 0.17778246104717255, 0, -0.26607027649879456, 0.17778246104717255, 0, - -0.2956414818763733, 0.12245865166187286, 0, -0.39335814118385315, 0.16293418407440186, 0.10689251124858856, -0.4175867438316345, 0.08306305855512619, - 0.10689251124858856, -0.39335814118385315, 0.16293418407440186, 0.10689251124858856, -0.2956414818763733, 0.12245865166187286, 0, -0.2956414818763733, - 0.12245865166187286, 0, -0.3138512969017029, 0.062428828328847885, 0, -0.4175867438316345, 0.08306305855512619, 0.10689251124858856, - -0.42576777935028076, -1.5126852304092608e-7, 0.10689251124858856, -0.4175867438316345, 0.08306305855512619, 0.10689251124858856, -0.3138512969017029, - 0.062428828328847885, 0, -0.3138512969017029, 0.062428828328847885, 0, -0.3199999928474426, -1.0426924035300544e-7, 0, -0.42576777935028076, - -1.5126852304092608e-7, 0.10689251124858856, -0.41758671402931213, -0.08306335657835007, 0.10689251124858856, -0.42576777935028076, - -1.5126852304092608e-7, 0.10689251124858856, -0.3199999928474426, -1.0426924035300544e-7, 0, -0.3199999928474426, -1.0426924035300544e-7, 0, - -0.3138512670993805, -0.0624290332198143, 0, -0.41758671402931213, -0.08306335657835007, 0.10689251124858856, -0.393358051776886, -0.16293445229530334, - 0.10689251124858856, -0.41758671402931213, -0.08306335657835007, 0.10689251124858856, -0.3138512670993805, -0.0624290332198143, 0, -0.3138512670993805, - -0.0624290332198143, 0, -0.29564139246940613, -0.12245883792638779, 0, -0.393358051776886, -0.16293445229530334, 0.10689251124858856, - -0.35401278734207153, -0.23654408752918243, 0.10689251124858856, -0.393358051776886, -0.16293445229530334, 0.10689251124858856, -0.29564139246940613, - -0.12245883792638779, 0, -0.29564139246940613, -0.12245883792638779, 0, -0.2660701870918274, -0.17778262495994568, 0, -0.35401278734207153, - -0.23654408752918243, 0.10689251124858856, -0.30106309056282043, -0.3010634481906891, 0.10689251124858856, -0.35401278734207153, -0.23654408752918243, - 0.10689251124858856, -0.2660701870918274, -0.17778262495994568, 0, -0.2660701870918274, -0.17778262495994568, 0, -0.2262740284204483, - -0.226274311542511, 0, -0.30106309056282043, -0.3010634481906891, 0.10689251124858856, -0.2365436553955078, -0.3540131449699402, 0.10689251124858856, - -0.30106309056282043, -0.3010634481906891, 0.10689251124858856, -0.2262740284204483, -0.226274311542511, 0, -0.2262740284204483, -0.226274311542511, 0, - -0.17778228223323822, -0.2660703957080841, 0, -0.2365436553955078, -0.3540131449699402, 0.10689251124858856, -0.16293397545814514, -0.3933582603931427, - 0.10689251124858856, -0.2365436553955078, -0.3540131449699402, 0.10689251124858856, -0.17778228223323822, -0.2660703957080841, 0, -0.17778228223323822, - -0.2660703957080841, 0, -0.12245845794677734, -0.29564154148101807, 0, -0.16293397545814514, -0.3933582603931427, 0.10689251124858856, - -0.08306281268596649, -0.4175868332386017, 0.10689251124858856, -0.16293397545814514, -0.3933582603931427, 0.10689251124858856, -0.12245845794677734, - -0.29564154148101807, 0, -0.12245845794677734, -0.29564154148101807, 0, -0.06242862716317177, -0.31385132670402527, 0, -0.08306281268596649, - -0.4175868332386017, 0.10689251124858856, 3.9984527688829985e-7, -0.42576777935028076, 0.10689251124858856, -0.08306281268596649, -0.4175868332386017, - 0.10689251124858856, -0.06242862716317177, -0.31385132670402527, 0, -0.06242862716317177, -0.31385132670402527, 0, 3.0899172998033464e-7, - -0.3199999928474426, 0, 3.9984527688829985e-7, -0.42576777935028076, 0.10689251124858856, 0.08306359499692917, -0.41758668422698975, - 0.10689251124858856, 3.9984527688829985e-7, -0.42576777935028076, 0.10689251124858856, 3.0899172998033464e-7, -0.3199999928474426, 0, - 3.0899172998033464e-7, -0.3199999928474426, 0, 0.06242923438549042, -0.3138512372970581, 0, 0.08306359499692917, -0.41758668422698975, - 0.10689251124858856, 0.16293466091156006, -0.39335793256759644, 0.10689251124858856, 0.08306359499692917, -0.41758668422698975, 0.10689251124858856, - 0.06242923438549042, -0.3138512372970581, 0, 0.06242923438549042, -0.3138512372970581, 0, 0.1224590316414833, -0.29564130306243896, 0, - 0.16293466091156006, -0.39335793256759644, 0.10689251124858856, 0.23654431104660034, -0.3540126383304596, 0.10689251124858856, 0.16293466091156006, - -0.39335793256759644, 0.10689251124858856, 0.1224590316414833, -0.29564130306243896, 0, 0.1224590316414833, -0.29564130306243896, 0, 0.17778280377388, - -0.26607006788253784, 0, 0.23654431104660034, -0.3540126383304596, 0.10689251124858856, 0.3010636270046234, -0.3010628819465637, 0.10689251124858856, - 0.23654431104660034, -0.3540126383304596, 0.10689251124858856, 0.17778280377388, -0.26607006788253784, 0, 0.17778280377388, -0.26607006788253784, 0, - 0.22627444565296173, -0.22627387940883636, 0, 0.3010636270046234, -0.3010628819465637, 0.10689251124858856, 0.3540132939815521, -0.23654340207576752, - 0.10689251124858856, 0.3010636270046234, -0.3010628819465637, 0.10689251124858856, 0.22627444565296173, -0.22627387940883636, 0, 0.22627444565296173, - -0.22627387940883636, 0, 0.26607051491737366, -0.1777821183204651, 0, 0.3540132939815521, -0.23654340207576752, 0.10689251124858856, - 0.39335834980010986, -0.16293370723724365, 0.10689251124858856, 0.3540132939815521, -0.23654340207576752, 0.10689251124858856, 0.26607051491737366, - -0.1777821183204651, 0, 0.26607051491737366, -0.1777821183204651, 0, 0.29564163088798523, -0.12245826423168182, 0, 0.39335834980010986, - -0.16293370723724365, 0.10689251124858856, 0.4175868630409241, -0.083062544465065, 0.10689251124858856, 0.39335834980010986, -0.16293370723724365, - 0.10689251124858856, 0.29564163088798523, -0.12245826423168182, 0, 0.29564163088798523, -0.12245826423168182, 0, 0.31385138630867004, - -0.06242842227220535, 0, 0.4175868630409241, -0.083062544465065, 0.10689251124858856, 0.42576777935028076, -1.2535783966427516e-8, 0.10689251124858856, - 0.4175868630409241, -0.083062544465065, 0.10689251124858856, 0.31385138630867004, -0.06242842227220535, 0, 0.31385138630867004, -0.06242842227220535, 0, - 0.3199999928474426, 0, 0, 0.42576777935028076, -1.2535783966427516e-8, 0.10689251124858856, 0.4175865948200226, 0.08306316286325455, - 0.10689251124858856, 0.31385117769241333, 0.062428902834653854, 0, 0.3199998736381531, 0, 0, 0.3199998736381531, 0, 0, 0.4257676303386688, - -1.2535783966427516e-8, 0.10689251124858856, 0.4175865948200226, 0.08306316286325455, 0.10689251124858856, 0.3933579623699188, 0.16293425858020782, - 0.10689251124858856, 0.29564133286476135, 0.12245870381593704, 0, 0.31385117769241333, 0.062428902834653854, 0, 0.31385117769241333, - 0.062428902834653854, 0, 0.4175865948200226, 0.08306316286325455, 0.10689251124858856, 0.3933579623699188, 0.16293425858020782, 0.10689251124858856, - 0.35401275753974915, 0.23654387891292572, 0.10689251124858856, 0.2660701274871826, 0.17778247594833374, 0, 0.29564133286476135, 0.12245870381593704, 0, - 0.29564133286476135, 0.12245870381593704, 0, 0.3933579623699188, 0.16293425858020782, 0.10689251124858856, 0.35401275753974915, 0.23654387891292572, - 0.10689251124858856, 0.30106306076049805, 0.30106326937675476, 0.10689251124858856, 0.2262740284204483, 0.22627416253089905, 0, 0.2660701274871826, - 0.17778247594833374, 0, 0.2660701274871826, 0.17778247594833374, 0, 0.35401275753974915, 0.23654387891292572, 0.10689251124858856, 0.30106306076049805, - 0.30106326937675476, 0.10689251124858856, 0.2365437150001526, 0.35401299595832825, 0.10689251124858856, 0.177782341837883, 0.26607027649879456, 0, - 0.2262740284204483, 0.22627416253089905, 0, 0.2262740284204483, 0.22627416253089905, 0, 0.30106306076049805, 0.30106326937675476, 0.10689251124858856, - 0.2365437150001526, 0.35401299595832825, 0.10689251124858856, 0.1629340499639511, 0.39335811138153076, 0.10689251124858856, 0.1224585697054863, - 0.2956414520740509, 0, 0.177782341837883, 0.26607027649879456, 0, 0.177782341837883, 0.26607027649879456, 0, 0.2365437150001526, 0.35401299595832825, - 0.10689251124858856, 0.1629340499639511, 0.39335811138153076, 0.10689251124858856, 0.08306301385164261, 0.41758671402931213, 0.10689251124858856, - 0.0624287948012352, 0.3138512670993805, 0, 0.1224585697054863, 0.2956414520740509, 0, 0.1224585697054863, 0.2956414520740509, 0, 0.1629340499639511, - 0.39335811138153076, 0.10689251124858856, 0.08306301385164261, 0.41758671402931213, 0.10689251124858856, -1.3816443811265344e-7, 0.42576777935028076, - 0.10689251124858856, -9.536743306171047e-8, 0.3199999928474426, 0, 0.0624287948012352, 0.3138512670993805, 0, 0.0624287948012352, 0.3138512670993805, 0, - 0.08306301385164261, 0.41758671402931213, 0.10689251124858856, -1.3816443811265344e-7, 0.42576777935028076, 0.10689251124858856, -0.0830632895231247, - 0.4175867438316345, 0.10689251124858856, -0.06242898479104042, 0.3138512969017029, 0, -9.536743306171047e-8, 0.3199999928474426, 0, - -9.536743306171047e-8, 0.3199999928474426, 0, -1.3816443811265344e-7, 0.42576777935028076, 0.10689251124858856, -0.0830632895231247, 0.4175867438316345, - 0.10689251124858856, -0.16293437778949738, 0.39335814118385315, 0.10689251124858856, -0.12245876342058182, 0.2956414520740509, 0, -0.06242898479104042, - 0.3138512969017029, 0, -0.06242898479104042, 0.3138512969017029, 0, -0.0830632895231247, 0.4175867438316345, 0.10689251124858856, -0.16293437778949738, - 0.39335814118385315, 0.10689251124858856, -0.23654407262802124, 0.35401299595832825, 0.10689251124858856, -0.1777825951576233, 0.26607027649879456, 0, - -0.12245876342058182, 0.2956414520740509, 0, -0.12245876342058182, 0.2956414520740509, 0, -0.16293437778949738, 0.39335814118385315, - 0.10689251124858856, -0.23654407262802124, 0.35401299595832825, 0.10689251124858856, -0.3010634481906891, 0.30106326937675476, 0.10689251124858856, - -0.2262742966413498, 0.22627416253089905, 0, -0.1777825951576233, 0.26607027649879456, 0, -0.1777825951576233, 0.26607027649879456, 0, - -0.23654407262802124, 0.35401299595832825, 0.10689251124858856, -0.3010634481906891, 0.30106326937675476, 0.10689251124858856, -0.3540131449699402, - 0.23654386401176453, 0.10689251124858856, -0.2660703957080841, 0.17778246104717255, 0, -0.2262742966413498, 0.22627416253089905, 0, -0.2262742966413498, - 0.22627416253089905, 0, -0.3010634481906891, 0.30106326937675476, 0.10689251124858856, -0.3540131449699402, 0.23654386401176453, 0.10689251124858856, - -0.3933583199977875, 0.16293418407440186, 0.10689251124858856, -0.29564160108566284, 0.12245865166187286, 0, -0.2660703957080841, 0.17778246104717255, - 0, -0.2660703957080841, 0.17778246104717255, 0, -0.3540131449699402, 0.23654386401176453, 0.10689251124858856, -0.3933583199977875, 0.16293418407440186, - 0.10689251124858856, -0.41758692264556885, 0.08306305855512619, 0.10689251124858856, -0.3138514459133148, 0.062428828328847885, 0, -0.29564160108566284, - 0.12245865166187286, 0, -0.29564160108566284, 0.12245865166187286, 0, -0.3933583199977875, 0.16293418407440186, 0.10689251124858856, - -0.41758692264556885, 0.08306305855512619, 0.10689251124858856, -0.4257678985595703, -1.5126852304092608e-7, 0.10689251124858856, -0.3200001120567322, - -1.0426924035300544e-7, 0, -0.3138514459133148, 0.062428828328847885, 0, -0.3138514459133148, 0.062428828328847885, 0, -0.41758692264556885, - 0.08306305855512619, 0.10689251124858856, -0.4257678985595703, -1.5126852304092608e-7, 0.10689251124858856, -0.41758689284324646, -0.08306335657835007, - 0.10689251124858856, -0.31385138630867004, -0.0624290332198143, 0, -0.3200001120567322, -1.0426924035300544e-7, 0, -0.3200001120567322, - -1.0426924035300544e-7, 0, -0.4257678985595703, -1.5126852304092608e-7, 0.10689251124858856, -0.41758689284324646, -0.08306335657835007, - 0.10689251124858856, -0.3933582305908203, -0.16293445229530334, 0.10689251124858856, -0.2956415116786957, -0.12245883792638779, 0, -0.31385138630867004, - -0.0624290332198143, 0, -0.31385138630867004, -0.0624290332198143, 0, -0.41758689284324646, -0.08306335657835007, 0.10689251124858856, - -0.3933582305908203, -0.16293445229530334, 0.10689251124858856, -0.35401299595832825, -0.23654408752918243, 0.10689251124858856, -0.26607027649879456, - -0.17778262495994568, 0, -0.2956415116786957, -0.12245883792638779, 0, -0.2956415116786957, -0.12245883792638779, 0, -0.3933582305908203, - -0.16293445229530334, 0.10689251124858856, -0.35401299595832825, -0.23654408752918243, 0.10689251124858856, -0.3010632395744324, -0.3010634481906891, - 0.10689251124858856, -0.22627414762973785, -0.226274311542511, 0, -0.26607027649879456, -0.17778262495994568, 0, -0.26607027649879456, - -0.17778262495994568, 0, -0.35401299595832825, -0.23654408752918243, 0.10689251124858856, -0.3010632395744324, -0.3010634481906891, 0.10689251124858856, - -0.23654380440711975, -0.3540131449699402, 0.10689251124858856, -0.17778240144252777, -0.2660703957080841, 0, -0.22627414762973785, -0.226274311542511, - 0, -0.22627414762973785, -0.226274311542511, 0, -0.3010632395744324, -0.3010634481906891, 0.10689251124858856, -0.23654380440711975, - -0.3540131449699402, 0.10689251124858856, -0.16293412446975708, -0.3933582603931427, 0.10689251124858856, -0.1224585697054863, -0.29564154148101807, 0, - -0.17778240144252777, -0.2660703957080841, 0, -0.17778240144252777, -0.2660703957080841, 0, -0.23654380440711975, -0.3540131449699402, - 0.10689251124858856, -0.16293412446975708, -0.3933582603931427, 0.10689251124858856, -0.08306297659873962, -0.4175868332386017, 0.10689251124858856, - -0.06242874637246132, -0.31385132670402527, 0, -0.1224585697054863, -0.29564154148101807, 0, -0.1224585697054863, -0.29564154148101807, 0, - -0.16293412446975708, -0.3933582603931427, 0.10689251124858856, -0.08306297659873962, -0.4175868332386017, 0.10689251124858856, 2.4250164187833434e-7, - -0.42576777935028076, 0.10689251124858856, 1.9073486612342094e-7, -0.3199999928474426, 0, -0.06242874637246132, -0.31385132670402527, 0, - -0.06242874637246132, -0.31385132670402527, 0, -0.08306297659873962, -0.4175868332386017, 0.10689251124858856, 2.4250164187833434e-7, - -0.42576777935028076, 0.10689251124858856, 0.08306345343589783, -0.41758668422698975, 0.10689251124858856, 0.062429118901491165, -0.3138512372970581, 0, - 1.9073486612342094e-7, -0.3199999928474426, 0, 1.9073486612342094e-7, -0.3199999928474426, 0, 2.4250164187833434e-7, -0.42576777935028076, - 0.10689251124858856, 0.08306345343589783, -0.41758668422698975, 0.10689251124858856, 0.16293452680110931, -0.39335793256759644, 0.10689251124858856, - 0.12245891243219376, -0.29564130306243896, 0, 0.062429118901491165, -0.3138512372970581, 0, 0.062429118901491165, -0.3138512372970581, 0, - 0.08306345343589783, -0.41758668422698975, 0.10689251124858856, 0.16293452680110931, -0.39335793256759644, 0.10689251124858856, 0.2365441471338272, - -0.3540126383304596, 0.10689251124858856, 0.17778268456459045, -0.26607006788253784, 0, 0.12245891243219376, -0.29564130306243896, 0, - 0.12245891243219376, -0.29564130306243896, 0, 0.16293452680110931, -0.39335793256759644, 0.10689251124858856, 0.2365441471338272, -0.3540126383304596, - 0.10689251124858856, 0.3010634779930115, -0.3010628819465637, 0.10689251124858856, 0.22627434134483337, -0.22627387940883636, 0, 0.17778268456459045, - -0.26607006788253784, 0, 0.17778268456459045, -0.26607006788253784, 0, 0.2365441471338272, -0.3540126383304596, 0.10689251124858856, 0.3010634779930115, - -0.3010628819465637, 0.10689251124858856, 0.3540131449699402, -0.23654340207576752, 0.10689251124858856, 0.2660703957080841, -0.1777821183204651, 0, - 0.22627434134483337, -0.22627387940883636, 0, 0.22627434134483337, -0.22627387940883636, 0, 0.3010634779930115, -0.3010628819465637, - 0.10689251124858856, 0.3540131449699402, -0.23654340207576752, 0.10689251124858856, 0.3933582305908203, -0.16293370723724365, 0.10689251124858856, - 0.2956415116786957, -0.12245826423168182, 0, 0.2660703957080841, -0.1777821183204651, 0, 0.2660703957080841, -0.1777821183204651, 0, 0.3540131449699402, - -0.23654340207576752, 0.10689251124858856, 0.3933582305908203, -0.16293370723724365, 0.10689251124858856, 0.41758668422698975, -0.083062544465065, - 0.10689251124858856, 0.3138512372970581, -0.06242842227220535, 0, 0.2956415116786957, -0.12245826423168182, 0, 0.2956415116786957, -0.12245826423168182, - 0, 0.3933582305908203, -0.16293370723724365, 0.10689251124858856, 0.41758668422698975, -0.083062544465065, 0.10689251124858856, 0.4257676303386688, - -1.2535783966427516e-8, 0.10689251124858856, 0.3199998736381531, 0, 0, 0.3138512372970581, -0.06242842227220535, 0, 0.3138512372970581, - -0.06242842227220535, 0, 0.41758668422698975, -0.083062544465065, 0.10689251124858856, 0.4257676303386688, -1.2535783966427516e-8, 0.10689251124858856 - ], - "normalized": false - }, - "normal": { - "itemSize": 3, - "type": "Float32Array", - "array": [ - 0.6971781849861145, 0.1386774480342865, -0.7033570408821106, 0.7108367085456848, 3.257474361362256e-7, -0.7033571004867554, 0.71083664894104, - 3.6210147413839877e-7, -0.7033571600914001, 0.71083664894104, 3.6210147413839877e-7, -0.7033571600914001, 0.6971781849861145, 0.1386774480342865, - -0.7033570408821106, 0.6971781849861145, 0.1386774480342865, -0.7033570408821106, 0.6567274928092957, 0.27202534675598145, -0.7033570408821106, - 0.6971781849861145, 0.1386774480342865, -0.7033570408821106, 0.6971781849861145, 0.1386774480342865, -0.7033570408821106, 0.6971781849861145, - 0.1386774480342865, -0.7033570408821106, 0.6567275524139404, 0.27202534675598145, -0.7033570408821106, 0.6567274928092957, 0.27202534675598145, - -0.7033570408821106, 0.5910391211509705, 0.394919753074646, -0.7033570408821106, 0.6567274928092957, 0.27202534675598145, -0.7033570408821106, - 0.6567275524139404, 0.27202534675598145, -0.7033570408821106, 0.6567275524139404, 0.27202534675598145, -0.7033570408821106, 0.5910391211509705, - 0.3949197828769684, -0.7033570408821106, 0.5910391211509705, 0.394919753074646, -0.7033570408821106, 0.5026374459266663, 0.5026374459266663, - -0.7033571004867554, 0.5910391211509705, 0.394919753074646, -0.7033570408821106, 0.5910391211509705, 0.3949197828769684, -0.7033570408821106, - 0.5910391211509705, 0.3949197828769684, -0.7033570408821106, 0.5026374459266663, 0.5026374459266663, -0.7033570408821106, 0.5026374459266663, - 0.5026374459266663, -0.7033571004867554, 0.39491966366767883, 0.5910391807556152, -0.7033571004867554, 0.5026374459266663, 0.5026374459266663, - -0.7033571004867554, 0.5026374459266663, 0.5026374459266663, -0.7033570408821106, 0.5026374459266663, 0.5026374459266663, -0.7033570408821106, - 0.39491966366767883, 0.5910391211509705, -0.7033571004867554, 0.39491966366767883, 0.5910391807556152, -0.7033571004867554, 0.27202531695365906, - 0.6567276120185852, -0.7033570408821106, 0.39491966366767883, 0.5910391807556152, -0.7033571004867554, 0.39491966366767883, 0.5910391211509705, - -0.7033571004867554, 0.39491966366767883, 0.5910391211509705, -0.7033571004867554, 0.27202528715133667, 0.6567275524139404, -0.7033570408821106, - 0.27202531695365906, 0.6567276120185852, -0.7033570408821106, 0.13867750763893127, 0.6971781849861145, -0.7033570408821106, 0.27202531695365906, - 0.6567276120185852, -0.7033570408821106, 0.27202528715133667, 0.6567275524139404, -0.7033570408821106, 0.27202528715133667, 0.6567275524139404, - -0.7033570408821106, 0.13867750763893127, 0.6971781849861145, -0.7033570408821106, 0.13867750763893127, 0.6971781849861145, -0.7033570408821106, - 1.3676421417585516e-7, 0.71083664894104, -0.7033571004867554, 0.13867750763893127, 0.6971781849861145, -0.7033570408821106, 0.13867750763893127, - 0.6971781849861145, -0.7033570408821106, 0.13867750763893127, 0.6971781849861145, -0.7033570408821106, 1.525836097471256e-7, 0.7108367085456848, - -0.7033571004867554, 1.3676421417585516e-7, 0.71083664894104, -0.7033571004867554, -0.13867735862731934, 0.6971781253814697, -0.7033571004867554, - 1.3676421417585516e-7, 0.71083664894104, -0.7033571004867554, 1.525836097471256e-7, 0.7108367085456848, -0.7033571004867554, 1.525836097471256e-7, - 0.7108367085456848, -0.7033571004867554, -0.13867734372615814, 0.6971781253814697, -0.7033571004867554, -0.13867735862731934, 0.6971781253814697, - -0.7033571004867554, -0.2720252573490143, 0.6567274332046509, -0.7033571600914001, -0.13867735862731934, 0.6971781253814697, -0.7033571004867554, - -0.13867734372615814, 0.6971781253814697, -0.7033571004867554, -0.13867734372615814, 0.6971781253814697, -0.7033571004867554, -0.2720252573490143, - 0.6567274928092957, -0.7033571600914001, -0.2720252573490143, 0.6567274332046509, -0.7033571600914001, -0.39491966366767883, 0.5910390019416809, - -0.7033572196960449, -0.2720252573490143, 0.6567274332046509, -0.7033571600914001, -0.2720252573490143, 0.6567274928092957, -0.7033571600914001, - -0.2720252573490143, 0.6567274928092957, -0.7033571600914001, -0.39491963386535645, 0.5910390615463257, -0.7033571600914001, -0.39491966366767883, - 0.5910390019416809, -0.7033572196960449, -0.502637505531311, 0.5026373863220215, -0.7033571004867554, -0.39491966366767883, 0.5910390019416809, - -0.7033572196960449, -0.39491963386535645, 0.5910390615463257, -0.7033571600914001, -0.39491963386535645, 0.5910390615463257, -0.7033571600914001, - -0.5026374459266663, 0.5026373863220215, -0.7033571600914001, -0.502637505531311, 0.5026373863220215, -0.7033571004867554, -0.5910391211509705, - 0.39491963386535645, -0.7033571600914001, -0.502637505531311, 0.5026373863220215, -0.7033571004867554, -0.5026374459266663, 0.5026373863220215, - -0.7033571600914001, -0.5026374459266663, 0.5026373863220215, -0.7033571600914001, -0.5910391211509705, 0.39491963386535645, -0.7033571600914001, - -0.5910391211509705, 0.39491963386535645, -0.7033571600914001, -0.6567275524139404, 0.2720252275466919, -0.7033571004867554, -0.5910391211509705, - 0.39491963386535645, -0.7033571600914001, -0.5910391211509705, 0.39491963386535645, -0.7033571600914001, -0.5910391211509705, 0.39491963386535645, - -0.7033571600914001, -0.6567275524139404, 0.2720251977443695, -0.7033571004867554, -0.6567275524139404, 0.2720252275466919, -0.7033571004867554, - -0.6971781849861145, 0.1386772245168686, -0.7033570408821106, -0.6567275524139404, 0.2720252275466919, -0.7033571004867554, -0.6567275524139404, - 0.2720251977443695, -0.7033571004867554, -0.6567275524139404, 0.2720251977443695, -0.7033571004867554, -0.6971781849861145, 0.1386772245168686, - -0.7033570408821106, -0.6971781849861145, 0.1386772245168686, -0.7033570408821106, -0.71083664894104, -1.9644313908884214e-7, -0.7033571004867554, - -0.6971781849861145, 0.1386772245168686, -0.7033570408821106, -0.6971781849861145, 0.1386772245168686, -0.7033570408821106, -0.6971781849861145, - 0.1386772245168686, -0.7033570408821106, -0.71083664894104, -1.8446674232563964e-7, -0.7033571004867554, -0.71083664894104, -1.9644313908884214e-7, - -0.7033571004867554, -0.697178065776825, -0.13867776095867157, -0.7033571004867554, -0.71083664894104, -1.9644313908884214e-7, -0.7033571004867554, - -0.71083664894104, -1.8446674232563964e-7, -0.7033571004867554, -0.71083664894104, -1.8446674232563964e-7, -0.7033571004867554, -0.697178065776825, - -0.13867774605751038, -0.7033571004867554, -0.697178065776825, -0.13867776095867157, -0.7033571004867554, -0.6567273139953613, -0.27202582359313965, - -0.7033571004867554, -0.697178065776825, -0.13867776095867157, -0.7033571004867554, -0.697178065776825, -0.13867774605751038, -0.7033571004867554, - -0.697178065776825, -0.13867774605751038, -0.7033571004867554, -0.6567272543907166, -0.27202582359313965, -0.7033571004867554, -0.6567273139953613, - -0.27202582359313965, -0.7033571004867554, -0.5910389423370361, -0.3949200212955475, -0.7033571004867554, -0.6567273139953613, -0.27202582359313965, - -0.7033571004867554, -0.6567272543907166, -0.27202582359313965, -0.7033571004867554, -0.6567272543907166, -0.27202582359313965, -0.7033571004867554, - -0.5910389423370361, -0.3949199914932251, -0.7033570408821106, -0.5910389423370361, -0.3949200212955475, -0.7033571004867554, -0.5026372671127319, - -0.5026376843452454, -0.7033571004867554, -0.5910389423370361, -0.3949200212955475, -0.7033571004867554, -0.5910389423370361, -0.3949199914932251, - -0.7033570408821106, -0.5910389423370361, -0.3949199914932251, -0.7033570408821106, -0.5026372075080872, -0.5026376843452454, -0.7033571004867554, - -0.5026372671127319, -0.5026376843452454, -0.7033571004867554, -0.3949193060398102, -0.5910392999649048, -0.7033571600914001, -0.5026372671127319, - -0.5026376843452454, -0.7033571004867554, -0.5026372075080872, -0.5026376843452454, -0.7033571004867554, -0.5026372075080872, -0.5026376843452454, - -0.7033571004867554, -0.39491933584213257, -0.5910392999649048, -0.7033571600914001, -0.3949193060398102, -0.5910392999649048, -0.7033571600914001, - -0.27202484011650085, -0.6567276120185852, -0.7033571600914001, -0.3949193060398102, -0.5910392999649048, -0.7033571600914001, -0.39491933584213257, - -0.5910392999649048, -0.7033571600914001, -0.39491933584213257, -0.5910392999649048, -0.7033571600914001, -0.27202484011650085, -0.65672767162323, - -0.7033571600914001, -0.27202484011650085, -0.6567276120185852, -0.7033571600914001, -0.1386767476797104, -0.697178304195404, -0.7033571004867554, - -0.27202484011650085, -0.6567276120185852, -0.7033571600914001, -0.27202484011650085, -0.65672767162323, -0.7033571600914001, -0.27202484011650085, - -0.65672767162323, -0.7033571600914001, -0.1386767327785492, -0.6971782445907593, -0.7033571004867554, -0.1386767476797104, -0.697178304195404, - -0.7033571004867554, 6.514948722724512e-7, -0.7108367085456848, -0.7033571004867554, -0.1386767476797104, -0.697178304195404, -0.7033571004867554, - -0.1386767327785492, -0.6971782445907593, -0.7033571004867554, -0.1386767327785492, -0.6971782445907593, -0.7033571004867554, 6.536043883897946e-7, - -0.71083664894104, -0.7033571004867554, 6.514948722724512e-7, -0.7108367085456848, -0.7033571004867554, 0.1386781930923462, -0.6971779465675354, - -0.7033571004867554, 6.514948722724512e-7, -0.7108367085456848, -0.7033571004867554, 6.536043883897946e-7, -0.71083664894104, -0.7033571004867554, - 6.536043883897946e-7, -0.71083664894104, -0.7033571004867554, 0.1386781930923462, -0.6971779465675354, -0.7033571004867554, 0.1386781930923462, - -0.6971779465675354, -0.7033571004867554, 0.2720262110233307, -0.6567271947860718, -0.7033570408821106, 0.1386781930923462, -0.6971779465675354, - -0.7033571004867554, 0.1386781930923462, -0.6971779465675354, -0.7033571004867554, 0.1386781930923462, -0.6971779465675354, -0.7033571004867554, - 0.2720262408256531, -0.6567271947860718, -0.7033570408821106, 0.2720262110233307, -0.6567271947860718, -0.7033570408821106, 0.3949204683303833, - -0.591038703918457, -0.7033569812774658, 0.2720262110233307, -0.6567271947860718, -0.7033570408821106, 0.2720262408256531, -0.6567271947860718, - -0.7033570408821106, 0.2720262408256531, -0.6567271947860718, -0.7033570408821106, 0.3949204683303833, -0.591038703918457, -0.7033569812774658, - 0.3949204683303833, -0.591038703918457, -0.7033569812774658, 0.5026381015777588, -0.5026369094848633, -0.7033569812774658, 0.3949204683303833, - -0.591038703918457, -0.7033569812774658, 0.3949204683303833, -0.591038703918457, -0.7033569812774658, 0.3949204683303833, -0.591038703918457, - -0.7033569812774658, 0.502638041973114, -0.5026369094848633, -0.7033570408821106, 0.5026381015777588, -0.5026369094848633, -0.7033569812774658, - 0.5910396575927734, -0.39491894841194153, -0.7033571004867554, 0.5026381015777588, -0.5026369094848633, -0.7033569812774658, 0.502638041973114, - -0.5026369094848633, -0.7033570408821106, 0.502638041973114, -0.5026369094848633, -0.7033570408821106, 0.5910395979881287, -0.3949189782142639, - -0.7033571004867554, 0.5910396575927734, -0.39491894841194153, -0.7033571004867554, 0.6567279100418091, -0.2720244228839874, -0.7033571004867554, - 0.5910396575927734, -0.39491894841194153, -0.7033571004867554, 0.5910395979881287, -0.3949189782142639, -0.7033571004867554, 0.5910395979881287, - -0.3949189782142639, -0.7033571004867554, 0.6567278504371643, -0.27202436327934265, -0.7033571004867554, 0.6567279100418091, -0.2720244228839874, - -0.7033571004867554, 0.697178304195404, -0.13867662847042084, -0.7033571004867554, 0.6567279100418091, -0.2720244228839874, -0.7033571004867554, - 0.6567278504371643, -0.27202436327934265, -0.7033571004867554, 0.6567278504371643, -0.27202436327934265, -0.7033571004867554, 0.697178304195404, - -0.13867664337158203, -0.7033571004867554, 0.697178304195404, -0.13867662847042084, -0.7033571004867554, 0.7108367085456848, 3.257474361362256e-7, - -0.7033571004867554, 0.697178304195404, -0.13867662847042084, -0.7033571004867554, 0.697178304195404, -0.13867664337158203, -0.7033571004867554, - 0.697178304195404, -0.13867664337158203, -0.7033571004867554, 0.71083664894104, 3.6210147413839877e-7, -0.7033571600914001, 0.7108367085456848, - 3.257474361362256e-7, -0.7033571004867554, -0.6971782445907593, -0.1386774629354477, 0.7033569812774658, -0.6971781849861145, -0.1386774629354477, - 0.7033570408821106, -0.7108367085456848, -1.1159099955193597e-7, 0.7033570408821106, -0.7108367085456848, -1.1159099955193597e-7, 0.7033570408821106, - -0.7108367085456848, -9.200498851669181e-8, 0.7033570408821106, -0.6971782445907593, -0.1386774629354477, 0.7033569812774658, -0.6567275524139404, - -0.27202561497688293, 0.7033569812774658, -0.6567274928092957, -0.27202558517456055, 0.7033569812774658, -0.6971781849861145, -0.1386774629354477, - 0.7033570408821106, -0.6971781849861145, -0.1386774629354477, 0.7033570408821106, -0.6971782445907593, -0.1386774629354477, 0.7033569812774658, - -0.6567275524139404, -0.27202561497688293, 0.7033569812774658, -0.5910391807556152, -0.3949199616909027, 0.703356921672821, -0.5910391211509705, - -0.3949199616909027, 0.703356921672821, -0.6567274928092957, -0.27202558517456055, 0.7033569812774658, -0.6567274928092957, -0.27202558517456055, - 0.7033569812774658, -0.6567275524139404, -0.27202561497688293, 0.7033569812774658, -0.5910391807556152, -0.3949199616909027, 0.703356921672821, - -0.5026376247406006, -0.502637505531311, 0.703356921672821, -0.5026376247406006, -0.5026374459266663, 0.703356921672821, -0.5910391211509705, - -0.3949199616909027, 0.703356921672821, -0.5910391211509705, -0.3949199616909027, 0.703356921672821, -0.5910391807556152, -0.3949199616909027, - 0.703356921672821, -0.5026376247406006, -0.502637505531311, 0.703356921672821, -0.3949197232723236, -0.5910391807556152, 0.7033570408821106, - -0.394919753074646, -0.5910391807556152, 0.7033569812774658, -0.5026376247406006, -0.5026374459266663, 0.703356921672821, -0.5026376247406006, - -0.5026374459266663, 0.703356921672821, -0.5026376247406006, -0.502637505531311, 0.703356921672821, -0.3949197232723236, -0.5910391807556152, - 0.7033570408821106, -0.27202528715133667, -0.65672767162323, 0.7033569812774658, -0.27202528715133667, -0.65672767162323, 0.703356921672821, - -0.394919753074646, -0.5910391807556152, 0.7033569812774658, -0.394919753074646, -0.5910391807556152, 0.7033569812774658, -0.3949197232723236, - -0.5910391807556152, 0.7033570408821106, -0.27202528715133667, -0.65672767162323, 0.7033569812774658, -0.13867752254009247, -0.6971781849861145, - 0.7033569812774658, -0.13867753744125366, -0.6971782445907593, 0.7033569812774658, -0.27202528715133667, -0.65672767162323, 0.703356921672821, - -0.27202528715133667, -0.65672767162323, 0.703356921672821, -0.27202528715133667, -0.65672767162323, 0.7033569812774658, -0.13867752254009247, - -0.6971781849861145, 0.7033569812774658, -1.2184446518404002e-7, -0.7108367085456848, 0.7033571004867554, -1.594157055251344e-7, -0.71083664894104, - 0.7033571004867554, -0.13867753744125366, -0.6971782445907593, 0.7033569812774658, -0.13867753744125366, -0.6971782445907593, 0.7033569812774658, - -0.13867752254009247, -0.6971781849861145, 0.7033569812774658, -1.2184446518404002e-7, -0.7108367085456848, 0.7033571004867554, 0.13867734372615814, - -0.6971781253814697, 0.7033571600914001, 0.13867731392383575, -0.6971781253814697, 0.7033571004867554, -1.594157055251344e-7, -0.71083664894104, - 0.7033571004867554, -1.594157055251344e-7, -0.71083664894104, 0.7033571004867554, -1.2184446518404002e-7, -0.7108367085456848, 0.7033571004867554, - 0.13867734372615814, -0.6971781253814697, 0.7033571600914001, 0.2720251977443695, -0.6567274332046509, 0.7033571600914001, 0.27202513813972473, - -0.6567274928092957, 0.7033571600914001, 0.13867731392383575, -0.6971781253814697, 0.7033571004867554, 0.13867731392383575, -0.6971781253814697, - 0.7033571004867554, 0.13867734372615814, -0.6971781253814697, 0.7033571600914001, 0.2720251977443695, -0.6567274332046509, 0.7033571600914001, - 0.3949195146560669, -0.5910390615463257, 0.7033572793006897, 0.3949195146560669, -0.5910390615463257, 0.7033572793006897, 0.27202513813972473, - -0.6567274928092957, 0.7033571600914001, 0.27202513813972473, -0.6567274928092957, 0.7033571600914001, 0.2720251977443695, -0.6567274332046509, - 0.7033571600914001, 0.3949195146560669, -0.5910390615463257, 0.7033572793006897, 0.5026373863220215, -0.5026372671127319, 0.7033572196960449, - 0.5026374459266663, -0.5026372671127319, 0.7033572196960449, 0.3949195146560669, -0.5910390615463257, 0.7033572793006897, 0.3949195146560669, - -0.5910390615463257, 0.7033572793006897, 0.3949195146560669, -0.5910390615463257, 0.7033572793006897, 0.5026373863220215, -0.5026372671127319, - 0.7033572196960449, 0.5910391211509705, -0.3949195444583893, 0.7033572196960449, 0.5910390615463257, -0.39491957426071167, 0.7033572196960449, - 0.5026374459266663, -0.5026372671127319, 0.7033572196960449, 0.5026374459266663, -0.5026372671127319, 0.7033572196960449, 0.5026373863220215, - -0.5026372671127319, 0.7033572196960449, 0.5910391211509705, -0.3949195444583893, 0.7033572196960449, 0.6567274332046509, -0.27202528715133667, - 0.7033571600914001, 0.6567274928092957, -0.27202534675598145, 0.7033571600914001, 0.5910390615463257, -0.39491957426071167, 0.7033572196960449, - 0.5910390615463257, -0.39491957426071167, 0.7033572196960449, 0.5910391211509705, -0.3949195444583893, 0.7033572196960449, 0.6567274332046509, - -0.27202528715133667, 0.7033571600914001, 0.6971781253814697, -0.13867710530757904, 0.7033572196960449, 0.6971781253814697, -0.13867712020874023, - 0.7033572196960449, 0.6567274928092957, -0.27202534675598145, 0.7033571600914001, 0.6567274928092957, -0.27202534675598145, 0.7033571600914001, - 0.6567274332046509, -0.27202528715133667, 0.7033571600914001, 0.6971781253814697, -0.13867710530757904, 0.7033572196960449, 0.7108365893363953, - 1.9644303961285914e-7, 0.7033571600914001, 0.7108365893363953, 1.8674414548058849e-7, 0.7033572196960449, 0.6971781253814697, -0.13867712020874023, - 0.7033572196960449, 0.6971781253814697, -0.13867712020874023, 0.7033572196960449, 0.6971781253814697, -0.13867710530757904, 0.7033572196960449, - 0.7108365893363953, 1.9644303961285914e-7, 0.7033571600914001, 0.6971780061721802, 0.1386774778366089, 0.7033572793006897, 0.6971780061721802, - 0.13867749273777008, 0.7033572793006897, 0.7108365893363953, 1.8674414548058849e-7, 0.7033572196960449, 0.7108365893363953, 1.8674414548058849e-7, - 0.7033572196960449, 0.7108365893363953, 1.9644303961285914e-7, 0.7033571600914001, 0.6971780061721802, 0.1386774778366089, 0.7033572793006897, - 0.656727135181427, 0.2720257043838501, 0.7033573389053345, 0.6567271947860718, 0.2720257341861725, 0.7033572793006897, 0.6971780061721802, - 0.13867749273777008, 0.7033572793006897, 0.6971780061721802, 0.13867749273777008, 0.7033572793006897, 0.6971780061721802, 0.1386774778366089, - 0.7033572793006897, 0.656727135181427, 0.2720257043838501, 0.7033573389053345, 0.5910387635231018, 0.3949199616909027, 0.7033572196960449, - 0.5910387635231018, 0.3949199616909027, 0.7033572793006897, 0.6567271947860718, 0.2720257341861725, 0.7033572793006897, 0.6567271947860718, - 0.2720257341861725, 0.7033572793006897, 0.656727135181427, 0.2720257043838501, 0.7033573389053345, 0.5910387635231018, 0.3949199616909027, - 0.7033572196960449, 0.5026370286941528, 0.5026376247406006, 0.7033572793006897, 0.5026370882987976, 0.5026376843452454, 0.7033572196960449, - 0.5910387635231018, 0.3949199616909027, 0.7033572793006897, 0.5910387635231018, 0.3949199616909027, 0.7033572793006897, 0.5910387635231018, - 0.3949199616909027, 0.7033572196960449, 0.5026370286941528, 0.5026376247406006, 0.7033572793006897, 0.39491918683052063, 0.59103924036026, - 0.7033572196960449, 0.394919216632843, 0.5910392999649048, 0.7033572196960449, 0.5026370882987976, 0.5026376843452454, 0.7033572196960449, - 0.5026370882987976, 0.5026376843452454, 0.7033572196960449, 0.5026370286941528, 0.5026376247406006, 0.7033572793006897, 0.39491918683052063, - 0.59103924036026, 0.7033572196960449, 0.27202481031417847, 0.6567276120185852, 0.7033572196960449, 0.2720247805118561, 0.6567276120185852, - 0.7033572196960449, 0.394919216632843, 0.5910392999649048, 0.7033572196960449, 0.394919216632843, 0.5910392999649048, 0.7033572196960449, - 0.39491918683052063, 0.59103924036026, 0.7033572196960449, 0.27202481031417847, 0.6567276120185852, 0.7033572196960449, 0.1386767327785492, - 0.6971782445907593, 0.7033571600914001, 0.138676717877388, 0.6971781849861145, 0.7033571600914001, 0.2720247805118561, 0.6567276120185852, - 0.7033572196960449, 0.2720247805118561, 0.6567276120185852, 0.7033572196960449, 0.27202481031417847, 0.6567276120185852, 0.7033572196960449, - 0.1386767327785492, 0.6971782445907593, 0.7033571600914001, -6.365750095937983e-7, 0.7108365893363953, 0.7033571600914001, -6.627138304793334e-7, - 0.71083664894104, 0.7033571600914001, 0.138676717877388, 0.6971781849861145, 0.7033571600914001, 0.138676717877388, 0.6971781849861145, - 0.7033571600914001, 0.1386767327785492, 0.6971782445907593, 0.7033571600914001, -6.365750095937983e-7, 0.7108365893363953, 0.7033571600914001, - -0.138678178191185, 0.697178065776825, 0.7033570408821106, -0.13867820799350739, 0.6971779465675354, 0.7033571004867554, -6.627138304793334e-7, - 0.71083664894104, 0.7033571600914001, -6.627138304793334e-7, 0.71083664894104, 0.7033571600914001, -6.365750095937983e-7, 0.7108365893363953, - 0.7033571600914001, -0.138678178191185, 0.697178065776825, 0.7033570408821106, -0.27202627062797546, 0.6567271947860718, 0.7033569812774658, - -0.27202630043029785, 0.6567271947860718, 0.7033570408821106, -0.13867820799350739, 0.6971779465675354, 0.7033571004867554, -0.13867820799350739, - 0.6971779465675354, 0.7033571004867554, -0.138678178191185, 0.697178065776825, 0.7033570408821106, -0.27202627062797546, 0.6567271947860718, - 0.7033569812774658, -0.3949204385280609, 0.5910387635231018, 0.703356921672821, -0.3949204683303833, 0.591038703918457, 0.7033569812774658, - -0.27202630043029785, 0.6567271947860718, 0.7033570408821106, -0.27202630043029785, 0.6567271947860718, 0.7033570408821106, -0.27202627062797546, - 0.6567271947860718, 0.7033569812774658, -0.3949204385280609, 0.5910387635231018, 0.703356921672821, -0.5026381015777588, 0.5026369094848633, - 0.7033569812774658, -0.5026381015777588, 0.5026369094848633, 0.7033569812774658, -0.3949204683303833, 0.591038703918457, 0.7033569812774658, - -0.3949204683303833, 0.591038703918457, 0.7033569812774658, -0.3949204385280609, 0.5910387635231018, 0.703356921672821, -0.5026381015777588, - 0.5026369094848633, 0.7033569812774658, -0.5910396575927734, 0.3949190378189087, 0.7033569812774658, -0.5910396575927734, 0.3949190676212311, - 0.7033569812774658, -0.5026381015777588, 0.5026369094848633, 0.7033569812774658, -0.5026381015777588, 0.5026369094848633, 0.7033569812774658, - -0.5026381015777588, 0.5026369094848633, 0.7033569812774658, -0.5910396575927734, 0.3949190378189087, 0.7033569812774658, -0.6567279696464539, - 0.27202433347702026, 0.7033570408821106, -0.6567279696464539, 0.2720243036746979, 0.7033570408821106, -0.5910396575927734, 0.3949190676212311, - 0.7033569812774658, -0.5910396575927734, 0.3949190676212311, 0.7033569812774658, -0.5910396575927734, 0.3949190378189087, 0.7033569812774658, - -0.6567279696464539, 0.27202433347702026, 0.7033570408821106, -0.6971784234046936, 0.13867659866809845, 0.7033569812774658, -0.6971784234046936, - 0.13867662847042084, 0.703356921672821, -0.6567279696464539, 0.2720243036746979, 0.7033570408821106, -0.6567279696464539, 0.2720243036746979, - 0.7033570408821106, -0.6567279696464539, 0.27202433347702026, 0.7033570408821106, -0.6971784234046936, 0.13867659866809845, 0.7033569812774658, - -0.7108367085456848, -9.200498851669181e-8, 0.7033570408821106, -0.7108367085456848, -1.1159099955193597e-7, 0.7033570408821106, -0.6971784234046936, - 0.13867662847042084, 0.703356921672821, -0.6971784234046936, 0.13867662847042084, 0.703356921672821, -0.6971784234046936, 0.13867659866809845, - 0.7033569812774658, -0.7108367085456848, -9.200498851669181e-8, 0.7033570408821106 - ], - "normalized": false - }, - "uv": { - "itemSize": 2, - "type": "Float32Array", - "array": [ - 0.8906737565994263, 0.9400254487991333, 0.4999995231628418, 0.9400254487991333, 0.4999995231628418, 0.0599745512008667, 0.4999995231628418, - 0.0599745512008667, 0.8906737565994263, 0.0599745512008667, 0.8906737565994263, 0.9400254487991333, 1.2663346529006958, 0.9400254487991333, - 0.8906737565994263, 0.9400254487991333, 0.8906737565994263, 0.0599745512008667, 0.8906737565994263, 0.0599745512008667, 1.2663346529006958, - 0.0599745512008667, 1.2663346529006958, 0.9400254487991333, 1.6125456094741821, 0.9400254487991333, 1.2663346529006958, 0.9400254487991333, - 1.2663346529006958, 0.0599745512008667, 1.2663346529006958, 0.0599745512008667, 1.6125456094741821, 0.0599745512008667, 1.6125456094741821, - 0.9400254487991333, 1.9160020351409912, 0.9400254487991333, 1.6125456094741821, 0.9400254487991333, 1.6125456094741821, 0.0599745512008667, - 1.6125456094741821, 0.0599745512008667, 1.9160020351409912, 0.0599745512008667, 1.9160020351409912, 0.9400254487991333, -0.6125462055206299, - 0.9400254487991333, -0.9160027503967285, 0.9400254487991333, -0.9160027503967285, 0.0599745512008667, -0.9160027503967285, 0.0599745512008667, - -0.6125462055206299, 0.0599745512008667, -0.6125462055206299, 0.9400254487991333, -0.26633548736572266, 0.9400254487991333, -0.6125462055206299, - 0.9400254487991333, -0.6125462055206299, 0.0599745512008667, -0.6125462055206299, 0.0599745512008667, -0.26633548736572266, 0.0599745512008667, - -0.26633548736572266, 0.9400254487991333, 0.10932528972625732, 0.9400254487991333, -0.26633548736572266, 0.9400254487991333, -0.26633548736572266, - 0.0599745512008667, -0.26633548736572266, 0.0599745512008667, 0.10932528972625732, 0.0599745512008667, 0.10932528972625732, 0.9400254487991333, - 0.49999934434890747, 0.9400254487991333, 0.10932528972625732, 0.9400254487991333, 0.10932528972625732, 0.0599745512008667, 0.10932528972625732, - 0.0599745512008667, 0.49999934434890747, 0.0599745512008667, 0.49999934434890747, 0.9400254487991333, 0.8906735181808472, 0.9400254487991333, - 0.49999934434890747, 0.9400254487991333, 0.49999934434890747, 0.0599745512008667, 0.49999934434890747, 0.0599745512008667, 0.8906735181808472, - 0.0599745512008667, 0.8906735181808472, 0.9400254487991333, 1.2663342952728271, 0.9400254487991333, 0.8906735181808472, 0.9400254487991333, - 0.8906735181808472, 0.0599745512008667, 0.8906735181808472, 0.0599745512008667, 1.2663342952728271, 0.0599745512008667, 1.2663342952728271, - 0.9400254487991333, 1.6125454902648926, 0.9400254487991333, 1.2663342952728271, 0.9400254487991333, 1.2663342952728271, 0.0599745512008667, - 1.2663342952728271, 0.0599745512008667, 1.6125454902648926, 0.0599745512008667, 1.6125454902648926, 0.9400254487991333, 1.9160020351409912, - 0.9400254487991333, 1.6125454902648926, 0.9400254487991333, 1.6125454902648926, 0.0599745512008667, 1.6125454902648926, 0.0599745512008667, - 1.9160020351409912, 0.0599745512008667, 1.9160020351409912, 0.9400254487991333, -0.6125462055206299, 0.9400254487991333, -0.9160027503967285, - 0.9400254487991333, -0.9160027503967285, 0.0599745512008667, -0.9160027503967285, 0.0599745512008667, -0.6125462055206299, 0.0599745512008667, - -0.6125462055206299, 0.9400254487991333, -0.266335129737854, 0.9400254487991333, -0.6125462055206299, 0.9400254487991333, -0.6125462055206299, - 0.0599745512008667, -0.6125462055206299, 0.0599745512008667, -0.266335129737854, 0.0599745512008667, -0.266335129737854, 0.9400254487991333, - 0.10932576656341553, 0.9400254487991333, -0.266335129737854, 0.9400254487991333, -0.266335129737854, 0.0599745512008667, -0.266335129737854, - 0.0599745512008667, 0.10932576656341553, 0.0599745512008667, 0.10932576656341553, 0.9400254487991333, 0.5000001788139343, 0.9400254487991333, - 0.10932576656341553, 0.9400254487991333, 0.10932576656341553, 0.0599745512008667, 0.10932576656341553, 0.0599745512008667, 0.5000001788139343, - 0.0599745512008667, 0.5000001788139343, 0.9400254487991333, 0.8906745314598083, 0.9400254487991333, 0.5000001788139343, 0.9400254487991333, - 0.5000001788139343, 0.0599745512008667, 0.5000001788139343, 0.0599745512008667, 0.8906745314598083, 0.0599745512008667, 0.8906745314598083, - 0.9400254487991333, 1.2663354873657227, 0.9400254487991333, 0.8906745314598083, 0.9400254487991333, 0.8906745314598083, 0.0599745512008667, - 0.8906745314598083, 0.0599745512008667, 1.2663354873657227, 0.0599745512008667, 1.2663354873657227, 0.9400254487991333, 1.6125465631484985, - 0.9400254487991333, 1.2663354873657227, 0.9400254487991333, 1.2663354873657227, 0.0599745512008667, 1.2663354873657227, 0.0599745512008667, - 1.6125465631484985, 0.0599745512008667, 1.6125465631484985, 0.9400254487991333, 1.9160029888153076, 0.9400254487991333, 1.6125465631484985, - 0.9400254487991333, 1.6125465631484985, 0.0599745512008667, 1.6125465631484985, 0.0599745512008667, 1.9160029888153076, 0.0599745512008667, - 1.9160029888153076, 0.9400254487991333, -0.6125451326370239, 0.9400254487991333, -0.9160019159317017, 0.9400254487991333, -0.9160019159317017, - 0.0599745512008667, -0.9160019159317017, 0.0599745512008667, -0.6125451326370239, 0.0599745512008667, -0.6125451326370239, 0.9400254487991333, - -0.2663339376449585, 0.9400254487991333, -0.6125451326370239, 0.9400254487991333, -0.6125451326370239, 0.0599745512008667, -0.6125451326370239, - 0.0599745512008667, -0.2663339376449585, 0.0599745512008667, -0.2663339376449585, 0.9400254487991333, 0.1093270480632782, 0.9400254487991333, - -0.2663339376449585, 0.9400254487991333, -0.2663339376449585, 0.0599745512008667, -0.2663339376449585, 0.0599745512008667, 0.1093270480632782, - 0.0599745512008667, 0.1093270480632782, 0.9400254487991333, 0.5000014305114746, 0.9400254487991333, 0.1093270480632782, 0.9400254487991333, - 0.1093270480632782, 0.0599745512008667, 0.1093270480632782, 0.0599745512008667, 0.5000014305114746, 0.0599745512008667, 0.5000014305114746, - 0.9400254487991333, 0.8906757831573486, 0.9400254487991333, 0.5000014305114746, 0.9400254487991333, 0.5000014305114746, 0.0599745512008667, - 0.5000014305114746, 0.0599745512008667, 0.8906757831573486, 0.0599745512008667, 0.8906757831573486, 0.9400254487991333, 1.2663366794586182, - 0.9400254487991333, 0.8906757831573486, 0.9400254487991333, 0.8906757831573486, 0.0599745512008667, 0.8906757831573486, 0.0599745512008667, - 1.2663366794586182, 0.0599745512008667, 1.2663366794586182, 0.9400254487991333, 1.6125476360321045, 0.9400254487991333, 1.2663366794586182, - 0.9400254487991333, 1.2663366794586182, 0.0599745512008667, 1.2663366794586182, 0.0599745512008667, 1.6125476360321045, 0.0599745512008667, - 1.6125476360321045, 0.9400254487991333, 1.9160038232803345, 0.9400254487991333, 1.6125476360321045, 0.9400254487991333, 1.6125476360321045, - 0.0599745512008667, 1.6125476360321045, 0.0599745512008667, 1.9160038232803345, 0.0599745512008667, 1.9160038232803345, 0.9400254487991333, - -0.612544059753418, 0.9400254487991333, -0.9160009622573853, 0.9400254487991333, -0.9160009622573853, 0.0599745512008667, -0.9160009622573853, - 0.0599745512008667, -0.612544059753418, 0.0599745512008667, -0.612544059753418, 0.9400254487991333, -0.266332745552063, 0.9400254487991333, - -0.612544059753418, 0.9400254487991333, -0.612544059753418, 0.0599745512008667, -0.612544059753418, 0.0599745512008667, -0.266332745552063, - 0.0599745512008667, -0.266332745552063, 0.9400254487991333, 0.10932832956314087, 0.9400254487991333, -0.266332745552063, 0.9400254487991333, - -0.266332745552063, 0.0599745512008667, -0.266332745552063, 0.0599745512008667, 0.10932832956314087, 0.0599745512008667, 0.10932832956314087, - 0.9400254487991333, 0.4999995231628418, 0.9400254487991333, 0.10932832956314087, 0.9400254487991333, 0.10932832956314087, 0.0599745512008667, - 0.10932832956314087, 0.0599745512008667, 0.4999995231628418, 0.0599745512008667, 0.4999995231628418, 0.9400254487991333, 0.8906737565994263, - 0.9400254487991333, 0.8906737565994263, 0.0599745512008667, 0.4999995231628418, 0.0599745512008667, 0.4999995231628418, 0.0599745512008667, - 0.4999995231628418, 0.9400254487991333, 0.8906737565994263, 0.9400254487991333, 1.2663346529006958, 0.9400254487991333, 1.2663346529006958, - 0.0599745512008667, 0.8906737565994263, 0.0599745512008667, 0.8906737565994263, 0.0599745512008667, 0.8906737565994263, 0.9400254487991333, - 1.2663346529006958, 0.9400254487991333, 1.6125456094741821, 0.9400254487991333, 1.6125456094741821, 0.0599745512008667, 1.2663346529006958, - 0.0599745512008667, 1.2663346529006958, 0.0599745512008667, 1.2663346529006958, 0.9400254487991333, 1.6125456094741821, 0.9400254487991333, - 1.9160020351409912, 0.9400254487991333, 1.9160020351409912, 0.0599745512008667, 1.6125456094741821, 0.0599745512008667, 1.6125456094741821, - 0.0599745512008667, 1.6125456094741821, 0.9400254487991333, 1.9160020351409912, 0.9400254487991333, -0.6125462055206299, 0.9400254487991333, - -0.6125462055206299, 0.0599745512008667, -0.9160027503967285, 0.0599745512008667, -0.9160027503967285, 0.0599745512008667, -0.9160027503967285, - 0.9400254487991333, -0.6125462055206299, 0.9400254487991333, -0.26633548736572266, 0.9400254487991333, -0.26633548736572266, 0.0599745512008667, - -0.6125462055206299, 0.0599745512008667, -0.6125462055206299, 0.0599745512008667, -0.6125462055206299, 0.9400254487991333, -0.26633548736572266, - 0.9400254487991333, 0.10932528972625732, 0.9400254487991333, 0.10932528972625732, 0.0599745512008667, -0.26633548736572266, 0.0599745512008667, - -0.26633548736572266, 0.0599745512008667, -0.26633548736572266, 0.9400254487991333, 0.10932528972625732, 0.9400254487991333, 0.49999934434890747, - 0.9400254487991333, 0.49999934434890747, 0.0599745512008667, 0.10932528972625732, 0.0599745512008667, 0.10932528972625732, 0.0599745512008667, - 0.10932528972625732, 0.9400254487991333, 0.49999934434890747, 0.9400254487991333, 0.8906735181808472, 0.9400254487991333, 0.8906735181808472, - 0.0599745512008667, 0.49999934434890747, 0.0599745512008667, 0.49999934434890747, 0.0599745512008667, 0.49999934434890747, 0.9400254487991333, - 0.8906735181808472, 0.9400254487991333, 1.2663342952728271, 0.9400254487991333, 1.2663342952728271, 0.0599745512008667, 0.8906735181808472, - 0.0599745512008667, 0.8906735181808472, 0.0599745512008667, 0.8906735181808472, 0.9400254487991333, 1.2663342952728271, 0.9400254487991333, - 1.6125454902648926, 0.9400254487991333, 1.6125454902648926, 0.0599745512008667, 1.2663342952728271, 0.0599745512008667, 1.2663342952728271, - 0.0599745512008667, 1.2663342952728271, 0.9400254487991333, 1.6125454902648926, 0.9400254487991333, 1.9160020351409912, 0.9400254487991333, - 1.9160020351409912, 0.0599745512008667, 1.6125454902648926, 0.0599745512008667, 1.6125454902648926, 0.0599745512008667, 1.6125454902648926, - 0.9400254487991333, 1.9160020351409912, 0.9400254487991333, -0.6125462055206299, 0.9400254487991333, -0.6125462055206299, 0.0599745512008667, - -0.9160027503967285, 0.0599745512008667, -0.9160027503967285, 0.0599745512008667, -0.9160027503967285, 0.9400254487991333, -0.6125462055206299, - 0.9400254487991333, -0.266335129737854, 0.9400254487991333, -0.266335129737854, 0.0599745512008667, -0.6125462055206299, 0.0599745512008667, - -0.6125462055206299, 0.0599745512008667, -0.6125462055206299, 0.9400254487991333, -0.266335129737854, 0.9400254487991333, 0.10932576656341553, - 0.9400254487991333, 0.10932576656341553, 0.0599745512008667, -0.266335129737854, 0.0599745512008667, -0.266335129737854, 0.0599745512008667, - -0.266335129737854, 0.9400254487991333, 0.10932576656341553, 0.9400254487991333, 0.5000001788139343, 0.9400254487991333, 0.5000001788139343, - 0.0599745512008667, 0.10932576656341553, 0.0599745512008667, 0.10932576656341553, 0.0599745512008667, 0.10932576656341553, 0.9400254487991333, - 0.5000001788139343, 0.9400254487991333, 0.8906745314598083, 0.9400254487991333, 0.8906745314598083, 0.0599745512008667, 0.5000001788139343, - 0.0599745512008667, 0.5000001788139343, 0.0599745512008667, 0.5000001788139343, 0.9400254487991333, 0.8906745314598083, 0.9400254487991333, - 1.2663354873657227, 0.9400254487991333, 1.2663354873657227, 0.0599745512008667, 0.8906745314598083, 0.0599745512008667, 0.8906745314598083, - 0.0599745512008667, 0.8906745314598083, 0.9400254487991333, 1.2663354873657227, 0.9400254487991333, 1.6125465631484985, 0.9400254487991333, - 1.6125465631484985, 0.0599745512008667, 1.2663354873657227, 0.0599745512008667, 1.2663354873657227, 0.0599745512008667, 1.2663354873657227, - 0.9400254487991333, 1.6125465631484985, 0.9400254487991333, 1.9160029888153076, 0.9400254487991333, 1.9160029888153076, 0.0599745512008667, - 1.6125465631484985, 0.0599745512008667, 1.6125465631484985, 0.0599745512008667, 1.6125465631484985, 0.9400254487991333, 1.9160029888153076, - 0.9400254487991333, -0.6125451326370239, 0.9400254487991333, -0.6125451326370239, 0.0599745512008667, -0.9160019159317017, 0.0599745512008667, - -0.9160019159317017, 0.0599745512008667, -0.9160019159317017, 0.9400254487991333, -0.6125451326370239, 0.9400254487991333, -0.2663339376449585, - 0.9400254487991333, -0.2663339376449585, 0.0599745512008667, -0.6125451326370239, 0.0599745512008667, -0.6125451326370239, 0.0599745512008667, - -0.6125451326370239, 0.9400254487991333, -0.2663339376449585, 0.9400254487991333, 0.1093270480632782, 0.9400254487991333, 0.1093270480632782, - 0.0599745512008667, -0.2663339376449585, 0.0599745512008667, -0.2663339376449585, 0.0599745512008667, -0.2663339376449585, 0.9400254487991333, - 0.1093270480632782, 0.9400254487991333, 0.5000014305114746, 0.9400254487991333, 0.5000014305114746, 0.0599745512008667, 0.1093270480632782, - 0.0599745512008667, 0.1093270480632782, 0.0599745512008667, 0.1093270480632782, 0.9400254487991333, 0.5000014305114746, 0.9400254487991333, - 0.8906757831573486, 0.9400254487991333, 0.8906757831573486, 0.0599745512008667, 0.5000014305114746, 0.0599745512008667, 0.5000014305114746, - 0.0599745512008667, 0.5000014305114746, 0.9400254487991333, 0.8906757831573486, 0.9400254487991333, 1.2663366794586182, 0.9400254487991333, - 1.2663366794586182, 0.0599745512008667, 0.8906757831573486, 0.0599745512008667, 0.8906757831573486, 0.0599745512008667, 0.8906757831573486, - 0.9400254487991333, 1.2663366794586182, 0.9400254487991333, 1.6125476360321045, 0.9400254487991333, 1.6125476360321045, 0.0599745512008667, - 1.2663366794586182, 0.0599745512008667, 1.2663366794586182, 0.0599745512008667, 1.2663366794586182, 0.9400254487991333, 1.6125476360321045, - 0.9400254487991333, 1.9160038232803345, 0.9400254487991333, 1.9160038232803345, 0.0599745512008667, 1.6125476360321045, 0.0599745512008667, - 1.6125476360321045, 0.0599745512008667, 1.6125476360321045, 0.9400254487991333, 1.9160038232803345, 0.9400254487991333, -0.612544059753418, - 0.9400254487991333, -0.612544059753418, 0.0599745512008667, -0.9160009622573853, 0.0599745512008667, -0.9160009622573853, 0.0599745512008667, - -0.9160009622573853, 0.9400254487991333, -0.612544059753418, 0.9400254487991333, -0.266332745552063, 0.9400254487991333, -0.266332745552063, - 0.0599745512008667, -0.612544059753418, 0.0599745512008667, -0.612544059753418, 0.0599745512008667, -0.612544059753418, 0.9400254487991333, - -0.266332745552063, 0.9400254487991333, 0.10932832956314087, 0.9400254487991333, 0.10932832956314087, 0.0599745512008667, -0.266332745552063, - 0.0599745512008667, -0.266332745552063, 0.0599745512008667, -0.266332745552063, 0.9400254487991333, 0.10932832956314087, 0.9400254487991333, - 0.4999995231628418, 0.9400254487991333, 0.4999995231628418, 0.0599745512008667, 0.10932832956314087, 0.0599745512008667, 0.10932832956314087, - 0.0599745512008667, 0.10932832956314087, 0.9400254487991333, 0.4999995231628418, 0.9400254487991333 - ], - "normalized": false - } - } - } - } - ], - "materials": [ - { - "uuid": "769df3ee-4567-40b7-8da4-473fb149f350", - "type": "MeshBasicMaterial", - "color": 16777215, - "map": "3874a02e-6d61-4cbb-8379-9c1436361bb4", - "envMapRotation": [0, 0, 0, "XYZ"], - "reflectivity": 1, - "refractionRatio": 0.98, - "blending": 2, - "side": 2, - "transparent": true, - "blendColor": 0, - "depthWrite": false - }, - { - "uuid": "6d9283b7-81c2-4063-84cc-f696054ce6f6", - "type": "MeshBasicMaterial", - "color": 16777215, - "map": "93d77365-4fc6-43a7-b19a-e2fb18ab38d0", - "envMapRotation": [0, 0, 0, "XYZ"], - "reflectivity": 1, - "refractionRatio": 0.98, - "blending": 2, - "side": 2, - "transparent": true, - "blendColor": 0, - "depthWrite": false - }, - { - "uuid": "7442c205-fb42-4fb9-baec-82a192b81351", - "type": "MeshBasicMaterial", - "color": 16777215, - "map": "65e3d0e9-bf33-48b4-a8a1-5bc966d46e4e", - "envMapRotation": [0, 0, 0, "XYZ"], - "reflectivity": 1, - "refractionRatio": 0.98, - "blending": 2, - "side": 2, - "transparent": true, - "blendColor": 0, - "depthWrite": false - } - ], - "textures": [ - { - "uuid": "3874a02e-6d61-4cbb-8379-9c1436361bb4", - "name": "GroundGlowEmitter_texture", - "image": "396bc86c-4059-45f7-b34f-f6228436b397", - "mapping": 300, - "channel": 0, - "repeat": [1, 1], - "offset": [0, 0], - "center": [0, 0], - "rotation": 0, - "wrap": [1001, 1001], - "format": 1023, - "internalFormat": null, - "type": 1009, - "colorSpace": "", - "minFilter": 1008, - "magFilter": 1006, - "anisotropy": 1, - "flipY": true, - "generateMipmaps": true, - "premultiplyAlpha": false, - "unpackAlignment": 4 - }, - { - "uuid": "93d77365-4fc6-43a7-b19a-e2fb18ab38d0", - "name": "GlowCircleEmitter_texture", - "image": "55a3bc1f-853b-4d4a-bbe1-77abe1ccb390", - "mapping": 300, - "channel": 0, - "repeat": [1, 1], - "offset": [0, 0], - "center": [0, 0], - "rotation": 0, - "wrap": [1001, 1001], - "format": 1023, - "internalFormat": null, - "type": 1009, - "colorSpace": "", - "minFilter": 1008, - "magFilter": 1006, - "anisotropy": 1, - "flipY": true, - "generateMipmaps": true, - "premultiplyAlpha": false, - "unpackAlignment": 4 - }, - { - "uuid": "65e3d0e9-bf33-48b4-a8a1-5bc966d46e4e", - "name": "BasicZoneBlueEmitter_texture", - "image": "a44aaf69-213b-4f68-96fc-304a19e9cdae", - "mapping": 300, - "channel": 0, - "repeat": [1, 1], - "offset": [0, 0], - "center": [0, 0], - "rotation": 0, - "wrap": [1001, 1001], - "format": 1023, - "internalFormat": null, - "type": 1009, - "colorSpace": "", - "minFilter": 1008, - "magFilter": 1006, - "anisotropy": 1, - "flipY": true, - "generateMipmaps": true, - "premultiplyAlpha": false, - "unpackAlignment": 4 - } - ], - "images": [ - { - "uuid": "396bc86c-4059-45f7-b34f-f6228436b397", - "url": "data:image/jpeg;base64,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" - }, - { - "uuid": "55a3bc1f-853b-4d4a-bbe1-77abe1ccb390", - "url": "data:image/jpeg;base64,iVBORw0KGgoAAAANSUhEUgAAAQAAAAEACAQAAAD2e2DtAAAACXBIWXMAAAsTAAALEwEAmpwYAAADGGlDQ1BQaG90b3Nob3AgSUNDIHByb2ZpbGUAAHjaY2BgnuDo4uTKJMDAUFBUUuQe5BgZERmlwH6egY2BmYGBgYGBITG5uMAxIMCHgYGBIS8/L5UBFTAyMHy7xsDIwMDAcFnX0cXJlYE0wJpcUFTCwMBwgIGBwSgltTiZgYHhCwMDQ3p5SUEJAwNjDAMDg0hSdkEJAwNjAQMDg0h2SJAzAwNjCwMDE09JakUJAwMDg3N+QWVRZnpGiYKhpaWlgmNKflKqQnBlcUlqbrGCZ15yflFBflFiSWoKAwMD1A4GBgYGXpf8EgX3xMw8BSMDVQYqg4jIKAUICxE+CDEESC4tKoMHJQODAIMCgwGDA0MAQyJDPcMChqMMbxjFGV0YSxlXMN5jEmMKYprAdIFZmDmSeSHzGxZLlg6WW6x6rK2s99gs2aaxfWMPZ9/NocTRxfGFM5HzApcj1xZuTe4FPFI8U3mFeCfxCfNN45fhXyygI7BD0FXwilCq0A/hXhEVkb2i4aJfxCaJG4lfkaiQlJM8JpUvLS19QqZMVl32llyfvIv8H4WtioVKekpvldeqFKiaqP5UO6jepRGqqaT5QeuA9iSdVF0rPUG9V/pHDBYY1hrFGNuayJsym740u2C+02KJ5QSrOutcmzjbQDtXe2sHY0cdJzVnJRcFV3k3BXdlD3VPXS8Tbxsfd99gvwT//ID6wIlBS4N3hVwMfRnOFCEXaRUVEV0RMzN2T9yDBLZE3aSw5IaUNak30zkyLDIzs+ZmX8xlz7PPryjYVPiuWLskq3RV2ZsK/cqSql01jLVedVPrHzbqNdU0n22VaytsP9op3VXUfbpXta+x/+5Em0mzJ/+dGj/t8AyNmf2zvs9JmHt6vvmCpYtEFrcu+bYsc/m9lSGrTq9xWbtvveWGbZtMNm/ZarJt+w6rnft3u+45uy9s/4ODOYd+Hmk/Jn58xUnrU+fOJJ/9dX7SRe1LR68kXv13fc5Nm1t379TfU75/4mHeY7En+59lvhB5efB1/lv5dxc+NH0y/fzq64Lv4T8Ffp360/rP8f9/AA0ADzT6lvFdAAAAIGNIUk0AAHolAACAgwAA+f8AAIDpAAB1MAAA6mAAADqYAAAXb5JfxUYAALVrSURBVHja7P152GXXdd4H/t537XPu/eav5sJYBYAjwFEkNVADCpoHy5JsAfbTdkLQkyLLsaS43VYidaqqHXfbThxLSSRbTtICuz20AdmybNmyZVko2JasiBRNUgQ4giiQGGv8quob771nr/7j3BpRmMjC4Dw6fB7WxfdV3bPPPmuvvda73vVuJa+HS7w+xnHVsSlfv4P7ii/z+9d/kpf+z2AAesUf7/U12a/HUbymBpC/v5C/7NnK/3N7gN+/Xsrc6fc9wCsfAF4cp/TaGbGuOnf5n7oB/CfmgvO1XCqvlKH9vgG8TG/wWnmx/j4/pf+TGMDv7/5f3vXfTe3tv9e1igJ+3wN8hT5Ar4kx5zWLAvz7K/Slv/zXAyL4QwK47pXeAvJVt2j9J2EEz/UBr9ZM3SX4K0LPALBwzZbp62YLyNfpqr/oEe9QvobDfDDhWR6mBR7RLFern+g/ZQPQ69kchK7Xw6m82yDdo1d/rIcFc4y5GUg6/j/Ka7KI/HpZ769XUOi9AjikN3OLHvNAKLn/NRjswZSe4DiPAbtJvvOVjQH+r4K/r1drvb8+A9B+HB/hbtCh/EH25dH8/6YubA13XYgIXo0RP6TM3TSIf6ddzHD6lTWAIdC+TraD1zYqUd4PwDNa1ueF7rCmDvnAq+q97sxv1hnO0PIbwE5OXqPvLVf/8QJw9jUK+V5PGUECh3W7yG/J5Pd8JIeCQzqYh3VQ+17Vnes38vtkir4q4SRf9Up6gP+gEbAf+Kd6Ldb2ax8PaPp/QjqU/1oHJH+/8QGd9XviUf9Xfpjr+X+8qmP6kObZZJPfY0ULeuyVTAMfZshf0BrwB/PVW/mvuysB/gz369v1A/4dvqBFfTDewO/S6L/kv9E38oVX9XHexAbJmKdYx+x+wdnUV2YAlV0ke/n/veK++HwYdY9eh++eW3SA6zmiLQprWN+iNd0j9DZ+27+m23nvqzbuuyT9Ju/nZsw3sY0FEvhz+soXmq8eAYhvZZ41/rngwCv4kOfB1fvz+aptryUE9E3aBVrKFe1kpz6jJ53xTXorj2ifJnT8Lvv1aoxbejCTNU5qUwN9hNNsY4Uj2vVKBYGnWWTMLBt8icd15JVzdJeBWReQ9tcHRVj38ECFu/N3fCIb79cJ3+Shbqw3aKzZHOlb8u3cXV+NCkHmLTrKIc5yloWco+UJ3srteeCVigEWeJIt5gg69rxEC/+ylsE00zr8OoMBpUzy/jzENt+uGxip480YxXvjM7Gl3Szq7SSPCP2QXo3x/HUyH2JCZYuzOgsUXRvP46v/cINnWOWrqQzzpVl4vryVf2H0h/UAB/sVp9dbLHCIW/MXc5630nHWaNFzpdMuhcxED/GM7uLngXv8SnuAu/MBvZdtOsl1nKFqwlrey2Ed+IrN4KpDP8l2vYMRv87cK4PKZO83JXQwz9vDA9NY4HUQAibAzzh5C9s8oOOYTosSMVPChVs5rtSdPDMtyd9fX9loREL3cDdvyV3AMJ3i0/wCB/nKt+crDOD90wR4Nj9Px4gt/tdXDgdQcvg8rHpZTvDaI0ES/GhK79Gfz5NsU7LpIQvRxpomXmOZuXwoZ3RUeoUZIZmQ+QD3s86/ZRO4E7GbG6bT9JVGIFcYwG8lwHaGmmXETrbx9lcMBzgEeSeHQLAgSA4B0i2vlz6xvJ5/wEP8TnY5oWNWJVuve4C0niMe1Vs4ROYrPlrBh7mbx9jgHK2kDQ3ZC/zIKxUDNIw4zvU8yogb+Zt6ZR7qMNLPcZBHlAw4wj26WxLsfx14gH5dPcVR1vQDPpNvTRPKUiI1qw0g9H7dmfflKx0E9pv8f88D7GdR83qCki3Pqgp+lvuuDKCvlQE07OaMzvJGfoUfz2vtXHtXcB9/Un9A/0Fwj+7mIc7odo7o9QQYfit3QT5Dq6fU2mocGjuy1dBt7uCndYSff8UD0Uxpt+7hIdpcyG0M2GRC5lEOcW8ezFfAAFZZ42Oc4xZW2c0DuvZrC0n7dRMneIK7dVx3g76Kh85jDq+LXEB6hPvrfuZYUGgrZRJJcy6Qi+zm2PlQ9hW8DiZk3p6HOEjLhFl+R5t0FODYK7MFrGjELt7MAuY427n7mq7H84le5ipPsUO/qyd1QBgGSA+zogchD78OTKA31YdzkhvMyQ7LYUdkwxZjHaptAvlqUEU/oIP5M0rgnIYsqQWOavc1ML6rGMCQIcfYyVy+i4b9/Nw1fRk96Cvwm3gb380b2KNO8Agwz736Ne6TdPB1AAYd1pMVPaCzGTTZ1QGd7NA8J3KekxxhS7/4KgFB91b0Y7kkWMxZZjM4x1158BoY33Og4B/R+9jGcU4y0aN8cz5AuaYSCffo/sxE5IZGmvAp7s5NjivZzkzel/dPU8LXHguUjujP6NtyXZuEcFAcpdOcR/W4lvKXfCx/sA5Nvhq+6AF+WiMqJ9inFT2eGzqQf8GL1zoGkB5hP59jwGcw4qe4jc9fU7S7h3oE2su/51HNa4biXezSHqq+ztKcDrxOsoDv1SgP5S7abDIptrJKM5p31FUeyVuR/lm+8sYId+ke4Gg+RcMOrq/LjNivzTxyrWOAzA+wwj7dxNsJzvLj+ip+Nq/5AwnI49zAKS0x1lKukwwZcTZh9ho81rW4/oz+fv5CPswTDHUOK1RtNww0ovUC7+c3E34+Xw1/BMlKLrBEl2NajYH7cjcPXmskUHojyzydqzzLOd6ZxxL+6jXGujIFHGZep5gwyNOp3MTsorKo+3nUrzUrTBLS38mP8H6nhBmok5SmkQB1eU7v4k69Ot4o80EAtlS5Rc6g4WPA+/RBvwja+jINQJl/gzPs0po+yipP8kW28RMvgnXp5VtzwiHO0LAT1GrimdzUl5hhH4/kYn0V0LUXm3IyQXpbPsQgnUOKi5K2dm7cqNUSn9BDfTTzqniAn9N+LTDW55lwkgk3AN9df6G+MNr6cg0g79If5E46lvOPM8szNCylXqTilC9zcgEO6m7ewFu1xVuYy5orzDPUOqfyztdNPTDzEP8rv8anNGZMCghKeEhT5/LZfEoHhH7oVRnJEf1IwmcJZrSkpzTDe0muRbJ8hQt5MMVyBmO+wA1Y38iRa94UKT3gw/kAn2QrT+o46t0rDTPcysf59OvEBCTyj6vNfcwyUIssOTuVrD7BDv5e/oJu0d/hyCtaDBaSdFfez4dY5M2s5jzbNGH1Gn2/r3Q193JEC2xpF5ss8e/46vOkJ10rxCvzngp3y2xRq1hT8aOIIBlivkqvD3JY5h/Q361/wnM4J7JF2tHY6nJGhW/KH8mjmXmgvqKjIDPzAHBn3qJPgT5Hm6uMJOAuX1MD6EuP72KEWeWk3sx2zeah3gO8COigl2jP6JAPOPN3mLCubTql+UxmfRNnOalH9F/m7nynXmtiSG+Cv5J/VP9PLWlFw2yrIuwYq1H1Zp7I/fqvfeBVGs+D3J2HOJZnOcuztIglHtMhHqzX1AD6B3+GhiE3MckxHYf52ZdoqS/xb+WheiTv0eP53pxnoj0EXZ3hM3oT4p3cwwofq68lMWQapgoO8lPcVSsD0ukgC3YoBprTDXqKP8YHxKvhr3QYBENN2Mb1suZ1iv15QHdd5e4vr+3uKi5kL1s8yW8S2mLIBzh+Tcsd96hn/xzQr+lkzpFsKpWUfDJv5BEO6Gtec9d/np/0E35brjBf1z2fM85wtapMJeoTegP/W/0Xee1JobrsUy9TdgRxQG1+HWMqNYP9wBwPXsUv58taplcxgGXW2Zk7NeQZvsB+Xv4jvpAC6APTIR+ppzjhJ1jIcRZ1GuusPs9SznLba14HyJTuTuk32a+nuY753NQkLRpFwdHkqrfnzXx7PnDNssDzYMulbJgLIjB6MA9xhEX9noZqWdWY9bxX+17i7quXvgXAj+sIW5zWMVb0KDuBgy/bA+QLhTQcVmaSfBPbauGTCjd5JpdyTnNsY/0aF5++vLfRG+mj+afZxrNGbRYViwCH2zJRy0N0zsyvsHKpF1BGuFQG6nbu4jTvyAkN25ljgUP8wWmu8pXEZVdmATnPu0hN+DpW+Rp2cpRD17DgKfpO93uQZnKg6/IMxz3SLAXnKmP+bE4pw6+lGWQPvh7KVb3JT2dBDlsSDcXKHFH1Vbk9xXt9MK/FUPOSR9aVHiB/wHdzJ8fr44BSmpDckt/5ghvQRcvIl54FJLexDCxwgrn8tzzDft52DcOc6VDyfuC3tKA1fRXDrFQeyYk69gHfIHg9cEJu0f+kP5a/mu/KihV2EJZkhUTDF3UmU997DTesfN5V/G38iFb4Y9pUaM7DfJp54PaXCNC9jC3ggenkr8os66uZ5BFuB5iiTromPqBf4ttZZSYTNNCztNpXb+bjiC8lvB4UuR6rD9e35Q9kaB5ZlqS0XDXQJGF3/jNu0SGuvbnmJXPVf/6z9WfzR3mcs7mVj1ZrkE+SHLjWOADcnYf5ZYJxngLOsNPwAJk9Nen5nIle5sMlSdIRGXojXTbs5h08oVN8Vsmdr4P1n9lvru/UR/l3bGUDApdQ48ZVcxrm0/xpPXbNQSBdsQVc6AzgOKnr6Oh0hh36RcHP5gvhAC9tDi8jhNynD3EXUJmnxbmox/I7tJgv/lKvpliVL5hnH/E2Wga5rgW39aTXu5GkbXmYo2Qefu2p4Qkf9LtY1DvZ0FYqwyIlR4ZHelbJpiS4dmPVZW47L4Pn7vH9+WhOWOUNWZhhjsMc1X7OL82XZgjPHaovfzE/zB/lGZbYQEy0kjVP5XmlkBeKdl+O6FMmeY8O1O2salYT1hnZuZMNCvtI/ULCodccBYTku7kvP5HFY7Vgh0oUqbjThJ3M6t8Dvcd65ay1x03u0gP5IU3Yz0jH1HGSAQfZn0d4KYygvOKTrm4Amffmr/IP+CMs0+kkT3NUp9jPf9/nKteEp3e493C6P9E/y2M8q6QyyzH9H9qVo+y4hW/XQ36N3T/wgO7STRLfyGIuZZUlyw6HPGCDwldlm/BDHMg79OW5+pfy0/szE/6U4CjWM+zIp5jJRtu4T+jAy7iXrogrrpoG3sd9LNOwxE7gnVmAvzjdtK/FAS8HLxG4/S51OYOyqXiX9nKSs/687s1x3lnvfs1zgLvzQX6Fu3WajnVjFUilcBPSHMmz+oQy4QE//GUsjXwelOTqqdsfy0+KFMe1rs/mpiZ8e97yIo4nL/nWlxQEHlaCkk/zRaQxu/WU9utjUyzqsPLLAH6u5l6lOYmf0mn28KXstOalXOccZ3xLvjUPKdy7vlfR6T/PT7+1zvAQ5xjmRMoSVtjCeDOGPKrfqz/pn89rQ5vXFaDQ5WM6zNvyICd4R87lf65gnf/ITh3i4LWkhB1MQW7jLYyYS5F5fc7zr/uG0Xw5ocbzT2omJGuJ/h7bs9GQiSrJhHn25gqtjvPrVbp/Ks72qsV7zzFUab/u4gt6Nx/zCGynslguBWmmprbxXfFXEh/5io1Ol4A+V5+3fQKxwEewnkyYcBN/Lh94UX0SXXYvXeVZLzGAhwTwLg4zyzq3cY6jCv6ibnxJtKd8yV5BScLR/BKfZ6QvkQzjlNd9jON8NH+Wu4DDuuc1TgOTo/XD+vfsy105AIwjlYGsojUt6kt5W96D8kBea2N8rhD8vfVndYgFvYezDBhm4Xp+iB98UX2SvGIzeK6BXWIAdybAMh9gQ3NEdjyda17hAa45NKe7+Yda1s7cwRLLFlFXNKOb83bB7co8mK9xQViHSVYz693sTKlNK8I2cjhZ9Ln6aN79FdQC83k+X71DXt6VK9qWE3bn27P6ejb5o/lX4i699PAvX8wD9NeG9tPlozyjGXbrTP3Y8/mOLxsK6q8/kp9glnU2vaVwejE/k6c0l+QZpFe2NUwvuv77Le8zOjxNnqsVSRcuoVCWNeZY1jGOA3DHy8bK9TKh20N5Nyt8WLewrlFM8kSuS3y8PviSzOwlBoFSAn80IXSzRszr5hzwLl7Ky3+5y+D+vFPflOvcohkV0Jhnaajs0AH9Ow5xMKW7XjEjyJfiA4Afyl/23/LTtJQiyzZW1IhssnrDf236+A+/bDeQL3Ph/B3BfgY5VptnMtirzfzrvvdlGJ6e566X4QCH+ZN6hscprHOcp1nnSyx/havpucBGT7l+KE/oHKcQGS2tbk97lL/Hfr5XB/OwMh98VTaBz17tGJgLrXDfwLEaqhGKIGpI6aIQUdjMj+SD+T6J/zGuhQ/Sc+KAi9df0gPAlr6ooee0qXlW+Iv579STV17enXV1D3CP4GD+73krSySn2YmYU6P/ZTqy53PKL5fCP20OFWwHn1IhUlibWsvUO/I+/pcqvxpI4M/o8zqrL111C1AvE8un81aN60xaEs4gwlJ1JHU1YU98uMKPdy/fB+lF3PXlv//5fAR4A0tZGQBH9Yw+QafeVH/yZQTpecUb86UvBuAplhkx4nG2MaFg7s6LxaBrAbMedg/zbHBDzua6KiXkHZpooi/qv+EHdOBVqQUmT7HIxgukq/u1oRF76KSUo8hhu5Zg4sLX6IM6Vg99Wajl1coHugQPuCIw1I9yiIMZnCJZJVjML/I4Wz0dRStfgf/xldH5L3OYAbfwnRRGucg+PfCCuebL80CZcLDen3cDH9Yyq4RaiwEzvtEjxnwPBziSXGNe8B++8G3fNv30X+gMFTh1ye+uvB7L6+qfzImKJTtBFmE5pDmtebcyD9WvzAivtuKfk1Ln/5vkMKPcyYoGDHJL35bfixPuFJziV/Ty7vo8QaB4gNBeWQP+A8ewruPrry7m+CIBlV4wuIIHuBtYyiXNSTTGM6zQ8Wh+jo8B5P0J33TNjOAt0z//lbrLRv4sm7zneVPBbzM+jDRKWUhVLrKjcSPrJLP1AfMVEsKuDNCeaxASHODP6X3appNqcsKqCnvU6R2CT7DBGvA3BPBrer73oKvCT1cEgYeE/g3fwTrmMbYJRvyWeobcwZcV0uSL5tnwDqpXFa4x8VBLsYMl3sQ+fiHvn5pbuWYeYHTh06npn7uY1808w4becxWwutcKPV2/Nbf8ucxojBWURMYOYuA5fXj65t72ZQMl+QJ1gUu95iP8bP5zjqtoRjXnCI4Q9Vnu1imM6Bi+7Dvlc7eAfp//GDWf1kjK5DE9TdaLvkpfYS3g/h4KzLvzflaULEULcnQubHI8H9dD/KQeyIP5dTqhvdck5nhAvyD4s/rv9MOaMMevCmCThv2MNGbnczap82Hg2/VP9SDfkAlOK9I2JiKdHSfzh7m7AjycL28Wnj/2f27O1D/FaeA72YlqVXCG9XyYZ/yo3soBQmMKZ3hA0LzsO/vKLWk/30/LIm+haBHn1+elFM18Sev/+dfC3dlLn98PRJ5Ng7ooGqjVDhaR7uQpHtbb/B/4R9zwFRvAAQAqQ25jG/M8zoDKh6YHrz6lPn+7VAjv4gGR0rv1eW7K9SBLuuC0iwmihCc+rb/Nz/mHvqxXn1eFf56HWaLM9+Td2qbV3K5RzqrLDf1wPsyOfEwLWfJGjWmYgOZf9hh85Zu7g8O5SytcT9SzDOj0A3rxlEYvGMRcfh2EfACxzHaqQs7IzvN51h3vzQ9zB/fkJ+sf4MmXHd0+93o/cDeLdJoHzjLL9UzYAGYpEhP2AOtXegBJmZl/vt6Ud+ct1cZYJQM7sikpNZ1ylZ/TKy0T10/nKe7Pc3Q6RauOGQoTvT3n9Y3AFk/nWzF7GXH0ZYfpl/MB+JN6L3dqljFPsOEFFlVyTS9GesqroAHPf9vDHNI93K81xpKHKnKZUcMiRe+r/zf+OR/1YW3XU7l4TUDeZ4G3sp+3M2bEDQTJH9EqZpmhNvRvueOqi0MSv56P+ynCtlGEZSIJm3EO9V16uF7LGsmV6MDFwPukyJZjucAc62zpGSq/qmPszwUGTHSMys2MWOOHn3OmYb40A+jj8/+Kw3mAp/ItCZFPaSGH7M1DL3Hij+iuC9nC1W/7gODBejCTw7Ts1nyicNVEJbb8hnzUd+fdvC0PcgPfdkGm9idf4iRfnq5Kd6nlsPZwnCGh3ZphQSMV1jlJ6gb6LH/pOZ6m3/QOKfO7tEknKpFFZFjGQZgm9uS6Xnkpi8OCwx4BX2RJp1S4jTEtVXdwRt/OTiYas6xkg6Dl/CESf+4lkvl9GfSRD+QP6wibfJF9VObzBu3jvS/hVIx+Fh7iyIvc7u6Eu3yHyYdzlZoThZqQhiLn8qz+t7xD380P5wPczhyz0xL1xpc1dTdyAFjjYzzBHE+zi1ktcSOnOQOssaA1QiMtPV85qKLf1SfqHD0QkCEpKH1rAD7tkn/RX278/1L81/nZ/OrcltAwzCZnvMU5Wp5krEiYZUJyEw0Np9niHEz1Vl8amHYFJexh7eNATiisY5L/CPyPU8GU56/Q6cKX5YvSRsSD9Z68R7CdXWrUBVKTJWSf1PfoTo7mr+sB3sQp3awvABAvZ7u8cD0BwDdxOwe0k3ktMuE2Vnhad2gfCxJDDVhklv1XPMz5p/igjtWuNLZSMkEhrFqKrAmLnNTTPPAK8hcPAHcAq+zUh/ROpC2W640secxxBgz5nzSRkALpFL/JrGYB+JD+EnBYL07T8eW/Xecv5VHNs6W9JFV7mPDu6bw+Pxh8nmu68BKd2sG8P+/WFzRyYyus2kgWwXwe4U26h3dpt97O7dPDK5e/rOn7swB8Hb+nE+zA+TRv1CxFb0EMuJk+Yt4SfPZSd5kXTwm/L+/RbK5npiON01KGjZTpouv0JA8Ah/SVrPrn59sc4hZ+C/hr7OYDfFzPZGFklGluyy7JMWPA+TgNW8Asx/lp/R8c5YtXfeMv3B2c8Ne4i2XOMagdAxbZ4Df1Sy/RxyU/lrDvhbcAJ4emx0P0Z19VgrbIxdVzrOgXOeIv5J0M8+Os824BfONLXjH3XzLS49MJmGVAak27NGAHLbdyjm/SHlYE6wJUr/Bcmb1EVHI7t9ShQkIRpqG4LwsX5SwbkA8kHMovfxt4oTBtP7NsAH+JwuN6PzdwQk3CgkIdi7S6k39FZYfO70R7qZxhCPza1ISeWwW8HB28woG9LQ8AAzWCEXNcz511+UUfTpeEgY+/4N984DyJSQ9owEjFJYhUAJGFoW5QU3/XT2o3sMATCY/rVl6aJPN+9jznZ5U3sldPM1aym1ltBwW7KQyxELzxqt/+u0p+xskb1EnOUt2FpMjikF3lBW7XdYJD+kq7Q/W8tQj4ZuDfa5n79Jte1Vk3bGpTlUc1o9S/cUUcp043ylMUDub3A0vA/3iJ187nwSCuaA79Di/ptxlwmr/Nqlb4lfxtL/NiYmgXrfgXX6RqmBcCzh+sT7GQxcLRurTFxILX+aDe4o6b8lhZVqOv0s/pr3NKf+ElitX+0hUZwRL/Tk/SULRMyvoiaJ0l38AppU5xhk3ByhU5gATvSfJbc8SWcpr/KbDCUigUeCs+D9zNoVR+5S//0kz7Zy6Z7QeAn8o9+Z25NzspU7jJLbZjkeT3U/R2rQg6Fgh2AfsStoCv5kO6fO6Fnr8aKOHv4Hq+kw1u0zeywCm2+TN8+8voQd911bLDlTGABH9DNzF2pTjBytCMzuU2fzZPdrfm27yUt7HKIOE4R1+iBzh6hXbWAZ7lBDuUPKNZdWpB0ClYY4EQGmus09qm54LBIB7Jk3oj89GlnC0mUoSxIzIjB/XNyZf99vW8uOCVaelf1m79yzxKy6aGMlUTtrFQV72LE+r4RK4BMGGT9/PLOh83fZIP5JXxRj5/NTBTeSYP6yh7NMObmMsxN5J57Dmr49L/viD/Lwk9o8NKpBealoN5N0JMvCNbHBGEorG1pOQd2tSN+mwe90k9wWf9JW3XcX3ti6qVStJ+zoJ6AzsA3MmmZrVCVTFa1axuYKRZlgnOUGQl2zmqK2s503oAx/Tz9WllNWksy1HcUCJQdZvhHdaXLaBzad3vfANoP4/Hp3+eB+HfRvJGBbM5pMn00GMNeTzmtI3rOS7UqaewvIW/z3dzWB9gC+m/mH7hlTSgi998RcEtU/w0n+YcZ+l4RjvqLMf5rUvc7+WbwZQ6dd4/5F/V3jw4DWwvYuqXQ9z9dB3QdzCTT8S2sFJFTYiiZJGJbo2Zap3LZ1lmyJBv4fSld3sBAOhpvePCvB7gjMge5J1lnhnEdibs4DRjUjtdZLZYYsSW0JHnHMMoPsnf8j6lIjpSGTKuBOmQHFEnN3M/fSNxvuz1Pz1sIvM5SOCOK54u80f0s/mzHGNHdqFxbVSXci4tM+FGKhskZqAzLOnv5b2IkcifuSJ/mgKcJKSUSHl5GnjY6BhbrOVermeZjuNs5/bLxZufe9bnBVOe4ev44OU9ruI5EiiZh7iXbT7rBUpJWYTUlqRlXbdqos96m2+h5YTEUDfQH2X/4nP8X2Z7BQ7wDjomqh6ypKpGxz1WalNkMJcTFQTahTnw3BWqT/OPc6UaEyUjrEKxsrElanKdZ7kv/87L8AC6II/wQhzgZ57zkzfoMEMaQ0OqzVarrLCe23nUZKObaKY93d+IuI+bWABO82PPWSR92ilBr9RzWRp4CPJz+X0UnmXCis5pb97LxzjwQiTWS85N+1Em01LqpQeaXf7ijiDdrhUez8ywaljFDos5dzHDanbCT2jADo0Z632cBr70kqKA7+E64H0A3Mt3cFId0mlCOxWYCfPMqlXkuma0S1usaV4zWuTtuvz19J9qfr2221IE1qRBtkuxsbsyo2G+Oe/jz7yMxZ8XxC8unp30UpS73pxopP21CcsaRfWmntSmW96aaBYYC97JzUz4dgp/mRWWGT63QjMNAzLP6/5cRgol7+Y4R5ml1RFW2cH1uo/lC7uGrvb2M8+fA8lhHrpEUkpXFZk9kPBI/miWnFGEAqcJoGncsIHUaLEqO72RWUJztNqJ+KYXbjjv2QZa00P8uvrY5AkepNAxo5btGmuWZc1oU2POakvJnGY1KwFn0QUA9dKN5m42OCW7SpElyNJJgcLIwAKf9EEd/rLKAVfzZ7pY+rmQ2AK8l9/R1wrQqCZSrW3OM5dZ5/TZ6W6ubDjKBPEpJnyAr2eFTf7aFV47L1NfkS4jhUrJsu7U4yzSaYuF+pEccxcH6IujFyOIKx+jN4/kXlb5R5eAKVcRMRTAoXxcx5lIKal0hrZENg7NeqLqiVMDdvMGIjt2sg6a6MdfwLEmcIjP6us4MgWPPiz0uAaa4xktaB37jAcaq9FZtRqrU6PwvLYJlrSDqg9N6yEXlbfEdvaqRpGlyIJVUkRtVGCQT8Sib3lZxYDnzki+IMnmA8AhfYR38VQOOMskBHbAllov+lPal19ShxQM9EmKHtMpwTJrgh/jJy6L1fu3dPlS9sXkLJNseIh97GJ7HtCcvlH/kqNauYKv9jx7cR7mH7Cgwr3P87DSNP4gtS+/g84DwsJujaOE5KAYZnOssc6wqXPa4It6IycxL052OMWYGda4azrWLpeQxjqrOZkZOTe0SWig+dwhaSxrjkZwJ2f4hUv8Vq/E/U/Vek1k9iWg6MnBlCKlhUvu1CdY1//0ki3g4tzpMkDu0mBq/3Oypr+hj6R90jBGQiajoTJhsT7q4DTJWLeywJgJIB7mlvwJls9n/5euXvUBIEiHLjWAg4B8PI9wlBWdYibxKrfzOCsXXIiuDierZwTcSQf5xDTyPP+Ahy/xGL0beUjid/U5N6pWEAoiQiquslsmZMyQsU2hVrtU6NhPeV59bAkOA2c15HHexgkenK6rO9jGSYU6nZEc6qJoLLGhVQAVtdrQLs5qnkHfBnbhFR1CucketqIRtuTEjUsVyBGEY692dTc+54V9+VvAeTTj8uu9/IX8GlYycptMOiXEkxoIn2QjS59Us85xBtwBPMvD2kPhIAf6kFy68EbyknsfRPKlPudQHvNdHMiuziqgfo7reCM3Ss9b4++jyR5hOsJPMsP/wPdfhqgfzCuKgdyZ6D28oZrW9nRy1dlZYqJgyallRR3ZrHOM2zmlNc3zYvSQR3VSM5rRzRd88rt1vcSQda1pCZyMuE7nBBNtY6TWqQ22NMuGPj+tUVzqt07xyRyOhYUJtWmsEkWRjd2q00k/5n/J6fzy+YCakmQuOtjL2ReLfIT/3bdolmXaTPU9K1W35GZW3uYldjLPG1nji+ymsMg2rud78rc14J+w/3xL6GWIYB+3Z4rMKzSCHsrkb+uYnI8y9huZYelCXemwxNWOSs4Ltav+KMP7LjePS7P3FHCf/iKHWY/WKeyoxW6KoymqkfMxcqtVm3XG/dktjOh4G2dfYCoPcog/lLCTz/K0DnGIJ3gPT2BgSdVLajSWXDUjqTUeCEFLeF3JFqf555dFNQDb2MqtksJRsgj3/srYKZRFm7yZb9EaX97xkVcwdaffsO+S3z6EuIfr8kFOURlJdKSL5t1pTpnr2p6wSkUMMOiT/N3sWGOc/4bv574p5VwXnu1S4CrzCiTwoOBIDplnkntVsnKCYznTswGmZ1g+nyNLjgjmdYj9F2juecW/OKzksPZzBwdzSzUHU2qFolp0BXUWTVpFa9Hams0dPOpGYy0QL6BR84AOKgW7NK/Q53SnnuBm3c4bWNQ5dSqyG8FGzNFK2WqsFdkznuGsrL3cxD96zvcP9EZawhlZ5YyMDFuOkJPqsdrYlt2UFv6VXL6kNeAzgnv7QE136kZ+Uwd4O3vIhEaSXL1WB6oM3TAQtIKGE5xgwhbfqw9oqEf7rEaX7L55yUZ8IRH1FaUz4ADFOzWPtcpv5wFO5b3mqq//Alc4E/gYsMadOnphGT2nuiU4xEe5N39DprhrUEi2QyqlTpoq0clMNKgj4JS3CNZU2KG1F5jAef4uTwDP0ug06xzIr2WLTzFixrtyoC2vSxStZlHLms9hCcleUKPjHNM8N/Y0Ml9cgLO6oa/0hVuFCiKILBlENiZydvK0ntT4eSL8l4YGgvqJdx+cDYHl6e/uA57MAf+CjZxzqsXULCplTRNVzVO1yijhNLMMmc9Kspdf5218lsMcOu9lNF360zzw8IWPlxnAXT6Uf1XLZD7BudzI4B3cxw/ovjzfGXQ1efpL49kB/4j9mReaXS7/69/D31HmH+JndKPOYBUU6exLbapFtbhxlrHmqGFm2aZ5D9jLzZrlxAtM5RsZ8FYmnJZyOwV4jHUWVbXJbg1In3InaVKKtqulsUivW8yqaLsCswpkHpn6rvuVrOVQwjYgJxQaF8J9g4AnPBPw1vym52weLz0IuKgIsn+6ZibAw1M4+3HgLjb4GtYVOUercF+b3OaZbDnHF3NOaEkz6nQDX1THv+Vv8YP8Hj8LF0K8acVaOa08HLwgFnKZAezmkH6CFSbMM8+EszrLt03l2/t/ckV/yHT0mv7vcRb5jxyZBgrT/ppLrhEHeCge081xRntplRBhLJfoIpR21uw6a4vITY0VuYPj7rTIaZ6/Ce+PcpJHdEyFjjV2kvykVjXPKkPwmmToGAhtgPv9clVzDg1N36fMPMcugzX7lowzIQyFkoVCqBIOtxkoPKvN/CJv1z/u/bX1Ms9Xulh3/Ewf/Seg0hPBpuM4yYM8pGNaypGHDEgjGMRaQuMBjcw8ItmR+1hK+EkW+V0aXUelXOGNn5uaX2YA/7DewR3cyCRHkrZSpI/xt/I5fUF5OYOut4Mfzcf4eh7nSOZVUh0paXlED/NVeTyfBJcoFwKrdAlFI5fGg8C1RFpnGKuyDszpa5l93rV1p97Abnr94TnBrTLX6bTkwBR3GrtByGPNZ6v0poo7LQi1Kgy5Uae4/hJKGGSFW1EOHaUmIVISrZKeIUiKIG6PDXZ9BVqh0s8IusuRYfWp9IMJfxiYzeRjGhCaKPv755xqqkJhoqEq51jmKRot5+f1LJ325h41fN3F8CIvyQEu1oSuyAL0g/y3rLKl4IkcaE9enx/nzotpYD6XZHixyHCYLT4LOjQF0i4FHPvrC/pa9vGM3qvtDhnLUcKmqKTFlvCoZHTMZKeODQ+QFjmlRbb6dPOq2GLDI3oLE2Bdz6hhhjmt6iwNaEupTrgGAWggu5FlNUhDVr3k7So8rnfrLvXVzvMjf0qbdM6UHTiCqJJVSiNcGlqGPNpt0Ex37y8nG+yXTwPcS+qDQtNqYF7KDH577lMKZjRxxQqqxpE0jPUk65gmz+kJHWNL+/Ix7c1TOs0JPcRvT9PMnIbkF8GA5wSBmYc5VP9WLudCjvOdOAec4BYOX76SeS57rv/Og3yaY3qCA712cF40m/P/dCQzp1ZdbbJzSFapkSFZKaKNCU0VrYrCq1pgVue8Q7DMLFvPywkYssUEAXt4RsmmfpANKkN1aj1Reo4Z5jRhwICBJszQqVia1RYtnRYY8yUezA+aS8DtmTpP23OBUJN2qNDgJKOv168yLLMuebms80sGhCXgzAVIWFMQ6NxlEy12arvOZNCqs+xIzyqInPMkxrlCpWrBYwZErvNJfjCPaY3I5Sx8bYL4BemSZdzrQV6l8HQw/1TcaGjUqOE0z/I0m7pEse85FfmLEJE4oiWO5115Z14kWFxOIJnj3+iLzLMjxtEoTcEhjCMaFVW3DoflzmteswnmVWhpiN6NxdXu33Lc1h51GrKLTjN8WmKsPUSuySyoOCxqzHinZllQR3jVESPLlREDrWhe9+oAl5Lo3qAJZBcKIlULkUo7FL1iSLj1jrqvrvHjOqxDnD91/OWhgLP03cv7gP0kcANHuXdanAL0Of4jDSNN1DCIpANnDDzvcTZZ2ZHPKnOOp6a4yWM6xRyr+jW+YfoVt1xpenme23EFEPTv8+8mOs1mjtmpRd7MU/yLy+CfvPqjZPIQ9+Zu3s9duqhHd74kJMGj2s6W3ka6aoGxigyZheJIZ8liTeRm6InCXYRmNXaoR7eeljjib7jqVD6qNjtuInOLdYkBpwVLOaNT0bBaBuDiToXUSLY8o04TW3OSNrwLM6Dh9kspVPoBH/EWpZiEkOzse4QDyxlFpRbsNd2u5fPt89JLYy9eXN96Wuc7H5IbAfgS+znCxxGwF/K0vpY25zRDI6cKCouO0jXs1G6Kx6xpg/cwA9zhGZ5mXg/zHRzh48rksoadi+wjrgSC4DNJQsnCRBucYVPflv/kIpnk+Yseklb4kA7wBEfyUk/RT0jmIQo36DQwD24YKClB2ERGRBY8cWPVnucyS3WRRe5lkQ1u0x/mQJ6AKzp5EQSrKiwxkjTWHg2AN7PgRpvMyNlqy5ZNGbIeKZG2pcaNJFu6kaGW9btTUFbK/Aktsye3OTpCxabpIk1Mj46zkEqZZZNndZzv5iiX6vu/ZD4YMEjoLjzWXYIZ7uQJfhQQA5KSX9AdKlRXbJTRegPcRmVDGyqcprKhc9oh8WmezT/OmP0MgTUQ3DulAOSFd3J+lL7SmR8QbGiNkgWxzI169yXN4S9QxOancz9HmLnspxfXwyFWPdbtfF5bzCSukZH9abyyHSUsuwpX0dhJw7mesUNFggX+lv4zfvqy4lKPl7yXVX1Oq3qPRKs292i7TmtFY+FGE9v2JCbulG7JbNR4oqKSuNUGG2o5k2dY9oqOCpJfNvxCPqkmFbacpZcJxrJDpTaJZDod1byKPsp+jgNHePnidl87/Rf3Aqv6MQ4AD+WEnwee0ndwRDu5weuaqCgiAgeBVeQsKr6eommVDzjLzWzwO2xCbuIpu+gXpiQ9TWOAb/Z5ZPCS3sDe0b9bMMzOTzHH+3Q9T5CZ9cVq3j2i8S7eNy3Z6MpMVw8KbdeC3qsZjak1RMkStrMhsqmKQmSku1o0R2hIozmSeYxoeR8/zG3TkOnyWz/tARNdx5uADeY0r1O6mQmw6nkGHkcqPFZVCKUHaphYjsCdw5a16i2P+Jn8GgT5fdkB12lIIQsOSxQKQWQgoVIYZGhb7GWWc4a5C1XVl84NSGA8/e/7gJZPcIQlhoIngW/hFB/n+iwMGDMmk0xVmxqh1uSYs8xoVR1DrepzfJInSK5jg7foN/h23nEZDeT8cecP5lX0ATJJ9Ddzk1RbH2fMsTzD/14/NmV4HRZXC3DyIiL0T5jl2Uu57hf2jENssEXqdAzV2SZSxnSlLwjbalIOEWWoxl0o1m0NVT0CVY6zjXV2Tqtlhy9wzw+QnM6RlnVGQ605sYJ386zDqZGKxw46z2lkKJrEhPDIlc5IcszojAqQbM99Ot6zmrmLZ1y0bgxWNaaowTIlQ66BJqpRWONmtiescVh3cufLSADvmvYlXZrPrLPGCj+R5Emg4Ve1wZqeBooLin7XGkRCNsx7jjm1rGXDmkZMmOez/FlG2qPH+B3+Nb+GOOy7dLnRTcPzK4PAHskasplzupXChA3d7Xf1W4cOpq6SBZyP8xPxAR5g89KA8cLfPqbjrixzMifAmFFPBAucAoqK1PRNV6kS6tRRKExYyC3XHLEm8SABOqpeG6snoRzgHjYVrPCsZjVOeVOLajXUshsXGq1r7EkIy8apcMoO44lxaLtCcx6r40ndmw+DMv8HzWort5ixKoWIcF+HL/2MST0wUDI4xRo36ze0qeWXsu4v2XQPJBfZOQLo9P9KTxkVJwSzOeAnci0XWCSxpFDndIMVIY881sgTn1bSsFx36w9wPb/HDRk5n/8FyRnBCkfy4nK8vA/ycg8guAsYudFeNrGO596eEzzl8ug5zYaZeSGuHDOUOEBCHtCDksgHBOh27WJVA+1gTZ0bd5GudtgUtxREQwisomwYa2yro2VW66qecyfzPYaT2p/SQxewhd3c6AEzTNjUHKc00VBztdUCY60z1MBNEJOQx9G4o8EqDLKoRRortEjrUxoocmh8CyCd5KMs6zqNKMWyMygyTteShSEmYCusDQZC5/SgOq0Av6GXQgfrPy/ryBWLpXJY79O7gR/TDOQ8RzjMHo80cKEoFVL0tATUEMyz6GBZW4x9XbS8KSPfocfYReFP8Z8zTnKZBzVF8hGkLsFvLiWFisx6RLBO5jN0jOrwarqFVyKBCSLzgB5hkHAkITnCgYS7/CYhOKHUBgOdYntWtSqyMyahIqdU0nIoLYsanZq0GhbU6BwrrHJO8GlIqLxTSafzGOSmTnETe5hnrCWldmSjp2IBMdI5dULF1lARtViDaCUaD7SliUJSNVpig441z/Jb2i30mObZqyf1jBYd1B5NjVoykKQCWZIAKdfZzTHenrdO6Zw3vYDLP78GD+s83tjD171J7J/m6zdwXPDT3EYyp7fqIDfnFqpDqopkMt0IRbY2tWww0Aaz6rRB8kb9rnay3ats07/gPo7x6yYPpKbL9Xxh/rzXuUIptHdQ1zOnZc8xctR/eTXKk65GdH4w38DgQsbYg44P5pOCw5yhrTcg0rNqRDZKmSKkptffUWQpRSXtiJIhty4yM9pw49GUbALX61lgSwkcMXxJfzGHXM+yU42qWlctM6sBrapaSZOwpUJSTHYeyoyjkWMiGTfa0qqDJvfrS8yxygf5FAu5jQlR04QtUzBGnsYsOIstz8aS1vWMnuF6PsAjOneVjqWLJPD7RE4PfJS0zJ0J75zO51HgKcHb89x5lqU28lh+UL+uwjZbtiwIlbBkN6Q3cjZat2zZOq7TOs28zugEe7k+v2B4H9+S+3Rl4Hkwr7IF9F3u+4HCDq3l2VzUlt55eRFGl/uD851CfSDwYb2PT/SZ9BQOOsx3JhzkXZx0AhPOaR1CGSIoKv0xTIRCvUaAumJah2a9RdpGywli4NStnNOfvlDefIh5/rN8RreyxG42aLCtyIEyaww0stV64LFLdMUWnSvpgavlGWF7K87PyqbezC4l/wcHuJs366xGtE2jdA8A22l6L2WKotTEeYYm1zybxV/NQzyr0SXNXs+Nlz8Okqr6BtC+B3AwndmDnNGeC81aB/gu/pz/PqbN72WJgoR6vXrA6VSqyVYrteR6KZk0iKfInOQ5LeqLvC/JD3CYD7Kp88F6X7O4KPZxqQHUTPhR0JbIdZ1gT+5lmQN5KRL4sedtcTuskeZ5JA8iPqm9iAMa28CX/H72ILZroHWgyYGrUarvClCjIAgLHDWKWltDitdcNdMHioxY4Tremw9zRF8gSVY4yk16QtKGrwNX7WA7i9qt0+rUCG2qqqGhKCgqke5KCHVKQlXFadmakxmTrOnWHOr9WtUa69oRVDuUQjb0YpGUGhld1Kaq8TBOei3fpGGO9V7O8rVX3y+noff3YX5c5gg/mpkrHNa9LEwrgJsUJsAA8V/rAB9Tx48j3RNnSDbdUAhwdY1QMFRxLevMe54FtjOTO1NZ/M18jkEe42bEMf0LPu2f4qPnG9GmL/JQntdCfI5M3ArQUbTATp/kCT3FLh285BTDdz23uqzzEcVX8yD/gMMk4hbg+1jJdR5XZZMT2uCUShZ1GmUbRDSYBmcLCjmEFekxtamMo/OmrVXm2OSm3NKQNX1ac6xrP1VwmOvYyyLfxwJLGQ7mNMRsebtPaE3bqzSMGXdRAneleOKWCVa6UKOLTlhqwbMKdjPgDEfZrq9lJ1uap6OLxtnTQCPDYJSKYjlK2oSomvgxTmqOh9jQ8wMlEjzI92sXb9SHOYx8DDjK3we+hSHnOErDMZLKN8qs50f4RbbxWc54kw6Em+JiE3KnVFHW6nVnbCrVcsb2Ih/Nz2ibpJandZhv45b89/zr870dU1pgXiLrc8XWfihrDjJzyI15s8w7eYTzgshXFgQu9gaK5F4+z508oIPAP6PoiM7kX+Vvc5g9fWeCTrKDVZLGVGkslb7jnsigVJcSRgrIIndI1euMWOKMxgw4rk328jCf5nqkd6nR51X1Ya1grTKvoGpeq6BGQ+302OGJbTwUlrtwbBgnAw1UlB5FKLzmgrWTEddpqONquF4znmhxWqdO13Bk1IIkN/3GRKGIGZ9y9TwzlFz3iHUOXUbDvvT6Zv0JgnewyoiDkAPBgm/Wg17mzeziDhqGGnOaX87/e57gDbpe19PmU3WWeYZujNQ3pCucLlmctLRaoEOxoAkNe+MP5jpzNCp8in8M7OdHp+Xf8zL4TMvXFwghhy+BI63QpsLHtJzwWwymGaQuKSs8t0lcJCf4Nb4GJBo2+I887XV9vd+nR7WlRZyN5NCwpCImUoyLQk2NLCrIJftE17Th4vRWdCpuCE20wRs0YsA5DdilBeBj+Q1cz1F2alPV54TGfZ+smtyKkN1pC0diZRBdGUtOhbBtjW06hbtSZUWi7Wxyjus4x15OZKV1ppuqPgS0XYh0oqKwGgQa5zLz2WpNE31BN/I7+jbB/xpXg83/JgA3szuP8TOCPw3cQuGh7Phe4OOC27JlxPX+jJcRq7yTLQ2wxpJMkaziINIypUreVOPTHsoUFphoL2Nm8zhPaZj/Un9I38pRNi9v1c2LQL3PF4J7gYj+LxQWNWaOeS8ypOSllJLP62q8gJ4rto0zXMfDJFt8jgWRT2iLvXyBRRb5hJLV3KFJ1yF6qQ0JZd8e0qqxe/UN1RqprSieqGo3rXZjUk/qVhp26kO6FRjoDdzKaW0ZBXMaeeBFFlnXZoxY0KabsuCJsnHYhZJZSk/qUKMIqdqSgqFmNHabewisZ/iIx2xopq9PEMaI6JySiPP0sM6JMBuseciN3OhbtYBYBj7KGZ2PubPvO1SyzD5u4NP5lAb60URngW/mq/MgDY8DTd4CwkPt5ZeoPpU35hf1JlrhOTurCZe+MG0RrVuNY8kNczRq1MVYK1gLmnCjaorKf9DtfAP/8JKN+uLpYVdIxCTU+/w/iOwUSabWcos7+KIyL3aHvj8vdphcrPs/6ORxbsp3MWGXD2uHvoN/w4S9eauezpt1A0vMK1QkxKREqcJZIrJVDwT1QWEhjFocLgK8FQ1DwYYmOZMnOMt1mEVSW4nPaBsTtUw4qwETdZ54LGmoTuNYwKWGdWGTYYLc2UCrjBqdJzGx3OVMog0ZtBdxTvPqamY01RlTRnAROPv6YRB2U5A9UuQcdzBDaItF/pXFd7DUN3tMfeu7EL/qszrKTk65TNvc3skBFnmQh5TcKoCvYQEojPUwe3LkFbdsstclB5luJIUtqVE1Khka1I7izhZKBgzl3MxU5Fm9l2/Qk/kwJT+jvZfSc843tCaXcQLzH4o6p9Na0IpmLG3TGqd08yWvHHZNP9xnXQIRHQYd4aM8xW3s4idZZ4XvYlXSOjeXtTzZx3t1oU4YKbPpW2xLLQosuVXpIkp/Ri8axkRyKgVrTo10i055RmsKTrGq05C38zneqTfjPKOnvCNn2KChGIUaWncqnrixLafPc3jGjAnsUGA1btRpoHmLNjfYyzBXbVkTxtFalOjNkpBqnwL2jAClqXjAlrZxXFva8KbmWMoKfC9wUCB9QSB9HJhlT/59zukcaxzg5/QXaLgTKOopNP9IX9KAIbDFkBFv1JMMuVWVldwOYVWFI7LpexTdZ87t0A2zDLBwYUFDSV2aBa1qjn18nDd7xNM5bQ01kBdKVpfpA+hT3CRT1aiNpLXV0PTZah7iDh3WIR7twwc+dBk/6F3cw8Hco3dqxJZ+iWey0Un9FMdYYbHOCkmFiWqIsTsyEkUVJaOHV1VKwUXVtWR2PUMqBlidx6q5gxuy5Qx72MauTH5EN8caG1RJoSVmmdUZr2nAUElnyzTqQjGOHh5ohWnsjhpFhc6hBnkcGWKoOZ1hRZXT2sYTzGvcT1ffBxqKKYW2EC4KGoeiYVbLGuoct3MS0WiFNi/qbmQ+oR4XPcScjvMt3JbnNE/Hd3KSc4LkJD+nRJzjTswWScNtuZ0uV2khT2g/E2r2HRS2lFapOXFRkdlQ9ZY3hTboNJsj1qJxaJU1vsgJwR6W8gKlq4L0kK7SGCL28KU8zQ5aKZc1ZMhuDfV9ZEpH9HDCQd7XpzQcvFTmgBUOCZ7IG/h1BvyAvlfBHj7CPAuc9sCdOqXwUAMPcDSmECpYRSWKQBkOQpS+/0Wd5S6KRKPOc5aSolkt6qxCdzPRRPsUkjqKFmPMDA1b0WishrkcahpYUEPRuarGBLsLMdBADcTYE48CDy2NdEZrCeZZBkZLTiTswFM1FWcAUcFKW6TC617QmVhiyE7BbXpGf+ZCgPQz3pEA/y136mZa/Wu9kR0ec5zPq5dyC57hjyPgnfo9tjDiLZgP87QWcg9zKhqTbiNkT7efQqPiaIJIDzWMIQM5S07o1Gg+h7nJTk4ps2GZN+W3cHhasVVkwp31qmngs5DbcktbKY08Iln3b+VfkgQ/2+9pOu/5D09jv97QP8ANrOkGnY3rnZgvsE2nmNf+lMyiujSVIJzYpSotlwyFIHBt+mKL5EhTRCfUCbVaUGhDg1zRhEVN2IZ4J5+KTVYpnGNLEzpIq9WM1PfZeEapLgqN5HS1bDu6aOgIrFBf5JUVnUND2hwD2wmteJkFyGQuIhUqammIXlgn5aLikGo4R7nAQIl1k3Yzj1ngT3AcqPyIYEUTATyUn9IWY4F5G9v9Lg31UcGQN7GoXcwwo9CQ9yl5d5o3aBe7jDrmMt0LPdWIhlIDKukq2TTeMDiGhOwi64zmha197GYnpzP4sG7RD+qw7pO0OM1R8xI+wCUB/QH26HQuO9zmLK2GrHIsAW7nIG/WMcRhDgm+BqkHZKWkaolHeEqDbNjML2F2cotSRzWXN2QgryqZyNjYRZGFkGloVLD74rAUqVTv5AayQgOgc3hTCzRsZvCs3q1nWchZFkHPaBvrdG41B0qKpbHanFPjiUYeO1xVHcYmFeGImNjC4a4o5GFseDujEKFOQzaZ0ZJ2xaSMI1vs0rdW0ReDogexAVGGjKiG1PW5kx0s6BZ9GAv+Fz3sFb6WwhnBDsMZ5riDlm08wxfzWwFYZkKHmLCYHXAb4mFt8Q0c8zyhMds8zAarlVC1FFEoioySMjGjJposklDnkVpPcjZnMQ1neLOO8zRvysd0I49n8l3khXae88WgCyc+HOADCuZV1dViqUtrSe/nEJm/pv9Zn9JuTnCQgyQzZOZ5ApTWOMaTXtaABVf+qZb0VG6yn4E6Zp3MIdAcckQmdigxJaMLpuJLDjXRUKJ3cm6pnlBjRrI9JCSd1ZhkOQfapae9i4WcU+vqLXXuJA1o+8JEjJzq3DnUykGgaiJLZV6BMnvqqeTODaJRyRGrntDResKAjkCpzCBsBVgyJjAlnVGKGKiIHGuiLQ1IPcWI73PLQ/w5hhzMAE7zT7XCID+rMcGyNnUbE58C0G0Y8wjvZIYG2BDM6gnBQiqXmHCOTs4UkaFQ1KDflEg7W08Mne0qK+jsrMxLGmhZO7SLfUJ/niHfwiH+gW8DJdPDrvKiB0imIiv7VDRUYwvNueWEFg3oV7mNb2LF26floP0c9kWGwI06xmxup2Oc5qt5n77aSxI7wKJVOjRDVZtAFPXQDGHZqJ9SgshUiKydQ335a02puWxyRsEyW2ww0YyuZ8JW7mZFQgw8VMdQIwWVVrCdHUjnp6tI2Dh7Qp2adCAcyqiOIIpaUi0tPatmVRMVmoK6YvVUUEqPo2ZQ+ubArrMLHVA5p5Paoes4W8RHOMJR/q326TcM/1zwB0hV3ZTXUTVHsMyCbgCeYFObHNPtOqPT3uSoziXayyZ7FNoNComRXUyTNWjUK2o5jduIUDYeatYFM1YPc4VDY5mTdJyTeEP+Ye/M/cBSznCf4O4837nhvp5/WGRygGf4N4yJ7IUbttjSW/MP5j6Jb2ablgBzjMN8yNI+0JFpifm2PMZ2PZPbRIQ+pc9mo/nc5lV25oAhlR0MZdmjIBW2e6Zd0EyJ4S0lSwkpW5ucaFIGRhkTS40rnVv2YVUq51jwMusOntFEs3SMU7RYHbPgCTMuqp5Mkz/3e49cMlyqZSmFPV3U0pZMdcNqBG0OswtHZuPitCGwelaApQuVgUKtGlkunqMy4CS38Ra2aYEP0JHcyBLfzL/gBJugfU4NtETHqoa5w7DIp3kPDwEbNLydc+zQJuZGRuxiVg0jLzBbMwki6NtCzi8Xa9K4TFxc3UiEhrQ06jTMYU60rO1MmNNxtXzRZ5Xs1SN5b5I9se6IwP1mcDDRvTqY29lP0uaYJpG9i2Ps9VEe1Jt4Wreg7PhF/i/azxken+r9iA/pad2uswxtZQ400LuobPeIeYklWWMVFTpaiSykezZFIXrt7WonxcpCFBw1O3c0LiLlTjPeiJJmUzMkC55hl2ZockOhmR7kDMva0EjDbGSIqlRrO1wUgWtUlawmJ06s7AnpnjgEVRbZKt14H/Peclvd024jG3qPVZRylhqyggB5gDM1x7wWfAJxTCusMe9/oGc0y5v4nGZ0j5b5owRn8racsKKz7NKa2tzSIpsEB/QGJlTmNGYrC0/qOt5Il/MKTTR0X2wvIoRNcahXKZCyUpqBTBsRqDOWG22FPWQV6brcrpbdCloe1zuy5YN6yEd9rw7rzn4LeGgaBN5X4Wbvz1ZZiieW1nKNnfqsDue7CApPyhJ/gM/pl/g2DvZaeoIP5DO5F3mPqoaaUIXom7V0SkOFBmqZkywmHhQ1iEKTDaW6timHGoIioYzKKDCayHJsMlLRHPKK9tJmaqhZz+acZrwTNMdIM4xlNWxo4q2oud4zp0orydHadCJqdBGGEgZNXKM4IyMcRpHz3q0l1tV6oLEmbZQWZ5aEgtPKnFLDKVgRrlGLYy5rjFFWte400m5O8gPcAGwy5BRzPMua3pi3saIud2lLk1zUWS/nab2dFT3NlyT2ssUKp1W4Luc4lrs08rjOakZySpEkUTJwLZSqakeoRP8kfdOS6UPoViVntF/BWcEcyxl8SvvyqP5nfkR35uPcl3dq2h185xTQvUd/RzfVGbWyOs2IkprxJjfnIU24TZ/nHYDyaS9zo7brOI8IzCFg05u+nnVCE3W6Lhc00IJanmWBmYSZahpXmhB0LcSkqEe1i2wTLnJ1hsPZH9EYkkINqLGq1KkoRaRG6lS1m0rRIihY9wz9hhZUmzCSQxOXppU9cUZY0RXHuOm7fNRzfZ2RxkOnZl20k6HnNbY07daIvoXJDmVIDiXFPYBlydqQrBx5iVY7WUrKXr2Rj2pG79Zv60/xpEZqdVOiVmN2alZrui4GGua8GjpaLXMyFwj6DeIUwzzFW7SsLu0F5FWnOzmwStilZyX12AlStm7cRvaJb1ZSI0ZOrWXDUAsyoVt4W/6KN+n4Eof1Nh3keIq/OYWCBfCX/fV0muSoL9WEu5Hm6qLFQWaVeQefZoEV7+Cr+UTOs+7bpqLQqRs5TcciQ+ZAMxpn8TopUxi5YxQLzMpGsiJDKlQi1KpRIaqzECVk5MiYKIRMw3wOVbwVqZHnQUN3rLODCUMNtBHV6wSdAjRhSKNxEg0TdzFjW1mE7ehZVO7PfUqFMvo2rzDG1Y3abBh4mA3rnoAjcCjdJ339mSEpRRc9gmB3GueAAuqoms11pU/VJSK36wYNmWes61jySa7nWXXaDtqbq3mKYa4yq1Wd0Ug7Na+kYBpSA+a95BUiBypssamQLEkO0c8WVpMlnEqPI0JuqkuJLuXqjhlVpHnMqja0w/O6MTu9hWSop5ljnhNO/3n9WA8Fj0Bokpu6PuWi1v0ZGUNlUe5NclZnabhBa17kDRQ+oEeZzwZIf5uSCRu5HWuiVGhRjZdywpCJRkoXR+miVVU6nBUUtailFXIG4UZpZ8HY6tQw8VCdquS+FN14nVmNpCrNCwaaY5EBQ615kVk2vaSqlmAj2pSKJIPTIMmpGkSNtNQqQiXDznCkia2wg6EqC3SStlT6GZ+eFYrreYkipSyb6EuDA1dMsK4V3aziiWb9pD7tt3Cdvkodhe3eZFZranKgDY1V4pwG2grFLBPto2ZH0RYwZp7FRB3KZdlrTAgNEKhzn8j2rElKX5vLVtFIHkTbTDxRUxpXkZseKrF2MavTGueYz8RYS3kLn+EZfZ8/nZ/gRP4MYATnpszem3Pglpo1+0Bs0zVn2PKv+CnWSJ5WsqKRxhpzlu2s8iyVU/qirqf4nKxBLuUkT2ikkZcomlfVSOG2TnIm0g1jR3GL05TMGgTK6GnXcu9ui0vUALtzOKeGL9mFiee8ndYlT7jmBmc80pIaTLIZbd9EbiI9cao65JLGqJaK1CiadKWXCKpNLZRJ4MhFm06NioIxLV1TyUgTclqu6gvmtkoWNRTHWM6JqlpmkKpWtc4W5oTWMWMW/EZq1txtqVFq4FRlm8Wp7NR5u9axZtnGLiWF1DkdY6DGc0TKcx57gEWW0lcgE5kgaqE4qlQnBY/UWBpgico4UGJadd6iYTHN9mzZpbN6K99Il/Kb+LM8wWEMD7ODI/qSjvGvdJKRq4tRyhEMOamSt+RyNmxnni0vAGc08i2sqqrTmLewm6JF5pVsakuhOaRCiWCiTUZUddqKAWENmqhRa99h70GGgqCx1apRn+JEp6KegyvjkVJQWMsuYYtRpucYaahqeznRIo02HRoxo0HvMqgKK4g2TDXClTSFcIarM+RxQ8kmQp64cah6y50Xs9OcVTpM2o2CokAiZawskKpSKW5MhhoGWNs4I7Pm6vAOnTQ8q9t5Vk9G6zNZWXdhoAmdljK8nQXWc4ywn9ImRSMaiXk2ULZaUuet7BjQOrIxhIoJNTQZCiuLWpOl4CiWEzF02jmjJGl1VmMN3CBP8gxSx8MUvVEnfT03sqrPc1A+zh2Q79exXPbXua98EhlO5bqKFrzT6YF2Ip1iR67LFN2Yk1xIGCE2OO15kkaFhjnw2FAdOavWA1vUVoUZtyahibCKipxWS5OFSCuzZGT0IESqQQ5JJRuPNNDIm2UzWjrNWeAZF1cmQmuIlqpO4UUGGsu0Pd0kpIaxO3fGYaK4BsXOqKJ0fTZtRxcWTNwoXN2o8zBHLrZq1B4D6GnhfW9xieKiqcsiZ10crMsaq8mGuVzihim+dk5izKomWlVoRs4Jsw5wQ6eZnDgYsoOBUmNtalONiorXLTc4Qg1N1OiRwB7OoihokZV2jEtXSimyi6rDNeSqBjTIJTW5SYt8AxPOeDsr2p5HmeFxPsebAO/qBRPzPT7GSt7gGfpyioISal29yhyrbFjs5JwWNGZTE6VXdDKXWeMW7cix0rN5Tiik3IvdYAddBlWlGJotxDSomqaBPdGKYhw0kpzu21w7pVNm4pSZ0UiFJrtMpapalJ3EKeR1tRoQqoBJh7aM26zh6Dxx386ri4RuZ8jVKkRElBqKolSqNqF5zVrakmpxsSOLw03PJsC9tgaWMiRaOaOVQEOqklZFQ7WaZVZohFjTyalawoJqJhMTjYTVKLRMR5NJp6HaTGBbWgNJWefYRG77g0psuceli0Ixpc+UlEw2Ef1eHn2829fZOy94UtZzhnlmWWeQVWdZp4DelpuMuY0ZnpDPnM8CcjtzTHppZAL1JRQWuoFOMestNpWe5VlmtJSSWMybNU9n4ln156sPFcwqVTilKkg1zrKpkbYiM6Nvyg9qqyaNNdXco9CzbopKRl+gwbXIqCuhFrzudN+kK9ljTwiLzTTSujs1VNlmybiUHh/HCGdfRo1JyCJK0MtSNZGRQVPCUSIdFGExciUkZaFTpdcyE5FW9HWTDJxtpsNk44gcaKAEbVA11rOxl1VWPeNTzGlVoXXXnNOIeVmV4gkpeUtVK4KJVrVPxyhqLBWEXLSYVsoKdYIwykKhQWr6TmWFIkuV3USJwKpukDPkGVc1zEaHITY1LnCddgaUvA3nkk7rMe/tN8wuVwzzzGk5236rCystApqW21wx4jpG2q1OVYtYa37E1lnmc1MDNRoyPV1PnXdlpwXGlsycLbK4KEmHu0YdIlToEfboO26ZRtwquHOqKrJKdRRoUxuuVhb6nWNL1kaOvBkjTzWEnb3oKCKpRmVaPJOquuiQ0hFBLRlpSrHtksWlK1JpihuNIz3QphRFXdUgG4VCbY2oKJU9cuBeMSjDVoZsMjLZUsEMVHVWa5Eq3KyWPWo1YSIpUBa3Kn1ImUUuO6iaoaHTHjqCEQEEm2xGZmeBa2axHNP7F1l9Qm0cuAmHiSwRoiNUFXSJNrWhlDymZQupMJ/FxV8knOzkTRS8JBRazjW9hSYlucppW1WpqmGO9XAG817gWba0RRCydlIZ5pb2AwNCsBnzanvGkc5qq441TktRmBBK9y/aZKPSy6/3mbY8zWyDknLJ2ouy9XSMkQdKAjNjSnG62AwZexBtFJxDhTbVN+8OqBrY066faotId4GLarHVM46cIeeFVUQh0IyazKzuVWUbRVgRGRRQVLvvnHOVlKGSvc/CEq3WNGCD0AzhsZZ5ls4tE+1QxxrzHjFL0GisgTqHSGuTdc4xT7GYyS13VEZ0miPdaYhcssQkS1QqGbS9b8xGpUcxJMqY6cs3nhRHhjSKoTenXik1EJqlzWSb2gyOUdmVI82zIk+yP5FnLj/JWXc90Nlry9mqwO68watZ1HiPiqonWvEYNKdOIbLT0F2iNlGvp7KsOQYea6ani/dTLtkla9pZh/TE5ugiex8QKpQaaqWUk4hAtVhSyfRAcqeaDdBmcjZWKZFptjRgogmtGo2dkWrpNHZ1X/3p0QAhpd1zTaeJXW966iuRxqElhUdqs9K4yrYmItK1YArnD3hyNuksbiTVQUzjEo2p2vKahhi8pnO8hZHQioaxQMcg8UgNSWQlZLbRZAilpJ2ayarUEk1shoQ7h4obmmbSCyl6ygltiJ6Z6MA1SpEbD2TSViqU2fc59fWqAZXtnFSnk05VdXm7FjihyuOarS6CRT2iVb0px2y6ZlWhmLBb2wM2bWY1rsEJmQ2sXWkXYAcjPeuO2QyN1av1tJLSE1d3ajRxT0Yv6lwlskzc2TWbLI4MQq703gD3C8MFLNXSt44pZeQxKEpKztJopIaanWY1EoocMmKWDoipayYUk9Kf+R1k4JBcoyqoYcm1RGSRM1CjxsUZ457s44nW1UKJxlYj6H19TNWc7B6ztAY1GlWNnTmnhUzMkC0j67TGOsNZI2WjWkLQCA3dizwVbWnoHrDaSaVhlaG6WKqZXYbIqTBprRFlemZJuMlQvxcGiqjRuZR00JREUVzcuXExLswW1LiNM7Iyd1CAohlvaJfmvYcaJs8Ct2teYw80V0tYnXuJ3LB750GbY9JBxhzWuuZz7BENIhmpCxSWW00iPXFgzaaEWxcmrgowU4auiopSUYsa930AReGSJSP7Ok2bklElMjUOFGpdKN5SkExyVhtOjZBwpy5GcprikVtXdZaKZaKzXE0EPT3ALtSivsfXPScpCyWjKrMgd+qEIroSnoosuw7hPC0LKZyIko2E7NIME2Y09lgbhCYSZzDbeEwDQ4CW3CjUl/PSjYZ9U5rIkSpbSuTqgRqEI7N6EqOiKP2hihBE6Zucz/clF8IN4SghilUMsqoyGkNjG4SjKLWohk4pK73FpoaM6HQ2GzxWUacJFVzo7Iqz39iQJqGInBEq3vAGG3nWlYHmREoLtOxQm1vAQHheRXIvqZtSdH2ZdkqpSRwZYakqmx6Cpen3s9qfyleIzlGyZFgjJKlGL0IwUlUhE7UelE1Gok8qJU0g0kGbY7WecUcX50u2oZ7G5Z4Z4Oh/LkKllr4zuSd6NzHnkcdRw1GxaqkmaCnqJ74/a1mZGYQUJZvUWKUjgjEbmA0vZcvOXGaZnZzT0BNtp5OYJ6NrwlJ4qImK0aYabbHJOg01TWGz7x12R1ERdayIcWQpsmrpQsVBm20O+tPsKDmYKIqMCIfIJtWVnuvVulWV5RzlwEX2mEHa2+tIrUrORYdXmVWv2beROxlnL6ImMhStC6leE81IrZa0TOfIRqGBxk6VbGzhirK6kzVxRI2qzJ7EHOpIE10RDbWkXFCGGqL23cE97tc4VNSkagaKrm8YTZznTdG4AYqlppSYKJXZaSJLIhrXCIooNupcnRTjdBdkRpoU1URE9JBK1HAQIUX2svWiX+QubqeiMioopwaQkm01NRwualWGYeR1T5R0WvWcFrWmxvPa6/AeSVbndVbtWtTRZVeCoKHkWK1mcomzoE6NGlm9tOKIdWU01TX6mbVDkT0vEdmhVuois0Sxw41j0tAqcU2i0ovJhKSgBYqUMGLCisLriBalt3mkoWDdO1nXUFEUnfvj3NKoZNXYE+xSq6xzqnXiDcsjYupY+jbjzpWhRKhqy11YExVVuiiNSzYuQHF0gwzC4Z4UZvozgyzXqEXU0pVS0qjrD2lSjdqf1Whsj13saBAd0hwdvdFWzATH0Kl0cYZ7kqmnSsH9sU+Wa98p1NQ2G5VsutIFKHFxF8WWw2WSXaFVZCQpgxSqEn1lrvQhJipqE9azJ72mJgoWMZtUNRlMOKNNDdUyVliyqFsixHowNTdrAhSjkZhuDyUdLnaqp81kKChRCEqPSTrkbGvj0vRCmzCtbtpjZ8yoUUXMWhqAxrIr59R5g74qEedzm7Fmc10mw8SUERg2hZSYzYitxLOunnXjdawuS3ZshTTGVMlVY4/cM/vnSHDX10wJdTao7wyLsCjZV7WiCqdqZK/Eb1pH2E4XNeroQFuSGqHiKnmLsRoXV1eLRlZn3BGqnjTJwLXUgqrTlvpzf9XnIWXaiWirdZmye0s2cmckVxVGslpXBV10VlEhcEZSle7jc5yFxlbBQ8udN5WqDFRoddYWzMg6J1iTyVSkGqW7kHHImU5JjYOR0MQymYzZDLk6qUWZhVCjYgjLfcLa4wANoeIZRBOhaBFuOoVbldLbTHqLwkBJ406TLKEKO9I5k5CMcizjhglNVlmtnSaVovTrtTa2nZrUxmYdMbEiwml1fS93QaWvkzkdDJ2eaKQ+EE8TpaSSiuWInvopSyVL9lMrg4npeRxBSWepzkgrGiy5xgyt5NoMXFRRFkmpgYYyYpCdxlElda4UKTI6pat7Ord66hnqq/um1MiGyHCpkYVGJhoNXIVCjRuyLfJ0m1L0M5pK06ubuEYlpFYJXa8dTNIyUGjQN6JPHYe8ZmhypMJEclhOC2egTJmOCVIXimJJ2bg39y6rG6qyOFBtaqQotdSGnkUBrjZ4qz/QVDXsMb2ibOtSOqe7GDHngUl56JQjwZO+0wigeINJX+i0ilz6Ywlakx0k4YgRjZZArSogjxU9IlmDQsWBqtDEVdaspXBjgTMUXUSRanGGO3ugqJrCv0a1yEIlBxSCpEygShNDF0lqk0YTJ6kQMW5azatz17SM6dRqFJ37PLlzRHEXXdNvmkzpHl3PnlHf7CNHyT4ALZSIScmSTgJi5J5CokLaiigqNUj6SEPVuIZCjrZrOtP0S7zVSFVVW5rzAKtjqWCxLrNEZcaN0/ZE0PX5gys4QE0JV2XPWZQQXZEi2oim95Fp2iyO0kg9hqqiUFEToWaKDZJNicYlZCvDOcElJCRlD/BNSI1pVFU0i9IwJpmJhtRWtpCdahOekhAiRJCKoeWRwxOHAQZR1DlJjz3Rpmrf8hENdtXEqS5GpXMjsoLJEoq+G6iqFhU1fZ1dRZazyXD2kW2Vw1FbsJOJJyo9Jwai0wQgSpRE49KV9FhBdacucBUxiRKSIvsD31RVqIHVd/gzpaC7F32mdE2YUp1uce9rrbZWDaS+EdTCouvllafNom57bCFR3yWWoSFjh6GWogWvqTLnRXdKVTUaqQSEoynh0m9HPWM9sUvfsyr3OiByiBqZGaF6/nTFaehKQWk7nQ19g1BDoxbZUlcyanP+qMvMVinSmbYnToXG6vHM/gCVXqKQVKte3StANHLjiM5B36cbORRepCdVTxLZuKohaQMqY1esqkn0IvsNNBU3cihKpmLc9/8WNYQyG5wlpUihdLYUNW7cTpp+DwlQKF16iayonoRLh2myK+EalWF2uUhxRGKHh7aquxJy6cspxVmqOqWJzuGuf/HTHZXAtVijqfrGrCIcpattWOEGZSRStUJ9Ezsiw5qULDX+/3T925Ysx24sCpoZ4JHFpd6jx/n/bzwvfSRyVoY7rB+AyCru3U0NSeS8VGVF+AUw2IVL0rr0HxSSL/wvAv+QRAT/8M3vTAE7LkoHCcoQLmlZ5FdQFRTiAL5q00YzkYaxfuUZSgrs5WS2SgHBxUtkIpgn2v6yhNXz6xNGaTHjDQmJhvFw8b9giGXbUjd4f5C4+IUQApJaOEGVko43acP8HwLfIsn23m1a+Ba1aVW+eKn/MaUKC6VwFW/mUh5wVWJhVZOs+2PRQDraL8wdzSwi3U1XIPjGMSQlk9aKE7kqi9DyC1tQxJaz4dEUo4hmTocESBXj9k9fTKSSwVQf5llBOi0lcYkitIplJJq1vMZZtYfB4GJgRYpejhNhaOcdL4bIf/hHf+tv/C9+c+mPSOuouJlKmYgAq/Cmsf0NzMFf8SaVCtrZF5YQRUZIC+Hlga8cphKrxDyL4ZQqSQRsO5AZJkOyzOStLXPrxdTGP/oOMqKYkgi+cenNje85N0OMjnIj3kyo8yr1IvRS0QoWAmSRIINXa+p5yywBkiIFZhQZbDVokkBkLYYS4VWrC7/+URuWtcy6SKi6rWHpxUtFqW/GDKJQrFImViaFYmN+DCoRPTYOwDpTa6FdSV2hapvaWlCTQ5ghr56vKxbdPqzLizJJp1tFABOwFEUGbCFbZpdOOjZ2AOT/hf/4f6HI+I/e/IYZDH6T2HAegtXW80SUMu/sAIqiMgyeWDzYoNYklZ+EGU4tXK2r5moRUEVAPVoLJsIvrpR5E2AqEoo3k4fFwLcKUuA/pjFnUXvo8oqMZLHSSQaaex5a3EiDomgWT7uSM1Uq3ALJO24EKjIXQ1CqJJSCCgXTUFKgc2ihU3c72vgQAfWMnnkuL/f0OBnGIWkYwRNACnkL+cW9vuM/pL6gF8VQ7uzre7d6P8G2UmKUQs/4qXrgng5LXQtkSMuynN90HAi9GHU5uapHRzDoTgow2uUwlsVl7IVrxcb/W/8t8MVbf4f1rbYC/qvn+ya/UyxEQiXmQgagcCRIgSTfCBUlZDC1PW1jtj2dcnhJi2s4CQt5VqWjf+UwaTgZ6Uia7ZIcGQQDfdeSOoFmAlgHxfLxsWgIlEw5qS1k019LMvWfIacEENY2Bf0HB+oRUD77sHk9ziBElhYqQcpRqYiL6cbUUn2TsXE2h6KkcJ6ryOUUhZXWDuNWon06nBleZXDphRcDrzhcquH6OkYA3ftboJp2Eu3vAa50W9MHhJCcCkqxeQjhS5fCKTAUzGD7M4lQtdda/4zrJJLworaveKf0lWeJxU1KMP4v/RP/S1CFUwIZjh1FaWP10Iwvt+74hHypwtxaVLwDy02T53jao8iFRc2hk5wupfEOijrUOhYq4dMz8hCqrYK1CcnZBmG0TneClzIqDrLLWaEbDAEOvKITovmetgxZEJKG+A8iSIRQZSUYSzspE6yodFFQAungC+EUUQguLrVCTxazxxxeEUiGyJfTOOHseWXbs0pla8k2FIgltkQSihKSsQIp1MpQlg5nmCP01RqViAolo5YXg1Iga405VR79RyeOjAilosJ2zw7KIz2hoNMaYemKZR4qU0dSoABUlE5ckaxYFg/fHLPvtVEIVTiPAkkLEb3veVDMhuHakZxkhKWocoNky0KUuJxeTC2o8iQuiosvrLgsXFh8MSIlkay0GIK92ENyg7xRI4oA4pgIFq4Q+w9YLZk6p4LhqFgSEdklC74jaH/xbT7iCVabPfYs0YDjrFJbQlJAx6/58qpVTbdcbUPaL6qpjgi+HJAjZFub6hZQFSDXwlfYBcay4+Q77kVdcaLioBpeYi/9QxASkYJVFDZzCbwQbbODQDqYK7Ai/uJ/+I5DpoUXrpDTrI9+CIIMWOFkKk3Hfjl33mYo/l90mJduVUuctPOL1pdeaedyIV+yiqL0iu/crB5gR0GBBR1lZ5SvnUNESQTTDwIQTJDJrmOSkTGnIwgkEiwQFvlWLYE+UWHGOj6sPAgVg6EqHWzeMG4XpFMOq+FpqcK4FDIA1rGbMRdBJ2E4jBeLBsKCRTZuwJ77JVOBCovpdPvs9MA9lUzkWC5x6Bl9xAlBVWbwqh5FBeDli6GVojaV0olIi3G4x3qeyVAcUY1/BfWhcXXX4ZajJ1XpaNN6NzcxZcallbkWQFwmAgvBi0H2yMPlbiCbx3TFpVcww0xBr9h6q/Ki9J9AUt8REl5sc3lLrYJo0nkcyRcUS57kUdo23wGXLoCm6YvuRcploX0CEtllpNMLqxYTNLzYPibhUJKLVVxIaQE6Vtvx8pYGTxXY2NGKaZuodu6I9tJwNCHj5MoVDoWJLoOoIEupri0iT6Id1RJkWIitTmlGgJiXjq6uLwxU082V1fMAB15YEBajxKTR07eUmVRGJJMBrlr4isQW9UonxCaO5vh4KVBJVkhdIMXV3EMHYvi1gUDUEMMYjhcRtfQXMAf0cxV2NmTnRthEcIkuEkkmnEeUWJexqfiL/w8d/x8cffHFG28xGbcsJygq5CCS0RLE5IYFnK8dSUahvVKRYF09BnM4uECGm3QdWki1TPWqFxazzOBFmY6jikiDKJIUVagIQV8gJdOp5AbACJg3QYq8E3ECaTofGwWJ3APbZB4F90S//A/bTRlscmKJ3EkpS0hfJYorXg6Sr1HXN7RJRIkCQkLPu5oYrkXh4gLPV3uJ9blA2amS4UAeHpROQAdSpjJDOrHhBS06KqQTCMqpQNXVr7mDakAIqoiWXNPBiICdJtdZ52pVnuZPgrIH9LNKSK8GYZTfWKIQNyK97BL5ltbNXppgmiGVDAbYERkpRQZ58sSiwgpIFWdFFqUiwyQzGSeYbAFhMryc1dfQhbCYdE9aIisZzoKUzRvNbEWRksE3S8jT7qmVGxJcaIe+kyCaDckqJWEVETy3DcXwO2DdXKeBUXWWHiHotM0fb4OFxRPOqlgBw8F11DUtLgnkZSohHIPhbIOoCh+vWviWkIRyn6WIcxABGrzAFYHiJeo4qyKkiitAputgIXRCIqpNHaKEg2TCJg5WrZQFdX9daWK961T068kIL4fTqqjmAhAuBjtmqk+OSC2AulO1nPijY8jOWOfFd4BHxJ+i0keX/pFYXOkyoDg6J+ASAueEEUGbaD6GT3JZmcybkdGvO9C+5anF+6TKkpQzffuGEPwHQSPt2rloo2DXITYvxf3C0SWXmSGKPDooHR2dYDoqPPlYJxQS6Uha5okSKIIXwE27oDg4dFaraQO6pYDIHYawo7LSUeSlqOTCcneuJssFRF+x1TZXS/8B+FIisLBKkSelcyEy9qUooRARwSCFUrzD4VziQirP1XyjBp7EyueIBxFtSd9XVA+eSYcS/CspKwGf5TgsOzjV69MCgOkDSsmMhXWLq9KATlYgX4JecZG88yziJYuLPIEkgAqsOQMM3omuFTXuo6jsaDiQ66IJFhCZyJkFXtA0scRLxKvSq4KBvxi4asG+kKKDjHV4HHR3I1oNzGn56NA8SMJ0rTxH+u6mRwyxgABXGMw6wlYE2mhdGzqgYleRTm07xC0D0TBRgCfSLm0S4qbo1XRq8OQSdICFYISqxjYyIC5EvTN4I2tRrEoeLQI+m7ITb8MZN4tZ+3DRiBSWY1ezN08k0J7eYrjloEAqdKq/U5LBBxdUVmSeI6XPxTIgvGI5gVKj9e0U1sYyFZKTqqVXpVMB0CfA0o2CcMdJN3qrPJRE9MDJgXOApGEeT/S0WUF7VRAImz4oiVwJNmwmxV6t2bEphm5cOGJICwtvhOVYjKqJlivHZhjad8QJbhQy9pHCiLtWpBZKKip2wEXR4qoFoAY7U5K49E0G4yDq8lnYqFzvfTjWgcEV4ZFzH5oFUKuA7r4Xl6JreYS/IpnV+RmwhLqRUn9HZgflObkKPCiIFeGM+xyBPMugvM6mDlYzjAQhlABD6/goVCKwHIrN9GkeEops+KkhjgZbshhr8Vb6SOss08GoK7LbNqJNkw1SqiAtRWFdION21p9UHW1vBv8BKbzySIV142L3II3mq8tJ8xz2x/FypY6ScLQalgyn+F7Sycy++6vJKIVw+sU3ZJccZv5Vm8mDu9Y5Mt8M0wWimJLrfS9tndpagDpDKA3SgTpsFJpCj1EDh8lJdrHboQKR3pBIHMNoqmVi2ptFmHBFAlx1h7U7A4C1gpBNpd+XxHUCq3V6thsIasCWMF+1mbyQubFg3w5uqNJ2IWEqjUM6VUIFs7N9GOmSVWCYIYaT4RAYPRJiF/PJDn0PqJeoQ1hlkOD2Ci50Qg8RajcVDiLcre51HomrpEWIiqqzirFIMP77LHzTvGWp3vwanwfI9HbwOI47BUzV/cBeeSMTSERQcWIxhNC8eq5J9FgKu24q5ONkFsptwI1vGUKW6RNESccwqK0SEFlvlqVgpSdLrFJVX2Axk30HxWl78r1X7qU/pEKHdadZJdHkEQynBRHiUbiSMPjeL9whgEvXIYkLlxLaX0iEpTxC84cBGq3sqoD8wk7eWWKiGEocE1EGFu+OFxddye0KhhA7EQRXg3Fq86u2n0zkCQLlxtCizKkCiLJDaqhn753CF82TWkeIgtw4gtjOypQLshnVBNKAfCHhXeBVqXjJ2u+i5DL5TSOaOJBYiZ1hmqn9xrpT4jHBqBKMZcRQZ+qsQ8XRSa8TTl8M9ERSjQ8oDsxW48M6teRzkMEylLtU+a6LooUivcLwdmozd0SxCcRsVI2nBSZMLgilK/5xGuETwupuzUzekIO3SZomRXpZnEAGUo4AuXZq1UFQTLjgv0rxcmBhIUDMiIklAm0UgQiQNXw9lg4WKqAige1kHa9sbJbM2FKlovJAojLf2xG+A4z0zCAKPFiAO50yTIcjbM9QKhn+WrddaWCRuJSVp0uysKoRVOHAbu3kpeWFFaG42xwlT8Hf4sFauniK50v3yg02i0jsQXUZZ7mIrO+6XnUQwXI4vamC4iQEKt9xrZ1Yo2EiAkBx4Q8Cq8oB40+kti9VFaEsW8QRyVLiFFBMHLgcR26XUdPt1E8UYReXcyZNjyonKC6XTLds1W0dsRFw8LaSLLWVPYIXyiqfqki5oOop/yXVhWVytb8OWDF6e9Js2nU6/cYyKWSVhOTtYIGErnJXk0wqKrbtMPTiqQSogBVhVksbe+jbAA8JhEyhaFsZ0aeEyLCcKxgqRODIpPgyC0gtayJ/1NGOI3to2GcVVpTS1PsyLphBn1rL5F+otZIVrmySjdr0Jk2Uo5DeUCK9D0Iut3okYrkEX3QFrzM29d2gQj1hwSriWwvSHv9QOCRniYvi37H2yTIlZL1hcvF9FAyXD80OQRnvmzzNS6bJpaU4xWC2xw7K6/CLO7Miyoj5unHa9ymYyqLhCBbjIOBGyQjHzP2WwkuvfTm02u4UnWYTPEPcTvLITX68lF4Ejy8bB3ZRgeRBGVnhAhKlUKUS5RQgGDoSGYjKtioo2mWAgdMD1qY+ucTgWe7EcpUoHC2DdlDlDIdFsL32hQIqHLqQDC7DWVdi1xEOK/mm/HWObxWNA6sZ/NThGM9vywnwiKcpbkBRziOZ7U4aVbhKcEfG18XAQgJcLsqh7S8H3w6El8qbyU1i+eaprF0Bq+5YcLKi7lQ5owJOsBztf4fNZpnjyNHHjGV6JF83X1Fsdv0hTmSYx4FDMxzRLdLEPoJRppo7KgmSdii4ShqnTS4TOa1fotzMNDmf+pqLsEoRuB1mQWQQKunYUllYtQvCQfMK6RB1XAyaLTh3th01WtcbfuNCdWKNo0BQbk7zK25oK1STYAKKJ90jhYl/IYpUBOA2jVxrlZSnzl5kchfIqhNyxC56O3wUlk6kQu9cZdgQw+2bcLhQzV5cC6Qq6Yi93Da6C+mIVDhJCJ4o2+Ch6+oxn+XoT80EWUxeOAbbQk0+CNosOut0q0iLaSBa1V2pkWn3DEahPNlOWvTYKIR4eMLJPMQlIWp1lBnodJKd0QF5IfByuoe1whWvaEgkuua3UCDAbM879OsPqEA1aRt/Qb4iEFaFX75ACZdVyYWsyERq/Aaq9YetRU73uEnV2QQC2X6kKkFFq7rKDyVD6ySuCiqu1uFW9PBldEEkWj4NcGwtVExcN4trXT3UIYUFHp1cxMKqFLS66EVkOJAKNcgrmq++Vx2xDk6KAletMC9IX1oQV/V7Ctg5RpuJwDNMHxdzBTvVAHWZ55HgkAf0usXURYYqIEGR7axSXd9a8QlIFNscjVRHJfXCKDO1Td0bSqNYESyRpcha1WFUrEqj2+aojC9HfFW4+8M4oRXcHcPt2VrCIisi801TLjSelygWLt8hs+CD5TAS/yShQpSdSIFC700MKYWax9LyMqfBAKRaFZDYf1KxZ1C1Iw4uHsChVysk2i7S7RIKslxMVSKdWL4qVAqsU+TLf7t1EhsAkz55QSi4NTcBKW2Td6xv8JETLR4nylTiVkPr6zgc6wCsLy9/OckCgsBCQLxwmpvni18mtgsJMlG6uOvSGZglCFsVhKoYRZ4ePaZF2YB44tkrSguJ44XehSnDlU/iQ730B4y6xVo6GPPHPEuqtqHeLaR2OnlRxfb8EvzFiFAgq/2AQMEoEl2+reqjeiuhSppvL7wtmagsO1zyiR0FB9NuqmaEq6OTmg3IhBQ7m+hKYPltRcxDbNTBANLhqMClP6Zfb768XlzlCsiSTk65YoA01UzBvt5m/BK+dBCHAFYdHgUWdnLx2CLDgE50dpECdTubTiVzcwVhIpxVuGgugMILSdWLIR5iXFRY3eTabOCsyfXfPd4K8bvWJoibdrDIs1R63W8BO33TwlZWhdMiGOcoZAfoGYu6Gw4xbApG073jWOECybPWfXIzwnEiw7IYDJ90XT4lwGrMfUVlycmopGr51T16E6c+sfShhY1gOHLVW8lFl3lilZEASrv7BqVrLW8gaZ6otIrBESSndRSJ9EKaFG1S/aplEEGdaLFotPViWusiseq7xskMDLkfftOnAbRKoI2b6mvcelbZBUN1iGJ8vf7Z2oHvDdX7Ui0iKlKkXtiwl1E3L/8TPAxW0LAjhOtGvEwGL4TlF12JqxFSr0l7CyfkDYA4uCp0AfjDLxb+0lu74AvGjcS2I13BApcLRShNoMlfgDOL1OmZ0jj30qYWgslN7GAkT0Y5FCoHv7ngwBFnpt5UCcAIHPIgnXVJuJxYyMGxLrakoXWuaHGppfDDuU+EN1UzMmmMW38sBgqWfAyUMgo4YCpLjHJEqWU0JWQtxSPiNHuat/q3Uz3gNcdH/lxMrhtXy8dlcdXBwjIaCqI7cwQoCtWs3o1w8PKq4/AuxAWwqG8xoet65bkUe189VVnNqDV44mbuiheddol2fbEAUqtNKRgkrg7TaTcAZiO03uMWLLVF2C3F2ptf9Q8Uec5+sap84Vh++T6M20vcRw6x+PXeKZ/x59kJOpMkVpM2kJV4PZleiCXEXlBTKa0A8wjXERtglRcT47hziOQLKwlURGLIV1j8C6SqucAsu7mzIKp9GBOfYCaoFr5iYfHKwlKfMcKqjCsy6lxYbdQQrWYWAJYqKk+4TRWzyd2gGaxsYhphjouYCKi9y9cVUHgZ1fh/gFh8COFTsZrF2T4vhBLL6kDHVAbY11sl5RWKtKGDWqWeTJr4wqqIRh1g1qvIy3aepuNw5qWXrfTF5VVdGXBcpBLPeRBMLF0WVxfehpIrIhcCGTGlppR0HKBipyvkYKYB4SS9bLDdetsWuY86ZyRlV2Qw2rW1Dnhi1YFQlyOyxAWcln7tRkOMStxIwPIqYDm9cPnrLH612/UTPw252pXEOVWUXIBUhFgMhE4VJepPmSwrk9vVRv9JKlWIDkRkW0Q19UMI0uhSq5fI5EQIBKrreelSVvMzcvqYq2mkBPpumdxluaskLkddEqEvHyLJDeIE3qHcK45RRh7QoQO1+fQivIvKQlj7rGLKrKIUxWO8UMoqUKhVpoZh1Qe/fXy3fb1UpAxEMb5wsHWKdembgXWg07iHs6C3oxwFQ6oyLJSe8KOgXJWjCc9aiWbnNybOlA9NpAsv/sFau0qFRAGtXPfCgrgAujCmdeLCYjD8RZ1QllHZ+LWsPuHduRU8jPSNrJt9BuzKA1ar2EAch11tBUKOcWPsSkSwiRQSsFsJGE6me2qH01hnv/jznAhtqwJV08kvAj6xwGWaA24nJVsFeCysXIho20bJX/g6/zBE+05u2QGtniBUGNLpYVqJrU4wXxbDtdP/wQ64DSrirFAdElnQ66wCl8Yclq+2iMNhKEGmUcGqhEuSwWMWCxFerpJqL+6D9sRhuhQgtg+Ek+10AzfCvA+T9KqIoUd1KlaFJQKVzugjfheAO6KQdRC8GitHSFiHa2mLb3dE2MQrLCdnIIz/YurpwPsxuZUBhCUxVRXR/XTicPnE6xzdEiN2XRDTUZZQLl8PZ7fKav9zJ5JtR5djEkEbu+t+qrICgjpIFSQclIMGUjqeoFguSJ5romsVGgpukDqIqM4NuPLsLRRaoSTE61ury+eTX7gJX4hYoC4YG0IhxdrJ77Pg0LHRSeWimKAQqwhaSlzFSKMA4fT8BpsLWxIjzuUj8e2lFh1sx/nCtyT75e968WbBZQYJYxVAnWSaoB2L1w5IgVWZbVd5YSLee2jsnGh30jGD09DGuYIcsYUCG1e1YQedWl7OdjB1VkowrhIXBIqEaasFJd15uIOcWuqAy4B8ZCwfBVJu84coW7IUMEFttZtXR1Q6uSrVZWmyet9Lk4NMijqCPfFViAqssjfDriU2tXw+83NUkbBcIMKZlyXh8rJOjnoQJdq+l6XdP8WmEiV64eq6A2TVsiH7dgpmVN80ClYU8ui6X7L/OooOrekU4x5hbywbq234jApJdYzUq75PyLh44lRDYWBii4cMt60uZXmPWaK8mQ60/UHgiXQsixcZy4cHVF8PRdXhqM8jTnsMJJJxVsS9QJibq7lIDDeUmT0TNLUMDu+ltwjggdD6pvOhIaObNrcbjVAtlmSBJMrZtxlZav8rsBgyjJIjo20UEIaTrh4rBw/ClE6Yo4Jo4dXlZZzCJR6W4mXhxRQ6Yst8sjLbgin6Guu+RYVMIOokcKDDWsHjKGaek2oX/H6JIIKmubwjTtUVt5OnN1CZ4AVWmHgBGVh1KZVg0UHjdCGupd3+J00SOYGFjaXdPo982VjnxGmXQ8kQavH2sXxieKTAAmwHxIvLl4OLI1jgdUgxXeeqpajgUfGiecKV8knn6FECEUKiQJdfgOV2AouhfJAvJTs1CAgYgsGWmQ7m9tLmqveArQUwa/W2H9MbqbYJU1XZZzKSIEsBN001Kyblb8okdBIDsFwINnvJI0kRB5qqdRIQHRAUZTINjHrFRI8WjSgidSl5KbFo3uaWSEMb4UiuI+EAjH1Cj3r5ZdN1GCjxLNlp2nwFDs8iLV5lhV6GvjZxtZJJTQspq80li033BhEEU/SuLDBYLL38D7M2zOyuxWCVQber5dc+0XRGAqlT1FVty9StRN/dwuKujYVOrDLihI3yZYcr2C5aq6hkWqDVcCYvOvLOonoS+OI6iva1DoIuPTHmHT2jdnVLs/XwXNhMl4vH5VVQuJguo24mkGWUYxJ8SmWTYGdrdOMZM8Z2RWxI5W7+WnjbpJFANCe69lgy0onXXshBMT/pqqfPUY7QLan9xb9PZJHrr/f3iqKNJTDeAV/l2WSKs0xDiYMFS0Xc8XqfXDhWizhcvEKH0iaz7IQrU6cxSw1s3qjsBaNmBtBl+ylEmjglrvK1BBeJwkI1/6jevQ1IrOSra+IK2k0F6fwOj50Sjiy+iL7lYS+cgkubAlG1KC6mXoqeo/H4+fHktoULvrzMEWOM4WIDQSV9Gje0lo8ILhgvbgcKwqkg/QadctNR9DoI4bCU4Em3xLpK6HpFTkd0gQlI8EbH1oHhBpfslo+rQV9wX9eF44ClymqbeT5L9Elc5u6Y20q/4o8ZUQsGffzqsArZx18KvJnZ7sdL44fanYBePorb1fUvBSzlkekLBL0UjJK+uvtvELmZCdXxui8c20I49+u6ecQd9FkEkwcp4Jg+rQ9VgXV8uPhsFCSMhXLQoAOXgEC0PgFdtYcX3dYLSJr7ZPS0x2DgBJcXsuFjXziVekMFrhKJhQsL9GIz1haSifYW6M6KIKrQJCuIX/7mRulUQCiU8rS6sirE5i7bWrjhkw3AOuhmHQq90Ei2Sgij7AqA4DgiMxri9nARKV/cuQCtrRTTYwY+PXcXggBVYxbVbllAeqlqyQASO3MH98481IljLzi+dhe1iReI4wvfTJhFu6oHzipwkUQFQ1mvIhfFF5aXOPrEwnPwA+Hg6emtZWDhjPNT1Fdn93WrXxVVLjIcKlPm6UFIFhzpBmpJLKevNmZo/UzjZq3orQTQLkfLgMtIX/P76vAX91IaBU2lvix21Fk6S5EOt6IYBFm01UeAkD5I3T6OEM2/+AfLG0eohLCBqm7fdN5StjvQ4kmgSB11picTiezUgyFydjsPUy5VBA/VmGT355f2DQGxoQtvQctZUvBTAjRqVT0XaqJIVzhp6NtFXFng8qZDtsRX3iGtYg+nhlclCTgbiMA3l9touQsIlZTO6q8eIKQXiKbQEEK1faU3A2j/ZAbJXYungmUU/FY6/eW3wmAgRVu1S0T4KIFCQiJpucSICi+motZykWnWwgtv2YhAor3rCuvcsXwaJ4+odChP25ijJJSNGQ8lgRgDk2TP/Uk49YGAeoQMU7gALnzxn+NC5DtE0wfCtp08+DJt1KYSe68Sy9pqTY1kMxlaTaJiWKoKAsIGi+pTJkqW0VeAF4WlNHvwA+HCfxlcurrus9pAiWBHJsscDgK9SpXn6DhIrIqAWxUfZUxHuyMW1omgF2qfuJEEvF+wTrcTlaAvsqiuYaTXfiknqpIKq/MM2funhdNCOL0rQaYO0sUV2Ge6jhSqvJ0ATsjfDKtuBiq5Rrke1ULp5bCkPmbxYsBe3rwsBBIHi23iWbRFalXgYvQUoV21q7YjOSnbDiwmF18IHhRXT9nd/+ueswon+iU5SrpQKN+6/G6oygKO4t2zJqePhb98I83D6AlpnBbxZKdpYKoKdO9IqujkjbaSHxc/ilHhqIXgvfJd5yv+OPPa3ZoiOKcI2lQFHuv2IHzh4nUqT9GhDRbXmXwEpibOAmwCR+MaFLzw5mXyDWjR4FGwXoRXj9cqGOQieTLihHIo6Qeatv2JgZeSR8k8qj9aPC5biYtvJ3ZB4S/fcDW9vhCSjXbLrKMO4ZHVpMRKguZFObC0ofqrqOWEUDxM0G+spNkCBrTtYvQNpgO16TSyEitazmwALycaWOKk1T3zAAImAy//0dLdnppaOiUTR0bQdWlLWzzoC6vHv/CkplZkdwDJgNiRcf3txG7TgKjIqLYQA4k4wdWlH3Ir6yZeKAfa8wA12dHTssAaDxJ2ckCpuENhHcV2FM9ZxGqdVP8lhbMiu+848FmxfKB4+aZ2UeMJGMzejLgUVkWstrpx1NjVt78rH88XcDFR2Mj45uKNsF38OscKsOD6BpW2j3ReKN4Wi0q3YX0goROxKrS4GDBgIQ1LS6ebQIbpwPIRoEqCR432s5KXg8K7ekLQ2RZLIQyJKXk9eQBohfpAKwBsCwgck6vvBVwuVPe6rcqWcaodBVNGoJhdPEobF02S6di5Hk/dLpva5Td4rE4nadOnZkO1UQRf3l7ndEA0tq8yXsrS55CyCcBuHKhnJOtc+IeX/uhyRRwrN/dC4qCYB4COizjB5j2n2vTtjSThgm3RDafDzrGEFdFQHa8zSSBs+syhUIhWKgCij3mAhYNzNi90XS8EsRli8dWJ4H1q4PYL5SOno/X7phHt2tXx5AawGHzhH/ZF1jkZ1f9lI0W4rkiEr+rwt1YXx8zgJZQ675cXOnxFDEb11AbVp6ntIZz3mcYuq/BWHrJQBg4Kq6CbJ3RDpGugnTjQiSokBCMYuMahNNF28TRMRJUIMxtxB2EL6EZKXWEk3NImWFehxsbFbAUL0O+gYdzivDbqpft2UKusA9Y5YRk3X+dELBe+CAcuJNxKU98zkSxMu8ok7ovoiUz15dkXRmIYdtPWEmDwPDRQLpeqz0xuXDpYeCth/Me7gIgyIwAcl0Io0cjGuRv2MeTFz4T9GfGUKF5cJ2IxXTzNqnX1I+r8LywvBBqCCKUTJZXqYnjxBVlOLrWsSh7COYFpsMlPcyYubwvHsFvQQIAWS0E6dQtcKF5nz25sdEyqWg0B9QzCjHbaCLB96Ns8YpQP1XQyxtYJKmvHcpxOhcXiBbnN1Dox5nd+LCoQRV9YvLwpbOHYwT8VjOI5CGq3mbBMt965T5Hw8Qq7zSbkpo1rAPGF8IvAsqzIc5FeTLc9XRv3dVU6hsMmlt+4vG2LrCuPhUXgres4ysQ+EYk3wLdRzgsnxxA84VpoUDElLJaJ5WuIo3dHujlVPrASN16oKUricfyEJCwGqqMWK7WqlUPtRU2KUemcVz7TOHzYgR4DBRk8C8Zywe0aqapt4B2EmChXFV63r3pUfDt9g6Gd9Zg6yBClMsRAUYA9nmH8ED2hsLFy7Z3e5uUdrnRBwAkOAPcwAjyZxAguv9UGGkd/+UYpvZyxYSRl8FWnyNQFeqE1ScIGeJVJK7bN4PJhlOLqeCvTCdZXyMnA+rASPL7AA6Q/RPHmEWLhaNWOTqIuL51afPvFNyoC6C7OitsG0xfAbRajcHk521cDZOfrLb9owfLVsErd7BdZfFWJyrOcFK6OuWjHP1w8XsIBEhcuU+ioyFMghGJ4HqqLOAxsZKiwsBm+HCinCsUyBRcPEmjJUynYChsWBNAZCFIJ1uVobkzTTgD3+cZqu89wo/01ZyrEjpLE0YKjFUCXUy8vR9h0Kwi6bAUKHh5U4x9Jy4hWrugs1cWTHtVL8SJKDEhhSMwiXN+KIo9Xm9DoOiFYnswWiy+F+0QAwzUoagvIVZV2YCNcDAPV5JqiUxtC+IV/vOrmhQ2jI5/Mi99wjwCSbaIEypfbbqxVO+07F0hFu9Y7kG1EUsCLC28fSlUa26I1/lyAdDHUptBNLzXDVwOqSpConHL2aZJcHVImx1iyGXSYnOGFyatORCFUBVsF0ctV41yDQpBihZai6NViHkA8MwZuaNdRjySVLNLCqlsCwSqh5FepgbSWUbeXK6YS0EyuYLdUKdY5K0ltqbx6Zs9xlekrlow4LwQuFA6Pk7cFohyK85xSwIV2IE385T5Fcojga64hw/2sehQGiDZdyyj4DwP0jajiO666nV370bUkCLte3Ig6QuIqqgCx/V58MQ0ulsnEhXCof/Rm2AJvlxO3ry7OWtCJNZKSbpDKaAcuZYny5TY5ulAMrHpwVbZjYd8ElmfSqlcVDPMGnfyDCN+TN0PU8h+vPGyPk1Pm7jzdFI7pOBma0XKpEwhdzXLremBoLrQ5/X1wsqF7sBwvg4zoqpGyh1bGKQREeMpHphOnwjusOqFsFalZB6RRbbuHZFSQWKTLG38F9vHF926v+gAtrkMZL7bxVmghfJGl8ECRDYX3HikT4WKC2KbSL8IHqbL5qj9WXLyRKKaqYyp0Y2EzUYkMDD8+BhSl2mLFJRDLWa+4HU6hAubF8LFw075wc1V+BEwJYHR1KC2y0om2Jn+IGdnnMMl6lvNgKwbZt7tNJO0Xjw9X0VKhTWNdQuqqt9pezhHmE0BJkmwIaoJV7F6UqprvWwSbgFw9zmcDQ8gCXMF3D66Mo/FrsPhZsCCeHDTWcWj0lHVfa58adANlsSSZ3tydxsrU5TamcNC3X7xV5yu+i+2MijwhNaUFRPp1KpKaPEc2DO5DsSc4QQ9Xo5jyvptmW8sH2/TLm1XFCLuZl7D9F+/6UmEnZH+mMsvt4BMl0tVEb/XLXZRfTsDbRNqao9vhS8lXyxbMYXrSq2kajGp1HqlaPZIB0NLy7vcNFIlitCO/Ryq+uE+2Y5qTwGbgoNhHZlKQ6nSeLgEssI7F7H2JR9p6Osn4CengpP0MHGUQclRJ8qEUF7/KAC5ljwF/TYMHiSlMpvhuRiXwE2sEbhOuuvjeETt0NyK6vCrm6MkqSOSyuU0uJlU3Q+0E/sRrowN2IMs0IZz59AHZYBUC5badKL3qxqpgMfTFOocHwcZOaxDV3hWqYmQtlUmhcGFxfTz0ezTcmRoHLHqNvfuut8Mb9qv1tm0IgGTbtIuAGSisaiPDVugtCwvnaF7nbL7h8TbQLpaNYAzoEmrCpLF4Y+EkfRh1NTnE4dQ+jSwWL1jZ3E92KlGbXPYVw2bQqrz4IHQkwM1+g6Ej8rZw6Mjm32iQkucvDBzIR/nUYc1fumGxToucThBOWilR9c4L52iuyi4xD1Km4fpW2tU56o3qQ2Ayj6yerpgdaQaiuiFG9cZpYmtD8jbSNxbfBBbA73M1E1AFE37RTu0DHxCHF3fLQd3On1Il2hzi6XvCqcXjzWScZFheALb79haumjmAE8QicRomldvurFs0v9T+opPV6Z9n+iBsrb5yjzVJHYtZFQu7te8o6NzxHx/a8mFw6e2F9oOLQgUQu0FktiWkj4Ku3u9ohSDxtHE4ExMB2ehSMG24PwlpiKVnbPXhBLBZzCIS75aZmDxOnshzKIiH5HIdWZMt8lLrLM2AdeOlOlvZDmTNMOgQPS6wxNUgE1Z0fK36vWPObMTwqgIHCxu2mO1thjYC3RBeOLUQ8faN7ka+/OZC0VD64u4jjGoK4dzWLjDnRAj6LF1IdKZRN3o4LqTaULUlIX2/GYNQCfbSVRUEvVqRTio51y8+DDtM+yAzvEuAL8LFjtrtXUyZUW8lri4dePkgO8wx2MyGRtPHmLbFYAdkk96aDd6xHbRYrfixLNd5EbEaZ8spIrE4J0gPrz4DgWhEMHJs5qrReVwFIaIsjGfvxl8ETiyKCwvpg+KiUN5Y2N4QiFOXjsNLxoLZI98GkC/pYJ6uaKOtLpqsiOaqNx86COY+KYfJd6V3x7BjUdgFsW5mB0RiJbJCTVAoLr5wMSmnjuRAhBwn1eq1dg964RsXvpHYUrmFoQ0hs3MBgE4szCryUIGUIKS/6k/A1Q7B/SQNP831oDJa/PbC8cJpvJ4bslE45fgvbNxULaqgF2uAJBcdpe1itPxcnfKHIGpwZ8dU79Podi8qGJUsh7+Z1fuKzApvj6F177w5usg9WSPfYEUsFGQhfSTQWhvqm+JeUYe6UGzPNZV0ykYTzm3CdttyBi66ie5khRfX1mK5zfdZBuVDDpOpVB7XFmz1RGEj5XPJhBMk/IJBu0LuIQFxbukEq0dBbUY8R3/P0dsVHw3vnJnri20ekzi07waG/Wr+ndLC8iQRIGGLNWBM54JIUBvA9MFvjJy3D1d6QE164UbggJHYdCyTt4lFAF4OHx8kAtsJVHWsBBbL2SVUL8oCZbM1JMawrzzbdfjFeoKhHJ0qkmDClXKt6VWHvPj5J7Rhg1Q0suilws3h3zLcmbwZWwpYpBdSaXCBKFZHxzGOdfACYhmVzt4gJpMoRQ+1erBFwwYLo1BhswR4KMZZ+gdXGYdJ4ga8RENVf/nNVHHRPkhYWcW3lF2UgKopOBJpFTPcoIwUJ1nMEi8nAzcKSajcTtRxXmAz8I/i4Xmnq6QvrSO25OxqeFO97+C5fB9GaK9kEHyoQmijIzEN8Vy8y84gtsHAjebOdDkLA2lRxJ3ZPAC6A4Ps6miqcAnlTi5ssUfHP0R7JUaeE7O7ZHX1HeJc//zpB2wDqT+1/LYq9T6h0sJBNqhQCITAAGsK2XY7aFFcmHj7i6w/aLC3HD7igZPLVFYggi2/n5pPEF2eRAHhwAijzbhRDhYSBwfhxd2aiTossjoItx2GCkQWMdNAlLGE9r1DcEFVnYWB8OIB3Jy+BTMbG/PyYWGBIi4n5NWU4564d+JPq9r75kJ0BndxHFZ6zDrmS0MPIIjQXQlzIwHQl7dhI+U6SPeySYbFMivIYroIkqeSUonprkLEggLV2d30KjIG0uMUVKqQ2eSrGn7eQRtk1aFcHbzGnyowsGskJsVoDBWEDpTsg7kd/cKbdBCM3dJCYKGKDG2H06EDRmedrOCOyp5RNnn1hCYzuCtkcpht3Qxa7CthTlUXtepWVFG4/FbCdRSwTSNV7Xgi0MkLQKlXUV8ELaiWzaWFrHQKvHhV3/BC4ihsJ8rSBVlKrKJa2dsxIX0jrrp4ZrJIBwzTbQ7x1NNuYK0FGDCLSH4PRkdLp12DfHO7bSEKrBfLxB1Zon0YDGwGL56OonEnkjTQZRKLhZwZJAcBMWGVg8VUsQKwfCTGPYKQmQLSnz6AcLRKdOgMOorYjSi5QpRLaDMJsApQZzL7MoJALW0H4BXnHAAoIQB2GAsR4gl0SlAg2pesO4+R1Dd22+gWTsTwa/WlA4tObgcP5IulFwjzWHDvB+/xMKcQAFgR6aiY1A+UUdWZ9TlWiVlLRnJjVanApmS0QNlCcJ3QmalVAl5da0IwrwJpI7vowqO2Qt/f09mIScA9NfeicVBOL961CBYB4eq/1YbtKNirUFcG68QkYo0kRK0+PVTn8owllIapA0ABM4wqLpwmPFlGhH3czAX+qwAwYKsyeYJiWJZBEfZBwMZRcDuro7yzh9XdCAOgN0LECRa5YBE1IRAid0r1CdMYbjIfflEjJMN1BouwFGwTKNtdy/mNYNc3f5AGE4W/cFpVQ1TKRnJZPO0+4/DVYQRQmZ0tc/nm6YgEEGFLuLSZls2u59vgNZtSxgArHLCVLRkXYspD+GO6O1Vfhw92mUph991AyDi4eLrM24Sb6iSbqrv9dj3S1ABsiaU2fhj7pCaJCbK1XEjcJxcc1fDvQdt/HauCVTNnZWw2StORTSMK49MOzhCI5WMctAFdonl14pHqkMWTwOkBfhLVnmWN+Bd32c1SCkZXqfroIztRtC1jAmgHoYcXNaC2jcBuOjifyVozIkK7IaxTcZCV0Yu9QN8Q04ebgN2UsG6JNHoZdiAS4AkogKEAz8psw6gCvvDHUHUYi4T0smpFM/A+EOsUgEWeDpPJnsg/dEA/Vf+gG/13yZLAg8ApX2Giq5jlG4DrcAElnjkIw5YVOw40wqCj9jgZOYUMBDcuEqhWBI1BSd/KNtm+POR0CxMR2zXpHFE/JUA7RYRv8WTqThHnzSR8SFZvL/iQBa/YXgWTV88snbaBQ2AdK2neXuxmUlpgCyjRAEO2SQ8eY92O9uhKGRJ5GGGyzGIKyWL0VIUG/HLhKPBHMso8tXAQvJMNqQiu5NJidqquXOFNtfsnzGjFrQtL9kGQvS+lCr3AXjoOFoVdknU85lCfw6gRYLkYGL+NBw1uoo0bFsCRrdP2nj0K8VYh63aQ3o4Z8b+wwUremxOFyHE46mGvqiPkm193uFxIz5iGNXhkMNmZEjqbYFCnJ4fuCGoDcBNPZgWUZtgEJ5YPFvvGS6KgoOldXN6h08d5T/IWcBxRpeTDN+o4i37R8upBwyCnh6A6vLGLEA8vxf2MHkKNqXZDQ7pwO2BurNpcLl0owy8ZxZuL5YXNV1so0mRxDR84+2Ih7ClCEm9fWrzGSsJjQBa4KVwKEMnHUc/gU2nymsJPTCyPtskjA+C/aFbdZ6lbHdClAm4HtjmAh7C1eoEoing76VrOFpe58TS2MWKX7mOq8iZo8iRimkx2kg4M4bCBXRth65DZzQPaUPZpV+QPet0z707eRdlUMTpWF32EHdSJVXTdF11+DGpoLMK7R26VKvSwMVCWqdjPVTCIDLJmvjzJRc1MaaNOdqil1LeIk4Xjp8xdeiMF3t15yLX1Iqt0Gwy41cGTGekLPSZJBMpsMxiHopbg6HA4vGkcJ7fh9n8HfrDAXgrVBQlQ2SPMzpruDPO+UWerz0yj13CDNY2PuIARQwGHeRDbYXOdd79Bp2BpOxzBUosM2dpAOthMngZ7H/ZJ8rBDWaQzDP2AoSiHy9kkxz4V8ElYtX9ErLNcO5++558vFOQgoIRdL0CnwlKdK2R3koEfRUTyAHjzC8dhEPVS1XKJJZPBVjAC4Wqr2mK3sQRcHIj4LkyoccPEt1Adk3HhDaCcOg53YkaruYDkzYUbaWYrABAGVnN6ew33PgRx8duTwNkmmQ5uvXCQZYJpt/e3ryY2oTmLrfsNdBD7eO16eU+LwR85AJ8HSwAolrNZhcGhCiAd2Cfa60WyGYBPP57gLb26S2dB5qNC6F+w0FdWw8CYn++hgoA4naAYYAE4ET7kweV4mD8/MODnyAJ1zOALB6XwRNU/sF9VXQEc34pToPAM2ZpkBjPrIAcRlUUo+5lZ5OEA6M39C6ibWZ+Hm9hlQAsj272EcqjQPghyOekS3wovFCauilUAvHA4sCmGECK2/mR1Tdnes07ax2rfEJPC5cMwo1pZUCGR9bP74XDEOREdWopuvAplcUZtv4QWfcM+lZX6dgjUYyDoUB1RBFpwVuwlUIzTRlKBVgxsRBdlFIR6Yl46PMZlhJGtAwYxLI/OA61gyIx2ouyF3ISin1uKv+Uhlh1+O1AOHuMs7zafKyVQZiEyBoEanDJttNQNJW3MLMSSEVUUyVC6EfOOdfDDR/c4lbCx1j7F2j5JzVxHB5uFD4kX/zBxc/lNIVkVvLErARdvLiCxWmNmSL5awkw7WgbbEYl+VGkdO5s0ihf++OKNru2Xk/Tq1gKmGoMrN5V53BXiAf86eOrpqJ/9NbiAh43UXv7GzB/b1woHZri9bwPgkiuZPjSASJ+WlrftyIdDD5ObYkkluudj/PxWg2REQLatcF9KhJ+xVT/3ZymIJUfcJ3hKClepxyxuG8eNeLxorMklUDFFg0UBR5uBKrZRFUDj5XKyL66ey/RAXVafTi2ns9UEcqPtxoVq45Mq0subQOfYAtcTAe8CYSS+1fOGmz2ja39ZT74eEzGMM0pIdhaAkJ0MgmW5g5OFqFVknxgBNE0Cs1jaZ6NX6ifatJVcqKf7Bz+PlR7mTs83O54isNw2zw0fdHBaObAgTwohTBbHDzgqm//PD3nTvZTXxGO0BoXP1xw6vKjdA1WxG9uuFfm0KfzcBqAt0zM6E4DMUAQl6hbjRIXT2ECe9HJfnol2SCMosaWgL8ESKayClgE5eWEobXxO15/J+UgLfhDB/nJ9UHgY1x2OWUmMq2j7Hjd2EhCDyxpfQIIHV7WnF0dKifbeLqH5O+2NdywE7UQ6YCzAUj46epDobI0Jo1kKmm0Y1OVfgU1/9WCAT5NdbKfMNrBuqLPPB8eTSew9gqhGEpaJbSN8M6hS3HxE1PgAC4DgLpirIVcOiWwcRF3w2MJHVy9sGyd1xT3DwL4v8AEtRiNE1opmuwuBiDckAUUlcYrhjWg6lvsJwmWeylAYRwqXU6GabIM+cJJ2cON6UMDOgEK76wuPqLo1VqJRCheE6rPTOWt3N9MYxuLNBhBQ2bxfE5YlIpEOhiNoQ9jjTJU+aMdNFVimxY2EuU4ncGW1zJzCcO57miDUQbYUvMViBqueyxcfwW3NKfCp1nu7BcqtiO+qxHPMsrF8EORSFGK5T1GV+Pj5PJd2Y0IBH1AusXp8Qnc/35k16giJwG7UX5BmcviUAQ94PceK2eG5Z2hNgWPsiB5DsVOXUW3F1wwqtmKvqDJZARb1arpmPzm2bC6oAqO3Q/MI0e1M+2DgwSd7G4kjtexS8UkVadziqPXdmxfrQObGwN+JeLjuTaZs0z6M7IjG5AE4xsywY5Meu+J351ibwqgK7QdG4HM2Ffp4kh+rzfG7fg7UQsxPMpd3+2C0IVsfgZ8de3rZk0QGVYiie9jnRi87lmV20ZyWLTpUk6ihYjz8jgeUSJuRxzHKW80tpSGPPl/qWaiDzGER3bAGC1jC7uxFV/vGtZJaApIzPuIj8l6E7YXTCUxqoLubYHGwbK5RTQgmih9ueB+SfJjCw3Iq9Og5cAzKjWQR1Swmn0Hj7AIUGqBEuJjsHJ/8JdnqC0JN+GyJ8mT4EguFjmputA9CC7+r5aZdESTgcNfvrQLFj6z94VnyKW3n9G7XT4om6tHLycEMKpn9Wa1aLSrt5DwhYAU6mI0EKaieiV1MxNWTBNrXuzTI9RhHjJaUbO8K/dYt+IcVimGS4fTSs46IrSd4s9OK2mtUKm2i/1tTZaAhNgXbHHBYzBQusXqUFhTY9J9unMwfQR0bwXgeY3Ob1CrDVkrOPPWxym6DswDNSgSjXrCQCGdHNiE1Ndnw+Cmis/EGF+j3/KRvIFuRjZY6qQKPdq3hkcYXMILPllj0AP6pEH7114MIdcDdHHGORqmemeHM0kAwZaJkYWWrNaPnfw3EdQP4oc6ONaxbjdv5BQ8JBe1J1qqhtn/pkZNC/d6f24kfexioJxlwIiV+tYQdAZ1UAPdKDGM4y5Fqk80Y199OUxJYaTmJUsFGFm23XivULIKZXAx/brDTOQemacLn+fSyph5fxmZHKZC4OnsciWwLTKgiG5nqPDqrLU8aWPLzyDWJ382zNYCmdR/CxF9iSz+mZ9dDmfRjZSSeFh45CEvNpvk5oD84C58roKHR/o1uHAut7u/bQqfDIpt3GjiSy8J5OEIuNjWpH8oYw4+mrit4TjJ3M9IbbcBBg09E82XQFeCve/+ziAtkoLqk6wFXQ8tHJ6Kn9nqQ4SHJtiRdGMMuEMEqLHUOFsRWLfkxFvlcpPNfLSZ+KAnukZvqUcBx+ugeuQ3QNb8XIAt04MADdhoru+OfQ/xhnmksCwhhPO3YZuTN5x2WH5P30RRjXbTMKkS7ebQP2Yx5MGeSP/Of4rilfJaCu8Vu2lvboh83/jnOxgIKUHQ33Bux2KDvE+jTR/soP4WxR5QtHLFab4jfs71+GTjC5icl/kxe8DgYjDvQEMk+FGFhe6IxqigGWYcBtXDnhGaUBNl9frVtLRJ3n6o+MB+SLNXMv6SR5Yh5YGzHt67/5kYY6/NuoFVOsIV+4xpAiKfQFtMuyi33gYTCoHqrubOzjox2zpifdvj4jUHHQLwxtgbhljuHusXho7XvM7eJV/1oIKJ9AX+1/R+7ihZdwg2+DNDZNcXwB8D+Ds3nbfoFOgxJImP+EA+fioIjBf2gpoDG5r0rcfAzVSOoCQbWfGaOZcXMgOvfHcp42zZdvW2lu2II2dnFHNWGGHRbxSRyROxtiESA2WCPemknA0CYQXHySdvCrvEl8gMJ4jOg6sc4XcKsioGFsy9DBlUwufyiIVyQgc5lZcubFj+uvg8tjx4qLNX5AVHsBK6mhVlPCcqY0ZG7Wh+dHvSZC0zL0vSsX9c+R6H7IG09D/JTcKPBX7J6gNzr0A50e66WUZBjeS8rpviDiF/3zKwGaJoPPnflp6UfQKWjptgC7F4MsxuewUF9WpemaEJUTW46s6JjOAnztNcXAVHRD733ttrjQY8Fh0dVAblhtrZ9iweVcRCDzc5ueKrnR1DRytdhCw55BegC3gkqkB1zB47+cnoRTfMWvjpawYFfKqi2IEOiZ1PNpdYcV8n2BYA7tTJnWIkSnoSlrO7fH+I3/yWwwphFdpnOZ+09pzh7vw8NBgAq1C8nXXOdx4P1zPpoeMYj5Zz/W48jGYbuPkqkDzUUAzn3qRgVcNifIfWHxfwZBnsQQmnOqLa4DZBImYwgWCF04vfI8GPQjeztihjzSpcqO4znUZ8fdDM5iJ/xqf38nF5dUJs/RkufUqzZQwDGl7ApZUPda++EMfFpQld2mubT8Hp2brcXBM4TLNGHHTtesrsFCJA0EH8Xx2qJUxcbbZT3Gff9uu+nvfmMhvrfn4a7UAaZdAWmJy/N/TTJo8HKag0C5ZMDPnRt32ndz+EyJFP611T/pw6QUGxAtSkodQ0/aIYV/JGGuXUWAzUbwaF0tgSV4JrRbQ/IaT6M6GmQCbIj+lq91LZcKmKBTWFnj7BjkM3Z/G0S9PsWHbjLftpBIrCqKcRC9bnd3suWczSGtM5F5YefmVZHm02FgO6EGT4t/XIwxqJsrBI6g65NsyBTnmu85/IoD87QTjzP8U9WNwp+tGFdceFfq4NAwOq42A9g0zm5ZUXrfDhcwKQq4B7vV9/pbTnVRfe8t57aGs9t+qNOG7bP3H5tUIvVYJAec7CPLKhPnTM2R25/5DFc9AzfG/UWreau9TXWDAW0opcquBRGVYhTzwpyT+Xb3ki/2Wj9qhuqfCbqnf3MB0B/5hcxBSWx8CYxXksmLmxfvZfGLZsLSZjIKU+mIW87o4FpZoYOGkXLObWrnoc28mVRXcO1Mfevevkp9392/C840DMZtufFPqf0w38Lf26QNsh4xnNsOBpNf2sDpx/ynvmxelX/9s8x3k+tODnCGunl3Pqad/rjYvWpYNXfxJ/qARor0Q52FFWk1HjemKA3PW7g3Q6eJ8VwIgTEBMXSVlieQcPc7F3KPqfXD4Dm6bqHXeuf4WpXAhTYtP+egQRzepk59wJPfNJzUKoGQ2oTRQbY1NGmWndcCU16bOVa1Tm++1PYN1uIM3f/TFNmdPGIgp452wOtTAHl574eoxjNvYjhCrG9CdCHmYZb9NwJHSDy3O+DMX4Ax5+d84vkzQfm+bQg1bIy4reLmWum13wmcS3K7DlIzxw6WnwA5XmWKJaKn35k6iJjgrVRfVXOmKr7sDGEizmj5+zRI1L91w/xzNL7AGhDeYqYwXpUA/iNCrqttDsQ93HLf/hn/Xpbf/JYobDyYaTMHSOg84T78zyhxQ1T/RSofmq5tml6Vm1v0+cR4kP7oGmo/LEO1Tx2uvQUYpgN91TEXX43zNvhNv3V6vHT/m1F2kCg6oEgf2Ddp2yeEga/OuHPBx75Cn7oC21tN7uPHdz02X2TgqjPmQCjG7IZZfVQ9Jn9Jzrzt4ugYRe1HHSqrSlEfw7VD6HhubAHz545IR5khOPO2HCQ4ZzzIVvz0GUD/QC8s9bmCwxi/oyN5ypobKDLnHxQJ/507ewEi5nR+3lrrmHZj8aheod/+Pb8LcGeW6BmPFUPHWce0FPUVZz+mo3a0fyJo2vXoV9Ezscp5N9ib/6g0c/fa3q65y4k//XbfJ5rqxnaBqqmoGPMkR1kNWrSfEj5mWJNf9VcPz0HXzsUdLoCAYVk1CzpqWieQw32D4b641vyQMUdk8WHgva5KSxKYbRsjkS0NnCsemYLcGqrIcW0k1W/+R7MDjWVpR8SXkMtjzxhqgdOUxFz6z/dC/43mc0vjBuPfc9T7vYYB26vd1J0PeayPyX/gFhz4qDkD27PTzM4e2dKuI/IC/yRfM4KGxrCA7bzA7n8rMzxNLV52je0n824j/MxQa8ueaKvlaHC8qc6KdpwRFcF/FmQzyVp/ouL2JxjPO61mA75sflpAnz5cV3sFDp3GBcP2oarE9f8wdkcnRwwyF+zTj9MuB6fIeboGqk3ssOlDGG6A34MPzpxfPyVqZ9X/XsSWHzs9+Zy+FnT5q8FMru2pwJ2d/JP4l8/ZukDHY6TH7ude7Len3xi/2jRf87/PoueXv436ab9oH+v14fB9uMYQfdUf3CNmuOX4Z6G8HzciMSangdqB4BhUqsa48RTtfW8dQaWDQ33Gv/ZXr8+qD/XZZ+Zbly1+Zj6EGBHLDeAOtor+eHwZIO4flCymimEPZOzpxMlf8HQLfKQnwruEVn0Ore7rzZq4PCfxdye6XhQmr60R4H5jAx/z96bnTczuxGNcs7mORv9EU0+rp6fBdGfc2oj/tTRz5TiX0vuuQDRvg1Dzvl9ZD13WD/w+dzRsdJ6XtVDYZM4hoF9q7QCYJRmHhdzJzXT74BmC6PJrn7kUp9KxgR/w2k/QNAn3UgaUuewucOeIX4b9tRsZYkPA+CnNuVHvzEwags6n53sQWSHA0iJ6ebjt7FETZ2gAV4eCtdvEcijtH16vblM/3Uj/6g3P9nt0y58+r/PSY6yPr3A03LO5UGx5uDRL2HXv+5QYtbW0wHN+o6HvPowwf792H+Kws8j0wwAHyibFUaLPJ9nOANdTpKE5yeyhy2HmZuQv6Q0/j+exu+7//OsHs71jMPjOVsldwaMnklAV7xTBLLJnvE4d4xxywMACG5C4SR8PurU6fyfo9bq2T/xjFt+tVB4Bj2/ppz9bVpA8pzKH/iBP8Sw5xE/cx7F0x/MCrcYfoqMmeH9tH6/wQj8n7UTPt9pSEn8lFqoT8nKX3/25xvMhfRQhmc9x7PMjx4vTPBha/0Cw93+6n66F45sphv+5wvWT0n+g//x//hhfumtqUf81CVPB2sPr378UDpPNZ7O82OdOGNEN+9wfiJZbaz+sXZWtxQg46cO+4w99VwO4MdenQ8Dvu+Hms9eRjVu+0MS/BhF/GCvQx0cu85Pjenm9vfwrl032hWg65vPVT8b6UPs9//x+jEkz+c7TzuoT11avwVsP0UAPxwBEJDA8/RE/NSbM8esR9jZy7bfUEfQ9dUw2/+p7KYEET8P4/cQDf+6tH6urqHv8KMkmOcypXLYUI/EKX/aQCIQ1fVi+4GThZ9BHRDyhz3Y/0MQUTHZvPgp5UaX9nPOfu6vn7XaS6v473XMX0e0f3VeGhrGvBJOlTvvSnwIW93H19wAT+Px+778Fxfx933jD4r2tKy/PpdVM7r8NRH4Vfb4g3EMaPyZ281Z0oDgZFjOadMlCR6FKsKP6YMg6gyA+Yy4PozPz7mvf58AD1/9g7aN/d3zqTTIRHRMzoAin1SdJ4XkuQVagt75U5od3zz1pGZAkVPAyz80bE1U88f44zlafu4DD2iJIuTp1p668QNn8vdPNXjBTLqfS3bqAn84xZ/X8YPi/GALs1z8UyH8G3fwDxPhXxV/64JZ+BDNP33AM37/kPU4PVM7kT19069LaXgl8kP00i/kegKUCmT5AQD7Gz88m/8fbTR+l7U/J9QzLBzy66dNN5+S1n7unPgsAzXJgYBR40aHsUDz5xx66CDPIAJPXza9wacq4s+j+jCWxny1rQHKNn0At+Tt14Fm4/cPM3eQ//Vu+TMz+DxcfrY5Hzpk15d8msDpGn6pPfmrMPRnyq6PcOBxk6yPZyB/lZEzi0cn+vBHPjw2A0+j4WlH/atCecw//TTS/6Khw0aJv+Bs/bt8+dcyaPrFg756RDDzyp8epE+lNFsF3l6e/NGczzU4oDI6ruW3fEdzzDehwZ+szx8IeGr/ZyX9PkyH+e3ntdowanys5v2UWfAnUuoHEvzsVvPh6Hygiucif07KXzc1H7LJT//2lGJP8fnzp35OEfzMEepX8z0REx8o7tcM6l93DAGcf+GLfCZ1z2fHryKBzcps/rw/U+wHJP83bkH8//vnh2nbqeIPJ9K/hmEP9+G5RloJQwYe6Wx3f0XPkJSzLfTpMvm5ZfDpF/VhK3+mlP+7tR5MsD5cCk9UUPWmfAxan/Xnf//lX4X3Qx+xnwiIGemift8EU3Ri+m78AOrP6//F9nzmnP/bs3yQ1afX9PNWf6FWQFuw/ay7Z/+z4zm7TJ4b1v7FRnmGjqNNGHj7KRr/VUD9qp9+w9L/5lUTPyS2uTd/SZn80X3jEZEoscdKDaiBZJ5MjB/aTlNsCs94vVdzsfdv02zdHjR44qBb1d8C3ehxDqvcxyV2327eiKJvmPCu5GGxWlv21Jof8jhHgvE5PmBsmCM8me1cH82vYVfH2qjcVhCFx5nm+dkK6LzzYS54mr++Ok73MPUQlgvt7IOnQsOeblo2W3jdpJMJF1DBbgHnI1F7/D1qxiC32gfZkwoym48Pc+J8CGxjpsyf+g9wP8sPHFBtekPwAe03Hgt7z89+EHPYlgYFmB5/PvtBtZak6bbV6J5G/R/zqPqIaT68ZyY/19Dce/Vwlh6mjzlWRSZwQFajHxNSNfilH3CoZsPGw+t/en/+9Ixj6zoS9BbPBB7WR4us/VRU89bqR4fy+4V7kHsY7TDkoQMNI8scv8MyP40DplEe1+6OCsDHU/Zzj8WDVz6fQMZDfBBtoTlZspttWk2A/vUdPgesf5kU+PP6Z17sRlfRm7If/ABPh00Pj2mVApM/S9jnOa8GtKimZ6sL9vd8bMOPxP9D9AugnPcz8+d2u9d9wMROrrZxWHOpnaMo/PFpC58ZQCQ34Jv3CJn5kZtHT2Tsx/T7p+U0cUwQN+Xq7/czk4XN2ff16M6MkR1Us5VaxWXBHWPEeuZN/5uA/cEr3IRuuz0Hho4J3o9koStWPrGAoR54ng4SmBR6d/ryD+uY7bp1foHTBHkeXAG/T8V/twNPJ3dP1wDuZ5PysAMBDog9a6rVHUO1eOCpIQwU8EaZOL9U0T+Ps6UoDVGMDHE8NGk/zdpTaNOwi2BVzc9Zhqsamh+aMG+7DoHtwnn4gAaM0/wUmOdfP3qTHzjXvmmUgGJPsc54uT6ART0TBz9yhELVLM1HsYwCcX5Q6jluezEdGGdWz6c46DKWNQT3GrTuDHrnf38GHBQ/2J79CepQz95mXLk7c/PDmnqWXP9c/gECfpUFBeD5Gf389+f3fmsJapi49bgiJKr/aB+s3mhVzMHpEcbzWJpKMHd/dbqbD+ntI36MS0mwcNMtQ+ABALeZEVETDXXc1drBxo3Em1CctlApHhg3p6z0hnCw2w+t/YWfbsp+VJrNFxzECK7nBwSnxnnKuPB8og/sZxcDZ25SPogQ3dMOg9vHsjGVnj2HfM8nC0b1lel4qg8Xtx8q4u5lz3FhmdGXUc2v8CZxaLa3qPScNrsvMxs3no5Jn5OgftWC/dJPy6bmCiBvuOdLOOyAnxm6cwM+JM5Enoym7LT9cA+pAQs30uAep036jE7tWWv9sITyAZxDxk8cFPorGYflhbJA3+hsErALzm/sTgXH9vZyAU3XsXi32lesNmMskNUBLbP07zb2mOCXAltjOxXaaV0JzPOLSHfGZ1itmXf/pN2JPJNOjdzzECPG7fOnv2pNTyEc1jC2N3p510HyCaNDn0YFo4Zmqh9XXCSM7dDt3ya0dktCuurbNjCYUC+77Jc7gECHcwhnytuD074gXa256LnO2kpsXGWnAF9k7cSeXKvFgnAPj6LaR4vlJu2codgWjoPwblcMbhDvj/qPIHf9g2zWKQJg762H9Nsx8PTNtnY/ADaPs8I3CoG34bcvH46n5ucwOxbI/lNuG0YECsWqjk7axpxXwuarubej3yPKYngTPBC308EyBBdQRGG5GN6sFqVhox0OT3tZY5swD8YkF51TdNPelAvZhl+oOrqeaGSAxmmrJ3t6rj0eSBcOUek3hQMwAco3Ox8Y7vp9T8ruQX+yjY0cz4SmV+959TF1zZvV5n4w5YPEHwQPmgy2Ebh5t3yodeObQDlpnKmMzwdILBYCmzE3cB82cPG4SNYUNP1DUqjN3Vo4lovwHnFk1/0HpbLwN9483ioUoPI/ThR2bW+5f8yHGcCHMdS3XrtN7YkelguF01SHT7F6cIPoM8OwNwLbMQe0XdjN1nbv08TGaQ0+3hNjX1N+HpfowLHVrhvVisApLY0uk0fU4cPy3cbxHj9TnNpYAE+lNoiNA+HbYvF4qzMAHrPE0+dDJ9qg3woQeIP4RgJ4TwF+EHNR3/0GmFOe36h2CuX2bfGG8A3Ss3EPisr+Y5Ol8e7eecxYwA2yZkmovqWnxqZ8pp1+js+ycHPh/lwoG7RwmvrTO6iZN/xT5kL57ZsgsXG8fKtK+IfgqR9q7xljgWbZbQQ27Ig2Ueg9+d3eQwa3247lPSQqcffj637C71Hq3iDWlHmmUbghsEoHBwUjx9vgRuGoyuwwmW9vdtmYRD3TmUJxoabaMN3XAk8R4gb91vI3aOBuWxe1icumaJ46ZPu2MPAHfR6cOeoL1eE5szjOXCeBG8RB4I2CJzdoT71zmLNMqMLGGy9sBrrQht944aQ3a+62b6xZDAeF4B+s8XXrPaqeD/RqZHizHcbKxNP67SmrjnNo081xuE1L9wDJXfxtng468vP//+EXN7/Rn4k2s0nfKtxYOJDfKCS/DSR2ZReXFje2iQ792igL99yTgQPgYDUgM/Dz8W7sAAU78O7zi5c3jhffEN9u/13zOfjh4I2+Am9c/NOkZJTNN3JOhN5dUbut5FoNzDfCpcILt03iQK3+cY/7t8UDe3Hb6OY2BjjsGn8jBmQ7c9YlDp6qLPA9AFjCOAbuDqXDBfqm8LbRHsxNv/gfIPmN00cJExvnMcXmwsbGzb5Z9PTRpO1i66i/G+CD5st2Rd6B3Td+2qD0RjBQ/rurUAvFgz+4x7Xz4jcPFv6uf/xfuClvdO+6QcHB43cnc42m7uAm8YYhF3u5lTFGii1R2a2GmXp8A0jvLk4pv/uO7XqnyG+QxI13Z+x+YKfTQxULxEb01wdI3waJ415i8j3R1O8Kap7XM3B4wDbjv2kD5dX2Vtikj4OGmFW8cffxTnrDbOcDzo7HvP4udfenz+B0GIHCNwotBf0bAv3fs63b+4W+Rx0b3om/AQh/I/F+yisCG6vnfnULuPDfFonCqu5lbTOwsbCn2Tm+5iwJb74R/nGx2y4k3nwDhA+Oyq6Tf8YxELUB/4fvsoQbf3yaR3kymjS18A1y+Y1o5GTyxMBgmbN02xuzerwkmTgdmXoqAOGNhzdfrmmqxrphCtS7WXns2NqEvCEVlomNboIHtu2v3nr7HuphP5icN9PfVOdQdHCzjWhUoenf3LiHSi+0nmT7hfdHI1czydDnxd5YcP9M4Hh/9Qas/unQCXA1Pct/jyVGoqaSAG5EN8c2gyfxzxQT7ycOrXt3fCN9UyKMv/GMo/80ouTj9sfoFOrbCeP70WTgePH0wWszdYrqUm0jYNwT0/Ht745w54J4CKe3v1FjBWXIZ36EPfrF72dC4cC3AzdfUxPX3JBTH7sf0J9GjFDugumJydxYExzYJ0rfqppytrG4bwfLG4lvuB08hlm6Z1rxhwF3hNtn6idsBXf7GU5lDgRuX3o72C6edvGyOnpqbN2KbzzYBR9Ix8TBmjPgjcDB9/yuxoGsi9HARqDveCFg3p0xBKLYSUJ6MuZG/Fvp/wdt9HhQnyp7YWPhzXIbigHhrmcTbwJJGj58EAHxD4xEmxq8sXCPBw/4RuGCjQrdjQhyI7F9+I0b3RItEDfI0OE/DKavvnXdDz5dU5C1sHJwZ1r2PzCujk5GIfE3y9djz+ZifXRjZbJfCUh/PyMn7vFLDfZS6gc4aNp0Mo9zwh7iZqt1Cv01Ewec+MaFzjr6nr35MT7z2z86CIH+nlyG7UcW2X6n3dSNwwnhwn8PJ3Nm6tgQ1rSFml8r5HztXs7PSI4G/0zmSGuRNh+1wkn+D36QzWz5Ev4B8T+PcgFtVhg6uHBTFm+cVs/y/flYPVNrTf924MZBNh23/nQjhPCfaWEa3ftTk4CNROAbSeHGf/mN5P+0BU673VgAtxdZp32uSZdvX2NZ9z8jPQ0c0IlvmOXjpa6kq5I3rKwby4mb/pwwgvG3gw0NJTp6hewUWMw04bGu7BZqfoVvXAMSafY58TeIa5pQIHozjDvSaRPHyRyeI97zGVm+YMjfuPAekKcBnsT3nCeC8I3Hm3XPtu3nS9ScFWNDN29hj6i/acfCAdDZ70q8cc+x9mCL1UjYHOd9Bx8sGH+P+/9B0DS+P9xOM2Zdm/BWDvJMFHs6h6IPv/HF27uLq4Z+XG5b6mvoUrsddHSoEm8voUdF+IbZdXgrXA++cSC4UruFBn2zDTUS/J7BULT07/xh4Q3ibpbT0PGOY7zNzrycjejQ97Yq7M9JEdh4Zo1duv0zY+pEQdXLw/wHx8XsBrGrcn64js+THVy0q/xwofiCuxaZHuZn1PPGqxkJFIz3AD+PuP/RwBe+cbfxVzd7k4jw2KT6M0P95hsEE/93I8hTp9bcN4dy4LCrdTnwP+Pa2S5Tpl2IH/eEzsN9WF8WT4HVqQtm+CQ36e2bB1u2cXgm0kRELf7daARyQKc27jfa2/ogED6KWfM1HDyYPKNE6IO25eyHdtC4x76qf+0nq+CwH1Ky6gmq6JaZU6r1aKMANMQs9lyvoxtQ1gEQ3h8WsVAMbCQawNZck3JDV49b3/O9Y6Blz5JI/D1Q9Bo0gkNoOTj4bxCF15wDn7H27P6ebTZ68ecXj+5Qs4UDD/+FTYujUIn/BwU8d2Bj8+CHjfQzEwycMTgZvta4iLUqcnvNdaGWsT/mL12cnZ4QNBfg6LSTJfbE1VQn9w3LL3w6tMbGKjYtjXCybANyKfxYU/Tdm9PdNqum/0ZP2dvpbkMVMyiaw9CpGo/Swh6yl8ag7htrOJNNymg7giRM3g7ZB8AasVvDVU10WqxHlIjo8wnBexxeKZQeijnKF8+IF++5QH55wHxCVPrWbynO/3yeOgcG1ocScmN9SJT9q5rZw+nmkYlC4kayTwgm/geYW8NNCiU/xxzQ1tA3l43jYLXBadeTJMo1iuLvblBoGMHd9iQOvCdoNrDBiXnqynrj/sVPIwMbl40QTsxR1X184sYCXA7BNy4DG+lD+aa88N8gkjXsYY6o1TiN/g84MhoUvlDeDkANt6rIZ46zJ/3wHz6a45Y4lynZdIUK6bLxjQM0CehjD8KizpSQ4WYi3Fg4Ld6uvg2DdEH84zcv9B3d85X5Xj9eenOcd0vLKe/63/3pGfYUgN/A/Ls+14NnY5855+tJcIB9+gT4/Uf7i+9BmT/y+LHr1NyGk7rdne+IPHtQKp/Ot/+Yc3Q70zPDjka6IcD711AzZi/e1YBUzkmzcPxQTzl5Gax/Gm6qUkyHO55L/hC+HxMbPgdkYLefj41/bJlV/ChD2bc4D1LN1OqryYNEouieJD5wbKOffS02Zzw/00qWOx2dOYPpv8de64Bt6+jG9wOFNTv4eZ6FnGlmzQsv/EuK9Ck5PT1RfXhBNfOQLvSe5dLfI/oibchuuEqV+L9nTBN+/Gc+0m7sDrlse/yxAn9st8bO+VOrNq+wyV/JbXFThflTk0XbeKNr2Nz7UX2M2Yw9xtADhMrFfFzEWI8LKYtToA9UTcj3Y6OC8OmJxk/MJ8NbuDv1NjrrpZeIf0wm6QKruFjnqcAaXXgMwzoJaXzJ3IVbh9EckwfRI5Ff8gHOCxjWJ2hPemnXU43N6zPEwpDua455DfDjDzrwgM3xecUPOygeTHegue5B/PkTPZ4PNx2sB/31/x0A4M25FYMzguMAAAAASUVORK5CYII=" - }, - { - "uuid": "a44aaf69-213b-4f68-96fc-304a19e9cdae", - "url": "data:image/jpeg;base64,iVBORw0KGgoAAAANSUhEUgAAAgAAAAIACAYAAAD0eNT6AAAACXBIWXMAAAsTAAALEwEAmpwYAAAKT2lDQ1BQaG90b3Nob3AgSUNDIHByb2ZpbGUAAHjanVNnVFPpFj333vRCS4iAlEtvUhUIIFJCi4AUkSYqIQkQSoghodkVUcERRUUEG8igiAOOjoCMFVEsDIoK2AfkIaKOg6OIisr74Xuja9a89+bN/rXXPues852zzwfACAyWSDNRNYAMqUIeEeCDx8TG4eQuQIEKJHAAEAizZCFz/SMBAPh+PDwrIsAHvgABeNMLCADATZvAMByH/w/qQplcAYCEAcB0kThLCIAUAEB6jkKmAEBGAYCdmCZTAKAEAGDLY2LjAFAtAGAnf+bTAICd+Jl7AQBblCEVAaCRACATZYhEAGg7AKzPVopFAFgwABRmS8Q5ANgtADBJV2ZIALC3AMDOEAuyAAgMADBRiIUpAAR7AGDIIyN4AISZABRG8lc88SuuEOcqAAB4mbI8uSQ5RYFbCC1xB1dXLh4ozkkXKxQ2YQJhmkAuwnmZGTKBNA/g88wAAKCRFRHgg/P9eM4Ors7ONo62Dl8t6r8G/yJiYuP+5c+rcEAAAOF0ftH+LC+zGoA7BoBt/qIl7gRoXgugdfeLZrIPQLUAoOnaV/Nw+H48PEWhkLnZ2eXk5NhKxEJbYcpXff5nwl/AV/1s+X48/Pf14L7iJIEyXYFHBPjgwsz0TKUcz5IJhGLc5o9H/LcL//wd0yLESWK5WCoU41EScY5EmozzMqUiiUKSKcUl0v9k4t8s+wM+3zUAsGo+AXuRLahdYwP2SycQWHTA4vcAAPK7b8HUKAgDgGiD4c93/+8//UegJQCAZkmScQAAXkQkLlTKsz/HCAAARKCBKrBBG/TBGCzABhzBBdzBC/xgNoRCJMTCQhBCCmSAHHJgKayCQiiGzbAdKmAv1EAdNMBRaIaTcA4uwlW4Dj1wD/phCJ7BKLyBCQRByAgTYSHaiAFiilgjjggXmYX4IcFIBBKLJCDJiBRRIkuRNUgxUopUIFVIHfI9cgI5h1xGupE7yAAygvyGvEcxlIGyUT3UDLVDuag3GoRGogvQZHQxmo8WoJvQcrQaPYw2oefQq2gP2o8+Q8cwwOgYBzPEbDAuxsNCsTgsCZNjy7EirAyrxhqwVqwDu4n1Y8+xdwQSgUXACTYEd0IgYR5BSFhMWE7YSKggHCQ0EdoJNwkDhFHCJyKTqEu0JroR+cQYYjIxh1hILCPWEo8TLxB7iEPENyQSiUMyJ7mQAkmxpFTSEtJG0m5SI+ksqZs0SBojk8naZGuyBzmULCAryIXkneTD5DPkG+Qh8lsKnWJAcaT4U+IoUspqShnlEOU05QZlmDJBVaOaUt2ooVQRNY9aQq2htlKvUYeoEzR1mjnNgxZJS6WtopXTGmgXaPdpr+h0uhHdlR5Ol9BX0svpR+iX6AP0dwwNhhWDx4hnKBmbGAcYZxl3GK+YTKYZ04sZx1QwNzHrmOeZD5lvVVgqtip8FZHKCpVKlSaVGyovVKmqpqreqgtV81XLVI+pXlN9rkZVM1PjqQnUlqtVqp1Q61MbU2epO6iHqmeob1Q/pH5Z/YkGWcNMw09DpFGgsV/jvMYgC2MZs3gsIWsNq4Z1gTXEJrHN2Xx2KruY/R27iz2qqaE5QzNKM1ezUvOUZj8H45hx+Jx0TgnnKKeX836K3hTvKeIpG6Y0TLkxZVxrqpaXllirSKtRq0frvTau7aedpr1Fu1n7gQ5Bx0onXCdHZ4/OBZ3nU9lT3acKpxZNPTr1ri6qa6UbobtEd79up+6Ynr5egJ5Mb6feeb3n+hx9L/1U/W36p/VHDFgGswwkBtsMzhg8xTVxbzwdL8fb8VFDXcNAQ6VhlWGX4YSRudE8o9VGjUYPjGnGXOMk423GbcajJgYmISZLTepN7ppSTbmmKaY7TDtMx83MzaLN1pk1mz0x1zLnm+eb15vft2BaeFostqi2uGVJsuRaplnutrxuhVo5WaVYVVpds0atna0l1rutu6cRp7lOk06rntZnw7Dxtsm2qbcZsOXYBtuutm22fWFnYhdnt8Wuw+6TvZN9un2N/T0HDYfZDqsdWh1+c7RyFDpWOt6azpzuP33F9JbpL2dYzxDP2DPjthPLKcRpnVOb00dnF2e5c4PziIuJS4LLLpc+Lpsbxt3IveRKdPVxXeF60vWdm7Obwu2o26/uNu5p7ofcn8w0nymeWTNz0MPIQ+BR5dE/C5+VMGvfrH5PQ0+BZ7XnIy9jL5FXrdewt6V3qvdh7xc+9j5yn+M+4zw33jLeWV/MN8C3yLfLT8Nvnl+F30N/I/9k/3r/0QCngCUBZwOJgUGBWwL7+Hp8Ib+OPzrbZfay2e1BjKC5QRVBj4KtguXBrSFoyOyQrSH355jOkc5pDoVQfujW0Adh5mGLw34MJ4WHhVeGP45wiFga0TGXNXfR3ENz30T6RJZE3ptnMU85ry1KNSo+qi5qPNo3ujS6P8YuZlnM1VidWElsSxw5LiquNm5svt/87fOH4p3iC+N7F5gvyF1weaHOwvSFpxapLhIsOpZATIhOOJTwQRAqqBaMJfITdyWOCnnCHcJnIi/RNtGI2ENcKh5O8kgqTXqS7JG8NXkkxTOlLOW5hCepkLxMDUzdmzqeFpp2IG0yPTq9MYOSkZBxQqohTZO2Z+pn5mZ2y6xlhbL+xW6Lty8elQfJa7OQrAVZLQq2QqboVFoo1yoHsmdlV2a/zYnKOZarnivN7cyzytuQN5zvn//tEsIS4ZK2pYZLVy0dWOa9rGo5sjxxedsK4xUFK4ZWBqw8uIq2Km3VT6vtV5eufr0mek1rgV7ByoLBtQFr6wtVCuWFfevc1+1dT1gvWd+1YfqGnRs+FYmKrhTbF5cVf9go3HjlG4dvyr+Z3JS0qavEuWTPZtJm6ebeLZ5bDpaql+aXDm4N2dq0Dd9WtO319kXbL5fNKNu7g7ZDuaO/PLi8ZafJzs07P1SkVPRU+lQ27tLdtWHX+G7R7ht7vPY07NXbW7z3/T7JvttVAVVN1WbVZftJ+7P3P66Jqun4lvttXa1ObXHtxwPSA/0HIw6217nU1R3SPVRSj9Yr60cOxx++/p3vdy0NNg1VjZzG4iNwRHnk6fcJ3/ceDTradox7rOEH0x92HWcdL2pCmvKaRptTmvtbYlu6T8w+0dbq3nr8R9sfD5w0PFl5SvNUyWna6YLTk2fyz4ydlZ19fi753GDborZ752PO32oPb++6EHTh0kX/i+c7vDvOXPK4dPKy2+UTV7hXmq86X23qdOo8/pPTT8e7nLuarrlca7nuer21e2b36RueN87d9L158Rb/1tWeOT3dvfN6b/fF9/XfFt1+cif9zsu72Xcn7q28T7xf9EDtQdlD3YfVP1v+3Njv3H9qwHeg89HcR/cGhYPP/pH1jw9DBY+Zj8uGDYbrnjg+OTniP3L96fynQ89kzyaeF/6i/suuFxYvfvjV69fO0ZjRoZfyl5O/bXyl/erA6xmv28bCxh6+yXgzMV70VvvtwXfcdx3vo98PT+R8IH8o/2j5sfVT0Kf7kxmTk/8EA5jz/GMzLdsAAAAgY0hSTQAAeiUAAICDAAD5/wAAgOkAAHUwAADqYAAAOpgAABdvkl/FRgAAs09JREFUeNrsvWeXJMexJXgtsqo1Go1uNLQGIUmCEJRPzNuZ3X+9Z2dndnZ2Ht+jeCRBEgQIrbvRjW6gdVWG74dwr/T0dB3uITLNzslTqSorKzPC77Vr18xJCAEODg4ODg6O3YqGPwIODg4ODg4mABwcHBwcHBxMADg4ODg4ODiYAHBwcHBwcHAwAeDg4ODg4OBgAsDBwcHBwcHBBICDg4ODg4ODCQAHBwcHBwcHEwAODg4ODg6O8WIv9onL5ZI/LQ6O8YK0S2P8NK+ri/675k8yXl/ICyw/9cfVpbVcb43ncFSOxWLBHwJHfQLAwcFRFdgb47Lw3LdwXNRj6vVMsDeJg0kAWgvQKyBXjy3l9aXj0mo/9YvtPiYKHBxMADg4tjp04LWB+J687Gs/97X7bRfzOfsWMkAW5UC/kJHdLy3grGf5OtAfADiUlwPjtu2iP0d/ros8qL/JwcExVQLAEhQHRxfL5dIEehtg7wM4Zvw8Lq+fkNePa/cdM57vuuivvzBAXv20EQQ4AN6U9lsN+NXlnudiPn5Xu9zR7jswnn/gIBRHxGCxWDAx4OBgBYCDYxSgJwPoFwbIK1A+LkHddTmp/TwJ4JS8nNQuOik4IcHwOICWiFr5t29KgASA7ySQHmpv+UhmJ6Lv5eO22BdCnNVumz6CYwAeVNwfwGkASyGEIht35edwxwD729rllryo23e0n67LXZ1YLJfLA0M9UMSAywkcHAlBsdsBu0yArABw7Ehmr4P9vpG5HzdA/ZR2OSOBUl3OyPtPG5cFgH0iWkqguwPgKgBBRArUdXOesAC8zbznCvM5FLNeOH6q6/r9xySZIADn5WdzTAixkNn9UhIX/XILwA3jvhsaabhlkIW7hpJwoJMCVgo4OJgAcHDkZPdmZq+ycDNbN8H9DICzAO4DcL+8fr987p7M2lsJZFeI6JYEOtN057oN2N35PuDPzYwpgghQ4H7y3D4thDgF4IL8TBupJhxKsL8uFY3rAL6X129YSIKpKtzVSIGuFLBKwMHBBICDYw3wVYa/Z2T3ekZ/WoK6upw1AP5+AOfkZV8C/W0A3xLRFZmxtth01SMT/GOyfpFJBCiCCFDEzxgSoG43xvUTQogLAB4AcFISgwMA1+TlukEQvpMkQV1uGoqBrhIcys+/ZULAwQSACQDHboG+CfjHLGCvA74C9wfQydnqckZm9YcArhHRtwC+hbudLnRxgb9JAnz39wX/VBIQUw4IkYDYSwPgASHEAwDOCSH2JJjfQFcuUZdvNZKgEwKTFNzTCQGXDDiYADAB4NjeLF+v3yvAV9K9ntWrTF4B/YPyckzW6K8Q0dcSSFxDcIQn4++b/U9ZASipAujXbWRA/TwphHgYwAXpMbgH4Bt5UaRAVw6UWqBKCYoQ6D4CVgc4mAAwAeCYcZav6vjKla8y/DNGZn8eXR36ggT6i/K+PSK6DeCSBPxDuKfgCfiH6aRk/KkKgIsc5Gb/MUQgxRAYSwBiVQDXbXV9TxKCh4QQJ+X3dhXAZUkKrsjLVUMpuKEpBKrrYAlgyeoABxMAJgAc8wD9Paxkfb1+f9YA/IsAHgLwMIALRNQA+E6C/TVLdu/6qQ+tqUEAhAfUQwSgbxZLGQQgFfBTCQAcwO/7eU6SgrNCiFYSgK8BXJLEQCcESiFQJQNlKjxkMsDBBIAJAMc0QV8Z92yAf14C/kUAj8jLCSK6B+BTadQzx9T6fiKgBPhAv3XcX4oAIOKxFPBPLQHEAD484J6qBJikwPezQddtcAHAk0KIY+jk/6/k5bK8XHUQgjtMBjiYADAB4Jgm6N9vZPgPy8ujAB4kIqCT9L+Qi7lIBH4fAfApAK0F0FMIADJJgO/3fNm+C/hjwR8ZBMC2b0GsAhBLAMyRyISu4+AxdCUDoCsTfCkVgq8NheA6kwEOJgBMADjGA/1jBuifQ+fQfxCdpP8ogMcAPA7gFBEdENEncgFvLaBvA3iXzO8De58K0DoAvrWAuIi4zwbmsdJ/LAFwPUYRpMHnA4AB9EggAC4fgI0A2EDfRQT0+84LIZ4SQuyjKwF8DuALqRBcwspceM0gA/eYDHAwAeDgKAP6+kAeZeLTQf+8BvqPSMB/EsBpIvqeiD7AyqlvA/xlBvDnmP90soDAYzFkAGL9xM0B++g1IuYxktJKJOgDbsnf91iqGTCWCCwchEB1GDwnhLhPAv2nkgx86SEDykSoDITcTcDBBICDIzHbV2N2TdBX0v6jGuifkaD/IVaT9ZYOsPcRABFQBEKgH9P6BxdRkMDuk/r79v2HTniKvN83F8BZGjCIgk36txGA2JZA286HqSUBGylQ109pZOCGJAOfY1UuuGwhA2pcMasCHEwAODg8oK+G8xzDql1P1fSVW/9xAE9I0L+fiG4S0ftysW0DlxQCYGb7sQQAWG3MEwPy+s/WAfpA2gyAWhmnT+63Zfiuxxrb4xHkYOF4PEQAQoRg4XiO73JaCPG8EOI0Ol/ApwA+k4RA9w1cx6q9UJUIeOgQBxMADo7lcqlvsKPq+mexquk/rGX5TwO4SER3JOjfwPoe8qkEwOYHcBEAHdjbENjLVjNbxu+r+cPxuE8NqA36MWTAm/UbP5vA7zU2RUC2aIZIQWPc5yMAPk9ADAFQykAD4IwkAyck8H+sqQNfoysTfIuum0D5BQ6kKrDkVYCDCQDHLoG+yrqUi/8kVg7+C1gZ+Z4A8AyApyUAfEhElxNB35XtLwOAH1vnby0Zvc/0ZxIIX1kAHvCHgwiEIrcE4HquL+u3Pdf3O43j+TZioBSDmJKAyxOg379AvC/ASQaEEBcBPCsJ4McaGVCeAaUKfI/VxkXKOMheAQ4mABxbC/yNI9tXdX1l5HtKAv8FIroq6/p3DFBPJQA+UiAct62mP7m4x4B968jUW6TX+UNu/1jwKEUAfLK/SwkIKQWNQ1mI6RSAoRI0HkKwcNx2dQWkqAHqckII8aycN/ANgI8AfCJVATVv4KpDFeDyAAcTAI6tAX7TyX8GnaFPSfyPSdB/Wmb7APCBHM6zdAB+iADYsnzzvmD2b0j5Lqe/DexNcgDY2/9sIJ463a8PIUjJ9mNA3nc95AuwqQoN7PMEfF0CjaYSNJEqgEsJaCLVgIXj9gJdW+Hzcq39RCMDX2BVIriGrqR11EHA5QEOJgAccwd+3dSnavsX0Un8T8pM/zkAD8hs/wOZCZkg7yIAInA9yfmvAb5LCYgB+1CfP+Df4hcOohCTzdfwBVBAIfCpBk0kIXCRAxsxiCUFG+UBDyHwGQIXDjJgXncRAnV9X3YRXJDZ/weSDKgSwWWsvAKmaZDLAxxMADgmD/pqQVT1fSXzX8DK0PeUBP3niGifiP4uF0Qz218mZPxLB+i3cLv6W3n8m+Y+E+CXiKvxu8DdB/qtB8Rzav19nhMj/8c+J9Yb0ATIQGiqoI8ULByE4MgsKNWmBu6uAVd74CJDEdBVgQeEEC8IIQ4kEfgAqxLB11hNHVR7ERwwEeBgAsAxZeDXjX2qhe8Cutq+MvT9AJ3Mf4eI/oZO8nQB/zKQ8fuIgKv+b2b5JuinAn7sbn4hyT+mb3+DLAj/CVxDAVh/YNW6ZwN13+/6sn4fMYjZPCiGEGyUBAx1wOYDCAG/SxFYeH6eFEK8JDsIPgbwvqYKqPLAdax8AmwY5GACwDFJ4D+hAb8u8z8L4AUAjxHRN7KF7zAS+GPlf6fpT4Kkbyc/gc12vhbx0/xitvKNyfaFA9RT+vxrAwMlPEYOskAZqkAMCQDc/gCTECzg7xRotC4DnykwtgwQIgJ70idwUaoA7wH4EOvlATVTgIkABxMAjskB/zkJ/MrU97wE/oeI6FMi+lQD8KUn088hADbQt7n6fS1+Jsi3EUDvyuZFAsjHGvxcCsJQJCB2c6BYUkAJ5IDg31/ARwxsUwV9rYIbXQMeMpBKAFzKwNFjQognhBBPoWsbfE+qAso0eBkrwyATAV6He+EyEwCOHOBvsN7Kp4x9j6Nz8j8P4CUA54joAyK6ZAF8HwFoLYQgJP27QH+JQFsfwvP8YzP+DbDXgD4F8FPn/I+1+FOiKkBI7ByQwJtCCmJKBaHZAfr1RQQZcKkCC8SZAxfY9AgshBAPoZspcB3A3yQR+FgqBMowqLcQskeACQATAI5qB5ue8ZvA/wy6+v7LRHQfEb2HbuhJmwH+IULgkvht/f1LSzYf09qXKvGHwN5l6MvJ8ENRajRw7PbAfYgCOcgBZZCCFJ+ASx0w71/AMS/AoQrEAn4UCZDXL0jD4PcA3gHwd3Q+ARsROOT2QSYATAA4agC/cvWfM4D/BQn890tj39UA4C89mX7rIAFCvz8R9EOZfwrgKwm/DQA74Df5pYB86rS/ECHIJQCpv5vSVUCe3/epCPoQIHgIQAohcHYNRJCBhYMMmNdd5QEfITgvDYPXJRF4zyAC17DagIiJABMAJgAcvQ4ufXOe01iZ+3Tgf4WIzhHRuxrw9wF/G/DroG9r8wuZ/GJBPwXwfT9TB/PktPhNTeoNzQpIJQgh8Pf+jCAEfciAaydBs0Sw8BCBXBKgiMCLQohrAP5qIQKqa+CeJAI8WZAJABMAjuiDSi1a+gAfZe57BsCLAF4B8DARvSvn87uA3gf+S7iNf60F+FtLhh8y9qUO8/EBvmtufyzYh4A+tfY/tUjxAuQQgxgfgJUoWAhByhChkGHQVAgaCxFo4J8LkEoCFkKIi0KIF9G1C/4VwLuSCCizoD5QiI2CTACYAHAEgV8Z/NQGPRck8D+tAf9TRPQhEX1pAfsQ8MeY/5aaxO8a8GPu0pcK+mb93ibpx5KAFLDv0+I3RTJAPZ8f4zMIkYKQSrAxl8DYNwBw+wJCBkHAPS/gKPuXZMBXEmhigd+8CCEeFUI8i65TQBGBjyURuILVxkNsFGQCwASAw/r9mkN8zqMb4PMUpNQP4AUiuiTH9aYAv6/fPwX4bVv4+giADfSPwNyzZa8vw08x7cVu6FMK2Ida2KnC78TuMeBSAkKEwGk21MiA+fwQGdAJQGi0sI8I+OYDpBCB52TnwHtYlQY+Qbf50FV0rYPsD2ACwASA4+h7VYvKcXRy/wPotuN9Ap2r/xUArxLRARH9WWYRS6wG+fhAP1b+byUYh3byMwFen9i3RMSmPRbQN7P8NhPwc0x+pUb71vx9GuD3U70ALsIQQwganzrgKBXYyMDCuO7zCmxc5N/xdQYsEonAnvy5L4T4oRBiH8BfJBF4H91AoUtYNwryzoNMAJgA7OiBoxaufQn896Pbne9xdHP6XwLwI+ns/w90tcRlwqWVJME56EcD/dD8fjPD14f2tBbQ12+ngH4ok0/p308B4SGIwNBRYl+BWNk/lhD4NhoKkQGbEqBn/6YS4NtmuDFUgdCgoL1Q9m+5nBJCvC6E+A7An9DNEfgAnVFQjRe+hdUWxFwWYALABGBHDhq9n1+v8z+Drs7/IwBPaAa/w4LAvzRMfS4CIBwKgKvOvwbkAdB3bdDjA3Tbc9pEsC4l+8/RBFgK+G3RBJ5LEeqAbS8BkwzYiIPLH9AgomNAB3utPFCKCOxpRsHPALyNdaOg8gfw/AAmAEwAduBgUYuHaut7AF2d/2l0df4fAniJiL4mog+xKfUfIq3mbwK/a+e+JezSvq3Ob2b7RxfNPxCT6Yey/FjATwX7Gtn+2JMAh1AFUu73EYKYuQM+ZaAxBhGFSgQ2Y6CtTdBUBFxEIMYTsGdeF0I8K/0B7wL4Mzp/wMfo/AFqkJDqFuCyABMAJgBbmPWr8b2qn/9JdHX+V9HJ/Qsi+hNW+5CHgN/M+A8TgN8n84dc/UckQIK+gFvO7wP6vt/pC/Y1Zf+h9wKoCfyp4O8D9pJkQO0gGDNR0DZW2FYeiCUCe5GKgEkEjgkhfiyEWEo14C/opgp+ilXb4G0AB6wGMAFgArBdWb/u7n8UXZ3/ZQCvAXiiaZo/ygXABPhU+d8F/KH6fmtRAQDHIB9Htl8D9EsA/thGwLFiSOCPIQQlyUCMKqCXDczsv0HYJ+AjAtFlAMt9Z9u2fQ1dWeCP6KYKfoBu10G9W4DVACYATABmnvUfQ9fTfz86d/9TWNX5f0REV4zteWMA/xBu138I+G1tfeZgH8Ai+Qckfl9dvy/o1yYBqSA/dw9AzHNLg39fMhDrF2gcqoBZEgDWxwa79hLwEQGTFOwlEAK1/fAFqQYof8An6LoFrks14B6rAUwAmADML+tXPf33yaxfuftfBfATInqQiH4nT3IF6rGyvzX7l8C/DAC/bWa/TfIPAX8o2y8N+inA3ifj37VBQH0VgVhCUIsMxKgCrvLAwqIOLAJEYGHpGkhVAva02yeFEG8IIa4A+AO6ssAHWDcJqtkBrAYwAWACMJOs/xS6TXsexmqK32voZvd/SkSfWkDepwKYdf6jer+sKbq27XUBf0x93wf0Mdl+DdCfgwegBjGgyr9bywNQigy4SgRNQBXQ2wkXDiJgMwm69hEgjQj4fAF7vuwfm90CTwohnkQ3N0C1DX6MbszwNciRwqwGMAFgAjDdrH+hZf2qp/8H6Nz9rxPRKdnTf0/L+F1gbwP+tTq/Bvy+HfxigV9gfZe/Utm+D/RzRvWWJAEpQL2NcwD6KAKlwN9GAHLIQIoq4GsdjCUCevugzR9gIwIuUqDuPy5nB9wG8Ht03QJ/x2p2gFID2BvABIAJwMSyfjW/X2X9z6Az+f0EXWvf34nokiXLj6n9b2T/mtzvqveHgH9pZPt9gT+FEKSCOXsAyqsEY3sAYtUAIH5nwlQi4Npq2EUEGg8JiFUBXGqA7g1QswPeQVcWeAfd7AClBnCnwJYQgD3+CGf95ZOW9Z9GN9DncXQ9/T8C8AYRHSeiX6Ob+HUId29/VL+/zPpdm/oI+A1/ucDfRoC+eV/KHP5cEpBKDmLBe8r7APjAVCT8jgg8RzgeE5HPN59Lic8h41gi7ScZxxoF/rdGHvcK3FuZeDVEJIzfXVj+3sI4vtX5pUiAEEIsAAipBqjHWgvo+wZt7QFo5RyQq0KI14QQD6NrGX4A3eyAz9F5A24ul0ulBvAUwZkGE4D5gr9i+CfRbdf7ELpa/8sAXkfn8H+fiL42QD5U57dm/Uad39faZ2b/5ha+Nqnf3MJ3iGy/pNRfywNQGthjX4sqvZaIeJ5IeCyWEPQBfxcZIOMxYVECWuN3zN83iUCjnSeKCDQGEWi0+9Tj6rxrpDK3kK9n2z+jdRCJPY0ECEkEfg/gYSHEf9ZIwDvovAGXIOcGLJdLNggyAeAYEPzNaX6PoXP4/wjAm0T0EBH9FnLEpwP4Qxm/LvebBj/XQB+znS9U4y8F/DVBfxfl/z5/gxJeK0QOfMCfQghEBkEIEQUb4Mfc32qqgEkEhOYR0ImAXipYwl4OaDXSsBRCKG+ATiJaB+jbFIE9+X6+IqJrQog3hRAXpcp4P1adAt9KNYANgjMM9gDML+tXkv9ZdEa/J9Ft3POaBH9XX7+rzc/8GZP127bqbQcE/lC2v0vy/7a1AYaeU7IVMPSc2MecWw0jziew0UHgMQu6NhXSFYK1YUFat4A+OdDlATCNgbo34DkhxIPoDIJ/wGpuwDdSDWCD4PCY0AuXWQGYF/jrRr9HADyLrq//TQDPNU3zB3RO3VDW72r/M939tnq/72LKjH2AP6bunwv0Y7v/h5T/a28HLBJfYyplgNBzUkoE+v/m8gM0PRUBM5PXSYAwfupqQGuUBRbY3EdjzQNgqAC6GvA+EV1q2/bnMvk4j67j6EN0ewpcQ1cSOGASwAoARznw1yV/ZfR7EcCPAfxUzvD/gwX0Y7N+E/zNKX8uud809Y0B/H2y/VJzAHKJQCpAz8VsNbdugNQ5AbGqgC37bwooAuY0QVeXwMbugkbLYKgjQFcD9Mf2pEGwBfBbdOOE30M3WvgK5MZCXBJgBYCj35erTmC1be9FdEa/VwC8AeA12d532QD8kPxvPtYKIWzjfUMb+Oi3oWUb5uY8wnIpCfxjgP4UdgGcAikYuhsg5f5YJSDlNiJUgRhjoE8RMPcTaIUQJLPwRjtfGu21hEUVIOM8XShibpgEXWqAef/RfXKeyENCiH9B50M6i2742MfoNhb6frlc3gF3CUw6mABMF/xVze4EVpK/Mvr9lIieIKLfoBvqc9An8zfAX3kAbKBv28gHjqy/dZCAIYB/6pn/kBsATaEEkAr6fYHfBdx9wT8E+CWJgG4GPCoDCCGgdQzohj6ygLkiAguDtC+EEK1mEjSNgSFC0ALYI6JL0iD4lhBCkYDT6AyCqiRwh7sEmABwpIG/GuxzSrJr1dv/EwA/py5+bcn6QzX/tZ8Oud831MfW3meT+83+/TYD+PtK/kNm/kPN/h8jk8r5mymqQG4ngA/kaysBMX4A12eZQgSgPbbE+gwBs2NA9weYXQHCUAtadDK+ep3WogbYzLzCRgbkWvRDIcT/oZGAE+hmBnwL4Jb0BXBJgAkARwD899DV+8+gq/c/ha63/w0Ab8k5/l84wN+W4R8kZP0m6Pvk/mVEnb8W8NfyANRSA2pm/btWAiiR9fchA31KAqlEoHX8DZtREMbvm2WBhXEbhhrgMgTuBwiBKgm8DeAxIcR/kSTgDDrD8ifofAE3ZKvgIa/yTAA4NoGfsNrBTw32eRbdHP+3ALzSNM3v0e3VfWgAu8/856v1H2Kzxm+T/W1yv03ibz2AHntfaRIQem5pNaAUGSgN7qmvRQVfN3Y2QEwJAAWz/hiwB9I7A/oSAdd9ZsfAkT9AcoBQWUAnAvprKzWgdQwQspUGNtQCIvqciK63bftLdLMC7pMq5oeQg4Pk9MBD9gUwAeBYgb8+1e9+AI+i28Tnx+gk/4tE9K+Q23JqmX0M6JuS/9KR9buA3+fu99X5QyDfFgb+0nX/Xd/+N/fvpCoCfUsAMcAfowSkgH1f8LcRgSZADvT7lAHwyB8gz21bWQBwewMa4/xdiI5NLAJgv2e5rV7ju6Zp/lX6Au6XSsApmdh8CeA6eHogEwCONfBX9f7zAJ5A1+L3OoBfENEtR73fJAFOImAAv2uwjy/rV3K/a0EA/HK/DfhRIfufagdACqjmAu8U9gKIeR8xakCfCYG5MwGGMAO6rpvZvYscuPwBRwqB1i1AOrA7zl1dDTgaL6zOdc84YVjAX2C9S+DXAF6UvgClBJxA1yp4FStfAJMAJgA7C/7K7Hca3WCNp7Aa7PMmEX0gd/DTwT5F8tdr/fpPAf9wHxGR9fvk/pTaf23gn7v5b6pSae6+An3NgEC/EkAfMuBTCfqqAHAQAZ8/wFUWEEKIhoiW2DQJxnYKCMMb4FIBNsBf3U9Ef0PXKvifNCXgmHzuN+hGCLM5kAnAzoL/cXliXMSq3v9zAK82TfNbyG03ETb86YqAafQrmfWbJCBG7h8K+Oe88c8czX8lFIESoN8H+FPIQG0VwEcETNA3P1+zLNA41AAY56mNDOjkQ5UETIPgPuwdA+ZFtQp+L6cHnpEqwHGJPZfRmQPvMglgArBL4K/Mfvdh1d//GjrJ/ykpn91LAH9f1p8C/rFZf6rcLzJIwBitf1OZ/DeVGQAhcE/9W7Xlf9f9Nf0AqS2BMeUA06SXWhZAphqwMH6qkgCwMgiGBnxZL03T/FoI8TMhxCl0Xqdjkkh8DWkO5A4BJgDbDvzK6X8CndNfmf1eB/BLIrpPmv0UqMfI/jbwtxn9XJP9+mb9sWSgL/BPsf9/yMx/zIx/KrMAUoG/b9bfB/xjsvzYx3PKAqlqABwkAA41wDXd03m/XNteF0L8b3INVOWARpKAO+AOASYAWwr+5mQ/Nc//DQC/IqIDuf+2Dv4+t38I/F0b+SyR5vB3Zf195P7aJCD02NBEYIjMfyomwJj3M7QSEHNfzOZA6An+riw/5vGYskCuGqCrCjAUAZ14iAgS4CoHKnPg7wG8LIT4z1iVA/ax2lqYJwcyAdha8FeT/dQWvm/JzP9LIvrMAu56r7+z7z8g+XvH+cp+/jYz6/fJ/bWAv+bmP6VAfxc2AMo1AdZQAkr4AULkoKQ3IEcFgAH6rrJAtBqAbnMg83dtWXyoJGBTDfe119KvCyL6K4AnDCVgX/4d1SHAJIAJwNaAv2rzU5P9XgXwUwA/a5rmb+gmZbkkfxfwH8Dt8g/V+zdq/oGsP8bkFwvotYF/DNf/WHP/p0IOaCQloLQKEAL0EMDHKASxRCB0n6ssEPpMySAPup/Al82bJYE9TQ1oHcqgSQT0DoHPiOhW27b/jHVjYCNJwE1uE2QCsC3gfwZdm9/T6Dbz+TmA17XJfgceAnCYAP6hmf56i8+yZ9bv6gLYhex/zu1/qX36pd5rrBIQA2AphKBUSaBkCSAE9DEqQAj0bcTA5w1YOIDc9AVAu71nlAT2EecLUPddbZrmj3JyoO4JUFMMb8rxwUwCmADMEvyPoevxV21+PwbwSwAvN03za3ST/Q7grvmb15Xkb5v2F5X1a5J/iVp/iUx/DrP/p5T91876a+0eKCKfX7Md0AXWpVSAVPDvqwLEgL7+uz5vALAqCbjImm1+gBojDK2c4PMBmK/1vewQ+KkQQi8FkLaWMglgAjAr8F9omf9DWLX5/YqIntPa/Mys3yb36/cvNfBfm/EPt8vfNdbX1cdbI+ufevY/pbr/mPP/S6gCJSYBhkA/BPAp6kBJFSBGIeijAvh+N6QGwKEG6LdNcBeec94sCUBuVayvE/uOc33fvI+I/g3Az4UQigAs5Pu4hG5WAA8MYgIwG/A/poH/8+i28f0HrcfflPxD0r9u9tNB35T+Xbv4uSR/kZDlp2T9c2j/Gzv7n9Pwn5IbCeUqAb7H+5oDx/QCpDyOHmpAA7uPgDQlAGqTL0dJoLF8Tof6c7TpgS7lQLiOA7k2/lwIsacRAPX+1G6CTAKYAEwe/O8D8LAE/9cB/CMRPST7YH2g7yoD2Fr8bLP9+0j+fbL+UnI/Z//Tyfr7qAE5PoOhhgLVVgGGaAcMEQKbGmC2EJpEAYgvCZj+AGg/hccc6Dr/9DbBXwP4qRwfrEhAg25g0PdMApgATBn8j2vg/wN0Pf7/SETniOg3kZn/RhnAAH9b1u8a8BNy+9fI+vtK/DX6/uee/U+pFTDlvZT2A0xJBehbDsj1B/hAP0cNcBoELb/vOkc3VEWNBJjntrUrwFACfgPgDYMEKCXgex4dzARgyuD/CIAXJPj/ExGdIKLfeYDf7PVfk/4N2d/s73fW+xMl/xSjX60SQN/rJbP9ks7/bcr+U1SA0PspoQLkdgTEAH9s1h/K9IH1rX1jwT+27h+rBoQMgrCQAVeXQOP53NVmYzZPQAxpV22CvwPwY0kCGo0EqDWXSQATgEmB/1kN/N+S4E9E9CcH+Nvc/janv8/stzHZzzPYJ1Xyr5H178K2v3N0/tciGqlegBhSMOQGQaXKAX22Cc4tAZjPCRkEzWx/DejletRok4TNEgBgMQcaHQKxSp/6nT8BeEUI8S8WJeA7JgFMAKaW+b+ogf+SiN4NgL8p/aus3wf+rpp/qN7fR/IvmfXPcdvfXDIwpAIwtVHANRSAHNDvQwbG3Ca4hhpggnyoJKBeo7Fk9ebna5KBtY19DDUg+riVUwNfFEL8s+VYYBLABGBU8D+Gddn/LQD/TER3iOgDeRLciwB9vd6vg79vR7/U6X6lJP8plQA4+5+vClB7LkCtPQJK7BRYuv4fowbklgR8vgDfsWEqAzZfgKssYE4NfBfAc5IEbDyPjYFMAMYE/4cl+L8pM38f+Jsuf1P2N+v9Nqe/1eyn2nfg35/bBfR9+/vHzv5jJMUa7v8ShKAUyE9RARhCBehjDCy9OdDQKkAI9H0RUxIAHL4ASynAdZ5DJwOSBPiOE1c54ANJAv7JssZxdwATgMHAXx/vq7v9/1nK/rngf2AB/EMEnP4Os19svX/srH/Ksj8rAHUVgFiw92XzKY/5DIAxwJ+S9ZcgArUMgSklAfNxwD8vYIHV1uJwZfgeEuA/kLq19QVJApYWEiB4YiATgNrgbw75UW5/0mr+KeB/4Bjw43X6W8x+ter9Y/X+l1IDaoL+1Hb9m1oXgO89pZQGxvQBhLJ8H9gDmx0APsBHJvingH5sSSDGF2AzBzYx57v0BCQfr0T0Hjpj4D9hs/x5g8cGMwGoDf6nNfB/HV2f/0ki+qMH/K1mPwv422R/oRGDHLNfbNvfHKT/qQ/9qbXz3650AeQoAKmgXwv4Y7N+IL0tMFcNoITvyjY4yOcLWFMKHC1/e4YioFQCGL8Tc67rJOCvAF4TQvyjoQSASQATgFrgv4/Vxj7PoRvvq4b8uPr8D2A3/NnA3+b0t/X320x/cIB/br1/m9z/sc9JJQhTVABqvl7pXQKHUABCAJ8K/MB2dAOEfsdWIgiaAyWg62bBQ9jNgsJDAqKOG5lwvWmQgKM1kLcSZgJQEvz3AJxCt6Xvs+g29vkHOd73Nx7wdw34UYa/mNn+Lrd/qtmvlOQ/5KY/U6v/l1AA5qoClNwPIBbo+yoAKdl+DPCnZP0liEDOJkGpoJ/iC7ApBroiYO4Z4Pou96DtIeBpEbR9n6QpAb8H8JYQ4h8MxbRFt5XwIZMAJgB9wJ808L8A4Gl0W/r+ioie1Db2cYG/dZMfzfB3CLvL3wr+Hqd/jtmvj+Q/553/hlQAxlYBcl+fKv+tPtl/rgJgA+8UgjCVHQJ9wJ2a6cNDGny+ABhAf0QQ5GZAoWPjENreAYoFWH6PQscDEf0WwC+EEL8y1lMB4JZUAqZqpGUCMAPwPwHgPICnAPwIwC+1LX29Er8t+w+A/6Er67eAf4zTv6/knyv97+rY3xqkYAwloM/f6DMgKKf+X1MBSAX+GCIQe70U+IeA3vdc0wcQIgUxJEC4cMdRDjDfN5nfndxK+BdybT001lEhlQAmAUwAkmIhwf8cgCcAvArg5wBesezqF5r0t0wE/yMpy9Pm53L6l6r3T0X630Xj35wXq5zMPxfsYwF/aAUgN+v3AT6Qtz1wbinARwp8HQJmm6Dtcz6MIAGUQCiJiP5NCPFLdCbse9oaKwDchjGZkIMJgC/71+f7Pw7gJQA/BfB60zS/hn26n3d7X4/b3wb+oR7/0k7/KZKAMRWAXDKQCt5zHACUk/HnZP6u59dqCayhANiy+xzwr1UKyDUH+joEjkiABPSlY0thHwkghxLg8gMQADRN829t2/4CwF25Rqs1uZUzAnhQEBOAJPB/FKv5/j9rmuZ38uCKafMzwf+wB/j72vxyzH616v+5hGDOCkAtFWAuSkDMe6xlCEwpB0ylJTBECmLmAQDD+AB85kD9to0ErM0KMAYGpSgBMceUun63aZrftW37U5n135XrsCoF8L4BTAC84O+a8verpmn+BuAG1iX/Qw8JMDP/5UjgP3T9v0/Wvy0z/8eW/GsRByr8fvpOCaw1H6C0AhDK9IG68wBSgd73XJcPwDcrIJUEEABykACbCqDfd6Npmnfatv0VgDtawrbUlADuDGAC4AV/1ev/OjrH/1cArnhAf4nwbP/DQuAf4/QfS/Kv3foXem4tBWBMFWBKakDKeyhtBuyT/ddUAHyAnkoEaoF/SR+AjwTo0ZcE6EqALdsn2E2BBOAqEX0lOwPuYFUOaNGNDOYZAUwA1sDfbPdTvf6/IqK7RPQJwsN9XLP9zSE/tcC/Vr1/irv+lQT+mj3/u2T+S/lfpmIGLKEAAPEtgHqmn3p9LPB3kYFQR0AfEkDQTHuGEnDgUQKOfso1+6RBAlQSdpPbA5kAmJ/DSXTtfk+ja/f7BRGdkcMmNqR9C/Af3Wcx/OmgP0fwn/PUv6mpACXBfioLGBV43ynlgFLZfyzoxygAKURg7F0CS/2sRQLUz6VGAsjjCdDB/4ggyL1Z3hBC/MIgAWrNPGDg4+xf9fqfQ9fu9wqAnxHR00T0v+B29pv9/ksASwnkS9in+i1HAv+Sc/9r7fo3lgKQSwZSADgXqOeSoYTeZ245oMRkQBeQx9wfUwqIyfJjHqu1UVCf/QFiXq80CVgaP5UaQJIEmNK/fvvAvI+I/gPAL4UQP8PKGKh3Bux0e+BOEwDp+D+GleP/JXSO/1cDg35sZOBQAvkh3Lv6LQuBf0yb3zYM/ylxO5UM9FUBxlACar7+WFMBYzP/UIbvAu0YMpBTCqgxICi2DRBIq+Xn1P9dJMA1MCiHBNhw6tCiBJigDwsxABH9u1QBbhkkYLnr7YE7SwA009996Bz/L6Bz/L/VNM1v5UGiMnyznm+b7W8z+7k29hETB/+pm/9qAP8Q0v8cZf/SewDUzPxDZKBmKWCoAUG5bYApoN934mAOCRAeIrC+ZaB/WJBNDbjbNM1v27Z9E10n121o5YBd7gzYSQJg7O73IDrH/2sAftE0zXtYTY6yzvLHZgnAZvY7hHtXv7YC+O+C+S+XCIyhAuSC9dyNSTXKAUNtEhQC/VLAH5P1x6oBQ5sBfYpAHxIQSy6XDsA3LzCu326a5j05KOiGoQTs7O6Bu6oALLBu+vshOtPfNQCXYR/l6ywBWHb204FfzBj8hyQBpYlATra/Da7/2gSCKry/El0BtbL/GgrAEK2BNTsBUkyIfUiA2nLYpwKESgD65TIRnZXlgBvoSgL3jASNCcCWZ/976Cb93Y910999cmepQ8QZ/5Ya+C8t4K8A3bbBz5zAf26T/0qRgRgwHbLWPxVloMaOglPpCpjKJkEmsPrUgFKTAUcnAXB3BbQaETh6XW1GQGMB/QMbESCiD9BtIfwzGOWAXTQF7hQBkNK/bvp7EcCbAJ6TG/wcRlx8g358rn/brn5Dgv8Uxv/WVACGUAFSAHAX5f/Y/6VkGSAE9jnZfyzo11IAgLqTAfuOBS5NAtSWwLZdBJeW19JnBJDmCjyIUAGIiH4vNw66rpGAA3R+gHaXSgE7QwCMuv9DAJ4H8BN0G/z8Du56v22a36FlxK8P/AXcW/rWBP+SUv9Uhv6Msc3vnIb+TL0EkJPxpxCCMToC+gA/MKwfIMXdn0IGbM+NJQHA5lbC+k6Dtu9deQFI6wyg2IvcM+ANjQTcUcndLvkBdoIAyEl/C6wm/alhPz8jovexMv3ZgH8Ju+M/lPkfZfcS+IcCf3b+l8n2eehPv/exi8OBTKCOAX6gfEcAkO/mL0UGepEAmak3GglwzQcA1mcE6ERAvaaNBNwmovdlKUAnAYcAbshywNZPCtwVBWCBzWE/PyWie0T0dSD794G/OdRHr/0vtazfB/6YEPjP1flfCvh58M8wRIEyX2ObygBjKQA54J/6GkBcOQBwbyXcGiOA9XkBOiFQ81ZcSsChQQr0UsDXAC4IIX4qScBNrDoDbmu/ywRgxtm/3u//CGS/PxE9Juv+S4Rlf9t9etZvtvqZ4N96wN+2q99cwX/Kdf8pyf9DgX2pv0EDvK9a7YFTKwP4gH8IBSCkDpSaCRC6z7WLoCIBrVQDEPAFmEAPxJcCGiJ6B92kwDdMErALfoCtJgBS+ld1f7XD32sAfkxE/w5Pjd8C/D7Hv63XX1iu24A1drzvEOA/lgKQSgRqqgA1QL8k2I+hEpTeBbAUKRARzx2rDJAD/HNRAHL/TmgrYfN5raEaUOAYMTsDzLkA+n2NUg6I6DeyFHAVwHdYtQe2y+Xy3jaXArZdAVBz/s8DeApdv/9Pm6Z5V5N6DmJIgAT/Fvatfc1ef33KH7BZApgy+PcZ9zulfv85T/yb84JTQvpPzfhTyUCOKjCEH6DWHgG1dwcsRQLMlsCFWkstI4MbuNsDDy1+AF0laLA5KfBvbdv+FMC3kgQcdQZgizcN2loCoM35vx/A4wBeBvAmEbXohv3YhvwsPZm/PtvfVft3DfqxAb7YAvCfS91/qqC/a9uRip7EQET+Tgk/QM3sH6izR8BQ8n9pEiCMTH+DEFgGBdnKAHpnwKFDCTCJwKG87xsielQI8aYkAUemQFkK2Mr9AraSAFjm/KuWv2eI6H86gN42yU+Z/mzb+NqMfwL+QT8uAJ8S+A+95S/X/eetFtRoC5yCH6BE9p9KBEqYAscaDNSHBPgeN1UBWzlgaRAIZQpcarsHNsZP0sBf+QH+LIT4R4kVigTcw2rToK3zA2yrAqC3/D2DruXvNbnJT6ju71IETODfqPVbHP++QT+m8SUlE9/G0b+1yUCJbH+Muv/UVYISs/9jXrOkH6BEl0CJMoANyIF6o4FrKQA5fxcGCYgaFITN9sDWpgBg0/0PS/a/4QeQmwa9BuAbdKZAc1wwE4CJZ/96y5+S/t8ioi/ll3kYCfyxpr/Ydj8f+Ke4/ucy+rcmESgJ/FMB/W0tB/SV/VMAPjXrT1UF5tAWONXRwK77YkgA4G4P9B1X+nwAkxTYygAE4BYRfSmEeAvAFUkCbgM42MZSQLNl4K9G/epb/L5ORPcT0adw1/k3tvH1TPqzmv7gb/cLZfu1RveWVgBSzIC5hsAYMhBzn+/+0GMxjwP2mQ2xYJb7u9tEDEp9fujxPeYcIyLhuPQ9J2V/jNjzcC5rh+v/sM1DCa2z+m1zKuvSGNnuM3svARwS0adEdD+A1yWGPCwx5ZjEGFYAJgj+JP8fU/p/lYh+HQB9W93fBf4bY34djv+YAzv1BJ6zAjCUCpCrBMRm4Vz7L5PRx/xPJc2BYw0KCpUF+rYIphgBx1IAQu/T/M7NQUG6L8DVGaD7AJbYNAU2minQ5gdYUwKI6Hdy18BL6NoDb6DrHFOjgreCtG9TCWCBbpe/c1hN+3uDiN7DqobjGvPrIgStg1X6TH9A2P2fyujnDv671PNfEpzntMiUbP/rQwhKkoEUIhC6b8jZALkjgof2AgDhaYGhVkEy1liTBLRY9waY3QAb4C9v3yOi9+SAoMsArkGbD4AtmRK4FQTAMu3vBwB+QkT7RHQJcQN/Yur+Kaa/WOlrLvI/Cl8vmfWPDfy73Pff5/+rORfAB9ixz5nabIC+A4KGIgEpCgQQ7xUA7KZAkzgsLZ+77gcwWwNdA4IuAXhcCPETSQLUfICtmRK4LQqAkv4fxGqjn5eI6H/Bv8GPDu76sB/vlr7YbO/z1f1D7X4pWf5UFIBUctCXCAwJ/EOA/i7W/Etl+KnZfqoqMBVTYN+hQCkgPIUyQEp7IOBvDTS7AtTPRpsPoJcDnEoAEf1RCPErAF+hMwV+D1kKkGoAE4CRs39d+leu/9fljOcDB+DbdvrT+/2D4K/NB0h1/IdAfergP9XWvyGAf8qb/AxNKmiA/4Eyfzc34x+CCNRSA0rtFDhmGSClPVCRANUVYG4W5CIB5nXTE6BfXwI4IKJ3hBCvA/ga3XyAmwDuLZfL5dy7AmZNADTp/wxWA39eI6JjRPQN3Nv6bgC/Jv37pvyZYO/L6nMBf2ryfy3w54E/81YMYt4TFf4bY5gCU4lASS9ADPCHsv6xSECKAhFSNYKmQLj3DdjwA0jC0Gg4YBsMdPSYxJKnhRCvoTMFqvkAB3MfEDR3BUAf+KNm/b9kcf0f+lQAx5x/qwIghDAn/AHxpr/U1ropDv8ZQwWYA/CXAOltKw2Uqv2nZvkpGX8u2Kdm/ilqwDZ4AVL+ru09m8eL1xSo+QFa47PTFYCj17OMCjazf7MU8AfZFfAluiFB32FVCmACMEL2r6T/swAeA/AiOuOf7vpfRqgANoe/S/63TfZLMf0B0zQA5oB/bj9zKqiXbPWb2qS/XfUC9K3957xGLR+AL/OPAf1YNSDWFxDrBahNAnIVANf/aHuuWR5oAwqAuq7AfunJ/vUSwRKrroDX0fkBrmJVCpjtgKBZDjXQev5PA3gI3Ta/Pyai09K5aTX52QiBzP4PYR8msQb6kXV/n+kvV3KfysY/MeSgjwoQSxDGGPqT81zX7+7q8J+an0vJ77DUcKDc47zP+SV6nuOlfvYpMwqPmgp4hgRpM1nMddpcuw8tHV+HPuwgoktEdBKdyfw5dFvMnwawJzGJFYCBQo37fQDAk+iMf68Q0e8DX+Sau18DdPO5vszfBuzQ7rddn6oCAJTpBMgB+9Ssf46O/yFBfhtMgCUUgpyRwdtmCERitl9DCSihANg8ALF+AL110MzsjxJgOVBIVwR0hUCVAvTWwD/JHQM/x6o18I6GIUwAKmf/atzvWXQ9/8r495XxRbjYnW+DH+tGP7K+lDrsJ3Q9VQEoTQLGAP85bfE7te19p6YY1NgEqDQhGLszYA6GwBokIPXvAHGmQCDODyAAtHI+gGvDoCU22wBdHQJ6m+AduVfAa+j8AFfQlQIO5jgbYI4lgAWAk+iMf08DeBXAw0T0Mfx9/qbxb4lN6d+8LGGX+3Pr/n0UgJJlgCHAP2XuuU8aTVUDSs33T5WeSwL0NuwVUPN/6LN/QJ/nhEoDyDymU/bHyL0+ZIKR+lhKadFXKrCVBUJzXMzR717cQLdXwMdE9JDEnqclFp2U2DSrmBUB0Ix/9wN4FN1GDT9umuZtBJz+FvAPSf+uXv4+df8pKABDgT963E5ZUMcGfgb7cf/XKREBgXwFq48vYGokoM8aI9DfD+BSaFtD9dXbwJeImxmjSMCf0XkBXpBYdD+A4xKjZhOzKQEYxr+LAJ4F8EMp83znYW/mMCBd+m8Dl6Um/7tq+il1/9wTotQJWgr8S7n+a078K/F43+fXeo1tIwV6DNUi2LcrwPf4UBMCfY551/NrlwNSXzd0n3k95Ac4ul8I0RrbBusDgnSfgCoHmKWAA6x3AuiegO+ICEKIHwL4AquuAFUKmMV5PicFoMH6xL8XAbxIRH8KsLZWA31Vz19aGKFt2p9IYJi5WX+O1J8K+qVZeao8OTT413D9s/N/HJWg72uUUAViHkeP47l0V0AptTFnrSkp/8esq16FVlvD28Cav9T2dmmx6RHbwBeJPS/Ky+MSm47PCVdnoQAYE/9U298PpfHPluFvSDxY1Xpa2Hf8c0n/tul/Jev+uQCfmhmNte1vSTIAzKPPf26GPVfQxN57n3kBVOC5cx4T3GcqYMpnnaoOmPfFKBlAeD6AShpdHQK2jYNUV4Ce9bvMgGpM8FdSBfgMXVfA9+hmA8xiQuBcmEqDzmRxHl3b30sAniKij+Bu9fM5/nVzn63nf2k4/0PZb5+6/zaN/o3JUGqA/xT6/IfO8kXCZcp/Y6jPeMg5AblqgEC+kbZWybG28bikH8D2u8JQfV2zAcw28cMYbJEY9JTEpCclRp2cC7ZO/k1qbX9qq9/nAfxIbvYT6vc/+kKNoQ8u2X8Ju/TvavkD0uv+UxjSMRT4l5BDcxfc1Ow3B1iGAMAxAXfK71P0/M5KKCc5w4JyicGcSUDKYynrj7kO29RbsxTgWvePEkTLrrCuxFKRgHfQGQKflxh1H4BjEruYAPQMc6vfV4joDBFdRlzbxtKhANiyf/PgCUn/OVl/3xOk74k4BfCfSstfX+AfAkjnHkP8PzW/w6m2BtYiATXWnFLG49TrtvZtOFQAmwIQ1R5IRJeJ6AyAV6Qa8KDErMmX2CdNAGRLxTGs2v5+AOBVIvpjIOtf29ZXa/Fw9YPq0r8ISHGu632z/tokoNSJCPTv96+Z9c8N+HfNJFjz/x2bCJRWA2rNB5hjO2DoMwity0oFWDqSPNMQaPrFfIrAocSkV7HeFnhs6m2BU1cAVNvfgwCekeB/iG4rRh8rOzS+SO+kP6xL/7aDwyU55Z58YqATEomvm5tV1AD/nGyrBvCXBivuCqj/edQaFNTnGJzKfAAkrjl91p6SfgDfdd8avbaeW0oBthkBNuw4hF9tvkVES0kCnpGYdXrqKsBkCYAx9OcxyaxeIKK3Eaj3W1hbqPYDuNtJcuWn1DJAbPtfajYce2L3UQFybw/l/u8D/GMBEhOCcVSBFCKQowbE/L2aJCCWGIiM9SWWMOTsPFqiFKDfBgJeMFjUZB/2yLbAF+TlMcxgONAkCYBj6M+rRPQF1tv+XOYM1fNvEgPnJj9Gv2iOxJSa9ZfO8rfB+V96l78xgZ9Bf1qf4dATA0vvGjjFjgBUXINS16OYddq21vvGwOulgBZhs/mBxKhXJWZNfrfAqSoADbrd/s6hG7DwAoAn5Lz/kCtzgwjAv8ufVSpCeek/5UTIJQNTN/+l3Fcq6x8L+DnqkIExiMCQakAMMRiCBMxlDcoqBSDOEOgC/kOPCvAxgCckZqnhQCemirWTe1OSKe1r2f8z6Jz/HyFii1+Nuem314x+2DT+tZYDpYT0NCfzX+pCMhb4l876GfiZCAyhBkyJBGCia1AN+d9aCrDMBrB1DdiwxFcKUF0BH6HrCHgW3eC6MwD2p6gCTJGVLCRjekDL/h8moi8js3+b9L/EpuljCb/0H5KXhpLdciW2FHafysBLgH+u5D90dhfz9xj4xyMCJb7DkqShREmgNglI9QGUXoNqlSNj12qzFGDDBnM2gIklPhXgSwAPY+UFUCrA5LwAk3IoWmr/Kvt/F37Tn+1Lc27wg3UTiCkF2aZNAcNI/7XH/5ZQAVLBf9vG/Jb4/WEZdebGJFOtW3q+Dxro90Mjcn2Pux6zjf4F0sYDxz7mejx2Q5+Uz6jmxkApGwbpY4L1x0gCc6t93uTAjYWGM41FCWg0EvCuEOJlAB8A+ArAdQB3lsvlckobBU2tRWEj+yeic3LSkq/f37bVr2vMr8kAbeDex2wytfG/IWYfWwscC/xL1vq3FvhrLCq+15woORiSCMSQAESCPSKBOgfokQisvvdQ6qfrNYH4fQJC123/H7C5Y6AgIp0Y6D/1nQJbbZ8Ac48AGwG4DOB5IcQPAHyMbp+AGxpeTSImUwLQsv8zWvb/MhG9h3DbX2jKn3kB0o1/JbP+1Cy99LjfGDBP+V/HAv+SY11nBf6LxULol137+xP5rvuWBGLvL9EGmHLeD70m9V1DbddzDIEh1TiEO2Yp4D0AL0ssuyixbVIdAVPyAKjs/5zM/n9ARPcDuAK/AUMf+uOr96+1fGjZv036d5UB+pCAIev+KSdy3+t9wH+qM/5L/n4V0J2iAjGx9zbk9z7GngF9BwL16QzoszbV2BPAW+v3rOsC6KRg2IfDbfgCDPPgYQCbrkgM+wHWOwIm4wWYBAGwZP9Py+w/VPv3mfxs/f5K+re1AuaO2RQ9T7QUMoCMkzFlYRka/EsvqCUzwUkA/5RBfybvuQQRqEkC+pwztUnAGGtTn8QqdVy5nhBu4AQ8u8V6MMhUAd6VKsDTU1QBpqIAqL5/NfXveSI6q2X/tg96bTqTJfs3pzoJuI1+scY/EXFATUH6rzn0pyb4l97Wt/aiz6A/n/9lKBJQeuOgnHMOPc/vsdamvkqq7TNuPYqA67beHWDOCTBVgI0uNOPnFYllz2M1HXAycwFGfxOWvv+nALykZf8t3HX/NuI+M/sPTfzrowbEAmwf1l3S9Ncn2x9y6E/JRXiyWf+2gP6E/7+hjo2SvoCpzAUYYm3KWUtz1+3QhEBXxh+DO2tkQGLZSxLb1HTAScwFmAILabCa+f+oZErnZPZv7cn0ZP+2QQ5m9m+bABVii6UktVwfQB+5DT0XgLmB/+yy/m0H/okSgSHUgLmQgD6JSp+1qeY8ABjrvGuN902ENTGkdWCOSwVQ169ITHseq50Cj08Bf6dAAPbQ7Z18AcCT6Fr/PoS7rhLLwjam/zna/mzSUgnjXwkSMGTrX+xitW3gP1rWv2vAP7H/v68aMDcSkHOO1xwNPBT42xI623evJgTahgOJHirAEkArMe0FiXEPSswbvQ1/VAIgd0k6JhnRI5CjE4noa4RHL8Zk/ynz/nONf31aWaYir/UlCGOC/xALOQP/dhOBmgSiNgnIIe59/QC116q+uwTGrt8hXHCNj3epAE7Mkpj2kMS4hyXmHRt7p8CxFYCFZELntez/U88H6duwwZb9C0v2LwJssI08cGrt+ofEkzAV9PssCENO/Su92c/oWT8D/2Q/n9okUiCv66VvJ0AJ4C+5DsWsfSXaA2M2Cwq1BQoHlvhUABdeKRLwqaYCnJfYt5sEYLlcNjL7PysZ0TMAHtcIQOv5MPtm/6ngPvaufznPr7nrX23wr525jQJuDPGT/6zGLAlMhQSUHlc+5i6BOW2BJVQAW4KqCMDjEuselth3TGLhzikACwAnsRr7+zwRXUKglgL/5j42xhYa+hPT9tdHTqvdAlhLPmTw56x/V9WAbSQBff+XEgOBSu4SmEsEotoCteFAPhXA1SXgxC6Jcc9LzHtAYuBoKsAoBEBr/TuDri7yFICniegDhLf6VczL9mXYsn8RyP5LZvyl5bSUk3XqU//GBP/BJX8G/ll/jqLHcTZVElA7gSmxdqX8vyUVAZcKIAIqwNLAIqf8j1UZ4AN0Q4GewgS2Ch5LAdDH/j4K4DkiugP7Fou+uczm1L9S2f8QJ08f5jzU1L/UBacm+NdelIuCFkP3VnymNcnmUCSgxPm/Td0AooAKYGKOb/+Zja3rJdY9J7HvHEYcDzwWAVBb/j4I4AlJAN5BuN1PgbpJBvpm/zlO0lIkIPfE6iPd5Wb7uYtaCfAfagHnrJ/VgKGOPVH4XEol7aLn2tGXFITUgBrg7yNquSqAiUleDJNY95zEvgclFo7SEjg4AZBtD8ehmf+IaA/AHUem7+u7NOc1619USva/bbP/Ywd85C4cKa+3M+DPEL21n/XUSECtczU3ken7+fTdurxEK2DsRkFIwCcbli0B3CGifaybAY+P0RI4hgKgzH/n0c1Gfo6I3kec4c/Gskxzhm3uf0z2n3LApLSl5JxINVWAuWz4Mwvw56x/Z9SAKZGAof0ANbL/EluXl+jcilUBXDgTMgFau9mI6O9SBXhMYuEoZsBBCYBsd9gHcB9Wc//PA7gK9z7MNvlfBLJ/UTD7LzlLW2QsCkP3/qcuLFMC/0HNfgz8O6cG1PShDEECcteCmjMBUtfL1HWtlAogPCqAwOYusz4fWysx7wGs9ge4D50ZcFBMHloB0Hf9ewSd/H8lMfO31f5dfZookP3HylCxj8cy5tjnI+OEKQm8UwL/bQUejml9F1MmAbXO7RLlzBojzEus4SLwt31zZoQDm9qQOkBEV9F1BDyCkXYJHJoAKPPfBXR9kE9pc/99H5he028d0sxRZm/M/K+d/Y+x81+OCjDU8J+ai+Do4M+S/3RJwAxKAjWO/9qTAae8llXZFdDAChum2HBHGBjVhhJaiX1PSyy8gBHMgIMRACltKPPfQxL8CcA9uPv+Y02B+pe2HDj7z5GvhjDP5Mp9fcFfDLj4DQ7+DLWsBoxIAkThczUX+GPXopy1bKhppjkqgA1rQqY/334B9zoIPJoJMPhkwCEVgD2sJv89BuBZInrPA+627N829lf/ImCwOQyU/efIZiUYcy15sIbjn8Gfg0nAcOdJrZ0CU0hFzV1MUxO0XBXAxA3AbTjXh9TFJK5LaYB/VmLiAxh4l8BBCIAx+U/1/ivz3zKCBOgfuPn4GsBLIwawadoYI/uPZbpjZ/9j1/0nC/4s+c+XBAz4vdU4fsci/FOaapqSWNVSAY7u10x+tjKBORjIZwDUL1ckFqqZAINOBhxKAVC9/8r897Q0QLQB0G9NdgX71o1tgLGJxIOidvZfY+JfLlNOyRx2DvwZSlkNmDEJKFEKmPPaljMK2EUGzJZy2wRa2+Z0bYgMWMyAxzFQS+BQBEA3/z0mCcD7gWzfnLW8MebXBH1L61/rYnMZB0TJ7L/UYtMH6FPZMgZe4Bj8OZgElD8HS64PU1jbSqgAwoERMMAchhHd5g9QLYEC7n0BTALwviQAj2FgM2B1AqBt+6t6/58kogZu85/p8g9NALTVYfpk/8g4oMZiyCVYcd/FSgy4sFVfyFny314SMND3OiQJmFopYOoqQC4xsJEBW0ugT8EWcM+3uScxUZ8JMIgZcAgFQN/45xHJdD6Evy7iav2ztf+Zg390ucaX/aPAAVOaIfcdEDSHLX+HXFg56+cY4zse8vif0lbBNYcC9VUBYh8TjqTxKLG0DAay3W4jzID6RbUEPoLODDjIBkFDEABT/r9IRJdhN0/EmP9sk/30Lyj0RcYoA6LAQddn+M/Udv0rvciJCSyoDP5MAqZEAmqfT2PvEpi7FpZQAUL/V+gxW0ugeX9rwaloM6DExIsYeCZAVQJgyP8PAniCiG4j7PY3gT7G/GdKNLEHZJ+d8Upk/zndAX2JQOoCIgouRgz+HEwCpn0elt4lMGet6+Nz6rvGuwiHD3Nck2lTzIA3ATwpicAgGwTVVgDUxj/n0O169BQRfYDNGoj1Q9EkFPgyf6P1LxX0c7Lkmsw45fl9T8TUzyblc2Hw52ASMBwJKLlBUKnEY8i1LnWtjv1srD+NlkDXvjNmGcC3z42aCfCUxMpzEjurYnRtArCHbrDBeQCPSlZzA+7eSVuG7/IHWMcAB6SdUq5/YPiNf0pl/7WkfwZ/DiYB0yMBJTL/mipA6nNz3f992sB9LYGuFsAW7jK3a6bNTXRqud4NUHUmQDUCoI3+VfL/40R0w/OhiIBEYmvxs5n/UroAROEDKZURl9j4Z4yFZuwFkMGfY9dJwFjn+BS8ACUTt5TBQGv4YpgBbYZApWTbklffTIAnoJUBauJ0TQVAd/8/hK7970OEd/hTH5pNEVhaviCR+MWWzv5LAvKUsv+xXP9VF2Nu8+MY8bgY4jyZyjbBpf/PIVWAWDxxlQBcuwf6drpVBOAjdD4AvQxQzQdQkwAo9/8D6FobXPK/yzAB2OV+6xAgz5dU0gfQR/av0Rs7ZIYxhPRfHfwZ6jhGPkaGMPJNZQvwqc48ya3/myBvkgGbCmDDNl+Z+47M+h9FVzo/DWCvVhmgCgEw5P+L6OT/63BL+6ZkEur9t03+E44vrqYPIOXgn3v2z+DPwSRgPiRgG1SAIdqfY+v/LjOgb2t6czJgqPVdLwM8jq50fh8qlgFqKQBK/r8fK/n/A8S1/5kT/toI1oXILy3ngEg94HYp+2fw52ASMD4J2BUVYIypgK5k0pb1A3Zvm7Dgm9M0SESfSALwkMTQE3MjAEr+P49O/j8D4Bb87kgT8G1bLtoG/gDuXf+Q+eWnHPyc/e/2Qs7BJGDsYBWg7vbnttut5TVtSoA5zM7V6aYnwXfQ1f4fxqoMsD8LAmAM/7kA4DEi+g7+tghT/ncxqlDv/5Dmv9rzsVHpRNuZ7J/Bn2Pix9CuqABDroFDmAFDMwFsirU5E0A4lAK9DPCYJABnUGlvgBoKgJL/z2JV/38f/p7+kPxvZv4+NWAo81+q9FWL+U4t+2fw52ASMDwJmLIKMKU1sKQZ0Jb1A3b/GpBWBvgYXRngIlZlgOLdALUIwCmspv+dkZJGaEBCrPzvGsqQC/q1gL8PA55y9s/gz8EkYD4kYK4qQCwhqGEGjCUDNv9ZnzKAfrktsVO1A55Chb0BihIATf4/g07+f5SI7sIu92/IJZHyv7DI/y6ppjTzRYTiUIIBTyH7n8qCx+DPwSSg7GvPQQUQPd5bjbXQ+zlITLIlrGvKtaUM4CIDQu6b8whWZYD90mWA0gqAav87qwgAgE8RuSMS0uX/mJp/n61+cw+4sQYA1V5gxEgLGIM/xy6QgKmch7VVgD7vqYQZMLct0PV4bBnANRTIioWyDPAYKrYDliYAavMf1f53noiuWFiOa4c/IE3+j2n5S/3CSzHfVMlLFDi5x8r+R5f+Gfw5toAE1DyPaiqiU1kLS+8JEPP+XR0CrjIAYB9uZ1PIv0U3SE/5AE6hsA+gGAGQk4r2sZr+9xARHcJf67fNRxa+DzJB/i/d+z/0AKCS7XhjZAUM/hxMAuZ7npVQREu9nzEGAiGAKbllgBD2md0Ah+h8AA9IAlB0c6CSCgBhVf9X43+/QFj214FdOEgCkC7/5zC8Pix3yua/KWT/DNIcHOOeU0P7fsZeE3PWe5HxujFlAOHAuhA+foF1H8AxibWTIwALrO/+9wgRXYZd/m899/uGA9lYWYwk04fBTsX4UorpTiZz5+yfg1WA0UlEToY/lzWxz/+FnpjjM/nZjIBWDCSiS1IBuCCxtWg7YEkCsCclivslAdgHcAB7n79L/rDtrpQq/+fIO6UP8ponMWf/DP4c208C5qgCzG1H1ByfVE4ZAA6g170BNkPgEsChxNIHJbaeRMF2wCIEQKv/q/7/h4joJtyGCHOqn5JL4Mj6bb2XJeX/Kc3/zznJx8j+Gfw5OKZNAoZSAVL/9lz3BQg934dZa6UBB+ZZLxJLH8JqHkAxH0ApBUDv/z8v3+wXiGv9012Qrov5gbfI2zin5vz/UiddKTduKvPnBZeDYzePyZJK4NzXxhTwdw0FsmX9Mc5/V2v8FxJTdR9AEewuRQDU+N/75Js8L2cZ++r85gfndUUWcv/3lYn61rZynlsjY4j93cll/wz+HDtGAmqoAGKA95P6O6XW0RJruo8M5HYDhDDO6Q+QWHpeXor6AEoSANX//yARkZH9+wYiuNr/4FEBxpL/+0pcOQd9rrt17KE9DP4cTAImei4VWi9KzkfJXUNT1+raZQDX5nSt47GlByP1oUCEdR/ANAiAUf9XBsDv4HY/+loiXK1+tg2AUlnjnCSuoVl7jeyfwZ+DScCwUVIFqKUmzqVEGgrftvRRWGao2s5OAHm5rhGAYj6AEgqAqv+fRmdSuEhEXyFu3rGrJcLXYgGHDFN7A6Dcg1oUPgFKqBezy/45OHY8pnRO11pr+m4MVGptz7ntw6mYdsDQQKCvsTICnkYhH0ApAnACqwFADwC4Br/BQTg+IKv076j/l7idyx77SFs55KGGylAr+2fpn4NVgPFKAX1UgBrZ+ZhrZc76XgRjLJglPKQAEXh5TYL/AxJrT0yFAOzJN3MWwAVZq2jhb/9rASwtu//5ZgHY7u/7hc153OVOZOMM/hx87M5SdZhLGaD0FsExGHZ0n8TAZQRethJbL0isPYEC8wBKEYCTWO0AeCMgdwS3+3UwJJfUUmvr3xqz+MeW/0tnEFXJBoM/B5OASZ3DY5QBSqw3tbZLj9l9VoSA3UESbD6AGxoBKDIQqCnw+6r+f79UAL6Ef5xvqAUiZg6AyPzCcg/Y3HGXNd3/JRYLHvHLwbHbx3LpNWKMboDSa2VOQunqCPABf0yirCsAX0oCcD8K+QBKEoBz8s1dQ0RvIyIH/2hlgpwvveQMgJIHWy7rnuoiMWkywcGxZTGX83HoMsCQI4GD90nsiiEENh+ADTOvSYw9NxUCoAYAnZFvag/h/v+Q418gzjABrLcE1gbaseZc55KXUgcyhl5sOPvnYBWg2jk3ROI0FZIxRMLVRoB8DL7FYOZSYuw5rIyAveYB9CUAygB4H7rpfweRGb7e/x/DjAB/q0Xu7SEGXIy1BfAsQZTBn4NJwCwVhz7r1xBrZs5an1sKiPUBAN2+ALHKuJAYq08E7OUD6EMA1ACgk/LNPADgG8T3N/r2Ua5Z/885SGowx5qvWfJ9zNZHwMGx5SEqndNDvccx1sxa7YCu1w5l/fBgog1Dv5FYe5/E3n2JxYMTgAarCYCqBfAS4ichAZtOyLUP0VH/j535P5f2v9TXqSX/c4bEwbG7x/jUOqdKEoEh2gGtmKT5AMzfi5mJY1MALmHVCXBKYnA2jvclAMewGgF8CsBteFyMsHcC+KSSFMCf0kGcI2Gl/F9TOMGKEgkGfw4mAZM+R2uXAfqsnVPzTsV62eDARF8X3W0Dc3sZAfsQgAWA45AtgETUwO30h5Hxhz4cl3tyaGDMrWWlHuRTY/+cFXFwbPcxP7W1p2TiNQWMiO0AAOyDglyqQCuxVrUCHkcPI2BfAqA6AO4HcGj5Z6wDgKQs0kZ8IDlf8JD1/6FbWvqy3jEzAg4OjmmDee5aMUYHVZ/JqUP5AFyEYAPYjZZBmyKgv8ahxNzenQC5BICw6gBQLYBXHPKFzwToJAGO+f+59f9cGarWwVv6RJ/rFqSc/XOwCjDPbH9Su4Uivwsg9vk5PgAblsVgoG9YXiux9pxGAPaQaQTsQwDWOgCI6DLiNzwIPSfE8krKOqV7WedW/x8NgBn8OZgEjHoOTK2EWmIN7ZvclVZPYicEAn5T4NH9EmuLdALkEgBlAFQE4AziDYCuLN71hYvEDzzmdg0wnmv9f7LqAAcHx9ackyV2Bcxdh4faFAgOVSD0P4a2DLYZAc9oBCDbCNiXAJySb2KBPANgjBIgUL/ePef6vxjh8+Dsn4NjXudCjTW0bzY9FR9ALUyJ3QoYSDQCSsy9Dz07AXIJwEIjAGeJaInIqX6aARDIMwCW2tRnajWsEifLlBYSBn8OjmHPiamc/3NbQ0tjiu01fPI+YDcC+soAS6xmARxDphEwlwDsoWs/UArALfidi656RxvBlBBJDPoythpy0tD1/ykuGBwcHNONMdaYuayhJUcBw4FxLfy+OFdH3S1NATiOzJHAfQjACXR9iGex6gBIkfed5j/P9KTYL3moXQCReJDOwfBT9T1y9s/BMdq5MZkSYI+/XXpfgNoDgXyYlrIBnkkarkjsPT00AWg0BUANAbrmePOhcYeI+CBKHKBi5BMqh0ykEpgh/mcGbw6O3cvo57qXypCfY6wRMAX/nL46IrquEQDVCpiM57kEYF9TAE4AuAv/dr+uLYDhuS9EAmqrALlqwFiMfPLtf5z9c3BM8hypsXZMYV+AXCWgD7b4wD+EeSkYegddB4DC4Kw9AfoQAPXH9+B2L7o+DBe4txjGAFjygBry4OWFjYODScA2qBJTXktrGQHN/W9icdHVTbcnMfjk0ATgmGQdZ4goZq6/bQRwTPaPgBJQ0wCYe+DVHGIxZibAAM7BMV/AndN5PKW1tJQRMKQCuEYC+zoBBFbTAAcjAAtNATiDdfk/5mIyoZTsvmTPfWkD4JROzklt/8vZPwfH7M6ZMfYQqb2W9jUC9nlPPkIQmgDoutyVGKyGASW3AuYSADUF8DSA6/DP9fe1+G3M+7cwoBoMrfaBPMRJyaDKwcExNcVhDh6Gmtm/Uw1wdAK0kVhpmwh4HasSwOAEQG0EdANhA19KX2QKixvjwGIDIGf/HBzbdu7skhFwTLzog4vm4zewKgEMQgBUC+CRAkBE32lvyLWzUSijby0fRJ8d/kp1AMydZJQ6oRnEOTjmm5VPdX2ZwppboxMgZqfANqQYwL9TICT26iWA5FbAVAKgtgE+Lv/ocQD3EK5VuGQOF7spfeCMvYvVVBcCzv45OFgFmDLxn/OuqiLif3JhYhuBq/e0ZFwNA0raFTBXAVAEYB9pLYCpLRI+5WCKHQBTOJEYdDk4OOa+tkx5Tc3pBICR1ceSApdSoV5nH+vjgKsqAHoJ4BT8PYwC4fpHLLiX/hLHml41xATASZzEnP1zcMzuXCq1rkx5TR2iEyDlveRgpvmaJ4ckAPtKASCi0CYGR2/ScPe3GQdP3z0ASp4AU/o7tXcRYyDn4Ni+jH2o3QentqbWVgNiMcw5BM/ASi++yl0BlQcgeRZA3xLAvQjwd2X6G5sHZbQADsVoOTj75+Dgc2r7iNBQCZarFdA3HTBmyN690RQAADcRN98/RvaP+RLGZpxi4gcrBwcHxzYCcc2/UXKDtVQDeMgUH8LXGxoBGEwBUHMAbiFsgBARH3Zq21/uFy5GPDDH+LtMJjg4OHiNqpfZ5zwv1djue/5trOYAVFcAFjoBIKJbgaw/JvNvE8hDzhe3Sy2AYuzXY6mSg6NOZJ5bo68JBf/mnFsBXWVwV/IchaMSg/U5AEnDgFIIAGGzBHDb8sZiXIt9ZZNY5jQF0J0SIDI4c3BwbMt6NaVWwFwy4/s7IQ8ANAVALwFEzwJIJQC6AnAcwAHSDAs+lgMPGyr5xe9yCyCTAg4OjiHWEm6v9vvdYmv8IWw9kFisKwBVCcC+/GMN/Nv5wvMP2qId+UAXMzw5J/WeWf7n4Ni5c4zX1vRoI/52zDAgdWmwagMchAAcl9dTHP21a+RzGJbBGTcHBwfHvNfWGkpBDjYq/F1gVQKoRgAarJcAUiYWxf6jKZK/GPhL4uDg4ODYPXKRgzWp94WM76GOOb0EEI3r2SUAInK96Y3rgSmAoQ9UbMEBtPVsmuV/Do5hIuNc2zXVcoqYEfOefNMAXdeFxOLqJQBdAdiHe2CBL5N3fkiOf7jWlzrVXfoYRDk4OHYtE9+2XVNFT6yLVQD0n/tDKAD6IKA2gulEjwEu9EGL3C9lS08sDg4ODl5rymFCjf1nQuOAQzjbYn0QUPU2wH3YWwBjdzXKBWs+2Pn/4ODg4LVibv9HLN6l7KKrtwLqCkA1AqCXAe4ib4c+MeEDoNRwB7FLJwjX/zk4ho2C59zc1qrSQ+Om9PnkYKeQWKzL/9UJwD66XYhKZPI5rYTMQDk4ODi2f83b1jU/Fvdi8PUeVgbAQQjAHoBlxj8Ww3DmlEUPsaGEmPHJwMHBwWRgzPVzSp9DzKZ3qVNxgc4DMKgCsMDKAxDzxn3yRekDQMzwRODg4ODgmNc6XovApO4XcE/D5cEUgMPAG07NXOc0LlLwCcLBwcExi/VwqtiSYgz0/e6hBv5VCYBOAtoMqaLElzPURkAM7hwcHBzbv6bV3BCo5Hv0lQCUAkC1FYAGQENEdyNkClHpH+ZgAsLBwcHn9Bw+91o4KABAYrEu/1chADAUgKXlTc61/s4nR2ZwCyAHB597W0qIpopnJta2WvafhOk5JQD1R9rEDzo07IAPOg4ODg4G5l1XCVI74ZZYr/9XUwBg/IFSbR6CD04ODg4Ojh1Zo0tho7DgchUFAIYC4HuDYgu/MA4ODg4OjjHISghnKYcE5JYACP5BQBwcHBwcHBz1CYMqASSTgNwSADwEIPTGmSBMi1lycHBw8Joyrc845fNeGthcRQGwkQAODg4ODg6OcSMLkxv+3Dg4ODg4OHYvcgkAy0EcHBwcHBzTiCxMbnr8oUXC75DjOked4M+Yg4OD15T5fsYpn/cilwSkEABhXBZ8oHBwcHBwcIxKwtTePMkTeVMVAPXibeBNUYF/ioODg4ODY9cAPfa55EjQqygA5h+K+Qeowgcxly+Mg4ODg4PX6JrYSBZcrkIAhKEANIn/gFnfID44OTg4ODh2fI0k2Ov/sZ+FKgG0GKAE0KIbPLCw/AM00wOKgTkzlsslf3YcHHzubSPRmCqemVjbSEw2y/PFFQDFMlohxPHAh9SXEOyqSsAsnIODg8/p7czuS/4+AYDE4uoKgDAUgKbQP0wDHshzaUfkk5WDg4MJyPDvjSb6P/pwVSkAy9oEQIH/IYA9yz9OPT5UmtEBS3xCcXBwcMxiPZwqtsS+55A/YE9isq4CVCUASwD7kSDv+2epwhdPfIJwcHBwMKmo/J6o0v8ZIggm7u5ruDwIATiEfxAQZX6AhHQH5NwP0pRWSiYSHBwc2wLiQ6yfU8v4YzAwFVcXEpMHJQDHEmUMZPxjc8+kGbA5ODgY9HdnDU3Bsr6lcfXYsaEJwAGA44iT9ceU6IcyGJLj506crNyOxMExbBQ85+a2VvVdY2lGn08MdpLE4oMhCIDK/g+w8gCYF5/UEStn0wQPPGbmHBwcnMXz/1H6vZi4GSqNm5d9iclKBaiqACgCEOP4d/1DTaEviDI+ZD7YOTg4OHitycGE0oo2eV6XEnBWEYBqCoBe/79nAfGQAuD9x4mIenwpczyAaYtPLA4ODo65r8dViIUF60J4SR6cVTh+D+s+gCoKgKr/3xNCIJKZmP9wM5MDYu7gTEP+PvsAODiGiYxzbdC1YALr6FzxowkkxNbrEovvYeUDGEQBsMkTIUYTMnJQ5H0xHyht2QHEwcHBwTH8mp6DNTEtfz4sjFEA9NuDKgB35fWcFr5aTnmawYHE5IKDg4Nj3mtraTIRMgGGiMFyCAVgrQQgWUbIqJDyTzUjf2E0w5NhUu+ZywAcHDt3jvHamh5NxN8OKQj6c1qZlA9CAFQJ4C7srYCAvVXB1x2QOv1oqA2BSrYq9nmtmtMAGbQ5ODjjLv16u7a2xpYBYrJ+H37aWgDvYr0EULUNUJUA7gA46XnDQHzvf+nhDlMaNsQzDTg4OHaVFGzL2loKY1Ln/FMAY09KLFYEoFobIAwF4LYQ4lQgq4+pbTQe1lQi+y/xfMr4YmkLTt7k1+MyAAdHncg8t7ZBMcxZW6eGHTHYl4yjQogTkgCoEkCb8oZTCUCrEQClAISkj1gnZC7DogJf6FigRTN/fQ4ODlYE5rhGDbkRUWzff04p/DSA21gvAVQlAKoEcBvAGaTV+GNr/6mySW3iMOTByqDNwcHBZGK4v1FyRH0fbIuR/M3HzkgsvotVCaC6AqAIwDGkGRbMv712vzH8YOytIhmIM4PLABwcfE5tCfEohUPmgB/9flcZPMZYf0wjAIOUAFQb4G0hxCLw5oC4aYApRkHffds2C4AGPECZCHFw7A7wDZVgDbXV+1hrf6qU73qObwqgE18lBqsSwGAeAKUAAGnTAGONgVT5i6PIL6X0STTVdhXOWDg4OPvfhfbqXByo8dmm7qLr6qobpQRwC6tdARvEjTIMuft9rshUJrarrYAMvBwcHHNfW+bUApgyA6AJ/I2YwXk65h4MSQCEoQDcg98HQMab9SkAfaT/0gfunFsBJ0MIWAXg4Jj1OTSl9WvOLYAu3PVhYhOBq8ckBt/CqgtApLzpPm2AtwHcEEKctbCSBpsGB4r4QHwMqNQezUPKPhjoYK19sjOQc3BsT7Y+lfVlCmtubgdA6nNNTPMpAfo2v+TAVggh7gNwA6s5ANUVAGC18cAd+cfPOD5EV3ZPEc+fykFT8++PaQQc7DNkFYCDYxbnTo01ZCgD4Jzwog8umo/fB+AmVmr8MvUN9yEAt+Ufvz9CsoBH5lj7xzJaAcfyAozNohlYOTg4pqY2bMME1JLY4msBBNzlcSBcUr9fJuGDEwBlPLgB4DjCtQrbP9hY/tnQB0uVDtS5dAKUOnC3OZPh4ODsf7ogu+0dACktgCYJQCKWHtcIwMFQBKDFqgRwUwhBkW9eMSDXP4sAIZhLJ0Do51yY/BQUDw4OjnHO4ymYD6ewlpboAPBh2lpSbFHAnUm0xN6bWG0G1Kb+c7kEQFcADgNA7vtQnMMQAgdEabaYwyBrn7BbBbisAnBw7Oy5Mre1tM/zU1vdY3DRRRwODQVgUAJwR2Mfx7HuUrT5ARrE7RsAhDdNyGFqoS8s5eBhIyAvbBwc23KO7JIBsHYHgAvcYzDP1wLYGBh73MDgQQmAmgVwE8B3QohzDtmi0S5A3OYGMeA/FbCjCr9XaiIgTej/5eDgmCaYl1yvaGLvbSqkKLQ3ju3+xpNMQ2LudxKDs1oAcwkA5B9T7OM7ABdg2dwHaZsE2ToBYtlX6eyfenzBUwXQSdT+WAXg4Bjt3Jhy/T91DR3aAJiLQ74OgNhNf8zXayTmfqcpAIc5H3ofAqDGAX8H4BQ2hxWE2gIbC2kI9UUObQQcciLgGD4ABmMODlYMhn79uayhpQyAPvD3KeQu+Z8k5n4nMfju0ARAzQK4ha4EsEB6J4CPATWRzK+EWWMuoEgTOJmL/A1WATg4qpwTUzn/57aGlsYU1x4Avsw/tQNgoRGArBkAfQhAqxGA7+Ufd9YrDGBPKRP4Mv9cmSbngI3tTBjqpB3CvDKHBY+Dg8G/7Dldopw6FGEJOe6H/jxcSkCqvO/DR/X4UmKvIgBtzgfYlwDclm/iBoCTsDsYmwhy4JN9KPOLoZ4Ha62BQFTwtaasRnBwcPA5mbJ2UsHX6qNa9MGWGBLgel4IM3X5/4bE3ttjEAC1K6AiANeEEBcjmI5P/qcESYUqHZglZbg572K1TZkPB8euZ/9jrRWl6/+lyyRU8bMM4ZnLFxfEUIm11zQCkLwLYAkCoGYB3JBv5nyAtcTMBtC9Ao3jA4zZNKhk9j/VXaxqv59B/kcmARwM/rNUB6bmoSqdPOVgjNXwZ2BZDAa6MFPh63mJuWonwIOhCQDQ1SBUK+A1dHsTwyFhNHCPBE6ZAVC6C6BvD2tNH8DYNazJKgccHByDnNNDeKj6vtcxBwCl/G1fZm8zADYOLIXE2mtYtQAucz/EvgTgnnwT14UQbQDUAbsRMGUHwZIMrwYwl/QBzHEBYRWAg2Mex/zU1p4x6/81MCJ2Zz8b6MNHFiTWXpfYm90B0JcAtFhNA1Rv5gTcZQCnIuD5Z13SSq4a0JcRpr72XHwAJZk9kwAOjuGOdZrIWlBqHRuj/l9rAFCO+78JYOhJA3PvItMAWIIAHGA1DOiqEOIhxz/dOLJ5c1Tw2ocpZZGYWkuqXFPCBzDWCTbkLGtWAjg4duMYn0ryVEo9KLne59T/FYS5Xtc1Jh9w752jDIBXsZoBkLUHQAkCoIyAqhPgKoCLxj/WwD8dEK5/FOHOAPT4wnIPlBpEYCrtgLmmHgZtDo5pgHapc3qo9zinvVRSlQCKUAMax302NUBdFAFQHQDZBsC+BABY7QnwPYBvhRD78E8xOrpIZ2RoBGKNUcCp0wD77guQI2lNqQzAGRIHx24f20N5p2qumTlrfclRwM7WP4mFUbgpMfZbibnZewCUIgCqE+CGfFOHABaWbL9xMJyYDYJ85sCm4IE8hpO1xAlZs5Y1CtlgEsDB4F/tnBvbOzUEORlSyW08GX8KvsVg5kJi7LdYtQAu+3x4fQmAmgioWgGvAjgXkDOi5x0DTh/AUAcyFTrgYg/iKZGM1PfAoM3Bwedj7Fo5VOv0GLsAKuiK7QQwa/+usvn9EmOvYdUB0Pb5EEsRgFvoXInfCCEeQXggUBOhCtT0AVDlgy10kE+lDDBZ0GYVgIOz/0mQiaHl/5w1dewhQK7fj9ndL5T9Hz0uhHgUwDcSa3vtAVCKAEBKEqoT4AqA+xDf/hcC/hgfAKGOCbCko7VG338JNpvynMGzDiYBHAz+kzqHh5D/qdB7Lb1WxuJObP3fthVwE0iU75MYqzoADvt+2KUIgDICXtUGAjWBf3yhyfuu9gc4WJHvQJlrO2CtMsCsQZRJAAcfu7OMsXZQHbr9L5Tx2zDs6D6JgYsIvGyEEAKrDoDeBsBSBEANBPoenTnhOoCzcLcxNEgzTLj2BShxO/fAKbGz1VBlAGR8JpNRAXgh5WDwr5b991kvS6xXtdfKEolc1kwAC2bFbpTnwsuz6Gr/ygDYawBQDQJwU765y7JWkeIDsMkdLpnFJ7WUUAJyDIE5LS6lF4QpOHK3QnXg4JhJRj3W3x9K/s8lD33X9tQEMIRToam4rjK5Wf+/PDkCsFgs1EAgZQS8jM6tGOMDIE8PpO/+vpJSSYNIjex86DJAaRVgLhkVB8c2HatU4Pyutd5MUf7PeV0TO83pftFYZtkl0JcQn5XYqgyAhxJ7R1cAgK4X8TakEVDWKkLzAGx1D8A9Icl1EKVKWzlKQO4BM0Q3ABU6oCefsTAJ4NhR8B/7fB1iYFoJNaCvepGqClDg/dom3pqPLRDXAQCsDIC3UaD+X5oA3MGqE+BbIcR5+OcBwMJ6FpFsyfUFxG4aVEoqSmW2NRyuQ7D23L/DJICDwX8Y8C+Z/Y+xLpVaR0us6T4SECQNmqoNCwlYwK6GO8sCQogL6KT/K1gZAJclDq5SBEAfCPQtgK8BPGqoAD6DwyIgm7iklpSDoaRENEQZoLbENVsgZRLAwcdkVZKxy2tjzGfhmv5n/o5vH5wF/MY/ffrfIxJTVf2/d/9/UQKg+QAUAbgkhLgPcf39jRyY5KqHmB90E/mF1yoDUCITrTXnOvf9lzrpRlMBmARw7Aj400DnaI21peTs/1jllXq8/77yfwizYjDPNf9f1f+vSYw9KFH/L6kAAF1NQvkAvpEsZc+iAiy0nwtslgGc85MjywCpjHZKI4Fr/40h2n6YBHAw+I8P/kOtAVNdE2uNAPa+lsPUbiMJuvq98GT/C5lcKwNgsfp/aQJg+gC+FkI8hHAnQKgd0GRWsMguvrbBUqa/nIMv9+Qc6iBHpUVmGxZgDo45H3tDZf9TXhP7qhp9MMdq5oO7LGA1zAshHgbwFVYGwGL1/9IEQMis/wa6aUVfAXgMcR6AUDsg4PcHxB4MOeyvxIFXU/7faRWAg2MLY47Zf+r7G2K79BLrfar8b3P++/xtrqxf3X4MXf3/Klb1f1HqCypGAIx5AN9KBWAP4YFANlckHL9Xqhug71CgGkw35+Tpo1yMnVGwCsDB2f+8z9WpzkfpO/ynpPy/0fUmx//6OgL09r99SQCuSWwtVv8vrQAAq3kAaiDQt7CPBbY5/9V7WcDfFRAjyeQyvJJEIJXpjjnusnRmwSSAg8G/LnAPuTPolMx/JYE/lQTkyv96rd+mDCwcGHmfxFC9/r8secCVJgAtVhsDfQPgCyHEc3AbHWwGQZdMAqSXAajHF55zItQY95tDOvqc4LVfY44LMwfHlI6xqZ3LQ5r/+kj9OSQg5vGQ/G8bgOfCwKP7hRDPAvhCYqmq/7clP9SiBGCxWLToygBHPgAhxAnEmfway+6AfcsAiPyic4E+lfGOKXkh8fOgkRYwJgEcuwj+Q752re3ShzL/1VqncxJJl/yv4xewvvufEwOxPgDoNDovnar/H0iMnawCAHQtCkc+APnGTyDCCIjN1oiYMkDMUKDSGwP5Hh/b+FJykcktdzAJ4GDwr3MubMsa0WcNLNH7X2Lr38bynMaR9S8QNgDql5MSO9UAoFso2P5XkwDo7YCXAHwmywCuPkdTBkktA4SAeMhdAUsw3ympAKUWISYBHAz+w4N/33N+G9fAHBLgIx6x8j8QVwZX5r+nAHyOrv5fvP2vGgGQEsVddD6AK+h8AGfhngWwVh6ILAM0jv2WCfksb2pmwCkz/KEWNSYBHAz+w5yHU8r+hzT/pWLEhhlQYpFL0gc23f/BuThy/v+XWM3/v1da/q+lAEBKFWos8Ffy+qkA60kpA5hfTINysn/qgT+EGXBuKsCuLOAcDP5TiCln/6X+txLmv9jfjbndOPDU5f5P6f8/gc7x/5XE0JvovHXFoxYBUGWAa4grA5hdArYyQGMB/wb2OkxKGWDK+wLUOtl2RgVgEsAx0WNmV7L/GmvfkPP/fS1/ZvIKT2a/gL/0bZP/L0kMLe7+r0oApFRxD6t2wM8tZQDXKERdKnG1WeiSilOagb0kMLeBQHNWAZgEcDD41z0vOPvvt36H8MFLUCxYZcWsFPkfwAOSAHwjMfTurAiADLU74FV0tYzvAZyBvxOgweZQoFBtJTQTYGwzYOr7mZsKwCSAg8F/WuA/9ew/93mlgL+v+c+nRptY5drwzin/CyHuScy8KjH0sOT0v6EIgCoDXJdSxqdysMEC4RKArQyg3+fL+lMz/b4HEEXeV2tBGFIFqLnt6GAkgIkAx0jHxRDnCWWe20Nm/9TzuZTwXkua/2ABfRODbOr2Av55/7r8/wyAT7Ea/1t8+t8gBEDrBlDbA38uhDgD/+6AR3URzVmpP8/8kEkrAwBhT0AKUahZDijBiIdQAWikha86QDMJ4Bj4WBjrHBhi7HeN7H+I1r8UbNjAF61jzTvYxzP730YAzmO9/a+a/F9bAQBWQ4FUGeA7dPONbeAe/HDgniRIkcBfa0+AUipAjS2J+6gAfTMPJgEcDP7DEfO+iiNn/2HcAMI1fxPHFpYk14Zzp9GVyr/ASv4/qCX/D0EA1OZA19CVAT4x9gZY+OQQjTmZmb/+xbhmAvgOtj7O/9IqQOzBP8R+2DG3mQRwMPjPA/xLt0JPea1LXatzfQCu3n9gs3Xd1fvf2DBQCPE8gE+wav+7XTP7r04AjG6Ay+jaAU8irh2QHOqA+YHqX45ZLkglA333hc5hu6knS+kTpDRwMwngYPAfFvxLv9ZQ2X/uPiolzNgpoO9KQAF3R5vN/OdMdtHJ/6fQ1f8vS8y8u1gsljUP0GaAk0ANBboipY3LQoiLcHcB2AgBwe4ZANwDgkKTAWPr/6kHXJ95ALWZ8RClgDEWRiYBHNsO/qh4vg7hdyqR/ffp/+/zGFlA32b+M3HK2+9vgP9FyB10JVbeQIXZ/2MQAH0o0FcAPgbwbOADOfrAImYCuMyAMV9kH/BPPShL1cdSyUDuIjKVrgAmARzbDv59XpMKnct93kdppTM1+y89+Icc2X93Y9385/QByOfFkoBnJTZ+iU7+rzL7f3ACYCkDfCqEaAEcg9sHEJJOTJnFNiMghuH1Bf8aKsBY87H7nvypi+AQ082yAIKJwHYC/4TBv2bdP+Vvj7XvydSm/9lwA7Ab0G3mdVcp21X/PyYx8ROs5P8qs//HUACAVRngqpQ4PpaGh5APYAFgYWwQpH/ogHsyoEkGbCpAqa6AsfYGKDEfO9X3MMTCNioJYDWAs/4ZgH/u65dcH6awtpV0/duy/zVfmUWRhi0Z1bL/BfzzbhqJhR9j3f1/OMQBOxQBWKLrZ7yGbsDBx7Lf0VcCcDGrkBkwtiUwlvnNZW+AXIacsmD0yQaYBHAw+Jc7N0pL/9u4tvVpA/e1/gF+859NqfaZ/85LAqCG/9zFAPL/YARA9jEeoDM2XAbwmWQ65xGujbjMgNYBQUZLIDm+6L5y0ZgqwNCGwNzFZ+zFk0kAg/+cwb/2+TfFfU/GyP59oL+GIUbrHzx4tAiBvnY5L7HwM4mNN1C5938MBQBS0riNzuDwJYCPhBA/CDAk0wy4gLsOswgwNlMZKD0hcCimPPSCkPJatQaUjE4CmAjMC/i3APz7GmxrbX+eoibMIfu3Sf6uDrOjnBZu/9nC6P1f+JJaiYEfYWX+u42B5P9BCYAxGlgNBQKAfdjNgAsXGbB8kPoXZvoDMLIKkDKTYEwVIGUByVmgZk0CWA3grH/C4F9D+h8q+w/13o+Z/es/GwfWeBNXbG7/q1/2JQZ+IjHxO3S9/+1QB3Az8PmpzwT4HKvJgL6NElwtgc5xwBYz4BAqQJ8SQGmmnCvzpWb+NODiNxkSwERg57P+McA/9/k1dz0dey2rnf2b5j/APwa48WT/NvPfcxL8P5eYOJj5bywC0GI1E0CVAS7AXjNZBJiUa/KSbXOG2ipAyuPIODFKy2a5C0qJqWCzJwGsBux01j8W+A917qWAbsraVIIM1Gj/C2X/tmTThT+u7N+qAEjs+0hi4TWJje2QB/KgBEBKGwfQZgKgq3ucQ9x4YGXys33wa6xtYBUglRyMNRRoyG2CmQRwMPiPC/5jS/81N/4ZOvs3DYAbiaixg23ICHhOYp8++vdgSPl/DAUAWG0Q9K2UPt4XQrxoYUm+D3JhYWEL+Ccz9VEBYp6XI5tNdShQqVbAsUgAlwR2APhHkPznDP61tjovuZaVLAHkrOsx2b9NbY7FLF3+fxHA+xIDlflvOfR5NDgBkJsb3AVwHauZAIcATiC+JdBlulgrARRUAWzPI5QtAeQuLCWHAuUuKCUXt1JZPasBnPWPmfXXAv+a52oqERjCzByztsYMdyuV/dv6/UP4ZD5+UmKe6v2/jgE2/pmKAgCszIDfSAnkfSHEy3CPBl67T5NaFsaXt/B8mSVUgJzrtdoCkXHS9M38cxeznSEBTAS27vOdGviXJPKlWppLrGElHf+ls38YwG/eNjHJh18LiXXvS+z7BiOY/8YmAPoGQV8C+FAIcQLAnocE2IyApuNyw6iRoAI0I4A/Ff6JjBM6RSmotUFQ7GI5eV8AqwFb9ZnWLEGVAv/Sdf++u/6VLgHUIAFNYvZvxRbE7fpn4tmexLoPsW7+W45xXo1CAIzJgJekFPKxbItYwD0X4Oi6ZdayzQuQogLUUARSTqC+i9QQJ8/YJKD2osxqAGf9Q5DNocC/dhIz9ammOaOAQ9n/Ru3fsuufq+9/ITHuY3m5hIEn/2UTgMViYb30VAFMM+DFAHvy7RWwKKQClCYCsSfQ1CYD5i40UyUBo6kBTARm9bnVPrbGAP8S/0utyX9DJDDkWONzs38X9oRwqxFCPISC5r++uDxWCUDfJlhNBvwIwJdCiCd8mb+hAtg2X3C1bPhUgNJjgUvX//s8v8YULSYBGYDGsD75z2pbwb/mQLOhev9LKpcEv6HQ3GDOhS8mBnlxS2LbFxLr1OS/e0O3/k2CAGgqwC10U5A+A/CeEOIp+OX/JvI+nbGF5gKQRwVIIQKEOmOB+xhwSslp20YCWA3grL/E8bBN4B+bOAyxVuWsr7bn+NZ2X/ZPCLf7xdyny/9PAXhPYt0ViX3LMc+5UQmAbHu4h64N4it0xojLshTgIgF7hVSABuO1AtbeJKjmeOCS8mFokahpDhxVDWAiMJnPozaJJNQD/5T/bQpjf0tn/aVaAJtC2f+eB/wfRDfw50OJdddl9r+7BEDGoWRC36Cbi/wegOcQUU+JVAEaiwrQeE5SCqgApWS1nPp/aVmt70KSslDltjVtpRrARGASwD9m1l8C/Gvt+FeiXDk1979P9T3CBCP7bzKzf9tjz0tsU61/tzBS69/UCIDaJfA6uraI94UQ36HbJ3nh+FBjVQDXlo1wqAA12gJLDtXoO0I49wTbdhIwmhqwi0RgAv/vEMfJlMC/5kZmKf/3UJ6lUNufLfu3qQG2MkBM9m+qAOclpr2PzgNwXWJeO/a5ODoB0FoCb2oqwN+EEC8hfptg130b5QCN4dXaFTBVwhpLXkuVClMWoLFJwOzUgF0gAhMB/iGOjTHBv8+5PWaZsu9amrpub6gDBjaYiWRO9q/k/5cA/E1imxr8M1rr39QUAKUCqMFAX0gV4HuLCrCIUAGs8j/8/Z012gJzwH+o4RolJLVaJIAGWoQnqQboQLkNZGBC/8tQWX/u8TvFmf9Dr02lSEBy2x/Cu/7pff+p2f/3WvZ/DSPs+jdpAiCZ0CG6oQiX0bVJvONQAWxqQKg301QBGov8k8Mea+4NkFvDKzFfe0wSUFoNGCNb3GkyMLH3TJi25D8V8J/L2hSz/sau5+a+MWb2H4MtzqE/Wvb/jsS0yxLjDqeQ/U9JAQBW44G/lUzp74YXwDdhKUYFaBC/UVBpQ2Bse2AfuQ2JJ9VUSMCUSwKTIQJTJwMTfG9Dfu99JP+pgn/J2f+xZCAnmfJdD63rettfCDNc2b8Pm1Tt/+8S077FiGN/J00ADBXgkqYCvOhRAVwfvGsus276C7UFljIEjlkKiD0ZYwF16BHBMQvrEGpAid+vBrhjgO7Yf38i3zWhbL2/NPjnnMdjrEk16v8xxj9yYIJv/5lFKOvXsv8Xtez/0tSyf6CrXUxqXcPKC/A5usFALwJ4kIguaR9uK38usarBCMnQhBBiqX1Brby+8ZO6J6svfmkcSEK7LSyPxV43X8f2uOt5JX6aJ7br78U8joTHEHgPSLgfjsdCv5vzWjV/vyogB8h10decaAxJ8kpL/jXAv09CkpPp1yYBJcsAZttfYyR75kY/DRE5PWgW8H9QZv/vSSy7NrXsf3IEYLFYiOVyaaoAfxVC/CMRXbEQAHV9aXms1b4cIa8LrLZzbLX7Wu1LbzUQVLdhXA8BaAqwxoB/zkImMv4uMoA+hgQgghiEgDz0WaR8VltLBLYEyKcM/EOBf63Wv1Ry0Hfjn1QSUEr6hwHowGa518z+Xa7+0LhfV+3//8NEa/+2D2qyKgCAS0KIRzwf+B7W92b2jRK2DQcK1fpzpk2lnFy5bLnGVMCh5gKk1D9TFt6SWRwK/z2O8sA/dNZfw+w3FPiX6PsfcsOfPq1/FCAIFDn0R9X+zdLyngf8H5EJ7LtYbfozuex/kgTA0RHwVyHEs3C3XGxcDEOgq7/TNwgiR2pK6QoYSn5LkdtSTrYSJCBlUUxdhIcyCeqvwWRgONAfkrz1PeaowPlQCvxjEwAqvAb1SYhy5//bjH8xY37X+v8NLAleJFb9derZ/1QVAF0FUFsFvwvgcyHE0wEVwKYKuNo2jr5ojd3FGgLRU7IqNRp46NnbQ5KAvmpAjeyOVYF5Z/u1VKKas/5Lg//QCUjprD92sx+v8U+u+RsT/mAf8rMXwJqj+yVGfT6H7H+yBEBTAW5qKsBfhBCPAdhHXFdAYxkRbF4I8RMC+5QCSuwSGFuLG3r2to+Np0qcY6sBNYCGycA0PsMax0Fu1p9bIhsK/GutPaWz/tR12rbWkwcfFsbMmBDu7EuM+ouW/d+cavY/ZQUAWO0RcE1jVO8JIV6NlGL2YDdx2EweQP9SQI1dAvsO5xhq9nbu7aHVgDGIAJOBcT+rWt97yaw/9VwpDf4115VcEhCT9feR/hGBCw0Sys4Sm97Tsv9rmMjM/9kRAEMF+AYrL8BxAKc8X4w5Iths12jg2DRIMkNzHGRsKaCPGkDoV8svOXs753/KvZ1yXwk1YGwiYAIcE4J6n0dN4M89PnPuKz0ACAnrCnquPb7nlOwACM1v0aV/3yY/TQA7fHhzSmKTqv1/M/Xsf+oKAOReyfew2inwPXSlgNciWJmtTzNUClgYEwL79p3GHNhDGnFKsPEhSUCuGlCTCJQG7V0jBDTQZzkk8OeQ1jHAf05rTknzn2/in2vwTwj4zdr/a+ik//ckVl0HcE9i2GSjmcGCcYhu7+QrAD6WKsBNIcTFCFa20JhcaFqgTf4vUQqYwolY2wyYIlH6ZM8xNg7KAaKaYL1thGCI/6fmd1hjg5/Uen/K+TY181+pxKOP9O/rALBO+dOy/0UIZ4QQF4UQN2X2/7HEqlsSuyYdkycAi8WiRbdd8HcAvkI3V/lPctDCXiRDM79QX+1HN4mULgUQxgP/McyAJSTQPmpAzOMlQKQmUJPjMnWgH/JzqUUWxpjx3+e8mlr3Uc4al7L+xEr/tp5/Z83fkTC6ksw9iUV/ktj0lcSqA4ldTAAKxBLAbQBXAXyKbm/lz2TLRUwpYM/C9mwSkF4KaCyLjOt6rEoQc8CX6AiYEgnI7QgoseDmLvo5wDU0OFPkZS5/p/ZnXPIYoMxjNAboc5W1ocC/5FqTuj7aNmmLkf6tG8IZ676pDu8hTvp/GsBnEpM+lRh1GxNt+5slAdBUADUi+AMAbwshHsV6W6B3FoBjlrPNGGiyyFApIFaWK9kOiEJSXikS0NcXkFISmDIRGIMMlADwqSoNVOi7GAL4a0n+Kedb7ba/2mtNat3fRQ5C0n9jwwINI2JmAKi2v7clJqkNf2aR/c9JAQDsbYHvCiF+bEoyni+tcbBBmyxkKwW42kly/QBDdATEnKB9TswhtgxOlWdrEIESZIBd/8N8XoThgL9v1p8D/qHzr5TxuFSWn7LWxSoANvXV7O4ye/5tsr+p+jaeZHKt5Cyx52+YUdvfbAmAYzjQn+VufmcRZwjcw+aEwAbhQRC+vQL6+AFyT8wpGXJSF6iYkkAJNSDl8aHIABOCep9Hje8y9/Hc4zqlBBDKiue4xvSp+9sG/kSt8Ua2v4cI4x+AsxJ7ZjP0Z+4KgGoLvIuuxeILdC0Xbwshfgi3IXDPI/OklAKsDDPASEvtDTC3dsChWwNLZPxTmBi47aSgpkeh5PNLAn+JrL/U+TjH9r/Yun9IoY2V/hcxWCIx522Z/X8hMenu1Nv+Zk0AZChDoGoL/IsQ4pLFEOgrBZi1HvKwQ9emETX8ACH5f2qtOaVJwBRaA6cwH2AbSEHN/6FWv/8UW/7mBv45a1ipur/Ns+VSAHTpP7SnzBquCCGeFkJcltm/avubjfFv1gRAmivuYb0t8I/SEHgi9ku0PMc1KbAhosayV4Dv+pQVgKFJQK2ugNpmwD4gM8TAoLEJwpDvhwp8HzWBv0/WX6oEgC1ZW3Lq/mvXZTSO5M7W9x+DD+r2Cbnd7x+xavv7Ht3Qn9nU/uesACgVQO0W+CmAd6QS8GP4zYBrYG/s8ewrBZDnoMv1A6QqACVP0CFIwDZ1BfTtOR8CoGngy5D/T63f21bX/5jgn9r2F6MAhNZVWzLmGvNrmsIXDlJgNf9JjHkH3dCfTyUGzTL7ny0BsBgCVVvgbSHEQwEGt/aYHPqwh7iuAFvJINcPgAInztAkIIUQpDyWqgbMsT1wl2r9Y3w2U2z3yz3O+/T+T2XYWCkFILbub27zG3L97xnD4TZc/uZjQoiHhBC30Q39+RAzNf5tgwJgMwS+C+A/hBAvYHM2QOiLtu0XkNIamOMHmIICkLpApEp1MZlMjV0D+xKBmqqAD/h2FfCHUAn6uv5L1f+RcD7kzL1H5rk9RQUgpe4f0/Lnm/MfKgPsS2z5A2Zu/NsKAiBDnxD4MTpTxrtCiJ8YX6KN2ZkqgNkG4pweZWkNDPkBaMIKQJ9JXSnXa+wV0GfBLmkGrAFo20AMSv8/Oa/T97tOfWzsWf9TSChKKACEtLq/0+RngP0e1qV/Fy6sYYfElHcB/BkzN/5tDQHQDIHfA/gawPvoDIGHAB5EoBMA67WgPYcC4OoWSPEDxDL3MU9YZN4Xe72EGpCSbZUmAmOaAac6rW/oMcSllYHSx83Y4377nNNTUQBS1lJbmx85Mn1z1r8PD0wV4EGJKX+UGPM1Zmz802NvC+REfULgZ+gMGg+3bfufmqa5Jh9XX6aQF9f11ri90K6rCwAIIlrIQRCtdiDqB4OQ97fGdXXACu1gFtp95uOu59X6iYj7fO/Zdh2Ox2JvI+I+3/2xj7keT31eCARK1wu3pXxAA/xun82jxpr4lwv4Y4F/SiLjA3yXmhpb97cpAKYa4Lu+ALDftu0rAP4fiS2fYYYT/7aWACwWC7FcLtWWwd+gm8p0AcAjQojXiOj3GggrkG+N6y2AloiE6FB94SEB0H63MYBNB3wbYNoeD4Hr0CQAEcTA955t12NIge/2UERgCDIwBCHYBcAvCfpTAv6+StvYtf/UOQC+rJ4cj9s8WCHwd23z65oWq4x/r6GT/t+W2PKNxJrZGv+2TQHAYrFol8vlgZRl1GyAB4UQDwO4SERfOzL8PTPD10iAsCzQQgNydJ4TCCFEq4G7rga4ABUBwI0B4SEVgJysv5QagMBzahGB2OeUAHQXwGwLMaCRXmdI4I8B9tJZf062P4QCkEIMYh83Jf5Q3d/l9bIBvVUFEEIo6f8/sN7zfzB36X+rCIAM1RVwDatSwENCiH8hom8t4G8qAa4yQKOpAAtjcSaLCiAMNcC8DstrAPMrA5QC/tjsv0/m31f+T832S2X4PnCaGjmgibwmFXhOra1+hyACc5X/Y01/tuw/WPfHSv7fw6pLzEcIjgkhXgLw37Ep/c/a+LeVBMBRCngAwEUhxJtE9G8ekN/T7yciyKxeVwf0UsCaKiD9AEu4ywAIkABgfmWAWFUAyFcDYohBDhEooQqkgHANyX8b2wap8u9R4cdygD8X7FPAPSc7rw3+oXY/E/xt7/UI8C11f5+BWw1987n910iAEOJNdF1lf8IWSv/bqAC4SgEXZCngcSL6zKEEbFw3SgE2EtBoi7kp/YdMgTGAHwvCUykDpNT/Xdl9bvY/BVNgH0IwxYx+yooBFXzuttX+5yr/wwB/p+kP9ro/ecB/Lwb0sZr297gQ4jt0Pf9bKf1vJQGQJOBwuVzewaoUcFaSgP9CRFdhl/sVWO9r10N+gPUzt1MNYGT8PhIgLKrAtpcBck2AczAF5qoDIbCZOzEYo/4/Z9PfnME/R/5Pcfwf7c0C91z/UN1/H/6e/5NCiCcA/Fd0436V9H9nsVgcbhtebh0BkLGUco3aMfAcOlPg60T0r4goA2jgvactxLa2QAXkQh6YyhSoH9RmSSCnPXAKZYDQfS4SE6MMxN4uQQRSH+tLBvoAeQjMxiYINJHXrQX6fYC/FNiHAHUqJCBV/neBv36fPunPtjeLbda/PtTNHPC24fTHuvHvDXSmv7exGvhzC1tU9996AqCVAm6gG9rwdwDnhBAPAniViN6GuwygX/Y1P8Ae1k2BsCgDZmugqzMgpj1wbBLQVwGILQPUzP5rlgBSAb6W7L8NXgCq+DulSwAs/5fZAtjX7rfh+Id/xK85xl2f9rcfAn2spv29KoT4DCvp/2uJIVsn/ZsMbBtJgLlXwN8A/E4IsY/NKYG+mpA5KthlNtFHBS9gH/8bOuhLnFhjSXmpI4FztkLtOzLYN0UwNPVtiImB274nQN//t9R3kXM85N6Xcjt1BHDKxj9TBX/zeVbHv7YRm2/tXWjgn9TyJxXifQC/Q+f634pZ/zupAGhxiNVeAZ8AuA/AA23b/kvTNN9h09WvS/9rw380P8CeJYsTBplydQboqoDZHjglJSDl7wHlhwINUQYYakhQTqY/p/a/morEnOv+KVn+EApALSWgL/jb2v1Cjn9bz/+eAf57AeDfl5c9AMfbtn0ZXcvf2xIrrmILZv3vNAGQrYEH6LZsvATgJDpT4HkhxFtE9Gu4/QAmEVCTAs1FWDgWaVtngA3cTRKQ2hVQgwQg4TEfIQhdTyEFuaA/RT9AHyBPAcVSZGEIVWKMnv+p1f1LAP6UwT9W9Qw5/lNMf8Gxv0KIt9C1+/0R3dbylyRmbK30vysKgPIDqA2DvgLwHoD7hRAPAHiZiP4Cy0RAedk3snwT+Bc+MqB1BgD29kAbCTAX8DFIQE0FYKwRwTGAP5QfoDQpGBO4h3xftev+2zLydwgSkNoO6Bv042z3szj+bQSggbuM6/IAqLr/S0KIL9FJ/+9i1fJ3b9vBfycIgIwlAL018LQkAf8HgIe1UcG2jX/WygES1M0OAJ0QAMa44EB7YMygoNgZACVIQCrJ6KMATK0tMHZQUAisS84HqEUOpgryua8x137/WoBfkwSkPic06CfU7ufq89dNf/qwHxcZWCMGQoiHhBAnAPwPGC1/2HLpf6cIgCwFmK2BZyQJ+N+I6Du4OwE2ygGW+QCHsHcGAPb2wNCMgNiRwS6QRiYZCP0OkFYScJEDnxrgUgdct0sQgb5kIJUQ5AB6LBjWJgrc9leODJRo+8sF+BqgnwL+zl5/Dfhtk/6sWb8E/5hhP3q//w/Q7fK30fK3TdP+WAHAUSngECs/wAlFAtq2fbNpmn91EABbOQAWJUC4CIAE9UZrKexLAmIBPua5qa+XqwDkGgG3tQywy22BU2z7m/JWv6UVgJqdA0OBvznpL1b230O3xe9bAH6Pruf/fazq/oe7IP3vHAGQJGAp/QDfAfgSnSlQKQFvENFvPQQA5k9NCYBFCbAu6gYJMDsEYklAjuTvyxhr7Q+QowDschmASwB5v79t4377KgBTmP4XC/5kAf9Qr7+e+ZvO/r3QRQjxOjqzn63uvxPS/04SAEkCDpfLpZoP8JkkAfcJIe4H8CwRfeABf1MNcHUG6M/fk8TAtpjrg4KGIAFDmwJTFICh/AA5qkAMGUglBKmgvs0lgLlt89sX+Ptk/alAP3Xw9w362YO93c/m+Dd3+Nt3gP+zQoibAP4dXd3/c2zxqF8mAPZYYn0+wCkAZ4QQ/zsRXUdXC4rtCBCG299ciJcwBi7J5y9nQgKA/JJA7vWaRCA3+59LGWDqJYCx5P+hdvvbZud/NfC3DPpRj9na/fY08N+HZ2MfrJcFLgghzgP4v7Be99/6fn8mAOsqQKvNB7gM4JhUAs60bfvPTdP8HusufeECf40Y6I8vQgt5JRKAwmSgD/iP1RFgA+rS2X8s0JcuAwyR4U9BIRhrzG8s6I9JBKY8AKgk+C9gb/czs3xbrX/fcv1k27YvoXP8q37/y9iRfn8mAG4S8D26mc/HpRJwum3bnzZN82vEzQLQVYDYxVmoA78wCRizHbDvTIChFIAxOwKG7AaYi6IwhWl/cwD+PuA/RPtfH/A3SwCuQT97MvsPgb4+6W+BbtLfWwB+g8749x52YM4/E4AwCdBNgV9IEnASwGk5KfDfHCoALKoADBIgIj7ftjIJSFUEYn8HEcQgVQ1IIQI+QJ9bR0Bt499USgLb5vwPPadkF0BJEpD63Njn9AH/0Na+eq//ngXovQqAnPT3VwC/RbcnzBdyzb+7a6Y/JgCbYQ4JOgbghBDiFIDXiej3iDME6jMC1G3bfABRkAQQ7OOFYzsA0IMojLlHQAzIlygFhIC8ZikgBtimUg6ggV9nStJ/abDvC/h9s/zUn7bZ/rng73P928b8Bo1/QojXhRCfA/g3AH/BDg77YQLgVwGEnA9wG50hZE8jAf8ZwItE9DfY6/7WBdhSDtCnBNq6AnJJgG8DoRpmQESCf592QF9GnwLycysF5AD6XHcO3IahP6UUgNz2vyl0AJQG/z0LCbBl/vsWsN83SYAQ4kUhxA0A/4rVJj/K9He4K8N+mADEk4BbAL6Rn8txSQL+C4AniOizQCZvzgiAxxJQiwSkZvJjmAFLtAOmKAAlSgE5ZCAn8+c5AOWUgKFc/ynAP0bWXxr0bWBfGvzXJvkR0b4ny7cZ/vaFEE/IMb//Fd1GPx/Jtf0Wgz8TABsJUKbAG+imQikl4KQcF6zGCNuMgNZJgFo5oDQJgIcElO4I6Av+Nc2AMQrAlLsCUsB9jF0AaysOpZWAMV3/JRSAucz+923pWxr8XfV+n/R/XgjxGID/BuAPWE3623nTHxOAeBLwtUYCTrRt+09N0/xBPmaCv3PxNTYDEoVIgGsrYdfWwrFkwAUmtScD5poBU26nKgAlSwEhUC5VEqgF1GMQh5oqwBT6/lMAvQbQp4L+UOBvjvjdD2T++s99dK3cLwP4f9FN+mPHPxOAJBKwlJMCv9MOvOOSBPxCaw8EPFsBW0iA6KkE6LsTmiRAfz/m7RQzYGlVABnkoBYR6KMA1Mr++5QEamb6QxOOIcb+TmXkb4msv2a27yMEPue/a1e/VPDf84B/iAQcb9v2TQC/Ruf4V2N+2fHPBCCLBFzXDsBj8gD7udw4CIj0A8A9LdBHAoQxLEhn360FIM2OgFRQrw3+ufL/1OYCpHYGpJKCFGCfmwlwG81/Y438HZIEmEDfOJ7jAn99ql8M+Lum+7nq/gsAx9q2/Rm6zX3+HcA76Nr9rjP4MwHICdUe+K12IB4DcFwI8TM5IyApA0skAa36HQCtYxdBgt8MWIoE+F7L9VhMtj/kgKBcVaCEArBLZsDa5r/SPoAxN/3pSwiGqv/HmP9cu/rZLjHg75P6zU6AfSHEz9D1+P8bup5/bvdjAtBLBVCdAXfQ7RnQaAfbPoCfGYOC9AXaWRqIIAFLD3kwSYCvI8BUBmLJABIIRK4aANQZEFRKAajdCjiUGXCqysIUpv/tsvQfA/o+sDfr/xQJ/tbtfD3g79re1xz083MhxEdYb/e7Cm73YwJQiATckgfU0UErhNgD8JbcQti1yIoIJSB6MbSQAPM6RZKCEEhMsRTQlwjkKgA+QM8F+9SSQKnMf8iSwVw2/alBBGq2/tXM9kNgXxr89Z39fBP9Fi4FQAjxlhz087/QtfupDX643Y8JQBES0EoScBPG1pRCiP8E4A0i+l3qQi3BnCwn7zKwgKmMvrUAuwgoAyIR5PuWAvqCfwkiEKsKxJCBlMdSgb6vIbAGYahJHoYw/qUAfAzIDwX8seBegwyE2v500NeJgLmLn29jnwb2Vr9gf7/+UwjxhhDiMoD/D12734foev1vSvBnxz8TgGIkQN898IgISBLwGhH9MXHhJUs5wAb+DVbegJaIGu13Wg3YXbMChAU0UzP90O8A420VTA7ikUoOYshAqgIwZvY/BYNgLRVgrq1/Y43+zQV/n9PfJAONzP518N/D5pa+Prd/Cvj/WAhxDcD/RGf84939mABUJwH3sJoDYJKAV4jor6mLl4UEmIvJEitvAEkSoP+O8hy4ZgUAmz4Agt0s6AKfvvsCDLlRUK4qEEMGSigAOfV/SviupgrsYygAc6r/jz3/PyT5O3v8NfA3s35CeGMfHfxDBsBjEvxfFkLc0cBfH/Rzj8GfCcAQJGBN5hJC/AuAF4jovcgsj3QpQAP0Q8v3YoK36g4gz6wAV4eAb2jQEPsC9NkoqC8R8D2nLxmopQCkgPoU2wKn0vpXS/bvC/ylAb8G+Fud/tjs8Tc39Gk84O8a7+vq/VeZ/wtyrfyf6Lb2ZfBnAjA4CdCnBepKwD8DeI6IPkheJdc9AYcG614aioDaWMgcGKSfrDGgn+ILiFEF+qoBuUQg9NxUcpAL+qVNgVPM/sdQAXZp5n/JrD/0nJQxv4TwgB+91z8k+6eC/74Q4jlpvv4f6Kb8/R085Y8JwAgkYCmVgO/Nk8MgAb5FhBwkIGYBa7XHW8esgNaR5TaOTDV08tTeJbAPEQgRg1RVIJYMpDyWSgpSgX2qQ4GmrADMvf5fMuv31fvXiECgx9/W4tcEwN/XAaBk/+fk5j4m+H8vM3/u9WcCMDoJ6FZsIf4J3TbC71pOLtuCQQAOJAkgqQTAyPxtaoDtdVSHwAJxvoCckkAI9OEA4xLgH5v1l2wLjLk/FvBTpf5tVQCo0POmUP+fm/s/p94P+J3+JvA3GtAvAOxJ8F9EgL4p+78ohFhI8NdH/DL4MwEYnQSofQPWFmNJAmKNgYYQYG0RPDROcPOgNzsETIk/1hcQs4/AlLsBXKBech5ADR/Ato4F5h3/plMCiJX8Ab/Zz9Xm11jAf0+Cv2/3Ph/4vyL3UVGZP8/3ZwIwWRKgO/KXkgT8mIj+FFiYNk5MKeurkoCuCJjg38DoEMC6OVA/qU2QB+JLAlMqAQwl/099ONBYKsBc6/9jjv0NAfTQWX+o7S/W7EeWzN+s9y8QHvDjavW7g25nv9+j29lPZf4M/kwAJkcC9IW4RVeX/0cAbxLR7yMXgTVCYJkauLFzoOV1WnnCunwBfUsCJdQA83EgvRUwtxzge04MGehDCFIVgDmrAFPN/nOy/dzsPxfkS2X9viw/pd7vMvuZCoA+3c8E+ejsXwjxuhDiOlZuf73mz+DPBGCyJEBYlIB/hH1ssJn9q9sH+nXNF0Aa6Ns8Aa0FzH2+gJSSQK4aAA9Ip/gDgOkMB/LdH6MC1PIBDKECTCX7LwH6fbP9MYE/hgzkSv5AuN7vm/Bngr9e9w/2/Mvxvpex3ufPhj8mALMgAcoYKDTgVSTgF3IDIZcpkBwnb2OQgEPYRwhvAI9WEgDsdX5bScCmLOSoAakqQE6m39cEWMIAGMryd1EF2ObsPwa8h8z+Y7L+PpI/YbPH35T7XU7/Pc9l3yAGx+TGPp+hG++rwF/v82fwZwIwKxKgKwH/AODnRPTvngWH4B4YZJsV4Dr5lxqIp5YE+qgBsYQgFfxzywG7qAKUVgNKkgrO/vNJQOpufiUkf1u932X2W1jMfmbG76r7HxNC/Ezu6ve/0M32/0AD/wMGfyYAcyIBAquxwToJOJBKwL8DuGs5MQ8sioC6n4w2QZc5sLW8hlkeCJUEAL9BMKXujwLgX3My4FxUgFRQH0MNqDH/nyf/lcn6AY/kT51caMr8vnq/DvZ7HvC3be2rZ//HJfj/Dd2Wvn9Ct7HPZfCEPyYAMyUB+thgRQAOARwIIe4KIX7RNM3vsBorbNb+XZtx2DoEzJq/Xg5YIwNE1MJdEtBBJmQQjFUDQkCPRPCPKQeUUgFS1YGUx0LAVlIJSCEQtUhDqdn/Q+38N7Xsv1rWD7/kb8r9Zr2/cTj9Y6T/PQBn2rZ9E53c/28A3ka3pa/a1Y/BnwnA7EnAWilAAv3dtm1/2jTNOwCuOk5y8hACpQYoP8Ch8fyl8VNXAfSSgEC8QTBGDYAHdEupAKkKQaoKEAL+kh0BQygBUwP4lN8Zsu9/jtl/k0AGfFm/z+hnSv6x4O/rANgHcL5t25cB/BrAbwD8BcAnAK6Ad/VjArBFJEBtJbxGAADcadv2l0T0BRF9hk3JHwFloLGQAGCzJGAjELb6v0sN8IG+aRocQgUA6ngBcu7LAf0pKAFDxRC7/g0J+lPJ/psEMmDN+hF2+dsk/4UB/jYCEHT9CyGeEEI8hq7H/7cA3gHwmUyGbjP4MwHYRhKglIA1EiCE+BWAU3InQUpRAYCj8cEm2C9hLwmsAbnqEjDUgAabRkCfGmA+d2wVIEQaSpOBVNAvqQT0VQSGzPhLZf41sv9SU/+GyP5zsv61pMEC/C6nv03+V/V+F/B7ZX+5o99JAP8Nq+l+XwC4JsH/kMGfCcA2kwBdCbgH4LYkAW8Q0X8EAL9xKQHoavtmlg/YSwLmYwsiWgohGqzPMohVA2CoATVVgFyFwPV4CeAvYQLs0wY45yFAUx7/O6X2vyaBDLhq/crUB6yc/TGSvzncx/y57yEBR8Y/OeDnBoD/is7pr6b7XQdwR4K/AAcTgC0kAWK5XB5KlqtIwKGmBPwSwK/krIC7HiLgJAhal0ADe0nA5QsQUk0QWG8XjOkUAOLKAjFAn5Pd9ykBDLk5UJ9WwG1oA6w5/ncK7X8xz+sD/LFkwKz129r7XJv52BQAW70/JfvfQ+f0/7kQ4hN0Tv8/omvz+xpyrj+AJYM/E4CtJwEADmWboNBIwD1JAn4uhPh50zS/lYyYPBcb46dAScBGAnRQV2oAZEkgplMgtiyQOvTHBWq1RgPXaAfMkf5jgDJmoRxSFRjaBDjV0b+lNwGKkfv7Zv0+8Dclf7PFL2bYz4m2bd9CV+dXTv+PsOrx59G+TAB2jgio0cGqJ/+IBAC42bbtW03T/BWdKcZXDjg07jvEZkmgwabs7yIBNjXA1ymQWhZIBfMh2gNDZCCkDqQQghRi4AP6bTMBzsEAOOTufylyvy3rt9X6Q07/kMs/J/s/37btK+iMfr/Fyun/DXjADxMAJgFLgW5q4JonQJKAXxDROSL6MEIFsJEBXQ0ws39dzg+pAbo3wMz0c8oCNYkAME5XQAyo11IDYhWBsTP+vll/TvY/VgmgL/AD8XJ/jax/YRj9bKDvzf6FEM8KIc4D+H/QbejzNwCfyqTmFtjpzwSAScBGm6DyBNwCcEMI8Qt0uwn+R4QKYMqBsQZBX0mgjVADdBCKKQuY5KC2ClCKDKSoAzmkIFcNmJoiMLXWv9KgX4MENA5iEJL7Y/r6KTLr9wF/SvavzH43Afxf6Mx+fwc7/ZkAcHhJgDBIwG1JAn4mhPhV0zS/wbovQDf6OVUALStoHAbBxqEG2DoFhCQTrQHmobKAjQiICipAjCrgu50C8jHAn6sE9FEDSigEU5v/v60lAB/Iu+4Lmfxis36C3ejXRIC+zfmv6v0/RWfw+3d09X7d7MdOfyYAHBYSIAAcyJKAbgy8ha5Wdr1t27eI6D0iumQBf1MFaDQwVwvDUmYISzkKuNGAGwbw60RAwNhDQDMJ6nMDSCMxZlkAnvtKEYG+qkCsEjDURMBSZsCaCsE2TAAca/pfau3fbAHWs/6F9hxfb78t69dr/Wb2v2+53zrpTwjxkBDiBXRy/++wqvdfxmor30Ne7ZkAcLiJgOoQMJWA7wFcF0L8DMAFIvob1o1/LmOgvigcqoVB8wboRKI1wN+8vQbYmhpggn5j/FsxpsAcIgAPeJeq/5cqAfQ1A7rAfopmwF2a/pfT/58D/ECcyY+wuXOfjQToWb9L8rcpANa6vxDiJSHECQD/Hd1c/3exmuynxvqy2Y8JAEcECVgabYIHkgTcAHBNCPFTdDsK/lYSBBf4bwA/1ocHNQAONTVA30K4wWY5QCcFtrKAaQ50+QNSiEAIEKcwD6BGCaAE2E9pDkCtrH8o0O+T7acCf0qdH3DL/eZWvmbWb4K9DehDE/+OCyHeFEJ8BeB/oJP830c33OcaeKwvEwCOLBKgfAFmi+ANqQS8IYT4maUk4GsR1EsDuhqwkPsJ6KDvmhfgKwu0xi6DOhHQ/QEpRGBsM2Bq1l9qHkAJ+X8odYAqPL/GwJ+xSEAq8AOWOj/Wa/2AW+4P7uTnqfXHZv665P8ndLL/X9Ht5HcJ68N9GPyZAHDkkAAArVYSOJAk4Ca60ZlXhRBvAXiMiP6YqQSY3gD9+WYpQL8sjcVKdQu49hUw/QG1icAYSkDK/TmgX9IUOBVCULLlbwjQHwr4bXV+E+j17H8B/2Y+C03184F9VOYvhHhNnuP/Hd1Uv/fQSf5qJz+u9zMB4ChEBA4Nc+BdjQRckRLcPzRN8wd0XgGbEuAaCGKqAY02N0BXDGxKgLmj4NF9Dn9ATSKAQNaeszlQDSUgBfT7Av5USgBDjwAee/OfWsAfGujj3cVPSv6xWb+rA+C+tm1/AuBDrIx+H2Il+d9C5/Lnej8TAI6CJED5Am5gfVbAd+iMNj9p2/YNIvqGiD6wKAFNpBrQqkVHDv9ZGkRAZf/mY60B6qWIgHCQAB/gp3YGxJCBvkpAqhoQC5xjzwao0QVQiwjUHv9LkWSgFPATNlv6Ngb8GHJ/E5n1b8j/QojnhBAPohvn+0esBvsol/8dsOTPBICjGgnQSwJ6m+D3kn1/I0sCvyKi32BlENQBP1YNOJRyoRogZIK+6QEwuwTUT5tRMIUI2LJ/83dLtQX2UQJC98cA/651AUxB/q+R7ZcCfmDd4Edwu/v1nn7SMn4T/FOyfvXzuBDip0KIywD+TwB/Rmf0+wLAt1i5/FnyZwLAMQAROFwul/qWwsoceA2dL+An0iD4PhF9bYC/C/h18Ndr/8okuLSoAaY/oMG6P8BGBNqCRCB0f2qJAJh2GWCOg4CmKP/3Af2awK+b/mKA32n2M+T+xpHhB01/QoiHhRDPo3P3/wGbRj+e6scEgGMMNcDoErirqQFXAVwWQryJlUHwwKIGLLFZCtCvH3UCGGWBFpslAbIoBGtEQgehCCIArLwFMX4AszxQQwkA6s8FyAX9oTsCpjIAaOjd/yiBENj6+GOBXz8XdTJgXl+r92vAv/CAfIz0vy+NfvcA/N+SAPwd60Y/3sKXCQDHiCRA31pYVwO+l9LcZakG/JKI3iWiy4grBeiqgCIJC6zKAofavgImGTAnCbaO7N5HBAD7QCGbHwDwlwdKgX9K1r+rA4Hm5v6PJQF9sn0YAJ8C/I1BApxDfTR3v0/uj5L+hRAXhRAvotu+9w/y50foxvlew6q3n41+TAA4JkAElhYScFOerN8A+EoI8QaAZ+SmQnexWRKwlQIabJoJW7mALTWToEkCWkMFaDxEgAAIi0egsRCB1PJAaKhQSQ9A3zbAFNCfw1bAY/gASrv/U7L9FOB3KW4u4DcJgAn+a61+GqDHSv/q/uNyE5/bAP4bulr/3wF8LteR78G9/UwAOCZJApRBUGXfyiD4nZTsvhZC/FgI8XMi+oiIPsdmm9+hRw1Qi9BayyC6gSC6P8AF+uo+dV3A3jUA+TgsGw4tjN8D7AZEmypggmhp8E8xBcYCfx/QLy3LUuXfnUoLYArowwR4GwmQ54nK8uEBfl0FcAH/kdlPk/sbA9Ab+Ov8G9eFEI8LIZ5GV+P/EzqH/8da1n8LXW8/Z/1bFLQ+vZVjG2K5XKoF4DiA+wBcAPAYgOcAvArgJ0R0gYh+L+W8Q40A6D+TLhKwW0MJ0O/Twd8kAsK4rsBdaKoA9Pu16zrgmwAoHIDoen7sbd/9IdWhJHCPfQKXngI4xva/BH/Nv0+2bxvpmwr8a5m/It4ZF5vsf1JOFb2Kbob/X7Fy+F/Rsn42+jEB4JgZEVgAOAbgJID7ATwE4CkALwL4EYAfaXMDUsD/UAP1PkQA2n3QHjOv+4iADuZiIDKQSghKAv/UT9i5tAD2AX1ykAAf8C8s1xeIm+gXA/yhmv8GCdD6+v8ss/730O3e9zW6IWO3OetnAsAxfzVgIdWAMwDOSzXgWQCvAHgNwKNN07yNrlxgAn0METg0AD6GCLQWFaA11AFYFAIhyUBrgGGMKlCCDJTO+mNOwLmepFPbACgV9GOzfSXz2/bdMPv5XTX+JgH4zS19Q8BvPn62bdsfAfgS3UCfv6Kb5vcFug6iG+BaPxMAjq1TA/alGnAWwEUATwL4AYAfSjUARPQ2OiPhYSQRaI3HQ0RAWAiBqzQQWx4QDvAvTQZKqQG1gb/WSU0Vf6/m9L9SoG/epgSZ3yX1uwb6+IDfrPfHAL9q7fuh7OB5GyuT32dY7+tnhz8TAI4tVgOOATgN4AEAjwB4Gl1Z4IcAXiSir4now0QSYLsvhwjYVAHAUhKwqAK5JYIYMgDjtXPBvwbwj3USDzkGOOX+xvO8GNCPlfhtGT+wLvPbsv2+wL8x5z8E/kKIZ4UQDwN4VwL/u+hMfl9hNc3vHmf9TAA4dkMN2ANwAp1J8EEAj6IrCygi8CQRvUNE3yDNFKiXBFxEQFgeExYy0MJeHvCpAoC7RBBLBmz3x6gDfbP+ufsAatf/fVl/ar0/BfR1iR+R2b7ZRmub42+285EH+JuIjN9GAi60bfuKzPLflsD/ITr5X7X23QFv4MMEgGOnSABpaoAyCV4E8Di6boGX0JUFzshJgreQ1hUQIgKtQxGwEQGzJKBPCGwtqoB+GwEyYLsdUgdSCEEp4J/biUoFnuPL+lMG+Ljud8n7OujDkt3rtxvLdZ/MT46MvykI/AsAp+Qkv5tYtfV9gK6n/zI0kx94mh8TAI6dJQK6SfA0gHPougWeBPA8OqPgD4noHhH9GZ0/wFUSaBOJQGshAy6zYGvJ/r1dA5ZLChlIUQdiScMYwN/394fYCjgm40+Zz++7PwX0YyR+U9q3Zf8bFwP0m0Tgb+CW/FWd/zg6qf+v6Or8n6Kr81/D+hhflvuZAHAwETgqC+jdAsof8IIkAj8goi+I6JNIBcB2u7U8fkQKtDq+TRWwlQUAt2nQRQYkF9ggA7bSgIgkBEDaLIHS8wCGVAlq1/5je/hjAN+U9jceN+R9H+jbTH2AW+63Zfu24T4LBxmIrfcv0A3kekoI8Ri6Pv6/oGvrU3V+3d3Pcj8HEwCODRKgFi7VLaD7A55B5w94FcATRPQBEX2VSQJchMAkAkv42wVDBCBEBo5AXZIBBNSBWEIQQxZib08F9EuRAUq87cvufYDvzPIl6jdwewB8oO8iAL72voUF+H2AnwT+QohHhBDPoavz/wVdnf8jdHX+K9Dc/QBalvs5mABw+IiAWqCOATiFzh/wIDp/gCICrwB4iIj+Jo2CscDvKgO4hga1DjIAuMsEqWRgI+OPUAdchAAZpCCVCMzRBEiRzw2BPSIA35Xl+xSBWNB3mfrgAP0G7uE+Lvk/iggIIR4UQryETtr/qwb8anb/dcgRvhL4We7nYALAkUQE9rBqGzyHlVHwGcjSABGdJaL30MmMoYzfBvZLhxqw1i6oEYFQx4AP/G3dA1a/ADZbDGOmC6aSghAxcAH/1E5cSlQFCOm1fxfYwwL45AF72/2+QT42ad/p7DeA37WRz8KhCCwiFIHzQogXhBDfSeB/TwP+y1jV+e+BR/hyMAHg6EkEdH+AbhTUicDLRHQfEb1rEIEcEuAiAqYq4BskhAQyICJIgI0QAP6SQMrcgRDw9zlRc3+37wZAKQbAnF33jh4LAL4L/CkB9PUMf8Phb8n2vTv4ZYL/eSHEi0KI79Ft0WsCv+rn5zo/BxMAjqIkQC1wan7AGQsR+AGAlwwiEFsOaD0qQAvPzIBEMmCdKNiDECCCFPiIQoqpMAfsS3cB9DEApuzC5wJ/E+xRAPDhAH8f6JPm5Hf19jee7D8G/NVtBfw3JPD/XQN+5ey/AdnPD67zczAB4KhMBPYtROAxSQSel0TgLIAPiOgy4roDWo8yYJsc2FqIwNIA7RiDYEgdQMR9yCAFLsCOlfr7EIUi60cEKYidxZ8K9kBY4kdElh9j9NOfp0v8rsvatr2RhGCDAAghLgJ4Tkr9f0Pn7v8I3cx+E/jZ4MfBBIBjUCKwMBSBB9B5BB5Dt+vg8+jKAxeJ6CMi+iKSALiUAfO2aQ7MJQN6R0FIHfCRgShSAMkMEghBKvAPvRdATIbvBXwJqkgE+1jQtw30aTwEIAb0zTp/EwB736CfNQIghHhMCPEMOln/PQn8n0jgV1K/nvHzIB8OJgAckyEC59B1DTyGbqDQs5IIPEZEl4jofYS7AlIJgM0UaJIBwD5HwNx9MMYrgMjbgH3IkA34beQglhTUBv8QCQiBvQvkbb/XOIiFD+hd2b4N4KEBNyygjgDo27L9WALgdP0LIZ4XQjwkgf49dCN7P5W3v8Gm1M/Az8EEgGOSROA0Vu2DjwB4AqvywNNEdIeI3pGLWWhAkAv0bcOCXPMCjm5rLX62rgFgfaqgq40w1SfQejL42JkBG25u4T+BS53cTtDXQN0G4AgoAz4nf+MAfx/QwwP45vQ+082vDwRygb5e498Y8hNBBmx9/yeEEC8LIU6gG9qjZP7P0A3wUe18Nxn4OZgAcMyBCCiPwHGs5ghcwMow+LRUBZ4jomPSMPhtJAFwmQFjyYCpDgD+rgFXy2DrIAIhJSCkCgD+FsNQ9J0wSD0fB+Ja9WKBPibThwPwff37KsuHBfBTQD/G/GcjAA9IY989dDP6P5QEQBn7rmDVx38XXOPnYALAMUMioOYInAJwFp1P4CF00wWVKvAcgPNEdIWIPlCLHcLmQJsPIDQ90Dda2FQHUoYItQ6gbyOyfp9RsE0E85xxw33G9IZ+v/G8Rgj0zd+3gbuZ9QMew5+R5ftG9zqn+iG9/q+u7wshnhNCXEDXIfOBlu1/KYH/W3ST+9QAH3b1czAB4Jg1GVDZj9p5UPcJPIyVafAZdOUBIX0CVyIIgG8DIatBEOGhQUe3HYRAwD9YyKYUuIhBbMYfaxSs1RlAEWSAAtdjJX/zfptnwObk37gYI39tWb5rqI/L6Odz/rtuX5D1fZJZ/kdYmfq+xnp9X9+hj/v4OZgAcGwNEVALo2ohPC1VgfPougceQVcieApdieCCVAXel4tiCgFwmgIdagBg7xqAQyGwgbwIKAVwkAWbIhC7kVAq4PctAcQSgtBgH9vjDezOf9cY3w3Z35LhAxZXv/H7iwABiLmYBOCYBP3zMtv/UIL+5+hq+5fl/d9hVd8/AO/Qx8EEgGPLiYBuGDyO1cZDulfgMXQlgqelKtCgmynwTSYBWAaUAJ8x0Nc6aI4JDpEClz8Alt9zgbdtw6IS4J9KAvTM3UUKzMcajzrgc/jbwD40q9+V6Ye27KXIrH+DAAghHkTXu9/KbP9jrCT+r7Gq7X8vs/27YGMfBxMAjh0lAwuHKvAAVh0Eqp3wGQAPyg6C99FJpilkQESoAi5CAGyaBYHN9kGBVeuhTdL37TAY8gUA4T0HBls7PNm9Tynwjeo1CYIJ9o2FGJjOflgyfJf873P7p2T9p2W2fxKdnP8RVu17ysn/rSPbZ5mfgwkAx84TAbWo6qbBM5oqcFEjA0+gKxOcJaKbkgzcRF45wGUI9BEC36wAFylwTQa0/T4Q9gOkDg8qDfo+EuACfliy+7XHLTP9XWAfM9nPl/H7NvSJAX8F+qclsH8iM30F+kriv4ZVbV9l+7wzHwcTAA4ODxlQpkG1AdF96IyDF7AyDz6KzjPwJIAzRPS97CK4hXBHQBsJ/LEEwDdJ0LzuBH/LroM+lcAF+n0nBFLE/TG9/RuEwTIMyNUB0ASuNwkEIIYI+EoA6vop6eK/T4L6p+hq+srBfxmdxH8NncSvNuY5AO/Kx8EEgIMjiQjoXoFjWJUIFBk4L8nAQwYZOEVE14joI3Ryq2smwNID9KGdBEP+AJchEIHHgIiSgGOPgVzAzyUEZubuyvxdKoDL3Od6LKbOH7ONb2zrX4NuUM8zQohzkliaoP+Nlukr0L+DVQsf1/Y5mABwcBRSBWLJwGOSEJwiontE9AlWuxP6SgExBMDM9kMEIAT+MaAf8gSUUgFSs39fzT+GDIRIQOy2vfBk/D4CYLt9XgjxlBDimAT9z9FJ+y7Qv4VV+54Cfc72OZgAcHBUJgPHLWRAGQgvSkLwiLw8KBPVr+XGRHcRHg4U0x3gIgW2x30qgY0g+DoGbIpBCPRTCEDK5j8UQQJsPgFXvb+Bf/te345+sbV/lfkfF0I8BuCiEKJBJ+N/ia6Wr6R9ZeQzM/27DPocTAA4OMYlA/tYDRo6hVVboSIEFzSFQPkHjhPRXakOfIs4c2COAhBzgYMgAP65AKnKQNL6EJnpux731ft9BCDmEqMA+Gr9D8gs/7jM3HXA/0aSAAX41y2ZPvfsczAB4OCYMBk4gdXkwTMaITiPVWeBIgQXZB37GhFdkou+rUXQVx5AIhFAxm04SEEMEfDdHyP9+7J/FwEosZNfSAGwZf4m4N8vd9o7J/0TV9D15evmPSXrfycBX7n37zDoczAB4OCYHxlQ3QS6OnAGq3KBTgiUSnBRqgZ7RHQLwCUi+hqbpYJUM2DfLYVT2gT7Zv8hFSCmzQ+VCEDI/LcQQjwM4CEhxCl0Ev23WEn5VwzAvy5B/4aR5d+T3zmDPgcTAA6OmZIBvZ1LqQNqp8LTGiE4q6kEihgoc+GDAPaJ6BDAFUkIbiOuJbCEAmADdl/tv+9sAIpQCFwtfzEegBQCEGr9OykB/4IQYk9m6t9gZdZTQK+Dvcrwb2K1495Rlg/egIeDCQAHx9arA2rbYlUuOGmQAtNLcF5TDE5JleAQwLdEdBWr0kGuEXAXCUCKAfB+OV//AQn2hxLAr2hgb9budbC/jZWsf1cDfM7yOZgAcHDssDpgKgSKFNiUAqUW3G8oB/cDOEZESwlMVyUxuAv/YKBUH0DIBAj0KwXEbgEcmvQXe9sc9nNcAv15dAN4FjJDv25k8te167bMXgd7PcPnLJ+DCQATAA4OKyEw5w4oU+EJSQj0iyIHpzXV4KxBDk5KtUBIELqDrpRwC6vJhaHsvy8J8N0fawLMBX9b1n9K1ugvyM91X26ZeyiBWwf577Rs/qYB8vrlDlamvaO+fEm4GPA5OJgAcHAkkQKTEOik4Lh2OWkQhNMGOVDX9cfUfQ2ABRG1GohdAyCkgtAiPBMgdmtgkQD2KcCvg3wjM3iSysgJdFP1Gg2QFYjf1MBcv++G8ZgCeDVX/66W2SuwP9Sye5b0OTiYAHBwVFMJdGKwj/USwnFNNbBdTmLde6CIg7qunqdeZ18C53EAh1JN2JMZMiT4XXMRBOlVuB749+6XtXUXwJ+TfxNS2TiUWfueBONGUzjuamRGyfG3tOu3NUC/47noQH8gL2uZPWf3HBxMADg4xlYK1IS5hUEMlGqgk4R9i5JwQrt+TLvsG7dtF/V3FrDvhqe/L33bXWVQXGoX226ISwm8B1rW7bqYz7mrEQIzcz8wwP2e9ncOjfclOLPn4NhCArBc8tbYHFsZrr3kdZKgKwj72n3mZd/zU72Wq1de39nORgDMHRRtMw4UIB8YIG3+tF0OjAz+0EI6zAuHJxaLBX8IHNmxxx8BB0f18IGZbWrdwkEYTOKwsGT1ZuYP+AfpAP5BRrAoAaZasLQAuQ3QzftcWxtzcHAwAeDg2PowZwQgQBR8s+7NdjoY111b8wLxmw/5NkeykQcODo6JBnsAODg4ODg4djAa/gg4ODg4ODiYAHBwcHBwcHAwAeDg4ODg4OBgAsDBwcHBwcHBBICDg4ODg4ODCQAHBwcHBwcHEwAODg4ODg4OJgAcHBwcHBwcTAA4ODg4ODg4xov/fwAHnhIg2IQLzgAAAABJRU5ErkJggg==" - } - ], - "object": { - "uuid": "da66c047-c0da-4a53-90dd-589c4e53e868", - "type": "Group", - "name": "MagicZoneGreen", - "visible": true, - "layers": 1, - "matrix": [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1], - "up": [0, 1, 0], - "children": [ - { - "uuid": "1921b779-fac1-42ec-b40a-a1af729bf6fa", - "type": "Group", - "name": "PortalDust", - "layers": 1, - "matrix": [ - -1.0000003044148624, 7.619931978698374e-8, 1.2979021821838232e-8, 0, -1.2979033855407715e-8, -1.8384778810981241e-7, -1.0000001522074553, 0, - -7.619930580271816e-8, -1.0000001522073967, 1.8384778922002538e-7, 0, 0, 0, 0, 1 - ], - "up": [0, 1, 0], - "children": [ - { - "uuid": "9ec4a1d9-3f3d-49a6-85fc-577cef6084a2", - "type": "ParticleEmitter", - "name": "PortalDustEmitter", - "layers": 1, - "matrix": [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1], - "up": [0, 1, 0], - "ps": { - "version": "3.0", - "autoDestroy": false, - "looping": true, - "prewarm": false, - "duration": 5, - "shape": { - "type": "cone", - "radius": 1.21, - "arc": 6.283185307179586, - "thickness": 0, - "angle": 0, - "mode": 0, - "spread": 0, - "speed": { "type": "ConstantValue", "value": 1 } - }, - "startLife": { "type": "IntervalValue", "a": 1, "b": 1.5 }, - "startSpeed": { "type": "ConstantValue", "value": -1 }, - "startRotation": { "type": "IntervalValue", "a": 0, "b": 6.283185 }, - "startSize": { "type": "IntervalValue", "a": 0.15, "b": 0.2 }, - "startColor": { "type": "ConstantColor", "color": { "r": 1, "g": 1, "b": 1, "a": 1 } }, - "emissionOverTime": { "type": "ConstantValue", "value": 25 }, - "emissionOverDistance": { "type": "ConstantValue", "value": 0 }, - "emissionBursts": [], - "onlyUsedByOther": false, - "instancingGeometry": "780917d8-bd1b-4d63-8aca-f79e3211f964", - "renderOrder": 0, - "renderMode": 0, - "rendererEmitterSettings": {}, - "material": "769df3ee-4567-40b7-8da4-473fb149f350", - "layers": 1, - "startTileIndex": { "type": "ConstantValue", "value": 0 }, - "uTileCount": 1, - "vTileCount": 1, - "blendTiles": false, - "softParticles": false, - "softFarFade": 0, - "softNearFade": 0, - "behaviors": [ - { - "type": "ForceOverLife", - "x": { "type": "ConstantValue", "value": 0 }, - "y": { "type": "ConstantValue", "value": 0 }, - "z": { "type": "ConstantValue", "value": 0 } - }, - { - "type": "SizeOverLife", - "size": { - "type": "PiecewiseBezier", - "functions": [ - { "function": { "p0": 0.8495575, "p1": 0.8495575, "p2": 1, "p3": 1 }, "start": 0 }, - { "function": { "p0": 1, "p1": 1, "p2": 0, "p3": 0 }, "start": 0.49871457 } - ] - } - }, - { "type": "RotationOverLife", "angularVelocity": { "type": "IntervalValue", "a": -3.1415925, "b": 3.1415925 } }, - { - "type": "ColorOverLife", - "color": { - "type": "Gradient", - "color": { - "type": "CLinearFunction", - "subType": "Color", - "keys": [ - { "value": { "r": 1, "g": 1, "b": 1 }, "pos": 0 }, - { "value": { "r": 0.59607846, "g": 1, "b": 0.050980393 }, "pos": 0.4587167162584878 }, - { "value": { "r": 0, "g": 1, "b": 0.047058824 }, "pos": 0.9518272678721293 } - ] - }, - "alpha": { - "type": "CLinearFunction", - "subType": "Number", - "keys": [ - { "value": 0, "pos": 0 }, - { "value": 1, "pos": 0.41690699626153965 }, - { "value": 1, "pos": 0.7580224307621881 }, - { "value": 0, "pos": 1 } - ] - } - } - } - ], - "worldSpace": true - } - } - ] - }, - { - "uuid": "cfe42db9-925f-4bd2-bc92-3d15a4e2b795", - "type": "Group", - "name": "GlowCircle", - "layers": 1, - "matrix": [1, 0, 0, 0, 0, -5.321248014494817e-8, -1.0000000532124802, 0, 0, 1.0000000532124802, -5.321248014494817e-8, 0, 0, 0, 0, 1], - "up": [0, 1, 0], - "children": [ - { - "uuid": "94cac5fe-52a9-431d-ba8a-19fe44a2cdc1", - "type": "ParticleEmitter", - "name": "GlowCircleEmitter", - "layers": 1, - "matrix": [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1], - "up": [0, 1, 0], - "ps": { - "version": "3.0", - "autoDestroy": false, - "looping": true, - "prewarm": false, - "duration": 2, - "shape": { - "type": "cone", - "radius": 0.01, - "arc": 6.283185307179586, - "thickness": 1, - "angle": 0.06981317007977318, - "mode": 0, - "spread": 0, - "speed": { "type": "ConstantValue", "value": 1 } - }, - "startLife": { "type": "ConstantValue", "value": 2 }, - "startSpeed": { "type": "ConstantValue", "value": 0 }, - "startRotation": { - "type": "Euler", - "angleX": { "type": "IntervalValue", "a": 0, "b": 0 }, - "angleY": { "type": "IntervalValue", "a": 0, "b": 0 }, - "angleZ": { "type": "IntervalValue", "a": 0, "b": 6.283185 }, - "eulerOrder": "XYZ" - }, - "startSize": { "type": "ConstantValue", "value": 4.1 }, - "startColor": { "type": "ConstantColor", "color": { "r": 0.45882353, "g": 1, "b": 0.28627452, "a": 0.4509804 } }, - "emissionOverTime": { "type": "ConstantValue", "value": 1 }, - "emissionOverDistance": { "type": "ConstantValue", "value": 0 }, - "emissionBursts": [], - "onlyUsedByOther": false, - "instancingGeometry": "f40b6ee0-aa01-46e0-b05a-d938b54eec83", - "renderOrder": 0, - "renderMode": 2, - "rendererEmitterSettings": {}, - "material": "6d9283b7-81c2-4063-84cc-f696054ce6f6", - "layers": 1, - "startTileIndex": { "type": "ConstantValue", "value": 0 }, - "uTileCount": 1, - "vTileCount": 1, - "blendTiles": false, - "softParticles": false, - "softFarFade": 0, - "softNearFade": 0, - "behaviors": [ - { - "type": "ForceOverLife", - "x": { "type": "ConstantValue", "value": 0 }, - "y": { "type": "ConstantValue", "value": 0 }, - "z": { "type": "ConstantValue", "value": 0 } - }, - { - "type": "ColorOverLife", - "color": { - "type": "Gradient", - "color": { - "type": "CLinearFunction", - "subType": "Color", - "keys": [ - { "value": { "r": 1, "g": 1, "b": 1 }, "pos": 0 }, - { "value": { "r": 1, "g": 1, "b": 1 }, "pos": 1 } - ] - }, - "alpha": { - "type": "CLinearFunction", - "subType": "Number", - "keys": [ - { "value": 0, "pos": 0 }, - { "value": 1, "pos": 0.5014572365911345 }, - { "value": 0, "pos": 1 } - ] - } - } - } - ], - "worldSpace": true - } - } - ] - }, - { - "uuid": "c86a5eb7-2571-4e87-b5fd-e68a0f965b0a", - "type": "ParticleEmitter", - "name": "MagicZoneGreenEmitter", - "layers": 1, - "matrix": [1, 0, 0, 0, 0, -2.220446049250313e-16, -1, 0, 0, 1, -2.220446049250313e-16, 0, 0, 0, 0, 1], - "up": [0, 1, 0], - "ps": { - "version": "3.0", - "autoDestroy": false, - "looping": true, - "prewarm": false, - "duration": 5, - "shape": { "type": "point" }, - "startLife": { "type": "ConstantValue", "value": 1 }, - "startSpeed": { "type": "ConstantValue", "value": 0 }, - "startRotation": { - "type": "Euler", - "angleX": { "type": "IntervalValue", "a": 1.5707963, "b": 1.5707963 }, - "angleY": { "type": "IntervalValue", "a": 0, "b": 6.283185 }, - "angleZ": { "type": "IntervalValue", "a": 0, "b": 0 }, - "eulerOrder": "XYZ" - }, - "startSize": { "type": "ConstantValue", "value": 2.8 }, - "startColor": { "type": "ConstantColor", "color": { "r": 1, "g": 1, "b": 1, "a": 1 } }, - "emissionOverTime": { "type": "ConstantValue", "value": 2.5 }, - "emissionOverDistance": { "type": "ConstantValue", "value": 0 }, - "emissionBursts": [], - "onlyUsedByOther": false, - "instancingGeometry": "780917d8-bd1b-4d63-8aca-f79e3211f964", - "renderOrder": 0, - "renderMode": 2, - "rendererEmitterSettings": {}, - "material": "7442c205-fb42-4fb9-baec-82a192b81351", - "layers": 1, - "startTileIndex": { "type": "ConstantValue", "value": 0 }, - "uTileCount": 1, - "vTileCount": 1, - "blendTiles": false, - "softParticles": false, - "softFarFade": 0, - "softNearFade": 0, - "behaviors": [ - { - "type": "ForceOverLife", - "x": { "type": "ConstantValue", "value": 0 }, - "y": { "type": "ConstantValue", "value": 0 }, - "z": { "type": "ConstantValue", "value": 0 } - }, - { - "type": "ColorOverLife", - "color": { - "type": "Gradient", - "color": { - "type": "CLinearFunction", - "subType": "Color", - "keys": [ - { "value": { "r": 0.6156863, "g": 1, "b": 0 }, "pos": 0 }, - { "value": { "r": 0.101960786, "g": 1, "b": 0.10980392 }, "pos": 1 } - ] - }, - "alpha": { - "type": "CLinearFunction", - "subType": "Number", - "keys": [ - { "value": 0, "pos": 0.004592965590905623 }, - { "value": 1, "pos": 0.5014572365911345 }, - { "value": 0, "pos": 1 } - ] - } - } - } - ], - "worldSpace": true - } - } - ] - } -} diff --git a/editor/src/editor/windows/fx-editor/VFX/types/quarksTypes.ts b/editor/src/editor/windows/fx-editor/VFX/types/quarksTypes.ts index 834baf98b..9b289f446 100644 --- a/editor/src/editor/windows/fx-editor/VFX/types/quarksTypes.ts +++ b/editor/src/editor/windows/fx-editor/VFX/types/quarksTypes.ts @@ -305,7 +305,6 @@ export interface QuarksMaterial { side?: number; transparent?: boolean; depthWrite?: boolean; - [key: string]: unknown; } /** @@ -325,7 +324,7 @@ export interface QuarksTexture { flipY?: boolean; generateMipmaps?: boolean; format?: number; - [key: string]: unknown; + channel?: number; } /** @@ -334,8 +333,6 @@ export interface QuarksTexture { export interface QuarksImage { uuid: string; url?: string; - data?: string; - [key: string]: unknown; } /** @@ -358,7 +355,6 @@ export interface QuarksGeometry { array: number[]; }; }; - [key: string]: unknown; } /** @@ -369,7 +365,6 @@ export interface QuarksVFXJSON { version?: number; type?: string; generator?: string; - [key: string]: unknown; }; geometries?: QuarksGeometry[]; materials?: QuarksMaterial[]; From 7c16a387fad6c7c218202abf09304dab7c9b02c8 Mon Sep 17 00:00:00 2001 From: Mikalai Lazitski Date: Fri, 12 Dec 2025 14:46:31 +0300 Subject: [PATCH 14/62] refactor: enhance VFXEffect structure and hierarchy management in FX Editor for improved node handling and clarity --- .../editor/windows/fx-editor/VFX/VFXEffect.ts | 281 +++++++++++++++++- .../src/editor/windows/fx-editor/VFX/index.ts | 1 + editor/src/editor/windows/fx-editor/graph.tsx | 64 +++- 3 files changed, 334 insertions(+), 12 deletions(-) diff --git a/editor/src/editor/windows/fx-editor/VFX/VFXEffect.ts b/editor/src/editor/windows/fx-editor/VFX/VFXEffect.ts index bec836137..46e5c26ff 100644 --- a/editor/src/editor/windows/fx-editor/VFX/VFXEffect.ts +++ b/editor/src/editor/windows/fx-editor/VFX/VFXEffect.ts @@ -1,17 +1,57 @@ -import { Scene, Tools, IDisposable } from "babylonjs"; +import { Scene, Tools, IDisposable, TransformNode } from "babylonjs"; import type { QuarksVFXJSON } from "./types/quarksTypes"; import type { VFXLoaderOptions } from "./types/loader"; import { VFXParser } from "./parsers/VFXParser"; import type { VFXParticleSystem } from "./systems/VFXParticleSystem"; import type { VFXSolidParticleSystem } from "./systems/VFXSolidParticleSystem"; +import type { VFXGroup, VFXEmitter } from "./types/hierarchy"; /** - * VFX Effect containing multiple particle systems + * VFX Effect Node - represents either a particle system or a group + */ +export interface VFXEffectNode { + /** Node name */ + name: string; + /** Node UUID from original JSON */ + uuid?: string; + /** Particle system (if this is a particle emitter) */ + system?: VFXParticleSystem | VFXSolidParticleSystem; + /** Transform node (if this is a group) */ + group?: TransformNode; + /** Parent node */ + parent?: VFXEffectNode; + /** Child nodes */ + children: VFXEffectNode[]; + /** Node type */ + type: "particle" | "group"; +} + +/** + * VFX Effect containing multiple particle systems with hierarchy support * Main entry point for loading and creating VFX from Three.js particle JSON files */ export class VFXEffect implements IDisposable { + /** All particle systems in this effect */ public readonly systems: (VFXParticleSystem | VFXSolidParticleSystem)[] = []; + /** Root node of the effect hierarchy */ + public readonly root: VFXEffectNode | null = null; + + /** Map of systems by name for quick lookup */ + private readonly _systemsByName = new Map(); + + /** Map of systems by UUID for quick lookup */ + private readonly _systemsByUuid = new Map(); + + /** Map of groups by name */ + private readonly _groupsByName = new Map(); + + /** Map of groups by UUID */ + private readonly _groupsByUuid = new Map(); + + /** All nodes in the hierarchy */ + private readonly _nodes = new Map(); + /** * Load a Three.js particle JSON file and create particle systems * @param url URL to the JSON file @@ -52,9 +92,14 @@ export class VFXEffect implements IDisposable { * @returns A VFXEffect containing all particle systems */ public static Parse(jsonData: QuarksVFXJSON, scene: Scene, rootUrl: string = "", options?: VFXLoaderOptions): VFXEffect { - const particleSystems = new VFXParser(scene, rootUrl, jsonData, options).parse(); + const parser = new VFXParser(scene, rootUrl, jsonData, options); + const particleSystems = parser.parse(); + const context = parser.getContext(); + const effect = new VFXEffect(); effect.systems.push(...particleSystems); + effect._buildHierarchy(context, particleSystems); + return effect; } @@ -67,27 +112,253 @@ export class VFXEffect implements IDisposable { */ constructor(jsonData?: QuarksVFXJSON, scene?: Scene, rootUrl: string = "", options?: VFXLoaderOptions) { if (jsonData && scene) { - const effect = VFXEffect.Parse(jsonData, scene, rootUrl, options); - this.systems.push(...effect.systems); + const parser = new VFXParser(scene, rootUrl, jsonData, options); + const particleSystems = parser.parse(); + const context = parser.getContext(); + + this.systems.push(...particleSystems); + this._buildHierarchy(context, particleSystems); + } + } + + /** + * Build hierarchy from parser context + */ + private _buildHierarchy(context: any, systems: (VFXParticleSystem | VFXSolidParticleSystem)[]): void { + // Build hierarchy from vfxData if available + const vfxData = context.vfxData; + if (!vfxData || !vfxData.root) { + return; + } + + // Create nodes from hierarchy + const rootNode = this._buildNodeFromHierarchy(vfxData.root, null, context.groupNodesMap, systems); + // Store root (we can't assign to readonly, so we'll use a workaround) + (this as any).root = rootNode; + } + + /** + * Recursively build nodes from hierarchy + */ + private _buildNodeFromHierarchy( + obj: VFXGroup | VFXEmitter, + parent: VFXEffectNode | null, + groupNodesMap: Map, + systems: (VFXParticleSystem | VFXSolidParticleSystem)[] + ): VFXEffectNode | null { + if (!obj) { + return null; + } + + const node: VFXEffectNode = { + name: obj.name, + uuid: obj.uuid, + parent: parent || undefined, + children: [], + type: "config" in obj ? "particle" : "group", + }; + + if (node.type === "particle") { + // Find system by name + const emitter = obj as VFXEmitter; + const system = systems.find((s) => s.name === emitter.name); + if (system) { + node.system = system; + this._systemsByName.set(emitter.name, system); + if (emitter.uuid) { + this._systemsByUuid.set(emitter.uuid, system); + } + } + } else { + // Find group TransformNode + const group = obj as VFXGroup; + const groupNode = group.uuid ? groupNodesMap.get(group.uuid) : null; + if (groupNode) { + node.group = groupNode; + this._groupsByName.set(group.name, groupNode); + if (group.uuid) { + this._groupsByUuid.set(group.uuid, groupNode); + } + } } + + // Process children + if ("children" in obj && obj.children) { + for (const child of obj.children) { + const childNode = this._buildNodeFromHierarchy(child, node, groupNodesMap, systems); + if (childNode) { + node.children.push(childNode); + } + } + } + + // Store node + if (obj.uuid) { + this._nodes.set(obj.uuid, node); + } + this._nodes.set(obj.name, node); + + return node; + } + + /** + * Find a particle system by name + */ + public findSystemByName(name: string): VFXParticleSystem | VFXSolidParticleSystem | null { + return this._systemsByName.get(name) || null; } + /** + * Find a particle system by UUID + */ + public findSystemByUuid(uuid: string): VFXParticleSystem | VFXSolidParticleSystem | null { + return this._systemsByUuid.get(uuid) || null; + } + + /** + * Find a group by name + */ + public findGroupByName(name: string): TransformNode | null { + return this._groupsByName.get(name) || null; + } + + /** + * Find a group by UUID + */ + public findGroupByUuid(uuid: string): TransformNode | null { + return this._groupsByUuid.get(uuid) || null; + } + + /** + * Find a node (system or group) by name + */ + public findNodeByName(name: string): VFXEffectNode | null { + return this._nodes.get(name) || null; + } + + /** + * Find a node (system or group) by UUID + */ + public findNodeByUuid(uuid: string): VFXEffectNode | null { + return this._nodes.get(uuid) || null; + } + + /** + * Get all systems in a group (recursively) + * Includes systems from nested child groups as well. + * Example: If Group1 contains Group2, and Group2 contains System1, + * then getSystemsInGroup("Group1") will return System1. + */ + public getSystemsInGroup(groupName: string): (VFXParticleSystem | VFXSolidParticleSystem)[] { + const group = this.findGroupByName(groupName); + if (!group) { + return []; + } + + const systems: (VFXParticleSystem | VFXSolidParticleSystem)[] = []; + this._collectSystemsInGroup(group, systems); + return systems; + } + + /** + * Recursively collect systems in a group (including systems from all nested child groups) + * This method: + * 1. Collects all systems that have this group as direct parent + * 2. Recursively processes all child groups and collects their systems too + */ + private _collectSystemsInGroup(group: TransformNode, systems: (VFXParticleSystem | VFXSolidParticleSystem)[]): void { + // Step 1: Find systems that have this group as direct parent + for (const system of this.systems) { + const mesh = (system as any).mesh || (system as any).emitter; + if (mesh && mesh.parent === group) { + systems.push(system); + } + } + + // Step 2: Recursively process all child groups + // This ensures systems from nested groups are also collected + for (const [, groupNode] of this._groupsByUuid) { + if (groupNode.parent === group) { + // Recursively collect systems from child group (and its nested groups) + this._collectSystemsInGroup(groupNode, systems); + } + } + } + + /** + * Start a specific system by name + */ + public startSystem(name: string): boolean { + const system = this.findSystemByName(name); + if (system) { + system.start(); + return true; + } + return false; + } + + /** + * Stop a specific system by name + */ + public stopSystem(name: string): boolean { + const system = this.findSystemByName(name); + if (system) { + system.stop(); + return true; + } + return false; + } + + /** + * Start all systems in a group + */ + public startGroup(groupName: string): void { + const systems = this.getSystemsInGroup(groupName); + for (const system of systems) { + system.start(); + } + } + + /** + * Stop all systems in a group + */ + public stopGroup(groupName: string): void { + const systems = this.getSystemsInGroup(groupName); + for (const system of systems) { + system.stop(); + } + } + + /** + * Start all particle systems + */ public start(): void { for (const system of this.systems) { system.start(); } } + /** + * Stop all particle systems + */ public stop(): void { for (const system of this.systems) { system.stop(); } } + /** + * Dispose all resources + */ public dispose(): void { for (const system of this.systems) { system.dispose(); } this.systems.length = 0; + this._systemsByName.clear(); + this._systemsByUuid.clear(); + this._groupsByName.clear(); + this._groupsByUuid.clear(); + this._nodes.clear(); } } diff --git a/editor/src/editor/windows/fx-editor/VFX/index.ts b/editor/src/editor/windows/fx-editor/VFX/index.ts index 0a58635b8..f3685927a 100644 --- a/editor/src/editor/windows/fx-editor/VFX/index.ts +++ b/editor/src/editor/windows/fx-editor/VFX/index.ts @@ -11,3 +11,4 @@ export * from "./systems/VFXSolidParticleSystem"; export * from "./systems/VFXParticleSystem"; export * from "./loggers/VFXLogger"; export * from "./VFXEffect"; +export type { VFXEffectNode } from "./VFXEffect"; diff --git a/editor/src/editor/windows/fx-editor/graph.tsx b/editor/src/editor/windows/fx-editor/graph.tsx index e2347681e..6875d1a43 100644 --- a/editor/src/editor/windows/fx-editor/graph.tsx +++ b/editor/src/editor/windows/fx-editor/graph.tsx @@ -1,6 +1,6 @@ import { Component, ReactNode } from "react"; import { Tree, TreeNodeInfo } from "@blueprintjs/core"; -import { Scene, Vector3, Color4 } from "babylonjs"; +import { Vector3, Color4 } from "babylonjs"; import { IFXParticleData, IFXGroupData, IFXNodeData, isGroupData, isParticleData } from "./properties/types"; import { AiOutlinePlus, AiOutlineClose } from "react-icons/ai"; @@ -18,7 +18,7 @@ import { ContextMenuSubContent, } from "../../../ui/shadcn/ui/context-menu"; import { IFXEditor } from "."; -import { VFXEffect } from "./VFX"; +import { VFXEffect, type VFXEffectNode } from "./VFX"; export interface IFXEditorGraphProps { filePath: string | null; @@ -244,7 +244,7 @@ export class FXEditorGraph extends Component { try { @@ -253,18 +253,68 @@ export class FXEditorGraph extends Component { - system.start(); - }); + // Build tree from VFXEffect hierarchy + const nodes = vfxEffect.root ? [this._convertVFXNodeToTreeNode(vfxEffect.root)] : []; + + this.setState({ nodes, selectedNodeId: null }); + + // Start systems + vfxEffect.start(); } catch (error) { console.error("Failed to load FX file:", error); } } + /** + * Converts VFXEffectNode to TreeNodeInfo recursively + */ + private _convertVFXNodeToTreeNode(vfxNode: VFXEffectNode): TreeNodeInfo { + const nodeId = vfxNode.uuid || vfxNode.name; + let nodeData: IFXNodeData; + + if (vfxNode.type === "particle" && vfxNode.system) { + // Particle system node + nodeData = { + type: "particle", + id: nodeId, + name: vfxNode.name, + system: vfxNode.system, + } as any; + } else if (vfxNode.type === "group" && vfxNode.group) { + // Group node + nodeData = { + type: "group", + id: nodeId, + name: vfxNode.name, + transformNode: vfxNode.group, + } as any; + } else { + // Fallback + nodeData = { + type: "group", + id: nodeId, + name: vfxNode.name, + } as any; + } + + const childNodes = vfxNode.children.length > 0 ? vfxNode.children.map((child) => this._convertVFXNodeToTreeNode(child)) : undefined; + + return { + id: nodeId, + label: this._getNodeLabelComponent({ id: nodeId, nodeData } as any, vfxNode.name), + icon: vfxNode.type === "particle" ? : , + isExpanded: vfxNode.type === "group", + childNodes, + isSelected: false, + hasCaret: vfxNode.type === "group" || (childNodes && childNodes.length > 0), + nodeData, + }; + } + /** * Updates node names in the graph (called when name changes in properties) */ From 0a337edaae3ab4e41bb2b6d508871ceddc8c2459 Mon Sep 17 00:00:00 2001 From: Mikalai Lazitski Date: Fri, 12 Dec 2025 15:11:48 +0300 Subject: [PATCH 15/62] refactor: streamline VFX hierarchy processing by consolidating node creation and transformation application for improved performance and clarity --- .../VFX/processors/VFXHierarchyProcessor.ts | 439 ++++++++++-------- 1 file changed, 246 insertions(+), 193 deletions(-) diff --git a/editor/src/editor/windows/fx-editor/VFX/processors/VFXHierarchyProcessor.ts b/editor/src/editor/windows/fx-editor/VFX/processors/VFXHierarchyProcessor.ts index de4e17166..3fd6be440 100644 --- a/editor/src/editor/windows/fx-editor/VFX/processors/VFXHierarchyProcessor.ts +++ b/editor/src/editor/windows/fx-editor/VFX/processors/VFXHierarchyProcessor.ts @@ -1,15 +1,15 @@ -import { Nullable, Vector3, Quaternion, TransformNode } from "babylonjs"; +import { Nullable, Vector3, TransformNode } from "babylonjs"; import { VFXParticleSystem } from "../systems/VFXParticleSystem"; import { VFXSolidParticleSystem } from "../systems/VFXSolidParticleSystem"; import type { VFXParseContext } from "../types/context"; import type { VFXEmitterData } from "../types/emitter"; -import type { VFXLoaderOptions } from "../types/loader"; import type { VFXHierarchy, VFXGroup, VFXEmitter, VFXTransform } from "../types/hierarchy"; import { VFXLogger } from "../loggers/VFXLogger"; import type { IVFXEmitterFactory } from "../types/factories"; /** * Processor for Three.js object hierarchy (Groups and ParticleEmitters) + * Creates all nodes, sets parents, and applies transformations in a single pass */ export class VFXHierarchyProcessor { private _logger: VFXLogger; @@ -24,253 +24,183 @@ export class VFXHierarchyProcessor { /** * Process the VFX hierarchy and create particle systems - * Uses pre-converted data (already in left-handed coordinate system) + * Creates all nodes, sets parents, and applies transformations in one pass */ public processHierarchy(vfxData: VFXHierarchy): (VFXParticleSystem | VFXSolidParticleSystem)[] { - const { options } = this._context; - const particleSystems: (VFXParticleSystem | VFXSolidParticleSystem)[] = []; - if (!vfxData.root) { - this._logger.warn("No root object found in VFX data", options); - return particleSystems; + this._logWarning("No root object found in VFX data"); + return []; } - this._logger.log("Phase 1: Creating nodes and building hierarchy", options); - // Phase 1: Create all nodes without transformations, build hierarchy - this._processVFXObject(vfxData.root, null, 0, particleSystems, false, vfxData); - - this._logger.log("Phase 2: Applying transformations", options); - // Phase 2: Apply transformations after hierarchy is established - this._processVFXObject(vfxData.root, null, 0, particleSystems, true, vfxData); - + this._logInfo("Processing hierarchy: creating nodes, setting parents, and applying transformations"); + const particleSystems: (VFXParticleSystem | VFXSolidParticleSystem)[] = []; + this._processVFXObject(vfxData.root, null, 0, particleSystems, vfxData); return particleSystems; } /** * Recursively process VFX object hierarchy - * @param applyTransformations If true, applies transformations. If false, creates nodes and builds hierarchy. + * Creates nodes, sets parents, and applies transformations in one pass */ private _processVFXObject( vfxObj: VFXGroup | VFXEmitter, parentGroup: Nullable, depth: number, particleSystems: (VFXParticleSystem | VFXSolidParticleSystem)[], - applyTransformations: boolean, vfxData: VFXHierarchy ): void { - const { options } = this._context; - const indent = " ".repeat(depth); + this._logObjectProcessing(vfxObj.name, depth); - if (!applyTransformations) { - this._logger.log(`${indent}Creating object: ${vfxObj.name}`, options); + if (this._isGroup(vfxObj)) { + this._processGroup(vfxObj, parentGroup, depth, particleSystems, vfxData); } else { - this._logger.log(`${indent}Applying transformations to: ${vfxObj.name}`, options); - } - - let currentGroup: Nullable = parentGroup; - - // Handle Group objects - if ("children" in vfxObj) { - const vfxGroup = vfxObj as VFXGroup; - if (!applyTransformations) { - // Phase 1: Create group without transformations, set parent - currentGroup = this._createGroupFromVFX(vfxGroup, parentGroup, depth, options); - } else { - // Phase 2: Apply transformations to group - currentGroup = this._context.groupNodesMap.get(vfxGroup.uuid) || null; - if (currentGroup) { - this._applyVFXTransformToGroup(currentGroup, vfxGroup.transform, depth, options); - } - } - - // Process children recursively - if (vfxGroup.children && vfxGroup.children.length > 0) { - if (!applyTransformations) { - this._logger.log(`${indent}Processing ${vfxGroup.children.length} children`, options); - } - for (const child of vfxGroup.children) { - this._processVFXObject(child, currentGroup, depth + 1, particleSystems, applyTransformations, vfxData); - } - } - } else { - // Handle Emitter objects - const vfxEmitter = vfxObj as VFXEmitter; - if (!applyTransformations) { - // Phase 1: Create particle system without transformations - const particleSystem = this._processVFXEmitter(vfxEmitter, currentGroup, depth, options, false); - if (particleSystem) { - particleSystems.push(particleSystem); - } - } else { - // Phase 2: Apply transformations to particle system - this._applyVFXTransformToEmitter(vfxEmitter, currentGroup, depth, options); - } + this._processEmitter(vfxObj, parentGroup, depth, particleSystems); } } /** - * Create a Group (TransformNode) from VFX Group data - * Phase 1: Creates node without transformations, sets parent + * Process a VFX Group object */ - private _createGroupFromVFX(vfxGroup: VFXGroup, parentGroup: Nullable, depth: number, options?: VFXLoaderOptions): TransformNode { - const { scene } = this._context; - const indent = " ".repeat(depth); + private _processGroup( + vfxGroup: VFXGroup, + parentGroup: Nullable, + depth: number, + particleSystems: (VFXParticleSystem | VFXSolidParticleSystem)[], + vfxData: VFXHierarchy + ): void { + const groupNode = this._createGroupNode(vfxGroup, parentGroup, depth); + this._processChildren(vfxGroup.children, groupNode, depth, particleSystems, vfxData); + } - this._logger.log(`${indent}Creating Group: ${vfxGroup.name} (without transformations)`, options); - const groupNode = new TransformNode(vfxGroup.name, scene); + /** + * Process a VFX Emitter object + */ + private _processEmitter(vfxEmitter: VFXEmitter, parentGroup: Nullable, depth: number, particleSystems: (VFXParticleSystem | VFXSolidParticleSystem)[]): void { + const particleSystem = this._createParticleSystem(vfxEmitter, parentGroup, depth); + if (particleSystem) { + particleSystems.push(particleSystem); + } + } - // Initialize with identity transform (will be applied in phase 2) - groupNode.position.setAll(0); - if (!groupNode.rotationQuaternion) { - groupNode.rotationQuaternion = Quaternion.Identity(); - } else { - groupNode.rotationQuaternion.set(0, 0, 0, 1); + /** + * Process children of a group recursively + */ + private _processChildren( + children: (VFXGroup | VFXEmitter)[] | undefined, + parentGroup: TransformNode, + depth: number, + particleSystems: (VFXParticleSystem | VFXSolidParticleSystem)[], + vfxData: VFXHierarchy + ): void { + if (!children || children.length === 0) { + return; } - groupNode.scaling.setAll(1); - // Set visibility - groupNode.isVisible = false; + this._logChildrenProcessing(children.length, depth); + children.forEach((child) => { + this._processVFXObject(child, parentGroup, depth + 1, particleSystems, vfxData); + }); + } - // Set parent FIRST (before applying transformations) - if (parentGroup) { - groupNode.setParent(parentGroup); - this._logger.log(`${indent}Group parent set: ${parentGroup.name}`, options); - } + /** + * Create a Group (TransformNode) from VFX Group data + * Creates node, sets parent, and applies transform in one go + */ + private _createGroupNode(vfxGroup: VFXGroup, parentGroup: Nullable, depth: number): TransformNode { + const { scene } = this._context; + this._logGroupCreation(vfxGroup.name, depth); - // Store in map for reference (needed for phase 2) - this._context.groupNodesMap.set(vfxGroup.uuid, groupNode); - this._logger.log(`${indent}Group stored in map: ${vfxGroup.uuid}`, options); + const groupNode = this._instantiateGroupNode(vfxGroup.name, scene); + this._setGroupParent(groupNode, parentGroup, depth); + this._applyGroupTransform(groupNode, vfxGroup.transform, depth); + this._storeGroupNode(vfxGroup.uuid, groupNode, depth); return groupNode; } /** - * Apply VFX transform to a Group node - * Phase 2: Applies pre-converted transformations (already in left-handed system) + * Instantiate a new TransformNode for a group */ - private _applyVFXTransformToGroup(groupNode: TransformNode, transform: VFXTransform, depth: number, options?: VFXLoaderOptions): void { - const indent = " ".repeat(depth); - this._logger.log(`${indent}Applying converted transform to group: ${groupNode.name}`, options); - - // Transform is already converted to left-handed, apply directly - groupNode.position.copyFrom(transform.position); - groupNode.rotationQuaternion = transform.rotation.clone(); - groupNode.scaling.copyFrom(transform.scale); - - const pos = groupNode.position; - const rot = groupNode.rotationQuaternion; - const scl = groupNode.scaling; - this._logger.log(`${indent}Group position: (${pos.x.toFixed(2)}, ${pos.y.toFixed(2)}, ${pos.z.toFixed(2)})`, options); - if (rot) { - this._logger.log(`${indent}Group rotation quaternion: (${rot.x.toFixed(4)}, ${rot.y.toFixed(4)}, ${rot.z.toFixed(4)}, ${rot.w.toFixed(4)})`, options); - } - this._logger.log(`${indent}Group scale: (${scl.x.toFixed(2)}, ${scl.y.toFixed(2)}, ${scl.z.toFixed(2)})`, options); + private _instantiateGroupNode(name: string, scene: any): TransformNode { + const groupNode = new TransformNode(name, scene); + groupNode.isVisible = false; + return groupNode; } /** - * Process a VFX ParticleEmitter - * @param applyTransformations If false, creates system without transformations. If true, applies transformations. + * Set parent for a group node */ - private _processVFXEmitter( - vfxEmitter: VFXEmitter, - currentGroup: Nullable, - depth: number, - options?: VFXLoaderOptions, - applyTransformations: boolean = false - ): Nullable { - const indent = " ".repeat(depth); - const emitterName = vfxEmitter.name; - - this._logger.log(`${indent}=== Processing ParticleEmitter: ${emitterName} ===`, options); - this._logger.log(`${indent}Current parent group: ${currentGroup ? currentGroup.name : "none"}`, options); - - // Log emitter configuration - if (options?.verbose) { - this._logger.log( - `${indent}Emitter config: ${JSON.stringify( - { - renderMode: vfxEmitter.config.renderMode, - duration: vfxEmitter.config.duration, - looping: vfxEmitter.config.looping, - prewarm: vfxEmitter.config.prewarm, - emissionOverTime: vfxEmitter.config.emissionOverTime, - startLife: vfxEmitter.config.startLife, - startSpeed: vfxEmitter.config.startSpeed, - startSize: vfxEmitter.config.startSize, - behaviorsCount: vfxEmitter.config.behaviors?.length || 0, - worldSpace: vfxEmitter.config.worldSpace, - }, - null, - 2 - )}`, - options - ); + private _setGroupParent(groupNode: TransformNode, parentGroup: Nullable, depth: number): void { + if (!parentGroup) { + return; } - // Calculate cumulative scale from parent groups - const cumulativeScale = this._calculateCumulativeScale(currentGroup); - - const emitterData: VFXEmitterData = { - name: emitterName, - config: vfxEmitter.config, - materialId: vfxEmitter.materialId, - // Transform is already converted, will be passed through emitterData - matrix: undefined, - position: undefined, - parentGroup: currentGroup, - cumulativeScale, - }; - - // Store VFX emitter data (including transform) in emitterData for use in factory - emitterData.vfxEmitter = vfxEmitter; + groupNode.setParent(parentGroup); + this._logGroupParentSet(parentGroup.name, depth); + } - if (options?.verbose && (cumulativeScale.x !== 1 || cumulativeScale.y !== 1 || cumulativeScale.z !== 1)) { - this._logger.log( - `${indent}Cumulative scale from parent groups: (${cumulativeScale.x.toFixed(2)}, ${cumulativeScale.y.toFixed(2)}, ${cumulativeScale.z.toFixed(2)})`, - options - ); - } + /** + * Apply transform to a group node + */ + private _applyGroupTransform(groupNode: TransformNode, transform: VFXTransform, depth: number): void { + this._logTransformApplication(groupNode.name, depth); + this._applyTransform(groupNode, transform); + this._logTransformDetails(groupNode, depth); + } - if (!applyTransformations) { - // Phase 1: Create particle system without transformations - const particleSystem = this._emitterFactory.createEmitter(emitterData); + /** + * Apply transform to a node (position, rotation, scale) + */ + private _applyTransform(node: TransformNode, transform: VFXTransform): void { + node.position.copyFrom(transform.position); + node.rotationQuaternion = transform.rotation.clone(); + node.scaling.copyFrom(transform.scale); + } - if (particleSystem) { - this._logger.log(`${indent}Particle system created successfully (without transformations)`, options); + /** + * Store group node in context map + */ + private _storeGroupNode(uuid: string, groupNode: TransformNode, depth: number): void { + this._context.groupNodesMap.set(uuid, groupNode); + this._logGroupStored(uuid, depth); + } - // VFX emitter data is already stored in emitterData, no need to store in particle system + /** + * Create a particle system from VFX Emitter data + */ + private _createParticleSystem(vfxEmitter: VFXEmitter, parentGroup: Nullable, depth: number): Nullable { + this._logEmitterProcessing(vfxEmitter, parentGroup, depth); + this._logEmitterConfig(vfxEmitter, depth); - // Handle prewarm - if (vfxEmitter.config.prewarm) { - particleSystem.start(); - } + const emitterData = this._buildEmitterData(vfxEmitter, parentGroup, depth); + const particleSystem = this._emitterFactory.createEmitter(emitterData) as VFXParticleSystem | VFXSolidParticleSystem | null; - return particleSystem as VFXParticleSystem; - } else { - this._logger.warn(`${indent}Failed to create particle system for ${emitterName}`, options); - return null; - } - } else { - // Phase 2: Apply transformations (this will be handled separately) + if (!particleSystem) { + this._logEmitterCreationFailed(vfxEmitter.name, depth); return null; } + + this._logEmitterCreated(depth); + this._handlePrewarm(particleSystem, vfxEmitter.config.prewarm); + + return particleSystem; } /** - * Apply VFX transform to emitter (Phase 2) - * For SPS, transformations are applied in initParticles (after buildMesh) - * For ParticleSystem, we need to find and update the emitter mesh + * Build emitter data from VFX emitter and parent group */ - private _applyVFXTransformToEmitter(vfxEmitter: VFXEmitter, _currentGroup: Nullable, depth: number, options?: VFXLoaderOptions): void { - const indent = " ".repeat(depth); - const emitterName = vfxEmitter.name; + private _buildEmitterData(vfxEmitter: VFXEmitter, parentGroup: Nullable, depth: number): VFXEmitterData { + const cumulativeScale = this._calculateCumulativeScale(parentGroup); + this._logCumulativeScale(cumulativeScale, depth); - // For SPS: transformations are applied in initParticles (called after buildMesh) - // Transform is already stored in _vfxEmitter and will be applied there - // For ParticleSystem: emitter is set during creation, but we need to apply transform if it's a mesh - // Note: ParticleSystem emitter transformations are handled during creation phase - // because emitter needs to be set before particle system starts - this._logger.log(`${indent}Transformations for emitter ${emitterName} (will be applied in initParticles for SPS)`, options); + return { + name: vfxEmitter.name, + config: vfxEmitter.config, + materialId: vfxEmitter.materialId, + parentGroup, + cumulativeScale, + vfxEmitter, + }; } /** @@ -289,4 +219,127 @@ export class VFXHierarchyProcessor { return cumulativeScale; } + + /** + * Handle prewarm configuration for particle system + */ + private _handlePrewarm(particleSystem: VFXParticleSystem | VFXSolidParticleSystem, prewarm: boolean | undefined): void { + if (prewarm && particleSystem) { + particleSystem.start(); + } + } + + // Type guards + private _isGroup(vfxObj: VFXGroup | VFXEmitter): vfxObj is VFXGroup { + return "children" in vfxObj; + } + + // Logging helpers + private _getIndent(depth: number): string { + return " ".repeat(depth); + } + + private _logInfo(message: string): void { + this._logger.log(message, this._context.options); + } + + private _logWarning(message: string): void { + this._logger.warn(message, this._context.options); + } + + private _logObjectProcessing(objectName: string, depth: number): void { + const indent = this._getIndent(depth); + this._logger.log(`${indent}Processing object: ${objectName}`, this._context.options); + } + + private _logChildrenProcessing(childrenCount: number, depth: number): void { + const indent = this._getIndent(depth); + this._logger.log(`${indent}Processing ${childrenCount} children`, this._context.options); + } + + private _logGroupCreation(groupName: string, depth: number): void { + const indent = this._getIndent(depth); + this._logger.log(`${indent}Creating Group: ${groupName}`, this._context.options); + } + + private _logGroupParentSet(parentName: string, depth: number): void { + const indent = this._getIndent(depth); + this._logger.log(`${indent}Group parent set: ${parentName}`, this._context.options); + } + + private _logTransformApplication(nodeName: string, depth: number): void { + const indent = this._getIndent(depth); + this._logger.log(`${indent}Applying transform to group: ${nodeName}`, this._context.options); + } + + private _logTransformDetails(node: TransformNode, depth: number): void { + const indent = this._getIndent(depth); + const pos = node.position; + const rot = node.rotationQuaternion; + const scl = node.scaling; + + this._logger.log(`${indent}Group position: (${pos.x.toFixed(2)}, ${pos.y.toFixed(2)}, ${pos.z.toFixed(2)})`, this._context.options); + if (rot) { + this._logger.log(`${indent}Group rotation quaternion: (${rot.x.toFixed(4)}, ${rot.y.toFixed(4)}, ${rot.z.toFixed(4)}, ${rot.w.toFixed(4)})`, this._context.options); + } + this._logger.log(`${indent}Group scale: (${scl.x.toFixed(2)}, ${scl.y.toFixed(2)}, ${scl.z.toFixed(2)})`, this._context.options); + } + + private _logGroupStored(uuid: string, depth: number): void { + const indent = this._getIndent(depth); + this._logger.log(`${indent}Group stored in map: ${uuid}`, this._context.options); + } + + private _logEmitterProcessing(vfxEmitter: VFXEmitter, parentGroup: Nullable, depth: number): void { + const indent = this._getIndent(depth); + this._logger.log(`${indent}=== Processing ParticleEmitter: ${vfxEmitter.name} ===`, this._context.options); + this._logger.log(`${indent}Current parent group: ${parentGroup ? parentGroup.name : "none"}`, this._context.options); + } + + private _logEmitterConfig(vfxEmitter: VFXEmitter, depth: number): void { + const { options } = this._context; + if (!options?.verbose) { + return; + } + + const indent = this._getIndent(depth); + const config = { + renderMode: vfxEmitter.config.renderMode, + duration: vfxEmitter.config.duration, + looping: vfxEmitter.config.looping, + prewarm: vfxEmitter.config.prewarm, + emissionOverTime: vfxEmitter.config.emissionOverTime, + startLife: vfxEmitter.config.startLife, + startSpeed: vfxEmitter.config.startSpeed, + startSize: vfxEmitter.config.startSize, + behaviorsCount: vfxEmitter.config.behaviors?.length || 0, + worldSpace: vfxEmitter.config.worldSpace, + }; + + this._logger.log(`${indent}Emitter config: ${JSON.stringify(config, null, 2)}`, options); + } + + private _logCumulativeScale(scale: Vector3, depth: number): void { + const { options } = this._context; + if (!options?.verbose) { + return; + } + + if (scale.x === 1 && scale.y === 1 && scale.z === 1) { + return; + } + + const indent = this._getIndent(depth); + this._logger.log(`${indent}Cumulative scale from parent groups: (${scale.x.toFixed(2)}, ${scale.y.toFixed(2)}, ${scale.z.toFixed(2)})`, options); + } + + private _logEmitterCreated(depth: number): void { + const indent = this._getIndent(depth); + this._logger.log(`${indent}Particle system created successfully`, this._context.options); + } + + private _logEmitterCreationFailed(emitterName: string, depth: number): void { + const indent = this._getIndent(depth); + this._logger.warn(`${indent}Failed to create particle system for ${emitterName}`, this._context.options); + } } From 173379ad6f58acaac47761cfbeca6addcad91f2d Mon Sep 17 00:00:00 2001 From: Mikalai Lazitski Date: Fri, 12 Dec 2025 17:13:20 +0300 Subject: [PATCH 16/62] refactor: update FX Editor properties to utilize VFXEffectNode for improved data handling and clarity --- .../editor/layout/inspector/fields/vector.tsx | 7 +- .../VFX/systems/VFXSolidParticleSystem.ts | 61 ++++ editor/src/editor/windows/fx-editor/graph.tsx | 299 ++---------------- .../src/editor/windows/fx-editor/layout.tsx | 4 +- .../editor/windows/fx-editor/properties.tsx | 103 +++--- .../fx-editor/properties/behaviors.tsx | 67 ++-- .../windows/fx-editor/properties/emission.tsx | 108 +++---- .../fx-editor/properties/emitter-shape.tsx | 206 ++---------- .../windows/fx-editor/properties/object.tsx | 124 +++++++- .../properties/particle-initialization.tsx | 103 +++--- .../properties/particle-renderer.tsx | 245 ++++++-------- .../windows/fx-editor/properties/types.ts | 90 ------ 12 files changed, 499 insertions(+), 918 deletions(-) delete mode 100644 editor/src/editor/windows/fx-editor/properties/types.ts diff --git a/editor/src/editor/layout/inspector/fields/vector.tsx b/editor/src/editor/layout/inspector/fields/vector.tsx index e2447bbb1..24977e195 100644 --- a/editor/src/editor/layout/inspector/fields/vector.tsx +++ b/editor/src/editor/layout/inspector/fields/vector.tsx @@ -22,10 +22,15 @@ export interface IEditorInspectorVectorFieldProps extends IEditorInspectorFieldP } export function EditorInspectorVectorField(props: IEditorInspectorVectorFieldProps) { - const value = props.object[props.property] as IVector4Like; + const value = props.object?.[props.property] as IVector4Like | undefined; const [pointerOver, setPointerOver] = useState(false); + // Return null if value is undefined or null + if (!value || typeof value !== "object") { + return null; + } + return (
setPointerOver(true)} onMouseLeave={() => setPointerOver(false)}>
[]; selectedNodeId: string | number | null; } @@ -44,7 +42,7 @@ export class FXEditorGraph extends Component[], nodeId: string | number): TreeNodeInfo | null { for (const node of nodes) { if (node.id === nodeId) { return node; @@ -59,184 +57,12 @@ export class FXEditorGraph extends Component { - if (n.id === nodeId) { - return { - ...n, - nodeData: data, - label: this._getNodeLabelComponent(n, data.name), - }; - } - if (n.childNodes) { - return { - ...n, - childNodes: this._updateNodeDataInTree(n.childNodes, nodeId, data), - }; - } - return n; - }); - } - /** * Gets node data by ID from tree */ - public getNodeData(nodeId: string | number): IFXNodeData | undefined { + public getNodeData(nodeId: string | number): VFXEffectNode | null { const node = this._findNodeById(this.state.nodes, nodeId); - return node ? (node.nodeData as IFXNodeData) : undefined; - } - - /** - * Sets node data for a node - */ - public setNodeData(nodeId: string | number, data: IFXNodeData): void { - const nodes = this._updateNodeDataInTree(this.state.nodes, nodeId, data); - this.setState({ nodes }); - } - - /** - * Gets or creates particle data for a node - */ - public getOrCreateParticleData(nodeId: string | number): IFXParticleData { - const existing = this.getNodeData(nodeId); - if (existing && isParticleData(existing)) { - return existing; - } - - const newData: any = { - type: "particle", - id: String(nodeId), - name: "Particle", - visibility: true, - position: Vector3.Zero(), - rotation: Vector3.Zero(), - scale: Vector3.One(), - emitterShape: { - shape: "Box", - direction1: Vector3.Up(), - direction2: Vector3.Up(), - minEmitBox: new Vector3(-0.5, -0.5, -0.5), - maxEmitBox: new Vector3(0.5, 0.5, 0.5), - radius: 1.0, - angle: 0.785398, - radiusRange: 0.0, - heightRange: 0.0, - emitFromSpawnPointOnly: false, - height: 1.0, - directionRandomizer: 0.0, - meshPath: null, - }, - particleRenderer: { - renderMode: "Billboard", - worldSpace: false, - material: null, - materialType: "MeshStandardMaterial", - transparent: true, - opacity: 1.0, - side: "Double", - blending: "Add", - color: new Color4(1, 1, 1, 1), - renderOrder: 0, - uvTile: { - column: 1, - row: 1, - startTileIndex: 0, - blendTiles: false, - }, - texture: null, - meshPath: null, - softParticles: false, - }, - emission: { - looping: true, - duration: 5.0, - prewarm: false, - onlyUsedByOtherSystem: false, - emitOverTime: 10, - emitOverDistance: 0, - }, - bursts: [], - particleInitialization: { - startLife: { - functionType: "IntervalValue", - data: { min: 1.0, max: 2.0 }, - }, - startSize: { - functionType: "IntervalValue", - data: { min: 0.1, max: 0.2 }, - }, - startSpeed: { - functionType: "IntervalValue", - data: { min: 1.0, max: 2.0 }, - }, - startColor: { - colorFunctionType: "ConstantColor", - data: { color: new Color4(1, 1, 1, 1) }, - }, - startRotation: { - functionType: "IntervalValue", - data: { min: 0, max: 360 }, - }, - }, - behaviors: [], - }; - - // Update tree and return the new data - this.setNodeData(nodeId, newData); - return newData; - } - - /** - * Sets particle data for a node - */ - public setParticleData(nodeId: string | number, data: IFXParticleData): void { - // Ensure type is set correctly - const newData: IFXParticleData = { ...data, type: "particle" }; - this.setNodeData(nodeId, newData); - } - - /** - * Gets or creates group data for a node - */ - public getOrCreateGroupData(nodeId: string | number): IFXGroupData { - const existing = this.getNodeData(nodeId); - if (existing && isGroupData(existing)) { - return existing; - } - - const newData: any = { - type: "group", - id: String(nodeId), - name: "Group", - visibility: true, - position: new Vector3(0, 0, 0), - rotation: new Vector3(0, 0, 0), - scale: new Vector3(1, 1, 1), - }; - - // Update tree and return the new data - this.setNodeData(nodeId, newData); - return newData; - } - - /** - * Sets group data for a node - */ - public setGroupData(nodeId: string | number, data: IFXGroupData): void { - // Ensure type is set correctly - const newData: IFXGroupData = { ...data, type: "group" }; - this.setNodeData(nodeId, newData); - } - - /** - * Checks if a node is a group - */ - public isGroupNode(nodeId: string | number): boolean { - const data = this.getNodeData(nodeId); - return data !== undefined && isGroupData(data); + return node?.nodeData || null; } public componentDidMount(): void {} @@ -272,46 +98,19 @@ export class FXEditorGraph extends Component { const nodeId = vfxNode.uuid || vfxNode.name; - let nodeData: IFXNodeData; - - if (vfxNode.type === "particle" && vfxNode.system) { - // Particle system node - nodeData = { - type: "particle", - id: nodeId, - name: vfxNode.name, - system: vfxNode.system, - } as any; - } else if (vfxNode.type === "group" && vfxNode.group) { - // Group node - nodeData = { - type: "group", - id: nodeId, - name: vfxNode.name, - transformNode: vfxNode.group, - } as any; - } else { - // Fallback - nodeData = { - type: "group", - id: nodeId, - name: vfxNode.name, - } as any; - } - const childNodes = vfxNode.children.length > 0 ? vfxNode.children.map((child) => this._convertVFXNodeToTreeNode(child)) : undefined; return { id: nodeId, - label: this._getNodeLabelComponent({ id: nodeId, nodeData } as any, vfxNode.name), + label: this._getNodeLabelComponent({ id: nodeId, nodeData: vfxNode } as any, vfxNode.name), icon: vfxNode.type === "particle" ? : , isExpanded: vfxNode.type === "group", childNodes, isSelected: false, hasCaret: vfxNode.type === "group" || (childNodes && childNodes.length > 0), - nodeData, + nodeData: vfxNode, }; } @@ -323,53 +122,12 @@ export class FXEditorGraph extends Component { - // Create node data based on type (only particle and group nodes should be here) - let nodeData: any; - if (node.type === "particle" && node.particleData) { - nodeData = { ...node.particleData, type: "particle" }; - } else if (node.type === "group" && node.groupData) { - // Group node - use groupData from loader if available - nodeData = { ...node.groupData, type: "group" }; - } else { - // Fallback for group node without groupData - nodeData = { - type: "group", - id: node.id, - name: node.name, // Use original name from JSON - visibility: true, - position: Vector3.Zero(), - rotation: Vector3.Zero(), - scale: Vector3.One(), - }; - } - - const treeNode: TreeNodeInfo = { - id: node.id, - label: this._getNodeLabelComponent({ id: node.id, nodeData } as any, nodeData.name), - icon: node.type === "particle" ? : , - isExpanded: node.type === "group", - childNodes: node.children ? this._convertToTreeNodeInfo(node.children, node.id) : undefined, - isSelected: false, - hasCaret: node.type === "group" || (node.children && node.children.length > 0), - nodeData: nodeData, - }; - - return treeNode; - }); - } - /** * Updates all node names in the tree from actual data */ - private _updateAllNodeNames(nodes: TreeNodeInfo[]): TreeNodeInfo[] { + private _updateAllNodeNames(nodes: TreeNodeInfo[]): TreeNodeInfo[] { return nodes.map((n) => { - const nodeData = n.nodeData as IFXNodeData | undefined; - const nodeName = nodeData ? nodeData.name : "Unknown"; + const nodeName = n.nodeData?.name || "Unknown"; const childNodes = n.childNodes ? this._updateAllNodeNames(n.childNodes) : undefined; return { ...n, @@ -421,21 +179,21 @@ export class FXEditorGraph extends Component): void { const nodeId = node.id; const nodes = this._updateNodeExpanded(this.state.nodes, nodeId as string | number, true); this.setState({ nodes }); } - private _handleNodeCollapsed(node: TreeNodeInfo): void { + private _handleNodeCollapsed(node: TreeNodeInfo): void { const nodeId = node.id; const nodes = this._updateNodeExpanded(this.state.nodes, nodeId as string | number, false); this.setState({ nodes }); } - private _updateNodeExpanded(nodes: TreeNodeInfo[], nodeId: string | number, isExpanded: boolean): TreeNodeInfo[] { + private _updateNodeExpanded(nodes: TreeNodeInfo[], nodeId: string | number, isExpanded: boolean): TreeNodeInfo[] { return nodes.map((n) => { - const nodeName = this._getNodeName(n); + const nodeName = n.nodeData?.name || "Unknown"; if (n.id === nodeId) { return { ...n, @@ -453,29 +211,20 @@ export class FXEditorGraph extends Component): "particle" | "group" { + return node.nodeData?.type || "particle"; } - private _handleNodeClicked(node: TreeNodeInfo): void { + private _handleNodeClicked(node: TreeNodeInfo): void { const selectedId = node.id as string | number; const nodes = this._updateNodeSelection(this.state.nodes, selectedId); this.setState({ nodes, selectedNodeId: selectedId }); this.props.onNodeSelected?.(selectedId); } - private _updateNodeSelection(nodes: TreeNodeInfo[], selectedId: string | number): TreeNodeInfo[] { + private _updateNodeSelection(nodes: TreeNodeInfo[], selectedId: string | number): TreeNodeInfo[] { return nodes.map((n) => { - const nodeName = this._getNodeName(n); + const nodeName = n.nodeData?.name || "Unknown"; const isSelected = n.id === selectedId; const childNodes = n.childNodes ? this._updateNodeSelection(n.childNodes, selectedId) : undefined; return { @@ -487,7 +236,7 @@ export class FXEditorGraph extends Component, name: string): JSX.Element { const label =
{name}
; return ( @@ -548,17 +297,17 @@ export class FXEditorGraph extends Component): void { const nodeId = node.id as string | number; this._handleAddParticles(nodeId); } - private _handleAddGroupToNode(node: TreeNodeInfo): void { + private _handleAddGroupToNode(node: TreeNodeInfo): void { const nodeId = node.id as string | number; this._handleAddGroup(nodeId); } - private _addNodeToParent(nodes: TreeNodeInfo[], parentId: string | number, newNode: TreeNodeInfo): TreeNodeInfo[] { + private _addNodeToParent(nodes: TreeNodeInfo[], parentId: string | number, newNode: TreeNodeInfo): TreeNodeInfo[] { return nodes.map((n) => { if (n.id === parentId) { const childNodes = n.childNodes || []; @@ -579,8 +328,8 @@ export class FXEditorGraph extends Component { + private _handleDeleteNode(node: TreeNodeInfo): void { + const deleteNodeById = (nodes: TreeNodeInfo[], id: string | number): TreeNodeInfo[] => { return nodes .filter((n) => n.id !== id) .map((n) => { diff --git a/editor/src/editor/windows/fx-editor/layout.tsx b/editor/src/editor/windows/fx-editor/layout.tsx index 03fe4321d..cba1208f5 100644 --- a/editor/src/editor/windows/fx-editor/layout.tsx +++ b/editor/src/editor/windows/fx-editor/layout.tsx @@ -196,9 +196,7 @@ export class FXEditorLayout extends Component this.props.editor.graph?.getOrCreateParticleData(nodeId)!} - getOrCreateGroupData={(nodeId) => this.props.editor.graph?.getOrCreateGroupData(nodeId)!} - isGroupNode={(nodeId) => this.props.editor.graph?.isGroupNode(nodeId) ?? false} + getNodeData={(nodeId) => this.props.editor.graph?.getNodeData(nodeId) || null} /> ), }; diff --git a/editor/src/editor/windows/fx-editor/properties.tsx b/editor/src/editor/windows/fx-editor/properties.tsx index 5d4b8abd5..006bd1f45 100644 --- a/editor/src/editor/windows/fx-editor/properties.tsx +++ b/editor/src/editor/windows/fx-editor/properties.tsx @@ -8,17 +8,15 @@ import { FXEditorParticleRendererProperties } from "./properties/particle-render import { FXEditorEmissionProperties } from "./properties/emission"; import { FXEditorParticleInitializationProperties } from "./properties/particle-initialization"; import { FXEditorBehaviorsProperties } from "./properties/behaviors"; -import { IFXParticleData, IFXGroupData } from "./properties/types"; import { IFXEditor } from "."; +import type { VFXEffectNode } from "./VFX"; export interface IFXEditorPropertiesProps { filePath: string | null; selectedNodeId: string | number | null; editor: IFXEditor; onNameChanged?: () => void; - getOrCreateParticleData: (nodeId: string | number) => IFXParticleData; - getOrCreateGroupData: (nodeId: string | number) => IFXGroupData; - isGroupNode: (nodeId: string | number) => boolean; + getNodeData: (nodeId: string | number) => VFXEffectNode | null; } export interface IFXEditorPropertiesState {} @@ -57,27 +55,24 @@ export class FXEditorProperties extends Component +

Node not found

+
+ ); + } + + // For groups, show only Object properties + if (nodeData.type === "group" && nodeData.group) { return (
{ this.forceUpdate(); this.props.onNameChanged?.(); @@ -89,39 +84,45 @@ export class FXEditorProperties extends Component + + { + this.forceUpdate(); + this.props.onNameChanged?.(); + }} + /> + + + + this.forceUpdate()} /> + + + + this.forceUpdate()} /> + + + + this.forceUpdate()} /> + + + + this.forceUpdate()} /> + + + + this.forceUpdate()} /> + +
+ ); + } return ( -
- - { - this.forceUpdate(); - this.props.onNameChanged?.(); - }} - /> - - - - this.forceUpdate()} /> - - - - this.forceUpdate()} /> - - - - this.forceUpdate()} /> - - - - this.forceUpdate()} /> - - - - this.forceUpdate()} /> - +
+

Invalid node type

); } diff --git a/editor/src/editor/windows/fx-editor/properties/behaviors.tsx b/editor/src/editor/windows/fx-editor/properties/behaviors.tsx index 4adce7088..bc506a1e4 100644 --- a/editor/src/editor/windows/fx-editor/properties/behaviors.tsx +++ b/editor/src/editor/windows/fx-editor/properties/behaviors.tsx @@ -7,71 +7,42 @@ import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigge import { HiOutlineTrash } from "react-icons/hi2"; import { IoAddSharp } from "react-icons/io5"; -import { IFXParticleData } from "./types"; +import type { VFXEffectNode } from "../VFX"; import { BehaviorRegistry, createDefaultBehaviorData, getBehaviorDefinition } from "./behaviors/registry"; import { BehaviorProperties } from "./behaviors/behavior-properties"; export interface IFXEditorBehaviorsPropertiesProps { - particleData: IFXParticleData; + nodeData: VFXEffectNode; onChange: () => void; } export function FXEditorBehaviorsProperties(props: IFXEditorBehaviorsPropertiesProps): ReactNode { - const { particleData, onChange } = props; + const { nodeData, onChange } = props; + + if (nodeData.type !== "particle" || !nodeData.system) { + return null; + } + + const system = nodeData.system; + // Get behaviors from system (system.behaviors for VFXParticleSystem) + const behaviors: any[] = (system as any).behaviors || []; return ( <> - {particleData.behaviors.map((behavior, index) => { - const definition = getBehaviorDefinition(behavior.type); - const title = definition?.label || behavior.type; + {behaviors.length === 0 &&
No behaviors. Behaviors are applied as functions to particles.
} + {behaviors.map((behavior, index) => { + // Behaviors are functions, not objects with properties + // We can show function name or type if available + const behaviorName = behavior.name || `Behavior ${index + 1}`; return ( - - {title} - -
- } - > - + +
Behavior function (editing not yet supported)
); })} - - - - - - {Object.values(BehaviorRegistry).map((definition) => ( - { - const behaviorData = createDefaultBehaviorData(definition.type); - behaviorData.id = `behavior-${Date.now()}-${Math.random()}`; - particleData.behaviors.push(behaviorData); - onChange(); - }} - > - {definition.label} - - ))} - - + {/* TODO: Add ability to add/remove behaviors */} ); } diff --git a/editor/src/editor/windows/fx-editor/properties/emission.tsx b/editor/src/editor/windows/fx-editor/properties/emission.tsx index 2c10f6c59..8eb1401fd 100644 --- a/editor/src/editor/windows/fx-editor/properties/emission.tsx +++ b/editor/src/editor/windows/fx-editor/properties/emission.tsx @@ -1,78 +1,58 @@ import { ReactNode } from "react"; -import { EditorInspectorSectionField } from "../../../layout/inspector/fields/section"; import { EditorInspectorNumberField } from "../../../layout/inspector/fields/number"; import { EditorInspectorSwitchField } from "../../../layout/inspector/fields/switch"; +import { EditorInspectorBlockField } from "../../../layout/inspector/fields/block"; -import { Button } from "../../../../ui/shadcn/ui/button"; -import { HiOutlineTrash } from "react-icons/hi2"; -import { IoAddSharp } from "react-icons/io5"; - -import { IFXParticleData } from "./types"; +import type { VFXEffectNode } from "../VFX"; +import { VFXParticleSystem, VFXSolidParticleSystem } from "../VFX"; export interface IFXEditorEmissionPropertiesProps { - particleData: IFXParticleData; + nodeData: VFXEffectNode; onChange: () => void; } export function FXEditorEmissionProperties(props: IFXEditorEmissionPropertiesProps): ReactNode { - const { particleData, onChange } = props; + const { nodeData, onChange } = props; + + if (nodeData.type !== "particle" || !nodeData.system) { + return null; + } + + const system = nodeData.system; + + // For VFXParticleSystem, show emission properties + if (system instanceof VFXParticleSystem) { + return ( + <> + + + + +
Emit Power
+
+ + +
+
+ {/* TODO: Add prewarm, onlyUsedByOtherSystem, emitOverDistance properties */} + {/* TODO: Add bursts support */} + + ); + } - return ( - <> - - - - - - + // For VFXSolidParticleSystem, show emission properties + if (system instanceof VFXSolidParticleSystem) { + return ( + <> + + + + {/* TODO: Add prewarm, onlyUsedByOtherSystem, emitOverDistance properties */} + {/* TODO: Add bursts support */} + + ); + } - - {particleData.bursts.map((burst, index) => ( - - Burst {index + 1} - -
- } - > - - - - - -
- ))} - -
- - ); + return null; } diff --git a/editor/src/editor/windows/fx-editor/properties/emitter-shape.tsx b/editor/src/editor/windows/fx-editor/properties/emitter-shape.tsx index 835dde7bf..7e67a069d 100644 --- a/editor/src/editor/windows/fx-editor/properties/emitter-shape.tsx +++ b/editor/src/editor/windows/fx-editor/properties/emitter-shape.tsx @@ -1,208 +1,56 @@ -import { Component, ReactNode, DragEvent } from "react"; -import { extname } from "path/posix"; +import { Component, ReactNode } from "react"; -import { EditorInspectorNumberField } from "../../../layout/inspector/fields/number"; import { EditorInspectorVectorField } from "../../../layout/inspector/fields/vector"; -import { EditorInspectorSwitchField } from "../../../layout/inspector/fields/switch"; -import { EditorInspectorListField } from "../../../layout/inspector/fields/list"; -import { EditorInspectorBlockField } from "../../../layout/inspector/fields/block"; -import { Button } from "../../../../ui/shadcn/ui/button"; -import { AiOutlineClose } from "react-icons/ai"; -import { getProjectAssetsRootUrl } from "../../../../project/configuration"; - -import { IFXParticleData } from "./types"; +import type { VFXEffectNode } from "../VFX"; +import { VFXParticleSystem, VFXSolidParticleSystem } from "../VFX"; export interface IFXEditorEmitterShapePropertiesProps { - particleData: IFXParticleData; + nodeData: VFXEffectNode; onChange: () => void; } -export interface IFXEditorEmitterShapePropertiesState { - meshDragOver: boolean; -} - -export class FXEditorEmitterShapeProperties extends Component { - public constructor(props: IFXEditorEmitterShapePropertiesProps) { - super(props); - this.state = { - meshDragOver: false, - }; - } - +export class FXEditorEmitterShapeProperties extends Component { public render(): ReactNode { - const { particleData } = this.props; - const shape = particleData.emitterShape.shape; + const { nodeData } = this.props; - return ( - <> - this.props.onChange()} - /> - {this._getShapeProperties(shape)} - - ); - } - - private _getShapeProperties(shape: string): ReactNode { - const { particleData } = this.props; - - if (shape === "Box") { - return ( - <> - -
Direction
- - -
- -
Emit Box
- - -
- - ); + if (nodeData.type !== "particle" || !nodeData.system) { + return null; } - if (shape === "Cone") { - return ( - <> - - - - - - - ); - } + const system = nodeData.system; - if (shape === "Sphere") { + // For VFXSolidParticleSystem, emitter shape is in config + if (system instanceof VFXSolidParticleSystem) { + const config = system.config; return ( <> - - - +
Emitter shape: {config.shape?.type || "Default"}
+ {/* TODO: Add shape-specific property editors based on config.shape.type */} ); } - if (shape === "Cylinder") { - return ( - <> - - - - - - ); - } + // For VFXParticleSystem, emitter is a separate object + if (system instanceof VFXParticleSystem) { + const emitter = (system as any).emitter; + + if (!emitter) { + return
No emitter found. Emitter shape properties are set during system creation.
; + } - if (shape === "Hemispheric") { + // Show basic emitter properties return ( <> - - - +
Emitter: {emitter.name || emitter.constructor.name}
+ {emitter.position && } + {emitter.rotationQuaternion && } + {emitter.scaling && } + {/* TODO: Add shape-specific properties based on emitter type (BoxEmitter, ConeEmitter, etc.) */} ); } - if (shape === "Mesh") { - return this._getMeshEmitterField(); - } - - // Point - no properties return null; } - - private _getMeshEmitterField(): ReactNode { - const { particleData } = this.props; - - return ( -
-
Mesh
-
this._handleMeshEmitterDrop(ev)} - onDragOver={this._handleMeshDragOver} - onDragLeave={this._handleMeshDragLeave} - className={`flex items-center px-5 py-2 rounded-lg w-2/3 ${ - this.state.meshDragOver ? "bg-muted-foreground/75 dark:bg-muted-foreground/20" : "bg-muted-foreground/10 dark:bg-muted-foreground/5" - } transition-all duration-300 ease-in-out`} - > -
{particleData.emitterShape.meshPath || "Drop mesh file here"}
- {particleData.emitterShape.meshPath && ( - - )} -
-
- ); - } - - private _handleMeshEmitterDrop = (ev: DragEvent): void => { - ev.preventDefault(); - ev.stopPropagation(); - this.setState({ meshDragOver: false }); - - try { - const data = JSON.parse(ev.dataTransfer.getData("assets")) as string[]; - if (!data || !data.length) { - return; - } - - const absolutePath = data[0]; - const extension = extname(absolutePath).toLowerCase(); - - const meshExtensions = [".x", ".b3d", ".dae", ".glb", ".gltf", ".fbx", ".stl", ".lwo", ".dxf", ".obj", ".3ds", ".ms3d", ".blend", ".babylon"]; - if (!meshExtensions.includes(extension)) { - return; - } - - const rootUrl = getProjectAssetsRootUrl(); - if (!rootUrl) { - return; - } - - const relativePath = absolutePath.replace(rootUrl, ""); - this.props.particleData.emitterShape.meshPath = relativePath; - this.props.onChange(); - } catch (e) { - console.error("Failed to handle mesh emitter drop", e); - } - }; - - private _handleMeshDragOver = (ev: DragEvent): void => { - ev.preventDefault(); - ev.stopPropagation(); - if (ev.dataTransfer.types.includes("assets")) { - this.setState({ meshDragOver: true }); - } - }; - - private _handleMeshDragLeave = (ev: DragEvent): void => { - ev.preventDefault(); - ev.stopPropagation(); - this.setState({ meshDragOver: false }); - }; } diff --git a/editor/src/editor/windows/fx-editor/properties/object.tsx b/editor/src/editor/windows/fx-editor/properties/object.tsx index d4c4a032c..451ad8a29 100644 --- a/editor/src/editor/windows/fx-editor/properties/object.tsx +++ b/editor/src/editor/windows/fx-editor/properties/object.tsx @@ -1,26 +1,124 @@ import { ReactNode } from "react"; +import { Quaternion } from "babylonjs"; import { EditorInspectorStringField } from "../../../layout/inspector/fields/string"; import { EditorInspectorVectorField } from "../../../layout/inspector/fields/vector"; import { EditorInspectorSwitchField } from "../../../layout/inspector/fields/switch"; -import { IFXParticleData } from "./types"; +import type { VFXEffectNode } from "../VFX"; +import { VFXParticleSystem, VFXSolidParticleSystem } from "../VFX"; export interface IFXEditorObjectPropertiesProps { - particleData: IFXParticleData; + nodeData: VFXEffectNode; onChange?: () => void; } +/** + * Creates a rotation inspector that handles rotationQuaternion properly + */ +function getRotationInspector(object: any, onChange?: () => void): ReactNode { + if (!object) { + return null; + } + + // Check if rotationQuaternion exists and is valid + if (object.rotationQuaternion && object.rotationQuaternion instanceof Quaternion) { + const valueRef = object.rotationQuaternion.toEulerAngles(); + + const proxy = new Proxy(valueRef, { + get(target, prop) { + return target[prop as keyof typeof target]; + }, + set(obj, prop, value) { + (obj as any)[prop] = value; + if (object.rotationQuaternion) { + object.rotationQuaternion.copyFrom((obj as any).toQuaternion()); + } + onChange?.(); + return true; + }, + }); + + const o = { proxy }; + + return ; + } + + // Fallback to rotation if it exists + if (object.rotation && typeof object.rotation === "object" && object.rotation.x !== undefined) { + return ; + } + + return null; +} + export function FXEditorObjectProperties(props: IFXEditorObjectPropertiesProps): ReactNode { - const { particleData, onChange } = props; - - return ( - <> - - - - - - - ); + const { nodeData, onChange } = props; + + // For groups, use transformNode directly + if (nodeData.type === "group" && nodeData.group) { + const group = nodeData.group; + + return ( + <> + + + {group.position && } + {getRotationInspector(group, onChange)} + {group.scaling && } + + ); + } + + // For particles, use system.emitter for VFXParticleSystem or system.mesh for VFXSolidParticleSystem + if (nodeData.type === "particle" && nodeData.system) { + const system = nodeData.system; + + // For VFXSolidParticleSystem, use mesh (common mesh for all particles) + if (system instanceof VFXSolidParticleSystem) { + const mesh = system.mesh; + if (!mesh) { + return ( + <> + +
Mesh not available
+ + ); + } + + return ( + <> + + + {mesh.position && } + {getRotationInspector(mesh, onChange)} + {mesh.scaling && } + + ); + } + + // For VFXParticleSystem, use emitter + if (system instanceof VFXParticleSystem) { + const emitter = (system as any).emitter; + if (!emitter) { + return ( + <> + +
Emitter not available
+ + ); + } + + return ( + <> + + {emitter.position && } + {getRotationInspector(emitter, onChange)} + {emitter.scaling && } + + ); + } + } + + return null; } diff --git a/editor/src/editor/windows/fx-editor/properties/particle-initialization.tsx b/editor/src/editor/windows/fx-editor/properties/particle-initialization.tsx index 772664c9b..0634957c5 100644 --- a/editor/src/editor/windows/fx-editor/properties/particle-initialization.tsx +++ b/editor/src/editor/windows/fx-editor/properties/particle-initialization.tsx @@ -1,64 +1,75 @@ import { ReactNode } from "react"; -import { Color4 } from "babylonjs"; -import { IFXParticleData } from "./types"; -import { FunctionEditor } from "./behaviors/function-editor"; -import { ColorFunctionEditor } from "./behaviors/color-function-editor"; +import { EditorInspectorBlockField } from "../../../layout/inspector/fields/block"; +import { EditorInspectorNumberField } from "../../../layout/inspector/fields/number"; +import { EditorInspectorColorField } from "../../../layout/inspector/fields/color"; + +import type { VFXEffectNode } from "../VFX"; +import { VFXParticleSystem, VFXSolidParticleSystem } from "../VFX"; export interface IFXEditorParticleInitializationPropertiesProps { - particleData: IFXParticleData; + nodeData: VFXEffectNode; onChange?: () => void; } export function FXEditorParticleInitializationProperties(props: IFXEditorParticleInitializationPropertiesProps): ReactNode { - const { particleData } = props; + const { nodeData } = props; const onChange = props.onChange || (() => {}); - // Initialize function values if not set - const init = particleData.particleInitialization; - - if (!init.startLife || !init.startLife.functionType) { - init.startLife = { - functionType: "IntervalValue", - data: { min: 1.0, max: 2.0 }, - }; + if (nodeData.type !== "particle" || !nodeData.system) { + return null; } - if (!init.startSize || !init.startSize.functionType) { - init.startSize = { - functionType: "IntervalValue", - data: { min: 0.1, max: 0.2 }, - }; - } - - if (!init.startSpeed || !init.startSpeed.functionType) { - init.startSpeed = { - functionType: "IntervalValue", - data: { min: 1.0, max: 2.0 }, - }; - } + const system = nodeData.system; - if (!init.startColor || !init.startColor.colorFunctionType) { - init.startColor = { - colorFunctionType: "ConstantColor", - data: { color: new Color4(1, 1, 1, 1) }, - }; + // For VFXParticleSystem, show initialization properties + if (system instanceof VFXParticleSystem) { + return ( + <> + +
Life Time
+
+ + +
+
+ +
Size
+
+ + +
+
+ +
Speed (Emit Power)
+
+ + +
+
+ + {system instanceof VFXParticleSystem && system.startColor && ( + + )} + {/* TODO: Add rotation properties */} + + ); } - if (!init.startRotation || !init.startRotation.functionType) { - init.startRotation = { - functionType: "IntervalValue", - data: { min: 0, max: 360 }, - }; + // For VFXSolidParticleSystem, initialization properties are in config (VFXValue format) + // TODO: Add proper editors for VFXValue (ConstantValue, IntervalValue, etc.) + if (system instanceof VFXSolidParticleSystem) { + const config = system.config; + // For now, show that properties exist but need proper VFXValue editors + return ( + <> +
+ Initialization properties are stored in config as VFXValue. Full editor support coming soon. +
+ {/* TODO: Add VFXValue editors for startLife, startSize, startSpeed, startColor */} + + ); } - return ( - <> - - - - - - - ); + return null; } diff --git a/editor/src/editor/windows/fx-editor/properties/particle-renderer.tsx b/editor/src/editor/windows/fx-editor/properties/particle-renderer.tsx index ce1586ab9..43d0e7505 100644 --- a/editor/src/editor/windows/fx-editor/properties/particle-renderer.tsx +++ b/editor/src/editor/windows/fx-editor/properties/particle-renderer.tsx @@ -1,22 +1,18 @@ -import { Component, ReactNode, DragEvent } from "react"; -import { extname } from "path/posix"; +import { Component, ReactNode } from "react"; import { EditorInspectorSectionField } from "../../../layout/inspector/fields/section"; import { EditorInspectorNumberField } from "../../../layout/inspector/fields/number"; -import { EditorInspectorColorField } from "../../../layout/inspector/fields/color"; import { EditorInspectorSwitchField } from "../../../layout/inspector/fields/switch"; import { EditorInspectorListField } from "../../../layout/inspector/fields/list"; import { EditorInspectorTextureField } from "../../../layout/inspector/fields/texture"; -import { Button } from "../../../../ui/shadcn/ui/button"; -import { AiOutlineClose } from "react-icons/ai"; -import { getProjectAssetsRootUrl } from "../../../../project/configuration"; - -import { IFXParticleData } from "./types"; +import { ParticleSystem, SolidParticleSystem } from "babylonjs"; +import type { VFXEffectNode } from "../VFX"; +import { VFXParticleSystem, VFXSolidParticleSystem } from "../VFX"; import { IFXEditor } from ".."; export interface IFXEditorParticleRendererPropertiesProps { - particleData: IFXParticleData; + nodeData: VFXEffectNode; editor: IFXEditor; onChange: () => void; } @@ -34,96 +30,110 @@ export class FXEditorParticleRendererProperties extends Component - this.props.onChange()} - /> - - {/* TODO: Material field */} - this.props.onChange()} - /> - - - - - - - {this._getUVTileSection()} + {isVFXParticleSystem && ( + <> + this.props.onChange()} + /> + this.props.onChange()} /> + + )} + {isVFXSolidParticleSystem && ( + <> +
Render Mode: Mesh
+ {/* For VFXSolidParticleSystem, material properties are on mesh.material */} + + )} {this._getTextureField()} - {this._getRenderModeSpecificProperties(renderMode)} - + {isVFXParticleSystem && ( + <> + this.props.onChange()} + /> + {this._getUVTileSection()} + + + )} + {isVFXSolidParticleSystem && this._getRenderModeSpecificProperties("Mesh")} ); } private _getUVTileSection(): ReactNode { - const { particleData } = this.props; + const { nodeData } = this.props; + + if (nodeData.type !== "particle" || !nodeData.system || !(nodeData.system instanceof VFXParticleSystem)) { + return null; + } + + const system = nodeData.system as VFXParticleSystem; return ( - - - - + + + {/* TODO: Add startTileIndex and blendTiles if available */} ); } private _getTextureField(): ReactNode { - const { particleData, editor } = this.props; + const { nodeData, editor } = this.props; - if (!editor.preview?.scene) { + if (nodeData.type !== "particle" || !nodeData.system || !editor.preview?.scene) { return null; } - return ( - this.props.onChange()} - /> - ); + const system = nodeData.system; + + // For VFXParticleSystem, use particleTexture + if (system instanceof VFXParticleSystem) { + return this.props.onChange()} />; + } + + // For VFXSolidParticleSystem, texture is on the mesh material + if (system instanceof VFXSolidParticleSystem && system.mesh && system.mesh.material) { + const material = system.mesh.material; + // Check if material has diffuseTexture or other texture properties + if ((material as any).diffuseTexture) { + return ( + this.props.onChange()} /> + ); + } + } + + return null; } private _getRenderModeSpecificProperties(renderMode: string): ReactNode { @@ -136,81 +146,20 @@ export class FXEditorParticleRendererProperties extends Component
Mesh
-
this._handleMeshDrop(ev)} - onDragOver={this._handleMeshDragOver} - onDragLeave={this._handleMeshDragLeave} - className={`flex items-center px-5 py-2 rounded-lg w-2/3 ${ - this.state.meshDragOver ? "bg-muted-foreground/75 dark:bg-muted-foreground/20" : "bg-muted-foreground/10 dark:bg-muted-foreground/5" - } transition-all duration-300 ease-in-out`} - > -
{particleData.particleRenderer.meshPath || "Drop mesh file here"}
- {particleData.particleRenderer.meshPath && ( - - )} -
+
{mesh ?
{mesh.name}
:
No mesh
}
); } - - private _handleMeshDrop = (ev: DragEvent): void => { - ev.preventDefault(); - ev.stopPropagation(); - this.setState({ meshDragOver: false }); - - try { - const data = JSON.parse(ev.dataTransfer.getData("assets")) as string[]; - if (!data || !data.length) { - return; - } - - const absolutePath = data[0]; - const extension = extname(absolutePath).toLowerCase(); - - const meshExtensions = [".x", ".b3d", ".dae", ".glb", ".gltf", ".fbx", ".stl", ".lwo", ".dxf", ".obj", ".3ds", ".ms3d", ".blend", ".babylon"]; - if (!meshExtensions.includes(extension)) { - return; - } - - const rootUrl = getProjectAssetsRootUrl(); - if (!rootUrl) { - return; - } - - const relativePath = absolutePath.replace(rootUrl, ""); - this.props.particleData.particleRenderer.meshPath = relativePath; - this.props.onChange(); - } catch (e) { - console.error("Failed to handle mesh drop", e); - } - }; - - private _handleMeshDragOver = (ev: DragEvent): void => { - ev.preventDefault(); - ev.stopPropagation(); - if (ev.dataTransfer.types.includes("assets")) { - this.setState({ meshDragOver: true }); - } - }; - - private _handleMeshDragLeave = (ev: DragEvent): void => { - ev.preventDefault(); - ev.stopPropagation(); - this.setState({ meshDragOver: false }); - }; } diff --git a/editor/src/editor/windows/fx-editor/properties/types.ts b/editor/src/editor/windows/fx-editor/properties/types.ts deleted file mode 100644 index f8f97e892..000000000 --- a/editor/src/editor/windows/fx-editor/properties/types.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { Vector3, Color4 } from "babylonjs"; - -export interface IFXGroupData { - id: string; - name: string; - visibility: boolean; - position: Vector3; - rotation: Vector3; - scale: Vector3; - type: "group"; -} - -export interface IFXParticleData { - type: "particle"; - id: string; - name: string; - visibility: boolean; - position: Vector3; - rotation: Vector3; - scale: Vector3; - emitterShape: { - shape: string; - [key: string]: any; - }; - particleRenderer: { - renderMode: string; - worldSpace: boolean; - material: any; - materialType: string; // MeshBasicMaterial or MeshStandardMaterial - transparent: boolean; - opacity: number; - side: string; - blending: string; - color: Color4; - renderOrder: number; - uvTile: { - column: number; - row: number; - startTileIndex: number; - blendTiles: boolean; - }; - texture: any; - meshPath: string | null; - softParticles: boolean; - }; - emission: { - looping: boolean; - duration: number; - prewarm: boolean; - onlyUsedByOtherSystem: boolean; - emitOverTime: number; - emitOverDistance: number; - }; - bursts: Array<{ - id?: string; - time: number; - count: number; - cycle: number; - interval: number; - probability: number; - }>; - particleInitialization: { - startLife: any; // Function: ConstantValue | IntervalValue | PiecewiseBezier - startSize: any; // Function: ConstantValue | IntervalValue | PiecewiseBezier - startSpeed: any; // Function: ConstantValue | IntervalValue | PiecewiseBezier - startColor: any; // ColorFunction: ConstantColor | ColorRange | Gradient | RandomColor | RandomColorBetweenGradient - startRotation: any; // Function: ConstantValue | IntervalValue | PiecewiseBezier - }; - behaviors: Array<{ - id?: string; - type: string; - [key: string]: any; - }>; -} - -export type IFXNodeData = IFXParticleData | IFXGroupData; - -/** - * Type guard to check if node data is a group - */ -export function isGroupData(data: IFXNodeData): data is IFXGroupData { - return data.type === "group"; -} - -/** - * Type guard to check if node data is a particle - */ -export function isParticleData(data: IFXNodeData): data is IFXParticleData { - return data.type === "particle"; -} From 8202dfef3d5b221561ddd0fded539eaff0c7f65e Mon Sep 17 00:00:00 2001 From: Mikalai Lazitski Date: Fri, 12 Dec 2025 18:56:03 +0300 Subject: [PATCH 17/62] refactor: update FX Editor properties to utilize VFXEffectNode for improved data handling and clarity --- .../fx-editor/VFX/behaviors/colorBySpeed.ts | 26 +- .../fx-editor/VFX/behaviors/colorOverLife.ts | 8 +- .../fx-editor/VFX/behaviors/forceOverLife.ts | 20 +- .../fx-editor/VFX/behaviors/frameOverLife.ts | 6 +- .../VFX/behaviors/limitSpeedOverLife.ts | 10 +- .../fx-editor/VFX/behaviors/orbitOverLife.ts | 28 +- .../VFX/behaviors/rotationBySpeed.ts | 57 ++- .../VFX/behaviors/rotationOverLife.ts | 27 +- .../fx-editor/VFX/behaviors/sizeBySpeed.ts | 36 +- .../fx-editor/VFX/behaviors/sizeOverLife.ts | 8 +- .../fx-editor/VFX/behaviors/speedOverLife.ts | 16 +- .../factories/VFXBehaviorFunctionFactory.ts | 186 --------- .../VFX/factories/VFXEmitterFactory.ts | 108 +----- .../VFXParticleSystemBehaviorFactory.ts | 65 ++++ .../VFXSolidParticleSystemBehaviorFactory.ts | 132 +++++++ .../VFXSystemFactory.ts} | 215 +++++----- .../src/editor/windows/fx-editor/VFX/index.ts | 6 +- .../fx-editor/VFX/parsers/VFXDataConverter.ts | 10 +- .../fx-editor/VFX/parsers/VFXParser.ts | 8 +- .../VFX/systems/VFXParticleSystem.ts | 171 +++++++- .../VFX/systems/VFXSolidParticleSystem.ts | 366 +++++++++++------- .../VFX/types/VFXBehaviorFunction.ts | 25 +- .../windows/fx-editor/VFX/types/context.ts | 4 +- .../windows/fx-editor/VFX/types/hierarchy.ts | 6 +- .../windows/fx-editor/VFX/types/index.ts | 2 +- .../fx-editor/VFX/utils/valueParser.ts | 138 +++++++ .../fx-editor/properties/behaviors.tsx | 77 +++- .../fx-editor/properties/emitter-shape.tsx | 7 +- .../properties/particle-initialization.tsx | 5 +- 29 files changed, 1072 insertions(+), 701 deletions(-) delete mode 100644 editor/src/editor/windows/fx-editor/VFX/factories/VFXBehaviorFunctionFactory.ts create mode 100644 editor/src/editor/windows/fx-editor/VFX/factories/VFXParticleSystemBehaviorFactory.ts create mode 100644 editor/src/editor/windows/fx-editor/VFX/factories/VFXSolidParticleSystemBehaviorFactory.ts rename editor/src/editor/windows/fx-editor/VFX/{processors/VFXHierarchyProcessor.ts => factories/VFXSystemFactory.ts} (53%) create mode 100644 editor/src/editor/windows/fx-editor/VFX/utils/valueParser.ts diff --git a/editor/src/editor/windows/fx-editor/VFX/behaviors/colorBySpeed.ts b/editor/src/editor/windows/fx-editor/VFX/behaviors/colorBySpeed.ts index c9cc89a3f..4b40ee122 100644 --- a/editor/src/editor/windows/fx-editor/VFX/behaviors/colorBySpeed.ts +++ b/editor/src/editor/windows/fx-editor/VFX/behaviors/colorBySpeed.ts @@ -1,19 +1,23 @@ -import { SolidParticle, Particle } from "babylonjs"; +import { SolidParticle, Particle, Vector3 } from "babylonjs"; import type { VFXColorBySpeedBehavior } from "../types/behaviors"; import { interpolateColorKeys } from "./utils"; -import { VFXValueParser } from "../parsers/VFXValueParser"; +import { VFXValueUtils } from "../utils/valueParser"; /** * Apply ColorBySpeed behavior to Particle + * Gets currentSpeed from particle.velocity magnitude */ -export function applyColorBySpeedPS(particle: Particle, behavior: VFXColorBySpeedBehavior, currentSpeed: number, valueParser: VFXValueParser): void { - if (!behavior.color || !behavior.color.keys || !particle.color) { +export function applyColorBySpeedPS(particle: Particle, behavior: VFXColorBySpeedBehavior): void { + if (!behavior.color || !behavior.color.keys || !particle.color || !particle.direction) { return; } + // Get current speed from particle velocity/direction + const currentSpeed = Vector3.Distance(Vector3.Zero(), particle.direction); + const colorKeys = behavior.color.keys; - const minSpeed = behavior.minSpeed !== undefined ? valueParser.parseConstantValue(behavior.minSpeed) : 0; - const maxSpeed = behavior.maxSpeed !== undefined ? valueParser.parseConstantValue(behavior.maxSpeed) : 1; + const minSpeed = behavior.minSpeed !== undefined ? VFXValueUtils.parseConstantValue(behavior.minSpeed) : 0; + const maxSpeed = behavior.maxSpeed !== undefined ? VFXValueUtils.parseConstantValue(behavior.maxSpeed) : 1; const speedRatio = Math.max(0, Math.min(1, (currentSpeed - minSpeed) / (maxSpeed - minSpeed || 1))); const interpolatedColor = interpolateColorKeys(colorKeys, speedRatio); @@ -34,15 +38,19 @@ export function applyColorBySpeedPS(particle: Particle, behavior: VFXColorBySpee /** * Apply ColorBySpeed behavior to SolidParticle + * Gets currentSpeed from particle.velocity magnitude */ -export function applyColorBySpeedSPS(particle: SolidParticle, behavior: VFXColorBySpeedBehavior, currentSpeed: number, valueParser: VFXValueParser): void { +export function applyColorBySpeedSPS(particle: SolidParticle, behavior: VFXColorBySpeedBehavior): void { if (!behavior.color || !behavior.color.keys || !particle.color) { return; } + // Get current speed from particle velocity + const currentSpeed = Math.sqrt(particle.velocity.x * particle.velocity.x + particle.velocity.y * particle.velocity.y + particle.velocity.z * particle.velocity.z); + const colorKeys = behavior.color.keys; - const minSpeed = behavior.minSpeed !== undefined ? valueParser.parseConstantValue(behavior.minSpeed) : 0; - const maxSpeed = behavior.maxSpeed !== undefined ? valueParser.parseConstantValue(behavior.maxSpeed) : 1; + const minSpeed = behavior.minSpeed !== undefined ? VFXValueUtils.parseConstantValue(behavior.minSpeed) : 0; + const maxSpeed = behavior.maxSpeed !== undefined ? VFXValueUtils.parseConstantValue(behavior.maxSpeed) : 1; const speedRatio = Math.max(0, Math.min(1, (currentSpeed - minSpeed) / (maxSpeed - minSpeed || 1))); const interpolatedColor = interpolateColorKeys(colorKeys, speedRatio); diff --git a/editor/src/editor/windows/fx-editor/VFX/behaviors/colorOverLife.ts b/editor/src/editor/windows/fx-editor/VFX/behaviors/colorOverLife.ts index d0d373956..a289205c9 100644 --- a/editor/src/editor/windows/fx-editor/VFX/behaviors/colorOverLife.ts +++ b/editor/src/editor/windows/fx-editor/VFX/behaviors/colorOverLife.ts @@ -39,12 +39,16 @@ export function applyColorOverLifePS(particleSystem: ParticleSystem, behavior: V /** * Apply ColorOverLife behavior to SolidParticle + * Gets lifeRatio from particle (age / lifeTime) */ -export function applyColorOverLifeSPS(particle: SolidParticle, behavior: VFXColorOverLifeBehavior, lifeRatio: number): void { - if (!behavior.color || !particle.color) { +export function applyColorOverLifeSPS(particle: SolidParticle, behavior: VFXColorOverLifeBehavior): void { + if (!behavior.color || !particle.color || particle.lifeTime <= 0) { return; } + // Get lifeRatio from particle + const lifeRatio = particle.age / particle.lifeTime; + const colorKeys = behavior.color.color?.keys ?? behavior.color.keys; if (!colorKeys || !Array.isArray(colorKeys)) { return; diff --git a/editor/src/editor/windows/fx-editor/VFX/behaviors/forceOverLife.ts b/editor/src/editor/windows/fx-editor/VFX/behaviors/forceOverLife.ts index e4e1be26b..6f12d6982 100644 --- a/editor/src/editor/windows/fx-editor/VFX/behaviors/forceOverLife.ts +++ b/editor/src/editor/windows/fx-editor/VFX/behaviors/forceOverLife.ts @@ -1,22 +1,22 @@ import { Vector3, ParticleSystem } from "babylonjs"; import type { VFXForceOverLifeBehavior, VFXGravityForceBehavior } from "../types/behaviors"; -import { VFXValueParser } from "../parsers/VFXValueParser"; +import { VFXValueUtils } from "../utils/valueParser"; /** * Apply ForceOverLife behavior to ParticleSystem */ -export function applyForceOverLifePS(particleSystem: ParticleSystem, behavior: VFXForceOverLifeBehavior, valueParser: VFXValueParser): void { +export function applyForceOverLifePS(particleSystem: ParticleSystem, behavior: VFXForceOverLifeBehavior): void { if (behavior.force) { - const forceX = behavior.force.x !== undefined ? valueParser.parseConstantValue(behavior.force.x) : 0; - const forceY = behavior.force.y !== undefined ? valueParser.parseConstantValue(behavior.force.y) : 0; - const forceZ = behavior.force.z !== undefined ? valueParser.parseConstantValue(behavior.force.z) : 0; + const forceX = behavior.force.x !== undefined ? VFXValueUtils.parseConstantValue(behavior.force.x) : 0; + const forceY = behavior.force.y !== undefined ? VFXValueUtils.parseConstantValue(behavior.force.y) : 0; + const forceZ = behavior.force.z !== undefined ? VFXValueUtils.parseConstantValue(behavior.force.z) : 0; if (Math.abs(forceY) > 0.01 || Math.abs(forceX) > 0.01 || Math.abs(forceZ) > 0.01) { particleSystem.gravity = new Vector3(forceX, forceY, forceZ); } } else if (behavior.x !== undefined || behavior.y !== undefined || behavior.z !== undefined) { - const forceX = behavior.x !== undefined ? valueParser.parseConstantValue(behavior.x) : 0; - const forceY = behavior.y !== undefined ? valueParser.parseConstantValue(behavior.y) : 0; - const forceZ = behavior.z !== undefined ? valueParser.parseConstantValue(behavior.z) : 0; + const forceX = behavior.x !== undefined ? VFXValueUtils.parseConstantValue(behavior.x) : 0; + const forceY = behavior.y !== undefined ? VFXValueUtils.parseConstantValue(behavior.y) : 0; + const forceZ = behavior.z !== undefined ? VFXValueUtils.parseConstantValue(behavior.z) : 0; if (Math.abs(forceY) > 0.01 || Math.abs(forceX) > 0.01 || Math.abs(forceZ) > 0.01) { particleSystem.gravity = new Vector3(forceX, forceY, forceZ); } @@ -26,9 +26,9 @@ export function applyForceOverLifePS(particleSystem: ParticleSystem, behavior: V /** * Apply GravityForce behavior to ParticleSystem */ -export function applyGravityForcePS(particleSystem: ParticleSystem, behavior: VFXGravityForceBehavior, valueParser: VFXValueParser): void { +export function applyGravityForcePS(particleSystem: ParticleSystem, behavior: VFXGravityForceBehavior): void { if (behavior.gravity !== undefined) { - const gravity = valueParser.parseConstantValue(behavior.gravity); + const gravity = VFXValueUtils.parseConstantValue(behavior.gravity); particleSystem.gravity = new Vector3(0, -gravity, 0); } } diff --git a/editor/src/editor/windows/fx-editor/VFX/behaviors/frameOverLife.ts b/editor/src/editor/windows/fx-editor/VFX/behaviors/frameOverLife.ts index 79d3fd610..9e046b63c 100644 --- a/editor/src/editor/windows/fx-editor/VFX/behaviors/frameOverLife.ts +++ b/editor/src/editor/windows/fx-editor/VFX/behaviors/frameOverLife.ts @@ -1,11 +1,11 @@ import { ParticleSystem } from "babylonjs"; import type { VFXFrameOverLifeBehavior } from "../types/behaviors"; -import { VFXValueParser } from "../parsers/VFXValueParser"; +import { VFXValueUtils } from "../utils/valueParser"; /** * Apply FrameOverLife behavior to ParticleSystem */ -export function applyFrameOverLifePS(particleSystem: ParticleSystem, behavior: VFXFrameOverLifeBehavior, valueParser: VFXValueParser): void { +export function applyFrameOverLifePS(particleSystem: ParticleSystem, behavior: VFXFrameOverLifeBehavior): void { if (!behavior.frame) { return; } @@ -28,7 +28,7 @@ export function applyFrameOverLifePS(particleSystem: ParticleSystem, behavior: V particleSystem.endSpriteCellID = Math.floor(frames[frames.length - 1] || frames[0]); } } else if (typeof behavior.frame === "number" || (typeof behavior.frame === "object" && behavior.frame !== null && "type" in behavior.frame)) { - const frameValue = valueParser.parseConstantValue(behavior.frame); + const frameValue = VFXValueUtils.parseConstantValue(behavior.frame); particleSystem.startSpriteCellID = Math.floor(frameValue); particleSystem.endSpriteCellID = Math.floor(frameValue); } diff --git a/editor/src/editor/windows/fx-editor/VFX/behaviors/limitSpeedOverLife.ts b/editor/src/editor/windows/fx-editor/VFX/behaviors/limitSpeedOverLife.ts index 2b11b7a42..d9f7a7ccb 100644 --- a/editor/src/editor/windows/fx-editor/VFX/behaviors/limitSpeedOverLife.ts +++ b/editor/src/editor/windows/fx-editor/VFX/behaviors/limitSpeedOverLife.ts @@ -1,19 +1,19 @@ import { ParticleSystem } from "babylonjs"; import type { VFXLimitSpeedOverLifeBehavior } from "../types/behaviors"; import { extractNumberFromValue } from "./utils"; -import { VFXValueParser } from "../parsers/VFXValueParser"; +import { VFXValueUtils } from "../utils/valueParser"; /** * Apply LimitSpeedOverLife behavior to ParticleSystem */ -export function applyLimitSpeedOverLifePS(particleSystem: ParticleSystem, behavior: VFXLimitSpeedOverLifeBehavior, valueParser: VFXValueParser): void { +export function applyLimitSpeedOverLifePS(particleSystem: ParticleSystem, behavior: VFXLimitSpeedOverLifeBehavior): void { if (behavior.dampen !== undefined) { - const dampen = valueParser.parseConstantValue(behavior.dampen); + const dampen = VFXValueUtils.parseConstantValue(behavior.dampen); particleSystem.limitVelocityDamping = dampen; } if (behavior.maxSpeed !== undefined) { - const speedLimit = valueParser.parseConstantValue(behavior.maxSpeed); + const speedLimit = VFXValueUtils.parseConstantValue(behavior.maxSpeed); particleSystem.addLimitVelocityGradient(0, speedLimit); particleSystem.addLimitVelocityGradient(1, speedLimit); } else if (behavior.speed !== undefined) { @@ -27,7 +27,7 @@ export function applyLimitSpeedOverLifePS(particleSystem: ParticleSystem, behavi } } } else if (typeof behavior.speed === "number" || (typeof behavior.speed === "object" && behavior.speed !== null && "type" in behavior.speed)) { - const speedLimit = valueParser.parseConstantValue(behavior.speed); + const speedLimit = VFXValueUtils.parseConstantValue(behavior.speed); particleSystem.addLimitVelocityGradient(0, speedLimit); particleSystem.addLimitVelocityGradient(1, speedLimit); } diff --git a/editor/src/editor/windows/fx-editor/VFX/behaviors/orbitOverLife.ts b/editor/src/editor/windows/fx-editor/VFX/behaviors/orbitOverLife.ts index 67ca08114..062e29bfa 100644 --- a/editor/src/editor/windows/fx-editor/VFX/behaviors/orbitOverLife.ts +++ b/editor/src/editor/windows/fx-editor/VFX/behaviors/orbitOverLife.ts @@ -1,17 +1,21 @@ import { Particle, SolidParticle } from "babylonjs"; import type { VFXOrbitOverLifeBehavior } from "../types/behaviors"; import { extractNumberFromValue, interpolateGradientKeys } from "./utils"; -import { VFXValueParser } from "../parsers/VFXValueParser"; -import { VFXValue } from "../types"; +import { VFXValueUtils } from "../utils/valueParser"; +import type { VFXValue } from "../types/values"; /** * Apply OrbitOverLife behavior to Particle + * Gets lifeRatio from particle (age / lifeTime) */ -export function applyOrbitOverLifePS(particle: Particle, behavior: VFXOrbitOverLifeBehavior, lifeRatio: number, valueParser: VFXValueParser): void { - if (!behavior.radius) { +export function applyOrbitOverLifePS(particle: Particle, behavior: VFXOrbitOverLifeBehavior): void { + if (!behavior.radius || particle.lifeTime <= 0) { return; } + // Get lifeRatio from particle + const lifeRatio = particle.age / particle.lifeTime; + // Parse radius (can be VFXValue with keys or constant/interval) let radius = 1; const radiusValue = behavior.radius; @@ -28,11 +32,11 @@ export function applyOrbitOverLifePS(particle: Particle, behavior: VFXOrbitOverL radius = interpolateGradientKeys(radiusValue.keys, lifeRatio, extractNumberFromValue); } else if (radiusValue !== undefined && radiusValue !== null) { // Parse as VFXValue (number, VFXConstantValue, or VFXIntervalValue) - const parsedRadius = valueParser.parseIntervalValue(radiusValue as VFXValue); + const parsedRadius = VFXValueUtils.parseIntervalValue(radiusValue as VFXValue); radius = parsedRadius.min + (parsedRadius.max - parsedRadius.min) * lifeRatio; } - const speed = behavior.speed !== undefined ? valueParser.parseConstantValue(behavior.speed) : 1; + const speed = behavior.speed !== undefined ? VFXValueUtils.parseConstantValue(behavior.speed) : 1; const angle = lifeRatio * speed * Math.PI * 2; // Calculate orbit offset relative to center @@ -54,12 +58,16 @@ export function applyOrbitOverLifePS(particle: Particle, behavior: VFXOrbitOverL /** * Apply OrbitOverLife behavior to SolidParticle + * Gets lifeRatio from particle (age / lifeTime) */ -export function applyOrbitOverLifeSPS(particle: SolidParticle, behavior: VFXOrbitOverLifeBehavior, lifeRatio: number, valueParser: VFXValueParser): void { - if (!behavior.radius) { +export function applyOrbitOverLifeSPS(particle: SolidParticle, behavior: VFXOrbitOverLifeBehavior): void { + if (!behavior.radius || particle.lifeTime <= 0) { return; } + // Get lifeRatio from particle + const lifeRatio = particle.age / particle.lifeTime; + // Parse radius (can be VFXValue with keys or constant/interval) let radius = 1; const radiusValue = behavior.radius; @@ -76,11 +84,11 @@ export function applyOrbitOverLifeSPS(particle: SolidParticle, behavior: VFXOrbi radius = interpolateGradientKeys(radiusValue.keys, lifeRatio, extractNumberFromValue); } else if (radiusValue !== undefined && radiusValue !== null) { // Parse as VFXValue (number, VFXConstantValue, or VFXIntervalValue) - const parsedRadius = valueParser.parseIntervalValue(radiusValue as VFXValue); + const parsedRadius = VFXValueUtils.parseIntervalValue(radiusValue as VFXValue); radius = parsedRadius.min + (parsedRadius.max - parsedRadius.min) * lifeRatio; } - const speed = behavior.speed !== undefined ? valueParser.parseConstantValue(behavior.speed) : 1; + const speed = behavior.speed !== undefined ? VFXValueUtils.parseConstantValue(behavior.speed) : 1; const angle = lifeRatio * speed * Math.PI * 2; // Calculate orbit offset relative to center diff --git a/editor/src/editor/windows/fx-editor/VFX/behaviors/rotationBySpeed.ts b/editor/src/editor/windows/fx-editor/VFX/behaviors/rotationBySpeed.ts index e98210f3c..8ca493fdb 100644 --- a/editor/src/editor/windows/fx-editor/VFX/behaviors/rotationBySpeed.ts +++ b/editor/src/editor/windows/fx-editor/VFX/behaviors/rotationBySpeed.ts @@ -1,29 +1,23 @@ -import { Particle, ParticleSystem, SolidParticle } from "babylonjs"; +import { Particle, SolidParticle, Vector3 } from "babylonjs"; import type { VFXRotationBySpeedBehavior } from "../types/behaviors"; import { extractNumberFromValue, interpolateGradientKeys } from "./utils"; -import { VFXValueParser } from "../parsers/VFXValueParser"; - -/** - * Extended Particle interface for custom behaviors - */ -interface ExtendedParticle extends Particle { - startSpeed?: number; -} +import { VFXValueUtils } from "../utils/valueParser"; /** * Apply RotationBySpeed behavior to Particle + * Gets currentSpeed from particle.direction magnitude and updateSpeed from system */ -export function applyRotationBySpeedPS( - particle: ExtendedParticle, - behavior: VFXRotationBySpeedBehavior, - currentSpeed: number, - _particleSystem: ParticleSystem, - valueParser: VFXValueParser -): void { - if (!behavior.angularVelocity) { +export function applyRotationBySpeedPS(particle: Particle, behavior: VFXRotationBySpeedBehavior): void { + if (!behavior.angularVelocity || !particle.direction) { return; } + // Get current speed from particle velocity/direction + const currentSpeed = Vector3.Distance(Vector3.Zero(), particle.direction); + + // Get updateSpeed from system (stored in particle or use default) + const updateSpeed = (particle as any).particleSystem?.updateSpeed ?? 0.016; + // angularVelocity can be VFXValue (constant/interval) or object with keys let angularSpeed = 0; if ( @@ -33,32 +27,33 @@ export function applyRotationBySpeedPS( Array.isArray(behavior.angularVelocity.keys) && behavior.angularVelocity.keys.length > 0 ) { - const minSpeed = behavior.minSpeed !== undefined ? valueParser.parseConstantValue(behavior.minSpeed) : 0; - const maxSpeed = behavior.maxSpeed !== undefined ? valueParser.parseConstantValue(behavior.maxSpeed) : 1; + const minSpeed = behavior.minSpeed !== undefined ? VFXValueUtils.parseConstantValue(behavior.minSpeed) : 0; + const maxSpeed = behavior.maxSpeed !== undefined ? VFXValueUtils.parseConstantValue(behavior.maxSpeed) : 1; const speedRatio = Math.max(0, Math.min(1, (currentSpeed - minSpeed) / (maxSpeed - minSpeed || 1))); angularSpeed = interpolateGradientKeys(behavior.angularVelocity.keys, speedRatio, extractNumberFromValue); } else { - const angularVel = valueParser.parseIntervalValue(behavior.angularVelocity); + const angularVel = VFXValueUtils.parseIntervalValue(behavior.angularVelocity); angularSpeed = angularVel.min + (angularVel.max - angularVel.min) * 0.5; // Use middle value } - particle.angle += angularSpeed * 0.016; // Assuming ~60fps + particle.angle += angularSpeed * updateSpeed; } /** * Apply RotationBySpeed behavior to SolidParticle + * Gets currentSpeed from particle.velocity magnitude and updateSpeed from system */ -export function applyRotationBySpeedSPS( - particle: SolidParticle, - behavior: VFXRotationBySpeedBehavior, - currentSpeed: number, - valueParser: VFXValueParser, - updateSpeed: number = 0.016 -): void { +export function applyRotationBySpeedSPS(particle: SolidParticle, behavior: VFXRotationBySpeedBehavior): void { if (!behavior.angularVelocity) { return; } + // Get current speed from particle velocity + const currentSpeed = Math.sqrt(particle.velocity.x * particle.velocity.x + particle.velocity.y * particle.velocity.y + particle.velocity.z * particle.velocity.z); + + // Get updateSpeed from system (stored in particle.props or use default) + const updateSpeed = (particle as any).system?.updateSpeed ?? 0.016; + // angularVelocity can be VFXValue (constant/interval) or object with keys let angularSpeed = 0; if ( @@ -68,12 +63,12 @@ export function applyRotationBySpeedSPS( Array.isArray(behavior.angularVelocity.keys) && behavior.angularVelocity.keys.length > 0 ) { - const minSpeed = behavior.minSpeed !== undefined ? valueParser.parseConstantValue(behavior.minSpeed) : 0; - const maxSpeed = behavior.maxSpeed !== undefined ? valueParser.parseConstantValue(behavior.maxSpeed) : 1; + const minSpeed = behavior.minSpeed !== undefined ? VFXValueUtils.parseConstantValue(behavior.minSpeed) : 0; + const maxSpeed = behavior.maxSpeed !== undefined ? VFXValueUtils.parseConstantValue(behavior.maxSpeed) : 1; const speedRatio = Math.max(0, Math.min(1, (currentSpeed - minSpeed) / (maxSpeed - minSpeed || 1))); angularSpeed = interpolateGradientKeys(behavior.angularVelocity.keys, speedRatio, extractNumberFromValue); } else { - const angularVel = valueParser.parseIntervalValue(behavior.angularVelocity); + const angularVel = VFXValueUtils.parseIntervalValue(behavior.angularVelocity); angularSpeed = angularVel.min + (angularVel.max - angularVel.min) * 0.5; // Use middle value } diff --git a/editor/src/editor/windows/fx-editor/VFX/behaviors/rotationOverLife.ts b/editor/src/editor/windows/fx-editor/VFX/behaviors/rotationOverLife.ts index 5d28c4211..0be4f3215 100644 --- a/editor/src/editor/windows/fx-editor/VFX/behaviors/rotationOverLife.ts +++ b/editor/src/editor/windows/fx-editor/VFX/behaviors/rotationOverLife.ts @@ -1,13 +1,13 @@ -import type { ParticleSystem, SolidParticle } from "babylonjs"; +import { ParticleSystem, SolidParticle } from "babylonjs"; import type { VFXRotationOverLifeBehavior } from "../types/behaviors"; -import { VFXValueParser } from "../parsers/VFXValueParser"; +import { VFXValueUtils } from "../utils/valueParser"; /** * Apply RotationOverLife behavior to ParticleSystem */ -export function applyRotationOverLifePS(particleSystem: ParticleSystem, behavior: VFXRotationOverLifeBehavior, valueParser: VFXValueParser): void { +export function applyRotationOverLifePS(particleSystem: ParticleSystem, behavior: VFXRotationOverLifeBehavior): void { if (behavior.angularVelocity) { - const angularVel = valueParser.parseIntervalValue(behavior.angularVelocity); + const angularVel = VFXValueUtils.parseIntervalValue(behavior.angularVelocity); particleSystem.minAngularSpeed = angularVel.min; particleSystem.maxAngularSpeed = angularVel.max; } @@ -15,19 +15,20 @@ export function applyRotationOverLifePS(particleSystem: ParticleSystem, behavior /** * Apply RotationOverLife behavior to SolidParticle + * Gets lifeRatio from particle (age / lifeTime) and updateSpeed from system */ -export function applyRotationOverLifeSPS( - particle: SolidParticle, - behavior: VFXRotationOverLifeBehavior, - lifeRatio: number, - valueParser: VFXValueParser, - updateSpeed: number = 0.016 -): void { - if (!behavior.angularVelocity) { +export function applyRotationOverLifeSPS(particle: SolidParticle, behavior: VFXRotationOverLifeBehavior): void { + if (!behavior.angularVelocity || particle.lifeTime <= 0) { return; } - const angularVel = valueParser.parseIntervalValue(behavior.angularVelocity); + // Get lifeRatio from particle + const lifeRatio = particle.age / particle.lifeTime; + + // Get updateSpeed from system (stored in particle.props or use default) + const updateSpeed = (particle as any).system?.updateSpeed ?? 0.016; + + const angularVel = VFXValueUtils.parseIntervalValue(behavior.angularVelocity); const angularSpeed = angularVel.min + (angularVel.max - angularVel.min) * lifeRatio; // Apply rotation around Z axis (2D rotation) diff --git a/editor/src/editor/windows/fx-editor/VFX/behaviors/sizeBySpeed.ts b/editor/src/editor/windows/fx-editor/VFX/behaviors/sizeBySpeed.ts index ad73a848d..781fc91bf 100644 --- a/editor/src/editor/windows/fx-editor/VFX/behaviors/sizeBySpeed.ts +++ b/editor/src/editor/windows/fx-editor/VFX/behaviors/sizeBySpeed.ts @@ -1,45 +1,45 @@ -import type { Particle, SolidParticle } from "babylonjs"; +import { Particle, SolidParticle, Vector3 } from "babylonjs"; import type { VFXSizeBySpeedBehavior } from "../types/behaviors"; import { extractNumberFromValue, interpolateGradientKeys } from "./utils"; -import { VFXValueParser } from "../parsers/VFXValueParser"; - -/** - * Extended Particle interface for custom behaviors - */ -interface ExtendedParticle extends Particle { - startSpeed?: number; - startSize?: number; -} +import { VFXValueUtils } from "../utils/valueParser"; /** * Apply SizeBySpeed behavior to Particle + * Gets currentSpeed from particle.direction magnitude */ -export function applySizeBySpeedPS(particle: ExtendedParticle, behavior: VFXSizeBySpeedBehavior, currentSpeed: number, valueParser: VFXValueParser): void { - if (!behavior.size || !behavior.size.keys) { +export function applySizeBySpeedPS(particle: Particle, behavior: VFXSizeBySpeedBehavior): void { + if (!behavior.size || !behavior.size.keys || !particle.direction) { return; } + // Get current speed from particle velocity/direction + const currentSpeed = Vector3.Distance(Vector3.Zero(), particle.direction); + const sizeKeys = behavior.size.keys; - const minSpeed = behavior.minSpeed !== undefined ? valueParser.parseConstantValue(behavior.minSpeed) : 0; - const maxSpeed = behavior.maxSpeed !== undefined ? valueParser.parseConstantValue(behavior.maxSpeed) : 1; + const minSpeed = behavior.minSpeed !== undefined ? VFXValueUtils.parseConstantValue(behavior.minSpeed) : 0; + const maxSpeed = behavior.maxSpeed !== undefined ? VFXValueUtils.parseConstantValue(behavior.maxSpeed) : 1; const speedRatio = Math.max(0, Math.min(1, (currentSpeed - minSpeed) / (maxSpeed - minSpeed || 1))); const sizeMultiplier = interpolateGradientKeys(sizeKeys, speedRatio, extractNumberFromValue); - const startSize = particle.startSize || particle.size || 1; + const startSize = particle.size || 1; particle.size = startSize * sizeMultiplier; } /** * Apply SizeBySpeed behavior to SolidParticle + * Gets currentSpeed from particle.velocity magnitude */ -export function applySizeBySpeedSPS(particle: SolidParticle, behavior: VFXSizeBySpeedBehavior, currentSpeed: number, valueParser: VFXValueParser): void { +export function applySizeBySpeedSPS(particle: SolidParticle, behavior: VFXSizeBySpeedBehavior): void { if (!behavior.size || !behavior.size.keys) { return; } + // Get current speed from particle velocity + const currentSpeed = Math.sqrt(particle.velocity.x * particle.velocity.x + particle.velocity.y * particle.velocity.y + particle.velocity.z * particle.velocity.z); + const sizeKeys = behavior.size.keys; - const minSpeed = behavior.minSpeed !== undefined ? valueParser.parseConstantValue(behavior.minSpeed) : 0; - const maxSpeed = behavior.maxSpeed !== undefined ? valueParser.parseConstantValue(behavior.maxSpeed) : 1; + const minSpeed = behavior.minSpeed !== undefined ? VFXValueUtils.parseConstantValue(behavior.minSpeed) : 0; + const maxSpeed = behavior.maxSpeed !== undefined ? VFXValueUtils.parseConstantValue(behavior.maxSpeed) : 1; const speedRatio = Math.max(0, Math.min(1, (currentSpeed - minSpeed) / (maxSpeed - minSpeed || 1))); const sizeMultiplier = interpolateGradientKeys(sizeKeys, speedRatio, extractNumberFromValue); diff --git a/editor/src/editor/windows/fx-editor/VFX/behaviors/sizeOverLife.ts b/editor/src/editor/windows/fx-editor/VFX/behaviors/sizeOverLife.ts index 3fca02b73..0aa7490f8 100644 --- a/editor/src/editor/windows/fx-editor/VFX/behaviors/sizeOverLife.ts +++ b/editor/src/editor/windows/fx-editor/VFX/behaviors/sizeOverLife.ts @@ -30,12 +30,16 @@ export function applySizeOverLifePS(particleSystem: ParticleSystem, behavior: VF /** * Apply SizeOverLife behavior to SolidParticle + * Gets lifeRatio from particle (age / lifeTime) */ -export function applySizeOverLifeSPS(particle: SolidParticle, behavior: VFXSizeOverLifeBehavior, lifeRatio: number): void { - if (!behavior.size) { +export function applySizeOverLifeSPS(particle: SolidParticle, behavior: VFXSizeOverLifeBehavior): void { + if (!behavior.size || particle.lifeTime <= 0) { return; } + // Get lifeRatio from particle + const lifeRatio = particle.age / particle.lifeTime; + let sizeMultiplier = 1; if (behavior.size.keys && Array.isArray(behavior.size.keys)) { diff --git a/editor/src/editor/windows/fx-editor/VFX/behaviors/speedOverLife.ts b/editor/src/editor/windows/fx-editor/VFX/behaviors/speedOverLife.ts index 6189689e3..c189e7f12 100644 --- a/editor/src/editor/windows/fx-editor/VFX/behaviors/speedOverLife.ts +++ b/editor/src/editor/windows/fx-editor/VFX/behaviors/speedOverLife.ts @@ -1,12 +1,12 @@ import { SolidParticle, ParticleSystem } from "babylonjs"; import type { VFXSpeedOverLifeBehavior } from "../types/behaviors"; import { extractNumberFromValue, interpolateGradientKeys } from "./utils"; -import { VFXValueParser } from "../parsers/VFXValueParser"; +import { VFXValueUtils } from "../utils/valueParser"; /** * Apply SpeedOverLife behavior to ParticleSystem */ -export function applySpeedOverLifePS(particleSystem: ParticleSystem, behavior: VFXSpeedOverLifeBehavior, valueParser: VFXValueParser): void { +export function applySpeedOverLifePS(particleSystem: ParticleSystem, behavior: VFXSpeedOverLifeBehavior): void { if (behavior.speed) { if (typeof behavior.speed === "object" && behavior.speed !== null && "keys" in behavior.speed && behavior.speed.keys && Array.isArray(behavior.speed.keys)) { for (const key of behavior.speed.keys) { @@ -35,7 +35,7 @@ export function applySpeedOverLifePS(particleSystem: ParticleSystem, behavior: V } } } else if (typeof behavior.speed === "number" || (typeof behavior.speed === "object" && behavior.speed !== null && "type" in behavior.speed)) { - const speedValue = valueParser.parseIntervalValue(behavior.speed); + const speedValue = VFXValueUtils.parseIntervalValue(behavior.speed); particleSystem.addVelocityGradient(0, speedValue.min); particleSystem.addVelocityGradient(1, speedValue.max); } @@ -44,12 +44,16 @@ export function applySpeedOverLifePS(particleSystem: ParticleSystem, behavior: V /** * Apply SpeedOverLife behavior to SolidParticle + * Gets lifeRatio from particle (age / lifeTime) */ -export function applySpeedOverLifeSPS(particle: SolidParticle, behavior: VFXSpeedOverLifeBehavior, lifeRatio: number, valueParser: VFXValueParser): void { - if (!behavior.speed) { +export function applySpeedOverLifeSPS(particle: SolidParticle, behavior: VFXSpeedOverLifeBehavior): void { + if (!behavior.speed || particle.lifeTime <= 0) { return; } + // Get lifeRatio from particle + const lifeRatio = particle.age / particle.lifeTime; + let speedMultiplier = 1; if (typeof behavior.speed === "object" && behavior.speed !== null && "keys" in behavior.speed && behavior.speed.keys && Array.isArray(behavior.speed.keys)) { @@ -70,7 +74,7 @@ export function applySpeedOverLifeSPS(particle: SolidParticle, behavior: VFXSpee speedMultiplier = startSpeed + (endSpeed - startSpeed) * t; } } else if (typeof behavior.speed === "number" || (typeof behavior.speed === "object" && behavior.speed !== null && "type" in behavior.speed)) { - const speedValue = valueParser.parseIntervalValue(behavior.speed); + const speedValue = VFXValueUtils.parseIntervalValue(behavior.speed); speedMultiplier = speedValue.min + (speedValue.max - speedValue.min) * lifeRatio; } diff --git a/editor/src/editor/windows/fx-editor/VFX/factories/VFXBehaviorFunctionFactory.ts b/editor/src/editor/windows/fx-editor/VFX/factories/VFXBehaviorFunctionFactory.ts deleted file mode 100644 index e09525882..000000000 --- a/editor/src/editor/windows/fx-editor/VFX/factories/VFXBehaviorFunctionFactory.ts +++ /dev/null @@ -1,186 +0,0 @@ -import { Particle, SolidParticle, ParticleSystem } from "babylonjs"; -import type { - VFXBehavior, - VFXColorBySpeedBehavior, - VFXSizeBySpeedBehavior, - VFXRotationBySpeedBehavior, - VFXOrbitOverLifeBehavior, - VFXForceOverLifeBehavior, -} from "../types/behaviors"; -import type { VFXValueParser } from "../parsers/VFXValueParser"; -import type { VFXPerParticleBehaviorFunction, VFXPerSolidParticleBehaviorFunction, VFXSystemBehaviorFunction, VFXPerParticleContext } from "../types/VFXBehaviorFunction"; -import { - applyColorOverLifeSPS, - applySizeOverLifeSPS, - applyRotationOverLifeSPS, - applySpeedOverLifeSPS, - applyColorBySpeedSPS, - applySizeBySpeedSPS, - applyRotationBySpeedSPS, - applyOrbitOverLifeSPS, - applyColorBySpeedPS, - applySizeBySpeedPS, - applyRotationBySpeedPS, - applyOrbitOverLifePS, -} from "../behaviors"; - -export class VFXBehaviorFunctionFactory { - public static createPerParticleFunctionsSPS(behaviors: VFXBehavior[], valueParser: VFXValueParser): VFXPerSolidParticleBehaviorFunction[] { - const functions: VFXPerSolidParticleBehaviorFunction[] = []; - - for (const behavior of behaviors) { - switch (behavior.type) { - case "ColorOverLife": { - const b = behavior as any; - functions.push((particle: SolidParticle, context: VFXPerParticleContext) => { - applyColorOverLifeSPS(particle, b, context.lifeRatio); - }); - break; - } - - case "SizeOverLife": { - const b = behavior as any; - functions.push((particle: SolidParticle, context: VFXPerParticleContext) => { - applySizeOverLifeSPS(particle, b, context.lifeRatio); - }); - break; - } - - case "RotationOverLife": - case "Rotation3DOverLife": { - const b = behavior as any; - functions.push((particle: SolidParticle, context: VFXPerParticleContext) => { - applyRotationOverLifeSPS(particle, b, context.lifeRatio, valueParser, context.updateSpeed); - }); - break; - } - - case "ForceOverLife": - case "ApplyForce": { - const b = behavior as VFXForceOverLifeBehavior; - functions.push((particle: SolidParticle, context: VFXPerParticleContext) => { - const forceX = b.x ?? b.force?.x; - const forceY = b.y ?? b.force?.y; - const forceZ = b.z ?? b.force?.z; - if (forceX !== undefined || forceY !== undefined || forceZ !== undefined) { - const fx = forceX !== undefined ? valueParser.parseConstantValue(forceX) : 0; - const fy = forceY !== undefined ? valueParser.parseConstantValue(forceY) : 0; - const fz = forceZ !== undefined ? valueParser.parseConstantValue(forceZ) : 0; - particle.velocity.x += fx * context.updateSpeed; - particle.velocity.y += fy * context.updateSpeed; - particle.velocity.z += fz * context.updateSpeed; - } - }); - break; - } - - case "SpeedOverLife": { - const b = behavior as any; - functions.push((particle: SolidParticle, context: VFXPerParticleContext) => { - applySpeedOverLifeSPS(particle, b, context.lifeRatio, valueParser); - }); - break; - } - - case "ColorBySpeed": { - const b = behavior as VFXColorBySpeedBehavior; - functions.push((particle: SolidParticle, context: VFXPerParticleContext) => { - applyColorBySpeedSPS(particle, b, context.startSpeed, valueParser); - }); - break; - } - - case "SizeBySpeed": { - const b = behavior as VFXSizeBySpeedBehavior; - functions.push((particle: SolidParticle, context: VFXPerParticleContext) => { - applySizeBySpeedSPS(particle, b, context.startSpeed, valueParser); - }); - break; - } - - case "RotationBySpeed": { - const b = behavior as VFXRotationBySpeedBehavior; - functions.push((particle: SolidParticle, context: VFXPerParticleContext) => { - applyRotationBySpeedSPS(particle, b, context.startSpeed, valueParser, context.updateSpeed); - }); - break; - } - - case "OrbitOverLife": { - const b = behavior as VFXOrbitOverLifeBehavior; - functions.push((particle: SolidParticle, context: VFXPerParticleContext) => { - applyOrbitOverLifeSPS(particle, b, context.lifeRatio, valueParser); - }); - break; - } - } - } - - return functions; - } - - public static createPerParticleFunctionsPS(behaviors: VFXBehavior[], valueParser: VFXValueParser, particleSystem: ParticleSystem): VFXPerParticleBehaviorFunction[] { - const functions: VFXPerParticleBehaviorFunction[] = []; - - for (const behavior of behaviors) { - switch (behavior.type) { - case "ColorBySpeed": { - const b = behavior as VFXColorBySpeedBehavior; - functions.push((particle: Particle, context: VFXPerParticleContext) => { - applyColorBySpeedPS(particle as any, b, context.startSpeed, valueParser); - }); - break; - } - - case "SizeBySpeed": { - const b = behavior as VFXSizeBySpeedBehavior; - functions.push((particle: Particle, context: VFXPerParticleContext) => { - applySizeBySpeedPS(particle as any, b, context.startSpeed, valueParser); - }); - break; - } - - case "RotationBySpeed": { - const b = behavior as VFXRotationBySpeedBehavior; - functions.push((particle: Particle, context: VFXPerParticleContext) => { - applyRotationBySpeedPS(particle as any, b, context.startSpeed, particleSystem, valueParser); - }); - break; - } - - case "OrbitOverLife": { - const b = behavior as VFXOrbitOverLifeBehavior; - functions.push((particle: Particle, context: VFXPerParticleContext) => { - applyOrbitOverLifePS(particle, b, context.lifeRatio, valueParser); - }); - break; - } - } - } - - return functions; - } - - public static createSystemFunctions(behaviors: VFXBehavior[], _valueParser: VFXValueParser): VFXSystemBehaviorFunction[] { - const functions: VFXSystemBehaviorFunction[] = []; - - for (const behavior of behaviors) { - switch (behavior.type) { - case "ColorOverLife": - case "SizeOverLife": - case "RotationOverLife": - case "Rotation3DOverLife": - case "ForceOverLife": - case "ApplyForce": - case "GravityForce": - case "SpeedOverLife": - case "FrameOverLife": - case "LimitSpeedOverLife": - // handled at emitter level - break; - } - } - - return functions; - } -} diff --git a/editor/src/editor/windows/fx-editor/VFX/factories/VFXEmitterFactory.ts b/editor/src/editor/windows/fx-editor/VFX/factories/VFXEmitterFactory.ts index c63e6d116..0efaf7bdd 100644 --- a/editor/src/editor/windows/fx-editor/VFX/factories/VFXEmitterFactory.ts +++ b/editor/src/editor/windows/fx-editor/VFX/factories/VFXEmitterFactory.ts @@ -6,17 +6,6 @@ import { VFXValueParser } from "../parsers/VFXValueParser"; import type { IVFXMaterialFactory, IVFXGeometryFactory } from "../types/factories"; import { VFXSolidParticleSystem } from "../systems/VFXSolidParticleSystem"; import { VFXParticleSystem } from "../systems/VFXParticleSystem"; -import { VFXBehaviorFunctionFactory } from "./VFXBehaviorFunctionFactory"; -import { - applyColorOverLifePS, - applySizeOverLifePS, - applyRotationOverLifePS, - applyForceOverLifePS, - applyGravityForcePS, - applySpeedOverLifePS, - applyFrameOverLifePS, - applyLimitSpeedOverLifePS, -} from "../behaviors"; import type { VFXBehavior, VFXEmissionBurst } from "../types"; import type { VFXParticleEmitterConfig } from "../types/emitterConfig"; @@ -57,14 +46,13 @@ export class VFXEmitterFactory { * Create a particle emitter from emitter data */ public createEmitter(emitterData: VFXEmitterData): Nullable { - const { config } = emitterData; const { options } = this._context; - // Check if we need SolidParticleSystem (mesh-based particles) - const useSolidParticles = config.renderMode === 2; - this._logger.log(`Using ${useSolidParticles ? "SolidParticleSystem" : "ParticleSystem"}`, options); + // Use systemType from emitter data (determined during conversion) + const systemType = emitterData.vfxEmitter?.systemType || "base"; + this._logger.log(`Using ${systemType === "solid" ? "SolidParticleSystem" : "ParticleSystem"}`, options); - if (useSolidParticles) { + if (systemType === "solid") { return this._createSolidParticleSystem(emitterData); } else { return this._createParticleSystem(emitterData); @@ -153,7 +141,7 @@ export class VFXEmitterFactory { */ private _createParticleSystemInstance(name: string, values: ParsedParticleValues): VFXParticleSystem { const { scene } = this._context; - return new VFXParticleSystem(name, values.capacity, scene, this._valueParser, values.avgStartSpeed, values.avgStartSize, values.startColor); + return new VFXParticleSystem(name, values.capacity, scene, values.avgStartSpeed, values.avgStartSize, values.startColor); } /** @@ -380,7 +368,7 @@ export class VFXEmitterFactory { this._addShapeToSPS(sps, particleMesh, capacity); this._configureSPSBillboard(sps, config); - this._applyBehaviorsIfNeededSPS(sps, config.behaviors); + this._applyBehaviors(sps, config.behaviors || []); particleMesh.dispose(); @@ -426,7 +414,7 @@ export class VFXEmitterFactory { vfxTransform: { position: Vector3; rotation: Quaternion; scale: Vector3 } | null ): VFXSolidParticleSystem { const { scene, options } = this._context; - return new VFXSolidParticleSystem(name, scene, config, this._valueParser, { + const sps = new VFXSolidParticleSystem(name, scene, config, { updatable: true, isPickable: false, enableDepthSort: false, @@ -437,6 +425,11 @@ export class VFXEmitterFactory { logger: this._logger, loaderOptions: options, }); + // Set parent after creation (will apply to mesh) + if (emitterData.parentGroup) { + sps.parent = emitterData.parentGroup; + } + return sps; } /** @@ -529,18 +522,6 @@ export class VFXEmitterFactory { } } - /** - * Applies behaviors to SPS if configured - */ - private _applyBehaviorsIfNeededSPS(sps: SolidParticleSystem, behaviors: VFXBehavior[] | undefined): void { - const { options } = this._context; - - if (behaviors && Array.isArray(behaviors) && behaviors.length > 0) { - this._applyBehaviorsToSPS(sps, behaviors); - this._logger.log(` Set SPS behaviors (${behaviors.length})`, options); - } - } - /** * Set the emitter shape based on Three.js shape configuration */ @@ -750,68 +731,13 @@ export class VFXEmitterFactory { /** * Apply behaviors to ParticleSystem + * Simply sets behaviorConfigs - the system will apply them automatically via proxy */ - private _applyBehaviorsToPS(particleSystem: ParticleSystem, behaviors: VFXBehavior[]): void { - const vfxPS = particleSystem as any as VFXParticleSystem; - if (!vfxPS || typeof vfxPS.setPerParticleBehaviors !== "function") { + private _applyBehaviors(particleSystem: VFXParticleSystem | VFXSolidParticleSystem, behaviors: VFXBehavior[]): void { + if (!particleSystem || !particleSystem.behaviorConfigs) { return; } - - this._applySystemLevelBehaviors(particleSystem, behaviors); - - const perParticleFunctions = VFXBehaviorFunctionFactory.createPerParticleFunctionsPS(behaviors, this._valueParser, particleSystem); - vfxPS.setPerParticleBehaviors(perParticleFunctions); - } - - /** - * Applies system-level behaviors (gradients, etc.) - */ - private _applySystemLevelBehaviors(particleSystem: ParticleSystem, behaviors: VFXBehavior[]): void { - const { options } = this._context; - const valueParser = this._valueParser; - - for (const behavior of behaviors) { - if (!behavior.type) { - this._logger.warn(`Behavior missing type: ${JSON.stringify(behavior)}`, options); - continue; - } - - this._logger.log(` Processing behavior: ${behavior.type}`, options); - this._applyBehaviorToPS(particleSystem, behavior, valueParser); - } - } - - /** - * Applies a single behavior to ParticleSystem - */ - private _applyBehaviorToPS(particleSystem: ParticleSystem, behavior: VFXBehavior, valueParser: VFXValueParser): void { - const behaviorHandlers: Record void> = { - ColorOverLife: (ps, b) => applyColorOverLifePS(ps, b as any), - SizeOverLife: (ps, b) => applySizeOverLifePS(ps, b as any), - RotationOverLife: (ps, b, vp) => applyRotationOverLifePS(ps, b as any, vp), - Rotation3DOverLife: (ps, b, vp) => applyRotationOverLifePS(ps, b as any, vp), - ForceOverLife: (ps, b, vp) => applyForceOverLifePS(ps, b as any, vp), - ApplyForce: (ps, b, vp) => applyForceOverLifePS(ps, b as any, vp), - GravityForce: (ps, b, vp) => applyGravityForcePS(ps, b as any, vp), - SpeedOverLife: (ps, b, vp) => applySpeedOverLifePS(ps, b as any, vp), - FrameOverLife: (ps, b, vp) => applyFrameOverLifePS(ps, b as any, vp), - LimitSpeedOverLife: (ps, b, vp) => applyLimitSpeedOverLifePS(ps, b as any, vp), - }; - - const handler = behaviorHandlers[behavior.type]; - if (handler) { - handler(particleSystem, behavior, valueParser); - } - } - - /** - * Apply behaviors to SolidParticleSystem - */ - private _applyBehaviorsToSPS(sps: SolidParticleSystem, behaviors: VFXBehavior[]): void { - const vfxSPS = sps as any as VFXSolidParticleSystem; - if (vfxSPS && typeof vfxSPS.setPerParticleBehaviors === "function") { - const perParticleFunctions = VFXBehaviorFunctionFactory.createPerParticleFunctionsSPS(behaviors, this._valueParser); - vfxSPS.setPerParticleBehaviors(perParticleFunctions); - } + particleSystem.behaviorConfigs.length = 0; + particleSystem.behaviorConfigs.push(...(behaviors || [])); } } diff --git a/editor/src/editor/windows/fx-editor/VFX/factories/VFXParticleSystemBehaviorFactory.ts b/editor/src/editor/windows/fx-editor/VFX/factories/VFXParticleSystemBehaviorFactory.ts new file mode 100644 index 000000000..8f9b3ee28 --- /dev/null +++ b/editor/src/editor/windows/fx-editor/VFX/factories/VFXParticleSystemBehaviorFactory.ts @@ -0,0 +1,65 @@ +import { Particle } from "babylonjs"; +import type { VFXPerParticleBehaviorFunction } from "../types/VFXBehaviorFunction"; +import type { VFXBehavior, VFXColorBySpeedBehavior, VFXSizeBySpeedBehavior, VFXRotationBySpeedBehavior, VFXOrbitOverLifeBehavior } from "../types/behaviors"; +import { applyColorBySpeedPS, applySizeBySpeedPS, applyRotationBySpeedPS, applyOrbitOverLifePS } from "../behaviors"; +import type { ParticleSystem } from "babylonjs"; + +/** + * Behavior factory for VFXParticleSystem + * Creates behavior functions from configurations + */ +export class VFXParticleSystemBehaviorFactory { + private _particleSystem: ParticleSystem; + + constructor(particleSystem: ParticleSystem) { + this._particleSystem = particleSystem; + } + + /** + * Create behavior functions from configurations + * Behaviors receive only particle and behavior config - all data comes from particle + */ + public createBehaviorFunctions(behaviors: VFXBehavior[]): VFXPerParticleBehaviorFunction[] { + const functions: VFXPerParticleBehaviorFunction[] = []; + + for (const behavior of behaviors) { + switch (behavior.type) { + case "ColorBySpeed": { + const b = behavior as VFXColorBySpeedBehavior; + functions.push((particle: Particle, _behaviorConfig: VFXBehavior) => { + applyColorBySpeedPS(particle, b); + }); + break; + } + + case "SizeBySpeed": { + const b = behavior as VFXSizeBySpeedBehavior; + functions.push((particle: Particle, _behaviorConfig: VFXBehavior) => { + applySizeBySpeedPS(particle, b); + }); + break; + } + + case "RotationBySpeed": { + const b = behavior as VFXRotationBySpeedBehavior; + functions.push((particle: Particle, _behaviorConfig: VFXBehavior) => { + // Store reference to system in particle for behaviors that need it + (particle as any).particleSystem = this._particleSystem; + applyRotationBySpeedPS(particle, b); + }); + break; + } + + case "OrbitOverLife": { + const b = behavior as VFXOrbitOverLifeBehavior; + functions.push((particle: Particle, _behaviorConfig: VFXBehavior) => { + applyOrbitOverLifePS(particle, b); + }); + break; + } + } + } + + return functions; + } +} diff --git a/editor/src/editor/windows/fx-editor/VFX/factories/VFXSolidParticleSystemBehaviorFactory.ts b/editor/src/editor/windows/fx-editor/VFX/factories/VFXSolidParticleSystemBehaviorFactory.ts new file mode 100644 index 000000000..90c37f448 --- /dev/null +++ b/editor/src/editor/windows/fx-editor/VFX/factories/VFXSolidParticleSystemBehaviorFactory.ts @@ -0,0 +1,132 @@ +import { SolidParticle } from "babylonjs"; +import type { VFXPerSolidParticleBehaviorFunction } from "../types/VFXBehaviorFunction"; +import type { + VFXBehavior, + VFXColorOverLifeBehavior, + VFXSizeOverLifeBehavior, + VFXRotationOverLifeBehavior, + VFXForceOverLifeBehavior, + VFXSpeedOverLifeBehavior, + VFXColorBySpeedBehavior, + VFXSizeBySpeedBehavior, + VFXRotationBySpeedBehavior, + VFXOrbitOverLifeBehavior, +} from "../types/behaviors"; +import { VFXValueUtils } from "../utils/valueParser"; +import { + applyColorOverLifeSPS, + applySizeOverLifeSPS, + applyRotationOverLifeSPS, + applySpeedOverLifeSPS, + applyColorBySpeedSPS, + applySizeBySpeedSPS, + applyRotationBySpeedSPS, + applyOrbitOverLifeSPS, +} from "../behaviors"; + +/** + * Behavior factory for VFXSolidParticleSystem + * Creates behavior functions from configurations + */ +export class VFXSolidParticleSystemBehaviorFactory { + /** + * Create behavior functions from configurations + * Behaviors receive only particle and behavior config - all data comes from particle + */ + public createBehaviorFunctions(behaviors: VFXBehavior[]): VFXPerSolidParticleBehaviorFunction[] { + const functions: VFXPerSolidParticleBehaviorFunction[] = []; + + for (const behavior of behaviors) { + switch (behavior.type) { + case "ColorOverLife": { + const b = behavior as VFXColorOverLifeBehavior; + functions.push((particle: SolidParticle, _behaviorConfig: VFXBehavior) => { + applyColorOverLifeSPS(particle, b); + }); + break; + } + + case "SizeOverLife": { + const b = behavior as VFXSizeOverLifeBehavior; + functions.push((particle: SolidParticle, _behaviorConfig: VFXBehavior) => { + applySizeOverLifeSPS(particle, b); + }); + break; + } + + case "RotationOverLife": + case "Rotation3DOverLife": { + const b = behavior as VFXRotationOverLifeBehavior; + functions.push((particle: SolidParticle, _behaviorConfig: VFXBehavior) => { + applyRotationOverLifeSPS(particle, b); + }); + break; + } + + case "ForceOverLife": + case "ApplyForce": { + const b = behavior as VFXForceOverLifeBehavior; + functions.push((particle: SolidParticle, _behaviorConfig: VFXBehavior) => { + // Get updateSpeed from system (stored in particle.props or use default) + const updateSpeed = (particle as any).system?.updateSpeed ?? 0.016; + + const forceX = b.x ?? b.force?.x; + const forceY = b.y ?? b.force?.y; + const forceZ = b.z ?? b.force?.z; + if (forceX !== undefined || forceY !== undefined || forceZ !== undefined) { + const fx = forceX !== undefined ? VFXValueUtils.parseConstantValue(forceX) : 0; + const fy = forceY !== undefined ? VFXValueUtils.parseConstantValue(forceY) : 0; + const fz = forceZ !== undefined ? VFXValueUtils.parseConstantValue(forceZ) : 0; + particle.velocity.x += fx * updateSpeed; + particle.velocity.y += fy * updateSpeed; + particle.velocity.z += fz * updateSpeed; + } + }); + break; + } + + case "SpeedOverLife": { + const b = behavior as VFXSpeedOverLifeBehavior; + functions.push((particle: SolidParticle, _behaviorConfig: VFXBehavior) => { + applySpeedOverLifeSPS(particle, b); + }); + break; + } + + case "ColorBySpeed": { + const b = behavior as VFXColorBySpeedBehavior; + functions.push((particle: SolidParticle, _behaviorConfig: VFXBehavior) => { + applyColorBySpeedSPS(particle, b); + }); + break; + } + + case "SizeBySpeed": { + const b = behavior as VFXSizeBySpeedBehavior; + functions.push((particle: SolidParticle, _behaviorConfig: VFXBehavior) => { + applySizeBySpeedSPS(particle, b); + }); + break; + } + + case "RotationBySpeed": { + const b = behavior as VFXRotationBySpeedBehavior; + functions.push((particle: SolidParticle, _behaviorConfig: VFXBehavior) => { + applyRotationBySpeedSPS(particle, b); + }); + break; + } + + case "OrbitOverLife": { + const b = behavior as VFXOrbitOverLifeBehavior; + functions.push((particle: SolidParticle, _behaviorConfig: VFXBehavior) => { + applyOrbitOverLifeSPS(particle, b); + }); + break; + } + } + } + + return functions; + } +} diff --git a/editor/src/editor/windows/fx-editor/VFX/processors/VFXHierarchyProcessor.ts b/editor/src/editor/windows/fx-editor/VFX/factories/VFXSystemFactory.ts similarity index 53% rename from editor/src/editor/windows/fx-editor/VFX/processors/VFXHierarchyProcessor.ts rename to editor/src/editor/windows/fx-editor/VFX/factories/VFXSystemFactory.ts index 3fd6be440..7495b5ec8 100644 --- a/editor/src/editor/windows/fx-editor/VFX/processors/VFXHierarchyProcessor.ts +++ b/editor/src/editor/windows/fx-editor/VFX/factories/VFXSystemFactory.ts @@ -3,30 +3,30 @@ import { VFXParticleSystem } from "../systems/VFXParticleSystem"; import { VFXSolidParticleSystem } from "../systems/VFXSolidParticleSystem"; import type { VFXParseContext } from "../types/context"; import type { VFXEmitterData } from "../types/emitter"; -import type { VFXHierarchy, VFXGroup, VFXEmitter, VFXTransform } from "../types/hierarchy"; +import type { VFXData, VFXGroup, VFXEmitter, VFXTransform } from "../types/hierarchy"; import { VFXLogger } from "../loggers/VFXLogger"; import type { IVFXEmitterFactory } from "../types/factories"; /** - * Processor for Three.js object hierarchy (Groups and ParticleEmitters) + * Factory for creating particle systems from VFX data * Creates all nodes, sets parents, and applies transformations in a single pass */ -export class VFXHierarchyProcessor { +export class VFXSystemFactory { private _logger: VFXLogger; private _context: VFXParseContext; private _emitterFactory: IVFXEmitterFactory; constructor(context: VFXParseContext, emitterFactory: IVFXEmitterFactory) { this._context = context; - this._logger = new VFXLogger("[VFXHierarchyProcessor]"); + this._logger = new VFXLogger("[VFXSystemFactory]"); this._emitterFactory = emitterFactory; } /** - * Process the VFX hierarchy and create particle systems + * Create particle systems from VFX data * Creates all nodes, sets parents, and applies transformations in one pass */ - public processHierarchy(vfxData: VFXHierarchy): (VFXParticleSystem | VFXSolidParticleSystem)[] { + public createSystems(vfxData: VFXData): (VFXParticleSystem | VFXSolidParticleSystem)[] { if (!vfxData.root) { this._logWarning("No root object found in VFX data"); return []; @@ -47,7 +47,7 @@ export class VFXHierarchyProcessor { parentGroup: Nullable, depth: number, particleSystems: (VFXParticleSystem | VFXSolidParticleSystem)[], - vfxData: VFXHierarchy + vfxData: VFXData ): void { this._logObjectProcessing(vfxObj.name, depth); @@ -66,7 +66,7 @@ export class VFXHierarchyProcessor { parentGroup: Nullable, depth: number, particleSystems: (VFXParticleSystem | VFXSolidParticleSystem)[], - vfxData: VFXHierarchy + vfxData: VFXData ): void { const groupNode = this._createGroupNode(vfxGroup, parentGroup, depth); this._processChildren(vfxGroup.children, groupNode, depth, particleSystems, vfxData); @@ -90,7 +90,7 @@ export class VFXHierarchyProcessor { parentGroup: TransformNode, depth: number, particleSystems: (VFXParticleSystem | VFXSolidParticleSystem)[], - vfxData: VFXHierarchy + vfxData: VFXData ): void { if (!children || children.length === 0) { return; @@ -103,70 +103,25 @@ export class VFXHierarchyProcessor { } /** - * Create a Group (TransformNode) from VFX Group data - * Creates node, sets parent, and applies transform in one go + * Create a TransformNode for a VFX Group */ private _createGroupNode(vfxGroup: VFXGroup, parentGroup: Nullable, depth: number): TransformNode { const { scene } = this._context; - this._logGroupCreation(vfxGroup.name, depth); + const groupNode = new TransformNode(vfxGroup.name, scene); + groupNode.id = vfxGroup.uuid; - const groupNode = this._instantiateGroupNode(vfxGroup.name, scene); - this._setGroupParent(groupNode, parentGroup, depth); - this._applyGroupTransform(groupNode, vfxGroup.transform, depth); - this._storeGroupNode(vfxGroup.uuid, groupNode, depth); + this._applyTransform(groupNode, vfxGroup.transform, depth); + this._setParent(groupNode, parentGroup, depth); - return groupNode; - } + // Store in context for potential future reference + this._context.groupNodesMap.set(vfxGroup.uuid, groupNode); - /** - * Instantiate a new TransformNode for a group - */ - private _instantiateGroupNode(name: string, scene: any): TransformNode { - const groupNode = new TransformNode(name, scene); - groupNode.isVisible = false; + this._logGroupCreation(vfxGroup.name, depth); return groupNode; } /** - * Set parent for a group node - */ - private _setGroupParent(groupNode: TransformNode, parentGroup: Nullable, depth: number): void { - if (!parentGroup) { - return; - } - - groupNode.setParent(parentGroup); - this._logGroupParentSet(parentGroup.name, depth); - } - - /** - * Apply transform to a group node - */ - private _applyGroupTransform(groupNode: TransformNode, transform: VFXTransform, depth: number): void { - this._logTransformApplication(groupNode.name, depth); - this._applyTransform(groupNode, transform); - this._logTransformDetails(groupNode, depth); - } - - /** - * Apply transform to a node (position, rotation, scale) - */ - private _applyTransform(node: TransformNode, transform: VFXTransform): void { - node.position.copyFrom(transform.position); - node.rotationQuaternion = transform.rotation.clone(); - node.scaling.copyFrom(transform.scale); - } - - /** - * Store group node in context map - */ - private _storeGroupNode(uuid: string, groupNode: TransformNode, depth: number): void { - this._context.groupNodesMap.set(uuid, groupNode); - this._logGroupStored(uuid, depth); - } - - /** - * Create a particle system from VFX Emitter data + * Create a particle system from a VFX Emitter */ private _createParticleSystem(vfxEmitter: VFXEmitter, parentGroup: Nullable, depth: number): Nullable { this._logEmitterProcessing(vfxEmitter, parentGroup, depth); @@ -176,18 +131,35 @@ export class VFXHierarchyProcessor { const particleSystem = this._emitterFactory.createEmitter(emitterData) as VFXParticleSystem | VFXSolidParticleSystem | null; if (!particleSystem) { - this._logEmitterCreationFailed(vfxEmitter.name, depth); + this._logWarning(`Failed to create particle system for emitter: ${vfxEmitter.name}`); return null; } - this._logEmitterCreated(depth); + // Apply transform to particle system + if (particleSystem instanceof VFXSolidParticleSystem) { + // For SPS, transform is applied to the mesh + if (particleSystem.mesh) { + this._applyTransform(particleSystem.mesh, vfxEmitter.transform, depth); + this._setParent(particleSystem.mesh, parentGroup, depth); + } + } else if (particleSystem instanceof VFXParticleSystem) { + // For PS, transform is applied to the emitter mesh + const emitter = (particleSystem as any).emitter; + if (emitter) { + this._applyTransform(emitter, vfxEmitter.transform, depth); + this._setParent(emitter, parentGroup, depth); + } + } + + // Handle prewarm this._handlePrewarm(particleSystem, vfxEmitter.config.prewarm); + this._logParticleSystemCreation(vfxEmitter.name, depth); return particleSystem; } /** - * Build emitter data from VFX emitter and parent group + * Build emitter data structure for factory */ private _buildEmitterData(vfxEmitter: VFXEmitter, parentGroup: Nullable, depth: number): VFXEmitterData { const cumulativeScale = this._calculateCumulativeScale(parentGroup); @@ -247,99 +219,90 @@ export class VFXHierarchyProcessor { this._logger.warn(message, this._context.options); } - private _logObjectProcessing(objectName: string, depth: number): void { - const indent = this._getIndent(depth); - this._logger.log(`${indent}Processing object: ${objectName}`, this._context.options); - } - - private _logChildrenProcessing(childrenCount: number, depth: number): void { + private _logObjectProcessing(name: string, depth: number): void { const indent = this._getIndent(depth); - this._logger.log(`${indent}Processing ${childrenCount} children`, this._context.options); + this._logger.log(`${indent}Processing object: ${name}`, this._context.options); } - private _logGroupCreation(groupName: string, depth: number): void { + private _logGroupCreation(name: string, depth: number): void { const indent = this._getIndent(depth); - this._logger.log(`${indent}Creating Group: ${groupName}`, this._context.options); + this._logger.log(`${indent}Created group node: ${name}`, this._context.options); } - private _logGroupParentSet(parentName: string, depth: number): void { + private _logChildrenProcessing(count: number, depth: number): void { const indent = this._getIndent(depth); - this._logger.log(`${indent}Group parent set: ${parentName}`, this._context.options); + this._logger.log(`${indent}Processing ${count} children`, this._context.options); } - private _logTransformApplication(nodeName: string, depth: number): void { + private _logEmitterProcessing(vfxEmitter: VFXEmitter, parentGroup: Nullable, depth: number): void { const indent = this._getIndent(depth); - this._logger.log(`${indent}Applying transform to group: ${nodeName}`, this._context.options); + const parentName = parentGroup ? parentGroup.name : "none"; + this._logger.log(`${indent}Processing emitter: ${vfxEmitter.name} (parent: ${parentName})`, this._context.options); } - private _logTransformDetails(node: TransformNode, depth: number): void { + private _logEmitterConfig(vfxEmitter: VFXEmitter, depth: number): void { const indent = this._getIndent(depth); - const pos = node.position; - const rot = node.rotationQuaternion; - const scl = node.scaling; - - this._logger.log(`${indent}Group position: (${pos.x.toFixed(2)}, ${pos.y.toFixed(2)}, ${pos.z.toFixed(2)})`, this._context.options); - if (rot) { - this._logger.log(`${indent}Group rotation quaternion: (${rot.x.toFixed(4)}, ${rot.y.toFixed(4)}, ${rot.z.toFixed(4)}, ${rot.w.toFixed(4)})`, this._context.options); - } - this._logger.log(`${indent}Group scale: (${scl.x.toFixed(2)}, ${scl.y.toFixed(2)}, ${scl.z.toFixed(2)})`, this._context.options); + const config = vfxEmitter.config; + this._logger.log(`${indent} Config: duration=${config.duration}, looping=${config.looping}, systemType=${vfxEmitter.systemType}`, this._context.options); } - private _logGroupStored(uuid: string, depth: number): void { + private _logParticleSystemCreation(name: string, depth: number): void { const indent = this._getIndent(depth); - this._logger.log(`${indent}Group stored in map: ${uuid}`, this._context.options); + this._logger.log(`${indent}Created particle system: ${name}`, this._context.options); } - private _logEmitterProcessing(vfxEmitter: VFXEmitter, parentGroup: Nullable, depth: number): void { + private _logCumulativeScale(scale: Vector3, depth: number): void { const indent = this._getIndent(depth); - this._logger.log(`${indent}=== Processing ParticleEmitter: ${vfxEmitter.name} ===`, this._context.options); - this._logger.log(`${indent}Current parent group: ${parentGroup ? parentGroup.name : "none"}`, this._context.options); + this._logger.log(`${indent}Cumulative scale: (${scale.x.toFixed(2)}, ${scale.y.toFixed(2)}, ${scale.z.toFixed(2)})`, this._context.options); } - private _logEmitterConfig(vfxEmitter: VFXEmitter, depth: number): void { - const { options } = this._context; - if (!options?.verbose) { + /** + * Apply transform to a node + */ + private _applyTransform(node: TransformNode, transform: VFXTransform, depth: number): void { + if (!transform) { + this._logWarning(`Transform is undefined for node: ${node.name}`); return; } const indent = this._getIndent(depth); - const config = { - renderMode: vfxEmitter.config.renderMode, - duration: vfxEmitter.config.duration, - looping: vfxEmitter.config.looping, - prewarm: vfxEmitter.config.prewarm, - emissionOverTime: vfxEmitter.config.emissionOverTime, - startLife: vfxEmitter.config.startLife, - startSpeed: vfxEmitter.config.startSpeed, - startSize: vfxEmitter.config.startSize, - behaviorsCount: vfxEmitter.config.behaviors?.length || 0, - worldSpace: vfxEmitter.config.worldSpace, - }; - this._logger.log(`${indent}Emitter config: ${JSON.stringify(config, null, 2)}`, options); - } + if (transform.position && node.position) { + node.position.copyFrom(transform.position); + } - private _logCumulativeScale(scale: Vector3, depth: number): void { - const { options } = this._context; - if (!options?.verbose) { - return; + if (transform.rotation) { + node.rotationQuaternion = transform.rotation.clone(); } - if (scale.x === 1 && scale.y === 1 && scale.z === 1) { - return; + if (transform.scale && node.scaling) { + node.scaling.copyFrom(transform.scale); } - const indent = this._getIndent(depth); - this._logger.log(`${indent}Cumulative scale from parent groups: (${scale.x.toFixed(2)}, ${scale.y.toFixed(2)}, ${scale.z.toFixed(2)})`, options); + if (transform.position && transform.scale) { + this._logger.log( + `${indent}Applied transform: pos=(${transform.position.x.toFixed(2)}, ${transform.position.y.toFixed(2)}, ${transform.position.z.toFixed(2)}), scale=(${transform.scale.x.toFixed(2)}, ${transform.scale.y.toFixed(2)}, ${transform.scale.z.toFixed(2)})`, + this._context.options + ); + } } - private _logEmitterCreated(depth: number): void { - const indent = this._getIndent(depth); - this._logger.log(`${indent}Particle system created successfully`, this._context.options); - } + /** + * Set parent for a node + */ + private _setParent(node: TransformNode | any, parent: Nullable, depth: number): void { + if (!parent || !node) { + return; + } - private _logEmitterCreationFailed(emitterName: string, depth: number): void { - const indent = this._getIndent(depth); - this._logger.warn(`${indent}Failed to create particle system for ${emitterName}`, this._context.options); + // Check if node has setParent method (TransformNode, AbstractMesh, etc.) + if (typeof node.setParent === "function") { + node.setParent(parent, false, true); + const indent = this._getIndent(depth); + this._logger.log(`${indent}Set parent: ${node.name || "unknown"} -> ${parent.name}`, this._context.options); + } else { + const indent = this._getIndent(depth); + this._logger.warn(`${indent}Node does not support setParent: ${node.constructor?.name || "unknown"}`, this._context.options); + } } } diff --git a/editor/src/editor/windows/fx-editor/VFX/index.ts b/editor/src/editor/windows/fx-editor/VFX/index.ts index f3685927a..c85038b89 100644 --- a/editor/src/editor/windows/fx-editor/VFX/index.ts +++ b/editor/src/editor/windows/fx-editor/VFX/index.ts @@ -5,10 +5,12 @@ export * from "./parsers/VFXDataConverter"; export * from "./factories/VFXMaterialFactory"; export * from "./factories/VFXGeometryFactory"; export * from "./factories/VFXEmitterFactory"; -export * from "./factories/VFXBehaviorFunctionFactory"; -export * from "./processors/VFXHierarchyProcessor"; +export * from "./factories/VFXSystemFactory"; export * from "./systems/VFXSolidParticleSystem"; export * from "./systems/VFXParticleSystem"; +export * from "./factories/VFXParticleSystemBehaviorFactory"; +export * from "./factories/VFXSolidParticleSystemBehaviorFactory"; export * from "./loggers/VFXLogger"; export * from "./VFXEffect"; +export * from "./utils/valueParser"; export type { VFXEffectNode } from "./VFXEffect"; diff --git a/editor/src/editor/windows/fx-editor/VFX/parsers/VFXDataConverter.ts b/editor/src/editor/windows/fx-editor/VFX/parsers/VFXDataConverter.ts index 2e940503d..7c8136e37 100644 --- a/editor/src/editor/windows/fx-editor/VFX/parsers/VFXDataConverter.ts +++ b/editor/src/editor/windows/fx-editor/VFX/parsers/VFXDataConverter.ts @@ -23,7 +23,7 @@ import type { QuarksRotationBySpeedBehavior, QuarksOrbitOverLifeBehavior, } from "../types/quarksTypes"; -import type { VFXTransform, VFXGroup, VFXEmitter, VFXHierarchy } from "../types/hierarchy"; +import type { VFXTransform, VFXGroup, VFXEmitter, VFXData } from "../types/hierarchy"; import type { VFXParticleEmitterConfig } from "../types/emitterConfig"; import type { VFXBehavior, @@ -58,7 +58,7 @@ export class VFXDataConverter { /** * Convert Quarks/Three.js VFX JSON to Babylon.js VFX format */ - public convert(quarksVFXData: QuarksVFXJSON): VFXHierarchy { + public convert(quarksVFXData: QuarksVFXJSON): VFXData { this._logger.log("=== Converting Quarks VFX to Babylon.js VFX format ===", this._options); const groups = new Map(); @@ -132,6 +132,9 @@ export class VFXDataConverter { // Convert emitter config from Quarks to VFX format const vfxConfig = this._convertEmitterConfig(obj.ps); + // Determine system type based on renderMode: 2 = solid, otherwise base + const systemType: "solid" | "base" = vfxConfig.renderMode === 2 ? "solid" : "base"; + const emitter: VFXEmitter = { uuid: obj.uuid || `emitter_${emitters.size}`, name: obj.name || "ParticleEmitter", @@ -139,10 +142,11 @@ export class VFXDataConverter { config: vfxConfig, materialId: obj.ps.material, parentUuid: parentUuid || undefined, + systemType, }; emitters.set(emitter.uuid, emitter); - this._logger.log(`${indent}Converted Emitter: ${emitter.name} (uuid: ${emitter.uuid})`, options); + this._logger.log(`${indent}Converted Emitter: ${emitter.name} (uuid: ${emitter.uuid}, systemType: ${systemType})`, options); return emitter; } diff --git a/editor/src/editor/windows/fx-editor/VFX/parsers/VFXParser.ts b/editor/src/editor/windows/fx-editor/VFX/parsers/VFXParser.ts index a2b83e7ae..7cdf5db3a 100644 --- a/editor/src/editor/windows/fx-editor/VFX/parsers/VFXParser.ts +++ b/editor/src/editor/windows/fx-editor/VFX/parsers/VFXParser.ts @@ -7,7 +7,7 @@ import { VFXValueParser } from "./VFXValueParser"; import { VFXMaterialFactory } from "../factories/VFXMaterialFactory"; import { VFXGeometryFactory } from "../factories/VFXGeometryFactory"; import { VFXEmitterFactory } from "../factories/VFXEmitterFactory"; -import { VFXHierarchyProcessor } from "../processors/VFXHierarchyProcessor"; +import { VFXSystemFactory } from "../factories/VFXSystemFactory"; import { VFXDataConverter } from "./VFXDataConverter"; import { VFXParticleSystem } from "../systems/VFXParticleSystem"; import { VFXSolidParticleSystem } from "../systems/VFXSolidParticleSystem"; @@ -23,7 +23,7 @@ export class VFXParser { private _materialFactory: VFXMaterialFactory; private _geometryFactory: VFXGeometryFactory; private _emitterFactory: VFXEmitterFactory; - private _hierarchyProcessor: VFXHierarchyProcessor; + private _systemFactory: VFXSystemFactory; constructor(scene: Scene, rootUrl: string, jsonData: QuarksVFXJSON, options?: VFXLoaderOptions) { const opts = options || {}; @@ -40,7 +40,7 @@ export class VFXParser { this._materialFactory = new VFXMaterialFactory(this._context); this._geometryFactory = new VFXGeometryFactory(this._context, this._materialFactory); this._emitterFactory = new VFXEmitterFactory(this._context, this._valueParser, this._materialFactory, this._geometryFactory); - this._hierarchyProcessor = new VFXHierarchyProcessor(this._context, this._emitterFactory); + this._systemFactory = new VFXSystemFactory(this._context, this._emitterFactory); } /** @@ -57,7 +57,7 @@ export class VFXParser { const dataConverter = new VFXDataConverter(options); const vfxData = dataConverter.convert(jsonData); this._context.vfxData = vfxData; - const particleSystems = this._hierarchyProcessor.processHierarchy(vfxData); + const particleSystems = this._systemFactory.createSystems(vfxData); this._logger.log(`=== Parsing complete. Created ${particleSystems.length} particle system(s) ===`, options); return particleSystems; diff --git a/editor/src/editor/windows/fx-editor/VFX/systems/VFXParticleSystem.ts b/editor/src/editor/windows/fx-editor/VFX/systems/VFXParticleSystem.ts index 798806c70..6fd1ce641 100644 --- a/editor/src/editor/windows/fx-editor/VFX/systems/VFXParticleSystem.ts +++ b/editor/src/editor/windows/fx-editor/VFX/systems/VFXParticleSystem.ts @@ -1,22 +1,179 @@ import { Color4, ParticleSystem, Scene } from "babylonjs"; -import type { VFXValueParser } from "../parsers/VFXValueParser"; import type { VFXPerParticleBehaviorFunction } from "../types/VFXBehaviorFunction"; +import type { + VFXBehavior, + VFXColorOverLifeBehavior, + VFXSizeOverLifeBehavior, + VFXRotationOverLifeBehavior, + VFXForceOverLifeBehavior, + VFXGravityForceBehavior, + VFXSpeedOverLifeBehavior, + VFXFrameOverLifeBehavior, + VFXLimitSpeedOverLifeBehavior, +} from "../types/behaviors"; +import { VFXParticleSystemBehaviorFactory } from "../factories/VFXParticleSystemBehaviorFactory"; +import { + applyColorOverLifePS, + applySizeOverLifePS, + applyRotationOverLifePS, + applyForceOverLifePS, + applyGravityForcePS, + applySpeedOverLifePS, + applyFrameOverLifePS, + applyLimitSpeedOverLifePS, +} from "../behaviors"; /** * Extended ParticleSystem with VFX behaviors support - * (logic intentionally minimal, behaviors handled elsewhere) + * Fully self-contained, no dependencies on parsers or factories */ export class VFXParticleSystem extends ParticleSystem { public startSize: number; public startSpeed: number; public startColor: Color4; - public behaviors: VFXPerParticleBehaviorFunction[]; - constructor(name: string, capacity: number, scene: Scene, _valueParser: VFXValueParser, _avgStartSpeed: number, _avgStartSize: number, _startColor: Color4) { + private _behaviors: VFXPerParticleBehaviorFunction[]; + private _behaviorFactory: VFXParticleSystemBehaviorFactory; + public readonly behaviorConfigs: VFXBehavior[]; + + constructor(name: string, capacity: number, scene: Scene, _avgStartSpeed: number, _avgStartSize: number, _startColor: Color4) { super(name, capacity, scene); - // behavior wiring omitted by design (see VFXEmitterFactory) + this._behaviors = []; + this._behaviorFactory = new VFXParticleSystemBehaviorFactory(this); + + // Create proxy array that updates functions when modified + this.behaviorConfigs = this._createBehaviorConfigsProxy([]); + } + + /** + * Get behavior functions (internal use) + */ + public get behaviors(): VFXPerParticleBehaviorFunction[] { + return this._behaviors; + } + + /** + * Create a proxy array that automatically updates behavior functions when configs change + */ + private _createBehaviorConfigsProxy(configs: VFXBehavior[]): VFXBehavior[] { + const self = this; + + // Wrap each behavior object in a proxy to detect property changes + const wrapBehavior = (behavior: VFXBehavior): VFXBehavior => { + return new Proxy(behavior, { + set(target, prop, value) { + const result = Reflect.set(target, prop, value); + // When a behavior property changes, update functions + self._updateBehaviorFunctions(); + return result; + }, + }); + }; + + // Wrap all initial behaviors + const wrappedConfigs = configs.map(wrapBehavior); + + return new Proxy(wrappedConfigs, { + set(target, property, value) { + const result = Reflect.set(target, property, value); + + // Update functions when array is modified + if (property === "length" || typeof property === "number") { + // If setting an element, wrap it in proxy + if (typeof property === "number" && value && typeof value === "object") { + Reflect.set(target, property, wrapBehavior(value as VFXBehavior)); + } + self._updateBehaviorFunctions(); + } + + return result; + }, + + get(target, property) { + const value = Reflect.get(target, property); + + // Intercept array methods that modify the array + if ( + typeof value === "function" && + (property === "push" || + property === "pop" || + property === "splice" || + property === "shift" || + property === "unshift" || + property === "sort" || + property === "reverse") + ) { + return function (...args: any[]) { + const result = value.apply(target, args); + // Wrap any new behaviors added via push/unshift + if (property === "push" || property === "unshift") { + for (let i = 0; i < args.length; i++) { + if (args[i] && typeof args[i] === "object") { + const index = property === "push" ? target.length - args.length + i : i; + Reflect.set(target, index, wrapBehavior(args[i] as VFXBehavior)); + } + } + } + self._updateBehaviorFunctions(); + return result; + }; + } + + return value; + }, + }); } - public setPerParticleBehaviors(functions: VFXPerParticleBehaviorFunction[]): void { - this.behaviors = functions; + /** + * Update behavior functions from configs + * Internal method, called automatically when configs change + * Applies both system-level behaviors (gradients) and per-particle behaviors + */ + private _updateBehaviorFunctions(): void { + // Apply system-level behaviors (gradients, etc.) - these configure the ParticleSystem once + this._applySystemLevelBehaviors(); + + // Create per-particle behavior functions + this._behaviors = this._behaviorFactory.createBehaviorFunctions(this.behaviorConfigs); + } + + /** + * Apply system-level behaviors (gradients, etc.) to ParticleSystem + * These are applied once when behaviors change, not per-particle + */ + private _applySystemLevelBehaviors(): void { + for (const behavior of this.behaviorConfigs) { + if (!behavior.type) { + continue; + } + + switch (behavior.type) { + case "ColorOverLife": + applyColorOverLifePS(this, behavior as VFXColorOverLifeBehavior); + break; + case "SizeOverLife": + applySizeOverLifePS(this, behavior as VFXSizeOverLifeBehavior); + break; + case "RotationOverLife": + case "Rotation3DOverLife": + applyRotationOverLifePS(this, behavior as VFXRotationOverLifeBehavior); + break; + case "ForceOverLife": + case "ApplyForce": + applyForceOverLifePS(this, behavior as VFXForceOverLifeBehavior); + break; + case "GravityForce": + applyGravityForcePS(this, behavior as VFXGravityForceBehavior); + break; + case "SpeedOverLife": + applySpeedOverLifePS(this, behavior as VFXSpeedOverLifeBehavior); + break; + case "FrameOverLife": + applyFrameOverLifePS(this, behavior as VFXFrameOverLifeBehavior); + break; + case "LimitSpeedOverLife": + applyLimitSpeedOverLifePS(this, behavior as VFXLimitSpeedOverLifeBehavior); + break; + } + } } } diff --git a/editor/src/editor/windows/fx-editor/VFX/systems/VFXSolidParticleSystem.ts b/editor/src/editor/windows/fx-editor/VFX/systems/VFXSolidParticleSystem.ts index a8578e3fc..57560ee7c 100644 --- a/editor/src/editor/windows/fx-editor/VFX/systems/VFXSolidParticleSystem.ts +++ b/editor/src/editor/windows/fx-editor/VFX/systems/VFXSolidParticleSystem.ts @@ -1,9 +1,15 @@ import { Vector3, Quaternion, Matrix, Color4, SolidParticleSystem, SolidParticle, TransformNode } from "babylonjs"; import type { VFXParticleEmitterConfig, VFXEmissionBurst } from "../types/emitterConfig"; -import type { VFXValueParser } from "../parsers/VFXValueParser"; import { VFXLogger } from "../loggers/VFXLogger"; import type { VFXLoaderOptions } from "../types/loader"; -import type { VFXPerSolidParticleBehaviorFunction, VFXPerParticleContext } from "../types/VFXBehaviorFunction"; +import type { VFXPerSolidParticleBehaviorFunction } from "../types/VFXBehaviorFunction"; +import type { VFXBehavior } from "../types/behaviors"; +import type { VFXShape } from "../types/shapes"; +import type { VFXColor } from "../types/colors"; +import type { VFXValue } from "../types/values"; +import type { VFXRotation } from "../types/rotations"; +import { VFXSolidParticleSystemBehaviorFactory } from "../factories/VFXSolidParticleSystemBehaviorFactory"; +import { VFXValueUtils } from "../utils/valueParser"; /** * Emission state matching three.quarks EmissionState structure @@ -26,78 +32,87 @@ interface EmissionState { */ export class VFXSolidParticleSystem extends SolidParticleSystem { private _emissionState: EmissionState; - private _config: VFXParticleEmitterConfig; - private _valueParser: VFXValueParser; - private _perParticleBehaviors: VFXPerSolidParticleBehaviorFunction[]; - private _parentGroup: TransformNode | null; + private _behaviors: VFXPerSolidParticleBehaviorFunction[]; + private _behaviorFactory: VFXSolidParticleSystemBehaviorFactory; + private _parent: TransformNode | null; private _vfxTransform: { position: Vector3; rotation: Quaternion; scale: Vector3 } | null; private _logger: VFXLogger | null; private _options: VFXLoaderOptions | undefined; private _name: string; - private _duration: number; - private _looping: boolean; private _emitEnded: boolean; - // Public properties for editor access - public get duration(): number { - return this._duration; - } - public set duration(value: number) { - this._duration = value; - if (this._config) { - this._config.duration = value; - } - } - - public get isLooping(): boolean { - return this._looping; - } - public set isLooping(value: boolean) { - this._looping = value; - if (this._config) { - this._config.looping = value; + // Properties moved from config + public isLooping: boolean; + public duration: number; + public prewarm: boolean; + public shape?: VFXShape; + public startLife?: VFXValue; + public startSpeed?: VFXValue; + public startRotation?: VFXRotation; + public startSize?: VFXValue; + public startColor?: VFXColor; + public emissionOverTime?: VFXValue; + public emissionOverDistance?: VFXValue; + public emissionBursts?: VFXEmissionBurst[]; + public onlyUsedByOther: boolean; + public instancingGeometry?: string; + public renderOrder?: number; + public renderMode?: number; + public rendererEmitterSettings?: Record; + public material?: string; + public layers?: number; + public startTileIndex?: VFXValue; + public uTileCount?: number; + public vTileCount?: number; + public blendTiles?: boolean; + public softParticles: boolean; + public softFarFade?: number; + public softNearFade?: number; + public worldSpace: boolean; + public readonly behaviorConfigs: VFXBehavior[]; + + /** + * Get/set parent transform node + */ + public get parent(): TransformNode | null { + return this._parent; + } + public set parent(value: TransformNode | null) { + this._parent = value; + if (this.mesh) { + this.mesh.setParent(value, false, true); } } - public get looping(): boolean { - return this._looping; - } - public set looping(value: boolean) { - this._looping = value; - if (this._config) { - this._config.looping = value; - } + /** + * Get behavior functions (internal use) + */ + public get behaviors(): VFXPerSolidParticleBehaviorFunction[] { + return this._behaviors; } + /** + * Get emit rate (constant value from emissionOverTime) + */ public get emitRate(): number { - if (!this._config || !this._config.emissionOverTime) { + if (!this.emissionOverTime) { return 10; // Default } - return this._valueParser.parseConstantValue(this._config.emissionOverTime); + return VFXValueUtils.parseConstantValue(this.emissionOverTime); } public set emitRate(value: number) { - if (this._config) { - this._config.emissionOverTime = { type: "ConstantValue", value }; - } + this.emissionOverTime = { type: "ConstantValue", value }; } + /** + * Get target stop duration (alias for duration) + */ public get targetStopDuration(): number { - return this._duration; + return this.duration; } public set targetStopDuration(value: number) { this.duration = value; } - - public get config(): VFXParticleEmitterConfig { - return this._config; - } - - public get behaviors(): VFXPerSolidParticleBehaviorFunction[] { - return this._perParticleBehaviors; - } - public set behaviors(value: VFXPerSolidParticleBehaviorFunction[]) { - this._perParticleBehaviors = value; - } private _normalMatrix: Matrix; private _tempVec: Vector3; private _tempVec2: Vector3; @@ -106,8 +121,7 @@ export class VFXSolidParticleSystem extends SolidParticleSystem { constructor( name: string, scene: any, - config: VFXParticleEmitterConfig, - valueParser: VFXValueParser, + initialConfig: VFXParticleEmitterConfig, // Initial config for parsing options?: { updatable?: boolean; isPickable?: boolean; @@ -123,15 +137,48 @@ export class VFXSolidParticleSystem extends SolidParticleSystem { super(name, scene, options); this._name = name; - this._config = config; - this._valueParser = valueParser; - this._perParticleBehaviors = []; - this._parentGroup = options?.parentGroup ?? null; + this._behaviors = []; + this._behaviorFactory = new VFXSolidParticleSystemBehaviorFactory(); + + // Initialize properties from initialConfig + this.isLooping = initialConfig.looping !== false; + this.duration = initialConfig.duration || 5; + this.prewarm = initialConfig.prewarm || false; + this.shape = initialConfig.shape; + this.startLife = initialConfig.startLife; + this.startSpeed = initialConfig.startSpeed; + this.startRotation = initialConfig.startRotation; + this.startSize = initialConfig.startSize; + this.startColor = initialConfig.startColor; + this.emissionOverTime = initialConfig.emissionOverTime; + this.emissionOverDistance = initialConfig.emissionOverDistance; + this.emissionBursts = initialConfig.emissionBursts; + this.onlyUsedByOther = initialConfig.onlyUsedByOther || false; + this.instancingGeometry = initialConfig.instancingGeometry; + this.renderOrder = initialConfig.renderOrder; + this.renderMode = initialConfig.renderMode; + this.rendererEmitterSettings = initialConfig.rendererEmitterSettings; + this.material = initialConfig.material; + this.layers = initialConfig.layers; + this.startTileIndex = initialConfig.startTileIndex; + this.uTileCount = initialConfig.uTileCount; + this.vTileCount = initialConfig.vTileCount; + this.blendTiles = initialConfig.blendTiles; + this.softParticles = initialConfig.softParticles || false; + this.softFarFade = initialConfig.softFarFade; + this.softNearFade = initialConfig.softNearFade; + this.worldSpace = initialConfig.worldSpace || false; + + // Create proxy array for behavior configs + this.behaviorConfigs = this._createBehaviorConfigsProxy(initialConfig.behaviors || []); + + // Initialize behavior functions from config + this._updateBehaviorFunctions(); + + this._parent = options?.parentGroup ?? null; this._vfxTransform = options?.vfxTransform ?? null; this._logger = options?.logger ?? null; this._options = options?.loaderOptions; - this._duration = config.duration || 5; - this._looping = config.looping !== false; this._emitEnded = false; this._normalMatrix = new Matrix(); this._tempVec = Vector3.Zero(); @@ -182,15 +229,12 @@ export class VFXSolidParticleSystem extends SolidParticleSystem { } private _initializeParticleColor(particle: SolidParticle): void { - const config = this._config; - const valueParser = this._valueParser; - if (!particle.color) { particle.color = new Color4(1, 1, 1, 1); } - if (config.startColor !== undefined) { - const startColor = valueParser.parseConstantColor(config.startColor); + if (this.startColor !== undefined) { + const startColor = VFXValueUtils.parseConstantColor(this.startColor); particle.props!.startColor = startColor.clone(); particle.color.copyFrom(startColor); } else { @@ -201,36 +245,27 @@ export class VFXSolidParticleSystem extends SolidParticleSystem { } private _initializeParticleSpeed(particle: SolidParticle): void { - const config = this._config; - const valueParser = this._valueParser; - - if (config.startSpeed !== undefined) { - const normalizedTime = this._emissionState.time / this._duration; - particle.props!.startSpeed = valueParser.parseValue(config.startSpeed, normalizedTime); + if (this.startSpeed !== undefined) { + const normalizedTime = this._emissionState.time / this.duration; + particle.props!.startSpeed = VFXValueUtils.parseValue(this.startSpeed, normalizedTime); } else { particle.props!.startSpeed = 0; } } private _initializeParticleLife(particle: SolidParticle): void { - const config = this._config; - const valueParser = this._valueParser; - - if (config.startLife !== undefined) { - const normalizedTime = this._emissionState.time / this._duration; - particle.lifeTime = valueParser.parseValue(config.startLife, normalizedTime); + if (this.startLife !== undefined) { + const normalizedTime = this._emissionState.time / this.duration; + particle.lifeTime = VFXValueUtils.parseValue(this.startLife, normalizedTime); } else { particle.lifeTime = 1; } } private _initializeParticleSize(particle: SolidParticle): void { - const config = this._config; - const valueParser = this._valueParser; - - if (config.startSize !== undefined) { - const normalizedTime = this._emissionState.time / this._duration; - const sizeValue = valueParser.parseValue(config.startSize, normalizedTime); + if (this.startSize !== undefined) { + const normalizedTime = this._emissionState.time / this.duration; + const sizeValue = VFXValueUtils.parseValue(this.startSize, normalizedTime); particle.props!.startSize = sizeValue; particle.scaling.setAll(sizeValue); } else { @@ -263,7 +298,7 @@ export class VFXSolidParticleSystem extends SolidParticleSystem { this._initializeParticleLife(particle); this._initializeParticleSize(particle); - this._initializeEmitterShape(particle, emissionState); + this._initializeEmitterShape(particle); } } @@ -315,20 +350,19 @@ export class VFXSolidParticleSystem extends SolidParticleSystem { particle.velocity.scaleInPlace(startSpeed); } - private _initializeEmitterShape(particle: SolidParticle, emissionState: EmissionState): void { - const config = this._config; + private _initializeEmitterShape(particle: SolidParticle): void { const startSpeed = particle.props?.startSpeed ?? 0; - if (!config.shape) { + if (!this.shape) { this._initializeDefaultShape(particle, startSpeed); return; } - const shapeType = config.shape.type?.toLowerCase(); - const radius = config.shape.radius ?? 1; - const arc = config.shape.arc ?? Math.PI * 2; - const thickness = config.shape.thickness ?? 1; - const angle = config.shape.angle ?? Math.PI / 6; + const shapeType = this.shape.type?.toLowerCase(); + const radius = this.shape.radius ?? 1; + const arc = this.shape.arc ?? Math.PI * 2; + const thickness = this.shape.thickness ?? 1; + const angle = this.shape.angle ?? Math.PI / 6; if (shapeType === "sphere") { this._initializeSphereShape(particle, radius, arc, thickness, startSpeed); @@ -353,9 +387,9 @@ export class VFXSolidParticleSystem extends SolidParticleSystem { private _handleEmissionLooping(): void { const emissionState = this._emissionState; - if (emissionState.time > this._duration) { - if (this._looping) { - emissionState.time -= this._duration; + if (emissionState.time > this.duration) { + if (this.isLooping) { + emissionState.time -= this.duration; emissionState.burstIndex = 0; } else if (!this._emitEnded) { this._emitEnded = true; @@ -374,16 +408,14 @@ export class VFXSolidParticleSystem extends SolidParticleSystem { private _spawnBursts(): void { const emissionState = this._emissionState; - const config = this._config; - const valueParser = this._valueParser; - if (!config.emissionBursts || !Array.isArray(config.emissionBursts)) { + if (!this.emissionBursts || !Array.isArray(this.emissionBursts)) { return; } - while (emissionState.burstIndex < config.emissionBursts.length && this._getBurstTime(config.emissionBursts[emissionState.burstIndex]) <= emissionState.time) { - const burst = config.emissionBursts[emissionState.burstIndex]; - const burstCount = valueParser.parseConstantValue(burst.count); + while (emissionState.burstIndex < this.emissionBursts.length && this._getBurstTime(this.emissionBursts[emissionState.burstIndex]) <= emissionState.time) { + const burst = this.emissionBursts[emissionState.burstIndex]; + const burstCount = VFXValueUtils.parseConstantValue(burst.count); emissionState.isBursting = true; emissionState.burstParticleCount = burstCount; this._spawn(burstCount); @@ -394,18 +426,16 @@ export class VFXSolidParticleSystem extends SolidParticleSystem { private _accumulateEmission(delta: number): void { const emissionState = this._emissionState; - const config = this._config; - const valueParser = this._valueParser; if (this._emitEnded) { return; } - const emissionRate = config.emissionOverTime !== undefined ? valueParser.parseConstantValue(config.emissionOverTime) : 10; + const emissionRate = this.emissionOverTime !== undefined ? VFXValueUtils.parseConstantValue(this.emissionOverTime) : 10; emissionState.waitEmiting += delta * emissionRate; - if (config.emissionOverDistance !== undefined && this.mesh && this.mesh.position) { - const emitPerMeter = valueParser.parseConstantValue(config.emissionOverDistance); + if (this.emissionOverDistance !== undefined && this.mesh && this.mesh.position) { + const emitPerMeter = VFXValueUtils.parseConstantValue(this.emissionOverDistance); if (emitPerMeter > 0 && emissionState.previousWorldPos) { const distance = Vector3.Distance(emissionState.previousWorldPos, this.mesh.position); emissionState.travelDistance += distance; @@ -432,12 +462,10 @@ export class VFXSolidParticleSystem extends SolidParticleSystem { } private _getBurstTime(burst: VFXEmissionBurst): number { - return this._valueParser.parseConstantValue(burst.time); + return VFXValueUtils.parseConstantValue(burst.time); } private _setupMeshProperties(): void { - const config = this._config; - if (!this.mesh) { if (this._logger) { this._logger.warn(` SPS mesh is null in initParticles!`, this._options); @@ -450,24 +478,24 @@ export class VFXSolidParticleSystem extends SolidParticleSystem { this._logger.log(` SPS mesh exists: ${this.mesh.name}`, this._options); } - if (config.renderOrder !== undefined) { - this.mesh.renderingGroupId = config.renderOrder; + if (this.renderOrder !== undefined) { + this.mesh.renderingGroupId = this.renderOrder; if (this._logger) { - this._logger.log(` Set SPS mesh renderingGroupId: ${config.renderOrder}`, this._options); + this._logger.log(` Set SPS mesh renderingGroupId: ${this.renderOrder}`, this._options); } } - if (config.layers !== undefined) { - this.mesh.layerMask = config.layers; + if (this.layers !== undefined) { + this.mesh.layerMask = this.layers; if (this._logger) { - this._logger.log(` Set SPS mesh layerMask: ${config.layers}`, this._options); + this._logger.log(` Set SPS mesh layerMask: ${this.layers}`, this._options); } } - if (this._parentGroup) { - this.mesh.setParent(this._parentGroup, false, true); + if (this._parent) { + this.mesh.setParent(this._parent, false, true); if (this._logger) { - this._logger.log(` Set SPS mesh parent to: ${this._parentGroup.name}`, this._options); + this._logger.log(` Set SPS mesh parent to: ${this._parent.name}`, this._options); } } else if (this._logger) { this._logger.log(` No parent group to set for SPS mesh`, this._options); @@ -530,8 +558,84 @@ export class VFXSolidParticleSystem extends SolidParticleSystem { this._resetEmissionState(); } - public setPerParticleBehaviors(functions: VFXPerSolidParticleBehaviorFunction[]): void { - this._perParticleBehaviors = functions; + /** + * Create a proxy array that automatically updates behavior functions when configs change + */ + private _createBehaviorConfigsProxy(configs: VFXBehavior[]): VFXBehavior[] { + const self = this; + + // Wrap each behavior object in a proxy to detect property changes + const wrapBehavior = (behavior: VFXBehavior): VFXBehavior => { + return new Proxy(behavior, { + set(target, prop, value) { + const result = Reflect.set(target, prop, value); + // When a behavior property changes, update functions + self._updateBehaviorFunctions(); + return result; + }, + }); + }; + + // Wrap all initial behaviors + const wrappedConfigs = configs.map(wrapBehavior); + + return new Proxy(wrappedConfigs, { + set(target, property, value) { + const result = Reflect.set(target, property, value); + + // Update functions when array is modified + if (property === "length" || typeof property === "number") { + // If setting an element, wrap it in proxy + if (typeof property === "number" && value && typeof value === "object") { + Reflect.set(target, property, wrapBehavior(value as VFXBehavior)); + } + self._updateBehaviorFunctions(); + } + + return result; + }, + + get(target, property) { + const value = Reflect.get(target, property); + + // Intercept array methods that modify the array + if ( + typeof value === "function" && + (property === "push" || + property === "pop" || + property === "splice" || + property === "shift" || + property === "unshift" || + property === "sort" || + property === "reverse") + ) { + return function (...args: any[]) { + const result = value.apply(target, args); + // Wrap any new behaviors added via push/unshift + if (property === "push" || property === "unshift") { + for (let i = 0; i < args.length; i++) { + if (args[i] && typeof args[i] === "object") { + const index = property === "push" ? target.length - args.length + i : i; + Reflect.set(target, index, wrapBehavior(args[i] as VFXBehavior)); + } + } + } + self._updateBehaviorFunctions(); + return result; + }; + } + + return value; + }, + }); + } + + /** + * Update behavior functions from configs + * Internal method, called automatically when configs change + */ + private _updateBehaviorFunctions(): void { + this._behaviors = this._behaviorFactory.createBehaviorFunctions(this.behaviorConfigs); } public override beforeUpdateParticles(start?: number, stop?: number, update?: boolean): void { @@ -569,22 +673,16 @@ export class VFXSolidParticleSystem extends SolidParticleSystem { return particle; } - const lifeRatio = particle.age / particle.lifeTime; - const startSpeed = particle.props?.startSpeed ?? 0; - const startSize = particle.props?.startSize ?? 1; - const startColor = particle.props?.startColor ?? new Color4(1, 1, 1, 1); - - const context: VFXPerParticleContext = { - lifeRatio, - startSpeed, - startSize, - startColor: { r: startColor.r, g: startColor.g, b: startColor.b, a: startColor.a }, - updateSpeed: this.updateSpeed, - valueParser: this._valueParser, - }; + // Store reference to system in particle for behaviors that need it + (particle as any).system = this; - for (const behaviorFn of this._perParticleBehaviors) { - behaviorFn(particle, context); + // Apply behaviors - they receive only particle and behavior config + // All data (lifeRatio, speed, etc.) comes from particle itself + // Behavior config is stored in closure by factory, so we pass it from behaviorConfigs + for (let i = 0; i < this._behaviors.length && i < this.behaviorConfigs.length; i++) { + const behaviorFn = this._behaviors[i]; + const behaviorConfig = this.behaviorConfigs[i]; + behaviorFn(particle, behaviorConfig); } const speedModifier = particle.props?.speedModifier ?? 1.0; diff --git a/editor/src/editor/windows/fx-editor/VFX/types/VFXBehaviorFunction.ts b/editor/src/editor/windows/fx-editor/VFX/types/VFXBehaviorFunction.ts index 80c16a13e..2c93499b2 100644 --- a/editor/src/editor/windows/fx-editor/VFX/types/VFXBehaviorFunction.ts +++ b/editor/src/editor/windows/fx-editor/VFX/types/VFXBehaviorFunction.ts @@ -1,29 +1,20 @@ -import { Particle, SolidParticle, ParticleSystem } from "babylonjs"; -import type { VFXValueParser } from "../parsers/VFXValueParser"; - -/** - * Context for per-particle behavior functions - */ -export interface VFXPerParticleContext { - lifeRatio: number; - startSpeed: number; - startSize: number; - startColor: { r: number; g: number; b: number; a: number }; - updateSpeed: number; - valueParser: VFXValueParser; -} +import { Particle, SolidParticle, ParticleSystem, SolidParticleSystem } from "babylonjs"; +import type { VFXBehavior } from "./behaviors"; /** * Per-particle behavior function for ParticleSystem + * Takes only particle and behavior config - all data comes from particle */ -export type VFXPerParticleBehaviorFunction = (particle: Particle, context: VFXPerParticleContext) => void; +export type VFXPerParticleBehaviorFunction = (particle: Particle, behavior: VFXBehavior) => void; /** * Per-particle behavior function for SolidParticleSystem + * Takes only particle and behavior config - all data comes from particle */ -export type VFXPerSolidParticleBehaviorFunction = (particle: SolidParticle, context: VFXPerParticleContext) => void; +export type VFXPerSolidParticleBehaviorFunction = (particle: SolidParticle, behavior: VFXBehavior) => void; /** * System-level behavior function (applied once during initialization) + * Takes only system and behavior config - all data comes from system */ -export type VFXSystemBehaviorFunction = (particleSystem: ParticleSystem, valueParser: VFXValueParser) => void; +export type VFXSystemBehaviorFunction = (system: ParticleSystem | SolidParticleSystem, behavior: VFXBehavior) => void; diff --git a/editor/src/editor/windows/fx-editor/VFX/types/context.ts b/editor/src/editor/windows/fx-editor/VFX/types/context.ts index 3fcc8feaf..22fc05f14 100644 --- a/editor/src/editor/windows/fx-editor/VFX/types/context.ts +++ b/editor/src/editor/windows/fx-editor/VFX/types/context.ts @@ -1,6 +1,6 @@ import { Scene, TransformNode } from "babylonjs"; import type { QuarksVFXJSON } from "./quarksTypes"; -import type { VFXHierarchy } from "./hierarchy"; +import type { VFXData } from "./hierarchy"; import type { VFXLoaderOptions } from "./loader"; /** @@ -12,5 +12,5 @@ export interface VFXParseContext { jsonData: QuarksVFXJSON; options: VFXLoaderOptions; groupNodesMap: Map; - vfxData?: VFXHierarchy; + vfxData?: VFXData; } diff --git a/editor/src/editor/windows/fx-editor/VFX/types/hierarchy.ts b/editor/src/editor/windows/fx-editor/VFX/types/hierarchy.ts index bf7f5b771..a4bf80409 100644 --- a/editor/src/editor/windows/fx-editor/VFX/types/hierarchy.ts +++ b/editor/src/editor/windows/fx-editor/VFX/types/hierarchy.ts @@ -30,12 +30,14 @@ export interface VFXEmitter { config: VFXParticleEmitterConfig; materialId?: string; parentUuid?: string; + systemType: "solid" | "base"; // Determined from renderMode: 2 = solid, otherwise base } /** - * VFX hierarchy (converted from Quarks) + * VFX data (converted from Quarks) + * Contains the converted VFX structure with groups and emitters */ -export interface VFXHierarchy { +export interface VFXData { root: VFXGroup | VFXEmitter | null; groups: Map; emitters: Map; diff --git a/editor/src/editor/windows/fx-editor/VFX/types/index.ts b/editor/src/editor/windows/fx-editor/VFX/types/index.ts index 5048e59cd..a7a24b695 100644 --- a/editor/src/editor/windows/fx-editor/VFX/types/index.ts +++ b/editor/src/editor/windows/fx-editor/VFX/types/index.ts @@ -37,6 +37,6 @@ export type { VFXBehavior, } from "./behaviors"; export type { VFXEmissionBurst, VFXParticleEmitterConfig } from "./emitterConfig"; -export type { VFXTransform, VFXGroup, VFXEmitter, VFXHierarchy } from "./hierarchy"; +export type { VFXTransform, VFXGroup, VFXEmitter, VFXData } from "./hierarchy"; export type { QuarksVFXJSON } from "./quarksTypes"; export type { VFXPerParticleContext, VFXPerParticleBehaviorFunction, VFXPerSolidParticleBehaviorFunction, VFXSystemBehaviorFunction } from "./VFXBehaviorFunction"; diff --git a/editor/src/editor/windows/fx-editor/VFX/utils/valueParser.ts b/editor/src/editor/windows/fx-editor/VFX/utils/valueParser.ts new file mode 100644 index 000000000..fdd157dbf --- /dev/null +++ b/editor/src/editor/windows/fx-editor/VFX/utils/valueParser.ts @@ -0,0 +1,138 @@ +import { Color4 } from "babylonjs"; +import type { VFXValue } from "../types/values"; +import type { VFXColor } from "../types/colors"; + +/** + * Static utility functions for parsing VFX values + * These are stateless and don't require an instance + */ +export class VFXValueUtils { + /** + * Parse a constant value + */ + public static parseConstantValue(value: VFXValue): number { + if (value && typeof value === "object" && value.type === "ConstantValue") { + return value.value || 0; + } + return typeof value === "number" ? value : 0; + } + + /** + * Parse an interval value (returns min and max) + */ + public static parseIntervalValue(value: VFXValue): { min: number; max: number } { + if (value && typeof value === "object" && "type" in value && value.type === "IntervalValue") { + return { + min: value.min ?? 0, + max: value.max ?? 0, + }; + } + const constant = this.parseConstantValue(value); + return { min: constant, max: constant }; + } + + /** + * Parse a constant color + */ + public static parseConstantColor(value: VFXColor): Color4 { + if (value && typeof value === "object" && !Array.isArray(value)) { + if ("type" in value && value.type === "ConstantColor") { + if (value.value && Array.isArray(value.value)) { + return new Color4(value.value[0] || 0, value.value[1] || 0, value.value[2] || 0, value.value[3] !== undefined ? value.value[3] : 1); + } + } else if (Array.isArray(value) && value.length >= 3) { + // Array format [r, g, b, a?] + return new Color4(value[0] || 0, value[1] || 0, value[2] || 0, value[3] !== undefined ? value[3] : 1); + } + } + return new Color4(1, 1, 1, 1); + } + + /** + * Parse a value for particle spawn (returns a single value based on type) + * Handles ConstantValue, IntervalValue, PiecewiseBezier, and number + * @param value The value to parse + * @param normalizedTime Normalized time (0-1) for PiecewiseBezier, default is random for IntervalValue + */ + public static parseValue(value: VFXValue, normalizedTime?: number): number { + if (!value || typeof value === "number") { + return typeof value === "number" ? value : 0; + } + + if (value.type === "ConstantValue") { + return value.value || 0; + } + + if (value.type === "IntervalValue") { + const min = value.min ?? 0; + const max = value.max ?? 0; + return min + Math.random() * (max - min); + } + + if (value.type === "PiecewiseBezier") { + // Use provided normalizedTime or random for spawn + const t = normalizedTime !== undefined ? normalizedTime : Math.random(); + return this._evaluatePiecewiseBezier(value, t); + } + + // Fallback + return 0; + } + + /** + * Evaluate PiecewiseBezier at normalized time t (0-1) + */ + private static _evaluatePiecewiseBezier(bezier: any, t: number): number { + if (!bezier.functions || bezier.functions.length === 0) { + return 0; + } + + // Clamp t to [0, 1] + const clampedT = Math.max(0, Math.min(1, t)); + + // Find which function segment contains t + let segmentIndex = -1; + for (let i = 0; i < bezier.functions.length; i++) { + const func = bezier.functions[i]; + const start = func.start; + const end = i < bezier.functions.length - 1 ? bezier.functions[i + 1].start : 1; + + if (clampedT >= start && clampedT < end) { + segmentIndex = i; + break; + } + } + + // If t is at the end (1.0), use last segment + if (segmentIndex === -1 && clampedT >= 1) { + segmentIndex = bezier.functions.length - 1; + } + + // If still not found, use first segment + if (segmentIndex === -1) { + segmentIndex = 0; + } + + const func = bezier.functions[segmentIndex]; + const start = func.start; + const end = segmentIndex < bezier.functions.length - 1 ? bezier.functions[segmentIndex + 1].start : 1; + + // Normalize t within this segment + const segmentT = end > start ? (clampedT - start) / (end - start) : 0; + + // Evaluate cubic Bezier: B(t) = (1-t)³P₀ + 3(1-t)²tP₁ + 3(1-t)t²P₂ + t³P₃ + const p0 = func.function.p0; + const p1 = func.function.p1; + const p2 = func.function.p2; + const p3 = func.function.p3; + + const t2 = segmentT * segmentT; + const t3 = t2 * segmentT; + const mt = 1 - segmentT; + const mt2 = mt * mt; + const mt3 = mt2 * mt; + + return mt3 * p0 + 3 * mt2 * segmentT * p1 + 3 * mt * t2 * p2 + t3 * p3; + } +} + diff --git a/editor/src/editor/windows/fx-editor/properties/behaviors.tsx b/editor/src/editor/windows/fx-editor/properties/behaviors.tsx index bc506a1e4..ebe39a317 100644 --- a/editor/src/editor/windows/fx-editor/properties/behaviors.tsx +++ b/editor/src/editor/windows/fx-editor/properties/behaviors.tsx @@ -8,6 +8,7 @@ import { HiOutlineTrash } from "react-icons/hi2"; import { IoAddSharp } from "react-icons/io5"; import type { VFXEffectNode } from "../VFX"; +import { VFXParticleSystem, VFXSolidParticleSystem } from "../VFX"; import { BehaviorRegistry, createDefaultBehaviorData, getBehaviorDefinition } from "./behaviors/registry"; import { BehaviorProperties } from "./behaviors/behavior-properties"; @@ -24,25 +25,81 @@ export function FXEditorBehaviorsProperties(props: IFXEditorBehaviorsPropertiesP } const system = nodeData.system; - // Get behaviors from system (system.behaviors for VFXParticleSystem) - const behaviors: any[] = (system as any).behaviors || []; + + // Get behavior configurations from system + let behaviorConfigs: any[] = []; + if (system instanceof VFXParticleSystem) { + behaviorConfigs = system.behaviorConfigs || []; + } else if (system instanceof VFXSolidParticleSystem) { + behaviorConfigs = system.behaviorConfigs || []; + } + + const handleAddBehavior = (behaviorType: string): void => { + const newBehavior = createDefaultBehaviorData(behaviorType); + newBehavior.id = `behavior-${Date.now()}-${Math.random()}`; + + // Directly modify the array - proxy will automatically update functions + behaviorConfigs.push(newBehavior); + onChange(); + }; + + const handleRemoveBehavior = (index: number): void => { + // Directly modify the array - proxy will automatically update functions + behaviorConfigs.splice(index, 1); + onChange(); + }; + + const handleBehaviorChange = (): void => { + // When behavior properties change, the proxy automatically detects it + // and updates the behavior functions. We just need to trigger UI update. + onChange(); + }; return ( <> - {behaviors.length === 0 &&
No behaviors. Behaviors are applied as functions to particles.
} - {behaviors.map((behavior, index) => { - // Behaviors are functions, not objects with properties - // We can show function name or type if available - const behaviorName = behavior.name || `Behavior ${index + 1}`; + {behaviorConfigs.length === 0 &&
No behaviors. Click "Add Behavior" to add one.
} + {behaviorConfigs.map((behavior, index) => { + const definition = getBehaviorDefinition(behavior.type); + const title = definition?.label || behavior.type || `Behavior ${index + 1}`; return ( - -
Behavior function (editing not yet supported)
+ + {title} + + + } + > + ); })} - {/* TODO: Add ability to add/remove behaviors */} + + + + + + {Object.values(BehaviorRegistry).map((definition) => ( + handleAddBehavior(definition.type)}> + {definition.label} + + ))} + + ); } diff --git a/editor/src/editor/windows/fx-editor/properties/emitter-shape.tsx b/editor/src/editor/windows/fx-editor/properties/emitter-shape.tsx index 7e67a069d..e495cb57d 100644 --- a/editor/src/editor/windows/fx-editor/properties/emitter-shape.tsx +++ b/editor/src/editor/windows/fx-editor/properties/emitter-shape.tsx @@ -20,13 +20,12 @@ export class FXEditorEmitterShapeProperties extends Component -
Emitter shape: {config.shape?.type || "Default"}
- {/* TODO: Add shape-specific property editors based on config.shape.type */} +
Emitter shape: {system.shape?.type || "Default"}
+ {/* TODO: Add shape-specific property editors based on system.shape.type */} ); } diff --git a/editor/src/editor/windows/fx-editor/properties/particle-initialization.tsx b/editor/src/editor/windows/fx-editor/properties/particle-initialization.tsx index 0634957c5..29c3c51ad 100644 --- a/editor/src/editor/windows/fx-editor/properties/particle-initialization.tsx +++ b/editor/src/editor/windows/fx-editor/properties/particle-initialization.tsx @@ -56,15 +56,14 @@ export function FXEditorParticleInitializationProperties(props: IFXEditorParticl ); } - // For VFXSolidParticleSystem, initialization properties are in config (VFXValue format) + // For VFXSolidParticleSystem, initialization properties are VFXValue format // TODO: Add proper editors for VFXValue (ConstantValue, IntervalValue, etc.) if (system instanceof VFXSolidParticleSystem) { - const config = system.config; // For now, show that properties exist but need proper VFXValue editors return ( <>
- Initialization properties are stored in config as VFXValue. Full editor support coming soon. + Initialization properties are stored as VFXValue. Full editor support coming soon.
{/* TODO: Add VFXValue editors for startLife, startSize, startSpeed, startColor */} From 97ae33932051ca7b1434b366f4ca9b67c8ded339 Mon Sep 17 00:00:00 2001 From: Mikalai Lazitski Date: Fri, 12 Dec 2025 19:21:23 +0300 Subject: [PATCH 18/62] refactor: enhance VFX component structure by removing unused parsers and consolidating emitter factories for improved clarity and performance --- .../editor/windows/fx-editor/VFX/VFXEffect.ts | 22 +- .../VFX/factories/VFXEmitterFactory.ts | 528 ++---------------- .../VFXParticleSystemEmitterFactory.ts | 147 +++++ .../VFXSolidParticleSystemEmitterFactory.ts | 100 ++++ .../src/editor/windows/fx-editor/VFX/index.ts | 1 - .../fx-editor/VFX/parsers/VFXParser.ts | 12 +- .../fx-editor/VFX/parsers/VFXValueParser.ts | 186 ------ .../VFX/systems/VFXParticleSystem.ts | 167 +++++- .../VFX/systems/VFXSolidParticleSystem.ts | 76 +-- .../windows/fx-editor/VFX/types/factories.ts | 13 +- .../windows/fx-editor/VFX/types/index.ts | 2 +- .../fx-editor/VFX/utils/valueParser.ts | 51 +- 12 files changed, 528 insertions(+), 777 deletions(-) create mode 100644 editor/src/editor/windows/fx-editor/VFX/factories/VFXParticleSystemEmitterFactory.ts create mode 100644 editor/src/editor/windows/fx-editor/VFX/factories/VFXSolidParticleSystemEmitterFactory.ts delete mode 100644 editor/src/editor/windows/fx-editor/VFX/parsers/VFXValueParser.ts diff --git a/editor/src/editor/windows/fx-editor/VFX/VFXEffect.ts b/editor/src/editor/windows/fx-editor/VFX/VFXEffect.ts index 46e5c26ff..6b160f9c8 100644 --- a/editor/src/editor/windows/fx-editor/VFX/VFXEffect.ts +++ b/editor/src/editor/windows/fx-editor/VFX/VFXEffect.ts @@ -4,7 +4,7 @@ import type { VFXLoaderOptions } from "./types/loader"; import { VFXParser } from "./parsers/VFXParser"; import type { VFXParticleSystem } from "./systems/VFXParticleSystem"; import type { VFXSolidParticleSystem } from "./systems/VFXSolidParticleSystem"; -import type { VFXGroup, VFXEmitter } from "./types/hierarchy"; +import type { VFXGroup, VFXEmitter, VFXData } from "./types/hierarchy"; /** * VFX Effect Node - represents either a particle system or a group @@ -95,10 +95,14 @@ export class VFXEffect implements IDisposable { const parser = new VFXParser(scene, rootUrl, jsonData, options); const particleSystems = parser.parse(); const context = parser.getContext(); + const vfxData = context.vfxData; + const groupNodesMap = context.groupNodesMap; const effect = new VFXEffect(); effect.systems.push(...particleSystems); - effect._buildHierarchy(context, particleSystems); + if (vfxData && groupNodesMap) { + effect._buildHierarchy(vfxData, groupNodesMap, particleSystems); + } return effect; } @@ -115,24 +119,26 @@ export class VFXEffect implements IDisposable { const parser = new VFXParser(scene, rootUrl, jsonData, options); const particleSystems = parser.parse(); const context = parser.getContext(); + const vfxData = context.vfxData; + const groupNodesMap = context.groupNodesMap; this.systems.push(...particleSystems); - this._buildHierarchy(context, particleSystems); + if (vfxData && groupNodesMap) { + this._buildHierarchy(vfxData, groupNodesMap, particleSystems); + } } } /** - * Build hierarchy from parser context + * Build hierarchy from VFX data and group nodes map */ - private _buildHierarchy(context: any, systems: (VFXParticleSystem | VFXSolidParticleSystem)[]): void { - // Build hierarchy from vfxData if available - const vfxData = context.vfxData; + private _buildHierarchy(vfxData: VFXData, groupNodesMap: Map, systems: (VFXParticleSystem | VFXSolidParticleSystem)[]): void { if (!vfxData || !vfxData.root) { return; } // Create nodes from hierarchy - const rootNode = this._buildNodeFromHierarchy(vfxData.root, null, context.groupNodesMap, systems); + const rootNode = this._buildNodeFromHierarchy(vfxData.root, null, groupNodesMap, systems); // Store root (we can't assign to readonly, so we'll use a workaround) (this as any).root = rootNode; } diff --git a/editor/src/editor/windows/fx-editor/VFX/factories/VFXEmitterFactory.ts b/editor/src/editor/windows/fx-editor/VFX/factories/VFXEmitterFactory.ts index 0efaf7bdd..e1f4a01cc 100644 --- a/editor/src/editor/windows/fx-editor/VFX/factories/VFXEmitterFactory.ts +++ b/editor/src/editor/windows/fx-editor/VFX/factories/VFXEmitterFactory.ts @@ -1,43 +1,25 @@ -import { Mesh, CreatePlane, Nullable, Color4, Matrix, ParticleSystem, SolidParticleSystem, Constants, Vector3, Quaternion } from "babylonjs"; +import { Mesh, CreatePlane, Nullable, Color4, Matrix, ParticleSystem, SolidParticleSystem, Constants, Vector3, Quaternion, Texture } from "babylonjs"; import type { VFXEmitterData } from "../types/emitter"; import type { VFXParseContext } from "../types/context"; import { VFXLogger } from "../loggers/VFXLogger"; -import { VFXValueParser } from "../parsers/VFXValueParser"; +import { VFXValueUtils } from "../utils/valueParser"; import type { IVFXMaterialFactory, IVFXGeometryFactory } from "../types/factories"; import { VFXSolidParticleSystem } from "../systems/VFXSolidParticleSystem"; import { VFXParticleSystem } from "../systems/VFXParticleSystem"; -import type { VFXBehavior, VFXEmissionBurst } from "../types"; import type { VFXParticleEmitterConfig } from "../types/emitterConfig"; -/** - * Parsed values for particle system creation - */ -type ParsedParticleValues = { - emissionRate: number; - duration: number; - capacity: number; - lifeTime: { min: number; max: number }; - speed: { min: number; max: number }; - avgStartSpeed: number; - size: { min: number; max: number }; - avgStartSize: number; - startColor: Color4; -}; - /** * Factory for creating particle emitters (ParticleSystem and SolidParticleSystem) */ export class VFXEmitterFactory { private _logger: VFXLogger; private _context: VFXParseContext; - private _valueParser: VFXValueParser; private _materialFactory: IVFXMaterialFactory; private _geometryFactory: IVFXGeometryFactory; - constructor(context: VFXParseContext, valueParser: VFXValueParser, materialFactory: IVFXMaterialFactory, geometryFactory: IVFXGeometryFactory) { + constructor(context: VFXParseContext, materialFactory: IVFXMaterialFactory, geometryFactory: IVFXGeometryFactory) { this._context = context; this._logger = new VFXLogger("[VFXEmitterFactory]"); - this._valueParser = valueParser; this._materialFactory = materialFactory; this._geometryFactory = geometryFactory; } @@ -64,186 +46,43 @@ export class VFXEmitterFactory { */ private _createParticleSystem(emitterData: VFXEmitterData): Nullable { const { name, config } = emitterData; - const { options } = this._context; + const { options, scene } = this._context; this._logger.log(`Creating ParticleSystem: ${name}`, options); - const parsedValues = this._parseParticleSystemValues(config); - const particleSystem = this._createParticleSystemInstance(name, parsedValues); - - this._configureBasicProperties(particleSystem, parsedValues); - this._configureRotation(particleSystem, config); - this._configureSpriteTiles(particleSystem, config); - this._configureRendering(particleSystem, config); - this._setEmitterShape(particleSystem, config.shape, emitterData.cumulativeScale, emitterData.matrix); - this._applyTextureAndBlendMode(particleSystem, emitterData.materialId); - this._applyEmissionBurstsIfNeeded(particleSystem, config, parsedValues.emissionRate, parsedValues.duration); - this._applyBehaviorsIfNeeded(particleSystem, config.behaviors); - this._configureWorldSpace(particleSystem, config); - this._configureLooping(particleSystem, config, parsedValues.duration); - this._configureRenderMode(particleSystem, config); - this._configureSoftParticlesAndAutoDestroy(particleSystem, config); - - this._logger.log(`ParticleSystem created: ${name}`, options); - return particleSystem; - } - - /** - * Parses all particle system values from config - */ - private _parseParticleSystemValues(config: VFXParticleEmitterConfig): ParsedParticleValues { - const emissionRate = config.emissionOverTime !== undefined ? this._valueParser.parseConstantValue(config.emissionOverTime) : 10; + // Parse values for capacity calculation + const emissionRate = config.emissionOverTime !== undefined ? VFXValueUtils.parseConstantValue(config.emissionOverTime) : 10; const duration = config.duration || 5; const capacity = Math.ceil(emissionRate * duration * 2); + const speed = config.startSpeed !== undefined ? VFXValueUtils.parseIntervalValue(config.startSpeed) : { min: 1, max: 1 }; + const size = config.startSize !== undefined ? VFXValueUtils.parseIntervalValue(config.startSize) : { min: 1, max: 1 }; + const startColor = config.startColor !== undefined ? VFXValueUtils.parseConstantColor(config.startColor) : new Color4(1, 1, 1, 1); + const avgStartSpeed = (speed.min + speed.max) / 2; + const avgStartSize = (size.min + size.max) / 2; + + // Create instance + const particleSystem = new VFXParticleSystem(name, capacity, scene, avgStartSpeed, avgStartSize, startColor); + + // Get texture and blend mode + const texture: Texture | undefined = emitterData.materialId ? this._materialFactory.createTexture(emitterData.materialId) || undefined : undefined; + const blendMode = emitterData.materialId ? this._getBlendModeFromMaterial(emitterData.materialId) : undefined; + + // Extract rotation matrix + const rotationMatrix = this._extractRotationMatrix(emitterData.matrix); + + // Configure from config + particleSystem.configureFromConfig(config, { + texture, + blendMode, + emitterShape: { + shape: config.shape, + cumulativeScale: emitterData.cumulativeScale, + rotationMatrix, + }, + }); - const lifeTime = config.startLife !== undefined ? this._valueParser.parseIntervalValue(config.startLife) : { min: 1, max: 1 }; - const speed = config.startSpeed !== undefined ? this._valueParser.parseIntervalValue(config.startSpeed) : { min: 1, max: 1 }; - const size = config.startSize !== undefined ? this._valueParser.parseIntervalValue(config.startSize) : { min: 1, max: 1 }; - const startColor = config.startColor !== undefined ? this._valueParser.parseConstantColor(config.startColor) : new Color4(1, 1, 1, 1); - - this._logParsedValues(emissionRate, duration, capacity, lifeTime, speed, size, startColor); - - return { - emissionRate, - duration, - capacity, - lifeTime, - speed, - avgStartSpeed: (speed.min + speed.max) / 2, - size, - avgStartSize: (size.min + size.max) / 2, - startColor, - }; - } - - /** - * Logs parsed particle system values - */ - private _logParsedValues( - emissionRate: number, - duration: number, - capacity: number, - lifeTime: { min: number; max: number }, - speed: { min: number; max: number }, - size: { min: number; max: number }, - startColor: Color4 - ): void { - const { options } = this._context; - this._logger.log(` Emission rate: ${emissionRate}, Duration: ${duration}, Capacity: ${capacity}`, options); - this._logger.log(` Life time: ${lifeTime.min} - ${lifeTime.max}`, options); - this._logger.log(` Speed: ${speed.min} - ${speed.max}`, options); - this._logger.log(` Size: ${size.min} - ${size.max}`, options); - this._logger.log(` Start color: R=${startColor.r}, G=${startColor.g}, B=${startColor.b}, A=${startColor.a}`, options); - } - - /** - * Creates ParticleSystem instance - */ - private _createParticleSystemInstance(name: string, values: ParsedParticleValues): VFXParticleSystem { - const { scene } = this._context; - return new VFXParticleSystem(name, values.capacity, scene, values.avgStartSpeed, values.avgStartSize, values.startColor); - } - - /** - * Configures basic particle system properties - */ - private _configureBasicProperties(particleSystem: ParticleSystem, values: ParsedParticleValues): void { - particleSystem.targetStopDuration = values.duration; - particleSystem.emitRate = values.emissionRate; - particleSystem.manualEmitCount = -1; - - particleSystem.minLifeTime = values.lifeTime.min; - particleSystem.maxLifeTime = values.lifeTime.max; - - particleSystem.minEmitPower = values.speed.min; - particleSystem.maxEmitPower = values.speed.max; - particleSystem.minSize = values.size.min; - particleSystem.maxSize = values.size.max; - - particleSystem.color1 = values.startColor; - particleSystem.color2 = values.startColor; - particleSystem.colorDead = new Color4(values.startColor.r, values.startColor.g, values.startColor.b, 0); - } - - /** - * Configures rotation settings - */ - private _configureRotation(particleSystem: ParticleSystem, config: VFXParticleEmitterConfig): void { - if (!config.startRotation) { - return; - } - - if (this._isEulerRotation(config.startRotation)) { - if (config.startRotation.angleZ !== undefined) { - const angleZ = this._valueParser.parseIntervalValue(config.startRotation.angleZ); - particleSystem.minInitialRotation = angleZ.min; - particleSystem.maxInitialRotation = angleZ.max; - } - } else { - const rotation = this._valueParser.parseIntervalValue(config.startRotation as any); - particleSystem.minInitialRotation = rotation.min; - particleSystem.maxInitialRotation = rotation.max; - } - } - - /** - * Checks if rotation is Euler type - */ - private _isEulerRotation(rotation: any): rotation is { type: "Euler"; angleZ?: any } { - return typeof rotation === "object" && rotation !== null && "type" in rotation && rotation.type === "Euler"; - } - - /** - * Configures sprite tiles for animation sheets - */ - private _configureSpriteTiles(particleSystem: ParticleSystem, config: VFXParticleEmitterConfig): void { - if (config.uTileCount === undefined || config.vTileCount === undefined) { - return; - } - - if (config.uTileCount > 1 || config.vTileCount > 1) { - particleSystem.isAnimationSheetEnabled = true; - particleSystem.spriteCellWidth = config.uTileCount; - particleSystem.spriteCellHeight = config.vTileCount; - - if (config.startTileIndex !== undefined) { - const startTile = this._valueParser.parseConstantValue(config.startTileIndex); - particleSystem.startSpriteCellID = Math.floor(startTile); - particleSystem.endSpriteCellID = Math.floor(startTile); - } - } - } - - /** - * Configures rendering properties (render order and layers) - */ - private _configureRendering(particleSystem: ParticleSystem, config: VFXParticleEmitterConfig): void { - if (config.renderOrder !== undefined) { - particleSystem.renderingGroupId = config.renderOrder; - } - if (config.layers !== undefined) { - particleSystem.layerMask = config.layers; - } - } - - /** - * Applies texture and blend mode from material - */ - private _applyTextureAndBlendMode(particleSystem: ParticleSystem, materialId: string | undefined): void { - if (!materialId) { - return; - } - - const texture = this._materialFactory.createTexture(materialId); - if (!texture) { - return; - } - - particleSystem.particleTexture = texture; - const blendMode = this._getBlendModeFromMaterial(materialId); - if (blendMode !== undefined) { - particleSystem.blendMode = blendMode; - } + this._logger.log(`ParticleSystem created: ${name}`, options); + return particleSystem; } /** @@ -266,88 +105,6 @@ export class VFXEmitterFactory { return blendModeMap[material.blending]; } - /** - * Applies emission bursts if configured - */ - private _applyEmissionBurstsIfNeeded(particleSystem: ParticleSystem, config: VFXParticleEmitterConfig, emissionRate: number, duration: number): void { - if (config.emissionBursts && Array.isArray(config.emissionBursts) && config.emissionBursts.length > 0) { - this._applyEmissionBursts(particleSystem, config.emissionBursts, emissionRate, duration); - } - } - - /** - * Applies behaviors if configured - */ - private _applyBehaviorsIfNeeded(particleSystem: ParticleSystem, behaviors: VFXBehavior[] | undefined): void { - if (behaviors && Array.isArray(behaviors) && behaviors.length > 0) { - this._applyBehaviorsToPS(particleSystem, behaviors); - } - } - - /** - * Configures world space setting - */ - private _configureWorldSpace(particleSystem: ParticleSystem, config: VFXParticleEmitterConfig): void { - if (config.worldSpace !== undefined) { - particleSystem.isLocal = !config.worldSpace; - const { options } = this._context; - this._logger.log(` World space: ${config.worldSpace}`, options); - } - } - - /** - * Configures looping setting - */ - private _configureLooping(particleSystem: ParticleSystem, config: VFXParticleEmitterConfig, duration: number): void { - if (config.looping !== undefined) { - particleSystem.targetStopDuration = config.looping ? 0 : duration; - const { options } = this._context; - this._logger.log(` Looping: ${config.looping}`, options); - } - } - - /** - * Configures render mode - */ - private _configureRenderMode(particleSystem: ParticleSystem, config: VFXParticleEmitterConfig): void { - if (config.renderMode === undefined) { - return; - } - - const { options } = this._context; - const renderModeMap: Record void> = { - 0: () => { - particleSystem.isBillboardBased = true; - this._logger.log(` Render mode: Billboard`, options); - }, - 1: () => { - particleSystem.billboardMode = ParticleSystem.BILLBOARDMODE_STRETCHED; - this._logger.log(` Render mode: Stretched Billboard`, options); - }, - }; - - const handler = renderModeMap[config.renderMode]; - if (handler) { - handler(); - } - } - - /** - * Configures soft particles and auto destroy settings - */ - private _configureSoftParticlesAndAutoDestroy(particleSystem: ParticleSystem, config: VFXParticleEmitterConfig): void { - const { options } = this._context; - - if (config.softParticles !== undefined) { - this._logger.log(` Soft particles: ${config.softParticles} (not fully supported)`, options); - } - - if (config.autoDestroy !== undefined) { - particleSystem.disposeOnStop = config.autoDestroy; - this._logger.log(` Auto destroy: ${config.autoDestroy}`, options); - } - } - /** * Create a SolidParticleSystem (mesh-based particles) */ @@ -368,7 +125,12 @@ export class VFXEmitterFactory { this._addShapeToSPS(sps, particleMesh, capacity); this._configureSPSBillboard(sps, config); - this._applyBehaviors(sps, config.behaviors || []); + + // Apply behaviors + if (config.behaviors && Array.isArray(config.behaviors) && config.behaviors.length > 0) { + sps.behaviorConfigs.length = 0; + sps.behaviorConfigs.push(...config.behaviors); + } particleMesh.dispose(); @@ -381,7 +143,7 @@ export class VFXEmitterFactory { */ private _calculateSPSCapacity(config: VFXParticleEmitterConfig): number { const { options } = this._context; - const emissionRate = config.emissionOverTime !== undefined ? this._valueParser.parseConstantValue(config.emissionOverTime) : 10; + const emissionRate = config.emissionOverTime !== undefined ? VFXValueUtils.parseConstantValue(config.emissionOverTime) : 10; const particleLifetime = config.duration || 5; const isLooping = config.looping !== false; @@ -522,25 +284,6 @@ export class VFXEmitterFactory { } } - /** - * Set the emitter shape based on Three.js shape configuration - */ - private _setEmitterShape(particleSystem: ParticleSystem, shape: any, cumulativeScale: Vector3, matrix?: number[]): void { - if (!shape || !shape.type) { - particleSystem.createPointEmitter(Vector3.Zero(), Vector3.Zero()); - return; - } - - const rotationMatrix = this._extractRotationMatrix(matrix); - const shapeHandler = this._getShapeHandler(shape.type.toLowerCase()); - - if (shapeHandler) { - shapeHandler(particleSystem, shape, cumulativeScale, rotationMatrix); - } else { - this._createDefaultPointEmitter(particleSystem, rotationMatrix); - } - } - /** * Extracts rotation matrix from Three.js matrix array */ @@ -549,195 +292,8 @@ export class VFXEmitterFactory { return null; } - const { options } = this._context; const mat = Matrix.FromArray(matrix); mat.transpose(); - const rotationMatrix = mat.getRotationMatrix(); - this._logger.log(` Extracted rotation from matrix`, options); - return rotationMatrix; - } - - /** - * Gets shape handler function for given shape type - */ - private _getShapeHandler(shapeType: string): ((ps: ParticleSystem, shape: any, scale: Vector3, rotation: Matrix | null) => void) | null { - const shapeHandlers: Record void> = { - cone: this._createConeEmitter.bind(this), - sphere: this._createSphereEmitter.bind(this), - point: this._createPointEmitter.bind(this), - box: this._createBoxEmitter.bind(this), - hemisphere: this._createHemisphereEmitter.bind(this), - cylinder: this._createCylinderEmitter.bind(this), - }; - - return shapeHandlers[shapeType] || null; - } - - /** - * Applies rotation to default direction vector - */ - private _applyRotationToDirection(defaultDir: Vector3, rotationMatrix: Matrix | null): Vector3 { - if (!rotationMatrix) { - return defaultDir; - } - - const rotatedDir = Vector3.Zero(); - Vector3.TransformNormalToRef(defaultDir, rotationMatrix, rotatedDir); - return rotatedDir; - } - - /** - * Creates cone emitter - */ - private _createConeEmitter(particleSystem: ParticleSystem, shape: any, scale: Vector3, rotationMatrix: Matrix | null): void { - const { options } = this._context; - const radius = (shape.radius || 1) * ((scale.x + scale.z) / 2); - const angle = shape.angle !== undefined ? shape.angle : Math.PI / 4; - const defaultDir = new Vector3(0, 1, 0); - const rotatedDir = this._applyRotationToDirection(defaultDir, rotationMatrix); - - if (rotationMatrix) { - particleSystem.createDirectedConeEmitter(radius, angle, rotatedDir, rotatedDir); - this._logger.log( - ` Created directed cone emitter with rotated direction: (${rotatedDir.x.toFixed(2)}, ${rotatedDir.y.toFixed(2)}, ${rotatedDir.z.toFixed(2)})`, - options - ); - } else { - particleSystem.createConeEmitter(radius, angle); - } - } - - /** - * Creates sphere emitter - */ - private _createSphereEmitter(particleSystem: ParticleSystem, shape: any, scale: Vector3, rotationMatrix: Matrix | null): void { - const { options } = this._context; - const radius = (shape.radius || 1) * ((scale.x + scale.y + scale.z) / 3); - const defaultDir = new Vector3(0, 1, 0); - const rotatedDir = this._applyRotationToDirection(defaultDir, rotationMatrix); - - if (rotationMatrix) { - particleSystem.createDirectedSphereEmitter(radius, rotatedDir, rotatedDir); - this._logger.log( - ` Created directed sphere emitter with rotated direction: (${rotatedDir.x.toFixed(2)}, ${rotatedDir.y.toFixed(2)}, ${rotatedDir.z.toFixed(2)})`, - options - ); - } else { - particleSystem.createSphereEmitter(radius); - } - } - - /** - * Creates point emitter - */ - private _createPointEmitter(particleSystem: ParticleSystem, _shape: any, _scale: Vector3, rotationMatrix: Matrix | null): void { - const { options } = this._context; - const defaultDir = new Vector3(0, 1, 0); - const rotatedDir = this._applyRotationToDirection(defaultDir, rotationMatrix); - - if (rotationMatrix) { - particleSystem.createPointEmitter(rotatedDir, rotatedDir); - this._logger.log(` Created point emitter with rotated direction: (${rotatedDir.x.toFixed(2)}, ${rotatedDir.y.toFixed(2)}, ${rotatedDir.z.toFixed(2)})`, options); - } else { - particleSystem.createPointEmitter(Vector3.Zero(), Vector3.Zero()); - } - } - - /** - * Creates box emitter - */ - private _createBoxEmitter(particleSystem: ParticleSystem, shape: any, scale: Vector3, rotationMatrix: Matrix | null): void { - const { options } = this._context; - const boxSize = (shape.size || [1, 1, 1]).map((s: number, i: number) => s * [scale.x, scale.y, scale.z][i]); - const minBox = new Vector3(-boxSize[0] / 2, -boxSize[1] / 2, -boxSize[2] / 2); - const maxBox = new Vector3(boxSize[0] / 2, boxSize[1] / 2, boxSize[2] / 2); - const defaultDir = new Vector3(0, 1, 0); - const rotatedDir = this._applyRotationToDirection(defaultDir, rotationMatrix); - - if (rotationMatrix) { - particleSystem.createBoxEmitter(rotatedDir, rotatedDir, minBox, maxBox); - this._logger.log(` Created box emitter with rotated direction: (${rotatedDir.x.toFixed(2)}, ${rotatedDir.y.toFixed(2)}, ${rotatedDir.z.toFixed(2)})`, options); - } else { - particleSystem.createBoxEmitter(Vector3.Zero(), Vector3.Zero(), minBox, maxBox); - } - } - - /** - * Creates hemisphere emitter - */ - private _createHemisphereEmitter(particleSystem: ParticleSystem, shape: any, scale: Vector3, _rotationMatrix: Matrix | null): void { - const radius = (shape.radius || 1) * ((scale.x + scale.y + scale.z) / 3); - particleSystem.createHemisphericEmitter(radius); - } - - /** - * Creates cylinder emitter - */ - private _createCylinderEmitter(particleSystem: ParticleSystem, shape: any, scale: Vector3, rotationMatrix: Matrix | null): void { - const { options } = this._context; - const radius = (shape.radius || 1) * ((scale.x + scale.z) / 2); - const height = (shape.height || 1) * scale.y; - const defaultDir = new Vector3(0, 1, 0); - const rotatedDir = this._applyRotationToDirection(defaultDir, rotationMatrix); - - if (rotationMatrix) { - particleSystem.createDirectedCylinderEmitter(radius, height, 1, rotatedDir, rotatedDir); - this._logger.log( - ` Created directed cylinder emitter with rotated direction: (${rotatedDir.x.toFixed(2)}, ${rotatedDir.y.toFixed(2)}, ${rotatedDir.z.toFixed(2)})`, - options - ); - } else { - particleSystem.createCylinderEmitter(radius, height); - } - } - - /** - * Creates default point emitter - */ - private _createDefaultPointEmitter(particleSystem: ParticleSystem, rotationMatrix: Matrix | null): void { - const defaultDir = new Vector3(0, 1, 0); - const rotatedDir = this._applyRotationToDirection(defaultDir, rotationMatrix); - - if (rotationMatrix) { - particleSystem.createPointEmitter(rotatedDir, rotatedDir); - } else { - particleSystem.createPointEmitter(Vector3.Zero(), Vector3.Zero()); - } - } - - /** - * Apply emission bursts via emit rate gradients - */ - private _applyEmissionBursts(particleSystem: ParticleSystem, bursts: VFXEmissionBurst[], baseEmitRate: number, duration: number): void { - for (const burst of bursts) { - if (burst.time === undefined || burst.count === undefined) { - continue; - } - - const burstTime = this._valueParser.parseConstantValue(burst.time); - const burstCount = this._valueParser.parseConstantValue(burst.count); - const timeRatio = Math.min(Math.max(burstTime / duration, 0), 1); - const windowSize = 0.02; - const burstEmitRate = burstCount / windowSize; - - const beforeTime = Math.max(0, timeRatio - windowSize); - const afterTime = Math.min(1, timeRatio + windowSize); - - particleSystem.addEmitRateGradient(beforeTime, baseEmitRate); - particleSystem.addEmitRateGradient(timeRatio, burstEmitRate); - particleSystem.addEmitRateGradient(afterTime, baseEmitRate); - } - } - - /** - * Apply behaviors to ParticleSystem - * Simply sets behaviorConfigs - the system will apply them automatically via proxy - */ - private _applyBehaviors(particleSystem: VFXParticleSystem | VFXSolidParticleSystem, behaviors: VFXBehavior[]): void { - if (!particleSystem || !particleSystem.behaviorConfigs) { - return; - } - particleSystem.behaviorConfigs.length = 0; - particleSystem.behaviorConfigs.push(...(behaviors || [])); + return mat.getRotationMatrix(); } } diff --git a/editor/src/editor/windows/fx-editor/VFX/factories/VFXParticleSystemEmitterFactory.ts b/editor/src/editor/windows/fx-editor/VFX/factories/VFXParticleSystemEmitterFactory.ts new file mode 100644 index 000000000..2362cac60 --- /dev/null +++ b/editor/src/editor/windows/fx-editor/VFX/factories/VFXParticleSystemEmitterFactory.ts @@ -0,0 +1,147 @@ +import { ParticleSystem, Vector3, Matrix } from "babylonjs"; +import type { VFXShape } from "../types/shapes"; + +/** + * Factory for creating shape emitters for ParticleSystem + * Creates emitters based on VFX shape configuration + */ +export class VFXParticleSystemEmitterFactory { + private _particleSystem: ParticleSystem; + + constructor(particleSystem: ParticleSystem) { + this._particleSystem = particleSystem; + } + + /** + * Create emitter shape based on VFX shape configuration + */ + public createEmitter(shape: VFXShape | undefined, cumulativeScale: Vector3, rotationMatrix: Matrix | null): void { + if (!shape || !shape.type) { + this._createPointEmitter(Vector3.Zero(), Vector3.Zero()); + return; + } + + const shapeType = shape.type.toLowerCase(); + const shapeHandlers: Record void> = { + cone: this._createConeEmitter.bind(this), + sphere: this._createSphereEmitter.bind(this), + point: this._createPointEmitter.bind(this), + box: this._createBoxEmitter.bind(this), + hemisphere: this._createHemisphereEmitter.bind(this), + cylinder: this._createCylinderEmitter.bind(this), + }; + + const handler = shapeHandlers[shapeType]; + if (handler) { + handler(shape, cumulativeScale, rotationMatrix); + } else { + this._createDefaultPointEmitter(rotationMatrix); + } + } + + /** + * Applies rotation to default direction vector + */ + private _applyRotationToDirection(defaultDir: Vector3, rotationMatrix: Matrix | null): Vector3 { + if (!rotationMatrix) { + return defaultDir; + } + + const rotatedDir = Vector3.Zero(); + Vector3.TransformNormalToRef(defaultDir, rotationMatrix, rotatedDir); + return rotatedDir; + } + + /** + * Creates cone emitter + */ + private _createConeEmitter(shape: VFXShape, scale: Vector3, rotationMatrix: Matrix | null): void { + const radius = ((shape as any).radius || 1) * ((scale.x + scale.z) / 2); + const angle = (shape as any).angle !== undefined ? (shape as any).angle : Math.PI / 4; + const defaultDir = new Vector3(0, 1, 0); + const rotatedDir = this._applyRotationToDirection(defaultDir, rotationMatrix); + + if (rotationMatrix) { + this._particleSystem.createDirectedConeEmitter(radius, angle, rotatedDir, rotatedDir); + } else { + this._particleSystem.createConeEmitter(radius, angle); + } + } + + /** + * Creates sphere emitter + */ + private _createSphereEmitter(shape: VFXShape, scale: Vector3, rotationMatrix: Matrix | null): void { + const radius = ((shape as any).radius || 1) * ((scale.x + scale.y + scale.z) / 3); + const defaultDir = new Vector3(0, 1, 0); + const rotatedDir = this._applyRotationToDirection(defaultDir, rotationMatrix); + + if (rotationMatrix) { + this._particleSystem.createDirectedSphereEmitter(radius, rotatedDir, rotatedDir); + } else { + this._particleSystem.createSphereEmitter(radius); + } + } + + /** + * Creates point emitter + */ + private _createPointEmitter(direction: Vector3, minDirection: Vector3): void { + this._particleSystem.createPointEmitter(direction, minDirection); + } + + /** + * Creates box emitter + */ + private _createBoxEmitter(shape: VFXShape, scale: Vector3, rotationMatrix: Matrix | null): void { + const boxSize = ((shape as any).size || [1, 1, 1]).map((s: number, i: number) => s * [scale.x, scale.y, scale.z][i]); + const minBox = new Vector3(-boxSize[0] / 2, -boxSize[1] / 2, -boxSize[2] / 2); + const maxBox = new Vector3(boxSize[0] / 2, boxSize[1] / 2, boxSize[2] / 2); + const defaultDir = new Vector3(0, 1, 0); + const rotatedDir = this._applyRotationToDirection(defaultDir, rotationMatrix); + + if (rotationMatrix) { + this._particleSystem.createBoxEmitter(rotatedDir, rotatedDir, minBox, maxBox); + } else { + this._particleSystem.createBoxEmitter(Vector3.Zero(), Vector3.Zero(), minBox, maxBox); + } + } + + /** + * Creates hemisphere emitter + */ + private _createHemisphereEmitter(shape: VFXShape, scale: Vector3, _rotationMatrix: Matrix | null): void { + const radius = ((shape as any).radius || 1) * ((scale.x + scale.y + scale.z) / 3); + this._particleSystem.createHemisphericEmitter(radius); + } + + /** + * Creates cylinder emitter + */ + private _createCylinderEmitter(shape: VFXShape, scale: Vector3, rotationMatrix: Matrix | null): void { + const radius = ((shape as any).radius || 1) * ((scale.x + scale.z) / 2); + const height = ((shape as any).height || 1) * scale.y; + const defaultDir = new Vector3(0, 1, 0); + const rotatedDir = this._applyRotationToDirection(defaultDir, rotationMatrix); + + if (rotationMatrix) { + this._particleSystem.createDirectedCylinderEmitter(radius, height, 1, rotatedDir, rotatedDir); + } else { + this._particleSystem.createCylinderEmitter(radius, height); + } + } + + /** + * Creates default point emitter + */ + private _createDefaultPointEmitter(rotationMatrix: Matrix | null): void { + const defaultDir = new Vector3(0, 1, 0); + const rotatedDir = this._applyRotationToDirection(defaultDir, rotationMatrix); + + if (rotationMatrix) { + this._createPointEmitter(rotatedDir, rotatedDir); + } else { + this._createPointEmitter(Vector3.Zero(), Vector3.Zero()); + } + } +} diff --git a/editor/src/editor/windows/fx-editor/VFX/factories/VFXSolidParticleSystemEmitterFactory.ts b/editor/src/editor/windows/fx-editor/VFX/factories/VFXSolidParticleSystemEmitterFactory.ts new file mode 100644 index 000000000..360124cd0 --- /dev/null +++ b/editor/src/editor/windows/fx-editor/VFX/factories/VFXSolidParticleSystemEmitterFactory.ts @@ -0,0 +1,100 @@ +import { SolidParticle, Vector3 } from "babylonjs"; +import type { VFXShape } from "../types/shapes"; + +/** + * Factory for initializing particle positions and velocities based on emitter shape for SolidParticleSystem + * This is used during particle initialization, not emitter creation + */ +export class VFXSolidParticleSystemEmitterFactory { + /** + * Initialize particle position and velocity based on emitter shape + */ + public initializeParticle(particle: SolidParticle, shape: VFXShape | undefined, startSpeed: number): void { + if (!shape) { + this._initializeDefaultShape(particle, startSpeed); + return; + } + + const shapeType = shape.type?.toLowerCase(); + const radius = shape.radius ?? 1; + const arc = shape.arc ?? Math.PI * 2; + const thickness = shape.thickness ?? 1; + const angle = shape.angle ?? Math.PI / 6; + + switch (shapeType) { + case "sphere": + this._initializeSphereShape(particle, radius, arc, thickness, startSpeed); + break; + case "cone": + this._initializeConeShape(particle, radius, arc, thickness, angle, startSpeed); + break; + case "point": + this._initializePointShape(particle, startSpeed); + break; + default: + this._initializeDefaultShape(particle, startSpeed); + break; + } + } + + /** + * Initialize default shape (point emitter) + */ + private _initializeDefaultShape(particle: SolidParticle, startSpeed: number): void { + particle.position.setAll(0); + particle.velocity.set(0, 1, 0); + particle.velocity.scaleInPlace(startSpeed); + } + + /** + * Initialize sphere shape + */ + private _initializeSphereShape(particle: SolidParticle, radius: number, arc: number, thickness: number, startSpeed: number): void { + const u = Math.random(); + const v = Math.random(); + const rand = 1 - thickness + Math.random() * thickness; + const theta = u * arc; + const phi = Math.acos(2.0 * v - 1.0); + const sinTheta = Math.sin(theta); + const cosTheta = Math.cos(theta); + const sinPhi = Math.sin(phi); + const cosPhi = Math.cos(phi); + + particle.position.set(sinPhi * cosTheta, sinPhi * sinTheta, cosPhi); + particle.velocity.copyFrom(particle.position); + particle.velocity.scaleInPlace(startSpeed); + particle.position.scaleInPlace(radius * rand); + } + + /** + * Initialize cone shape + */ + private _initializeConeShape(particle: SolidParticle, radius: number, arc: number, thickness: number, angle: number, startSpeed: number): void { + const u = Math.random(); + const rand = 1 - thickness + Math.random() * thickness; + const theta = u * arc; + const r = Math.sqrt(rand); + const sinTheta = Math.sin(theta); + const cosTheta = Math.cos(theta); + + particle.position.set(r * cosTheta, r * sinTheta, 0); + const coneAngle = angle * r; + particle.velocity.set(0, 0, Math.cos(coneAngle)); + particle.velocity.addInPlace(particle.position.scale(Math.sin(coneAngle))); + particle.velocity.scaleInPlace(startSpeed); + particle.position.scaleInPlace(radius); + } + + /** + * Initialize point shape + */ + private _initializePointShape(particle: SolidParticle, startSpeed: number): void { + const theta = Math.random() * Math.PI * 2; + const phi = Math.acos(2.0 * Math.random() - 1.0); + const direction = new Vector3(Math.sin(phi) * Math.cos(theta), Math.sin(phi) * Math.sin(theta), Math.cos(phi)); + particle.position.setAll(0); + particle.velocity.copyFrom(direction); + particle.velocity.scaleInPlace(startSpeed); + } +} + diff --git a/editor/src/editor/windows/fx-editor/VFX/index.ts b/editor/src/editor/windows/fx-editor/VFX/index.ts index c85038b89..25a8b4efd 100644 --- a/editor/src/editor/windows/fx-editor/VFX/index.ts +++ b/editor/src/editor/windows/fx-editor/VFX/index.ts @@ -1,6 +1,5 @@ export * from "./types"; export * from "./parsers/VFXParser"; -export * from "./parsers/VFXValueParser"; export * from "./parsers/VFXDataConverter"; export * from "./factories/VFXMaterialFactory"; export * from "./factories/VFXGeometryFactory"; diff --git a/editor/src/editor/windows/fx-editor/VFX/parsers/VFXParser.ts b/editor/src/editor/windows/fx-editor/VFX/parsers/VFXParser.ts index 7cdf5db3a..339736560 100644 --- a/editor/src/editor/windows/fx-editor/VFX/parsers/VFXParser.ts +++ b/editor/src/editor/windows/fx-editor/VFX/parsers/VFXParser.ts @@ -3,7 +3,6 @@ import type { QuarksVFXJSON } from "../types/quarksTypes"; import type { VFXLoaderOptions } from "../types/loader"; import type { VFXParseContext } from "../types/context"; import { VFXLogger } from "../loggers/VFXLogger"; -import { VFXValueParser } from "./VFXValueParser"; import { VFXMaterialFactory } from "../factories/VFXMaterialFactory"; import { VFXGeometryFactory } from "../factories/VFXGeometryFactory"; import { VFXEmitterFactory } from "../factories/VFXEmitterFactory"; @@ -19,7 +18,6 @@ import { VFXSolidParticleSystem } from "../systems/VFXSolidParticleSystem"; export class VFXParser { private _context: VFXParseContext; private _logger: VFXLogger; - private _valueParser: VFXValueParser; private _materialFactory: VFXMaterialFactory; private _geometryFactory: VFXGeometryFactory; private _emitterFactory: VFXEmitterFactory; @@ -36,10 +34,9 @@ export class VFXParser { }; this._logger = new VFXLogger("[VFXParser]"); - this._valueParser = new VFXValueParser(); this._materialFactory = new VFXMaterialFactory(this._context); this._geometryFactory = new VFXGeometryFactory(this._context, this._materialFactory); - this._emitterFactory = new VFXEmitterFactory(this._context, this._valueParser, this._materialFactory, this._geometryFactory); + this._emitterFactory = new VFXEmitterFactory(this._context, this._materialFactory, this._geometryFactory); this._systemFactory = new VFXSystemFactory(this._context, this._emitterFactory); } @@ -99,13 +96,6 @@ export class VFXParser { return this._context; } - /** - * Get the value parser - */ - public getValueParser(): VFXValueParser { - return this._valueParser; - } - /** * Get the material factory */ diff --git a/editor/src/editor/windows/fx-editor/VFX/parsers/VFXValueParser.ts b/editor/src/editor/windows/fx-editor/VFX/parsers/VFXValueParser.ts deleted file mode 100644 index ecc34b96f..000000000 --- a/editor/src/editor/windows/fx-editor/VFX/parsers/VFXValueParser.ts +++ /dev/null @@ -1,186 +0,0 @@ -import { Color4, ColorGradient } from "babylonjs"; -import type { IVFXValueParser } from "../types/factories"; -import type { VFXValue } from "../types/values"; -import type { VFXColor } from "../types/colors"; -import type { VFXGradientKey } from "../types/gradients"; -import type { VFXPiecewiseBezier } from "../types/values"; - -/** - * Parser for Three.js value types (ConstantValue, IntervalValue, Gradient, etc.) - */ -export class VFXValueParser implements IVFXValueParser { - /** - * Parse a constant value - */ - public parseConstantValue(value: VFXValue): number { - if (value && typeof value === "object" && value.type === "ConstantValue") { - return value.value || 0; - } - return typeof value === "number" ? value : 0; - } - - /** - * Parse an interval value (returns min and max) - */ - public parseIntervalValue(value: VFXValue): { min: number; max: number } { - if (value && typeof value === "object" && "type" in value && value.type === "IntervalValue") { - return { - min: value.min ?? 0, - max: value.max ?? 0, - }; - } - const constant = this.parseConstantValue(value); - return { min: constant, max: constant }; - } - - /** - * Parse a constant color - */ - public parseConstantColor(value: VFXColor): Color4 { - if (value && typeof value === "object" && !Array.isArray(value)) { - if ("type" in value && value.type === "ConstantColor") { - if (value.value && Array.isArray(value.value)) { - return new Color4(value.value[0] || 0, value.value[1] || 0, value.value[2] || 0, value.value[3] !== undefined ? value.value[3] : 1); - } - } else if (Array.isArray(value) && value.length >= 3) { - // Array format [r, g, b, a?] - return new Color4(value[0] || 0, value[1] || 0, value[2] || 0, value[3] !== undefined ? value[3] : 1); - } - } - return new Color4(1, 1, 1, 1); - } - - /** - * Parse gradient color keys - */ - public parseGradientColorKeys(keys: VFXGradientKey[]): ColorGradient[] { - const gradients: ColorGradient[] = []; - for (const key of keys) { - const pos = key.pos ?? key.time ?? 0; - if (key.value !== undefined && pos !== undefined) { - let color4: Color4; - if (typeof key.value === "number") { - // Single number - grayscale - color4 = new Color4(key.value, key.value, key.value, 1); - } else if (Array.isArray(key.value)) { - // Array format [r, g, b, a?] - color4 = new Color4(key.value[0] || 0, key.value[1] || 0, key.value[2] || 0, key.value[3] !== undefined ? key.value[3] : 1); - } else { - // Object format { r, g, b, a? } - color4 = new Color4(key.value.r || 0, key.value.g || 0, key.value.b || 0, key.value.a !== undefined ? key.value.a : 1); - } - gradients.push(new ColorGradient(pos, color4)); - } - } - return gradients; - } - - /** - * Parse gradient alpha keys - */ - public parseGradientAlphaKeys(keys: VFXGradientKey[]): { gradient: number; factor: number }[] { - const gradients: { gradient: number; factor: number }[] = []; - for (const key of keys) { - const pos = key.pos ?? key.time ?? 0; - if (key.value !== undefined && pos !== undefined) { - let factor: number; - if (typeof key.value === "number") { - factor = key.value; - } else if (Array.isArray(key.value)) { - factor = key.value[3] !== undefined ? key.value[3] : 1; - } else { - factor = key.value.a !== undefined ? key.value.a : 1; - } - gradients.push({ gradient: pos, factor }); - } - } - return gradients; - } - - /** - * Parse a value for particle spawn (returns a single value based on type) - * Handles ConstantValue, IntervalValue, PiecewiseBezier, and number - * @param value The value to parse - * @param normalizedTime Normalized time (0-1) for PiecewiseBezier, default is random for IntervalValue - */ - public parseValue(value: VFXValue, normalizedTime?: number): number { - if (!value || typeof value === "number") { - return typeof value === "number" ? value : 0; - } - - if (value.type === "ConstantValue") { - return value.value || 0; - } - - if (value.type === "IntervalValue") { - const min = value.min ?? 0; - const max = value.max ?? 0; - return min + Math.random() * (max - min); - } - - if (value.type === "PiecewiseBezier") { - // Use provided normalizedTime or random for spawn - const t = normalizedTime !== undefined ? normalizedTime : Math.random(); - return this._evaluatePiecewiseBezier(value, t); - } - - // Fallback - return 0; - } - - /** - * Evaluate PiecewiseBezier at normalized time t (0-1) - */ - private _evaluatePiecewiseBezier(bezier: VFXPiecewiseBezier, t: number): number { - if (!bezier.functions || bezier.functions.length === 0) { - return 0; - } - - // Clamp t to [0, 1] - const clampedT = Math.max(0, Math.min(1, t)); - - // Find which function segment contains t - let segmentIndex = -1; - for (let i = 0; i < bezier.functions.length; i++) { - const func = bezier.functions[i]; - const start = func.start; - const end = i < bezier.functions.length - 1 ? bezier.functions[i + 1].start : 1; - - if (clampedT >= start && clampedT < end) { - segmentIndex = i; - break; - } - } - - // If t is at the end (1.0), use last segment - if (segmentIndex === -1 && clampedT >= 1) { - segmentIndex = bezier.functions.length - 1; - } - - // If still not found, use first segment - if (segmentIndex === -1) { - segmentIndex = 0; - } - - const func = bezier.functions[segmentIndex]; - const start = func.start; - const end = segmentIndex < bezier.functions.length - 1 ? bezier.functions[segmentIndex + 1].start : 1; - - // Normalize t within this segment - const segmentT = end > start ? (clampedT - start) / (end - start) : 0; - - // Evaluate cubic Bezier: B(t) = (1-t)³P₀ + 3(1-t)²tP₁ + 3(1-t)t²P₂ + t³P₃ - const p0 = func.function.p0; - const p1 = func.function.p1; - const p2 = func.function.p2; - const p3 = func.function.p3; - - const t2 = segmentT * segmentT; - const t3 = t2 * segmentT; - const mt = 1 - segmentT; - const mt2 = mt * mt; - const mt3 = mt2 * mt; - - return mt3 * p0 + 3 * mt2 * segmentT * p1 + 3 * mt * t2 * p2 + t3 * p3; - } -} diff --git a/editor/src/editor/windows/fx-editor/VFX/systems/VFXParticleSystem.ts b/editor/src/editor/windows/fx-editor/VFX/systems/VFXParticleSystem.ts index 6fd1ce641..63f42712b 100644 --- a/editor/src/editor/windows/fx-editor/VFX/systems/VFXParticleSystem.ts +++ b/editor/src/editor/windows/fx-editor/VFX/systems/VFXParticleSystem.ts @@ -1,4 +1,4 @@ -import { Color4, ParticleSystem, Scene } from "babylonjs"; +import { Color4, ParticleSystem, Scene, Vector3, Matrix, Texture } from "babylonjs"; import type { VFXPerParticleBehaviorFunction } from "../types/VFXBehaviorFunction"; import type { VFXBehavior, @@ -11,7 +11,11 @@ import type { VFXFrameOverLifeBehavior, VFXLimitSpeedOverLifeBehavior, } from "../types/behaviors"; +import type { VFXShape } from "../types/shapes"; +import type { VFXParticleEmitterConfig, VFXEmissionBurst } from "../types/emitterConfig"; import { VFXParticleSystemBehaviorFactory } from "../factories/VFXParticleSystemBehaviorFactory"; +import { VFXParticleSystemEmitterFactory } from "../factories/VFXParticleSystemEmitterFactory"; +import { VFXValueUtils } from "../utils/valueParser"; import { applyColorOverLifePS, applySizeOverLifePS, @@ -33,17 +37,26 @@ export class VFXParticleSystem extends ParticleSystem { public startColor: Color4; private _behaviors: VFXPerParticleBehaviorFunction[]; private _behaviorFactory: VFXParticleSystemBehaviorFactory; + private _emitterFactory: VFXParticleSystemEmitterFactory; public readonly behaviorConfigs: VFXBehavior[]; constructor(name: string, capacity: number, scene: Scene, _avgStartSpeed: number, _avgStartSize: number, _startColor: Color4) { super(name, capacity, scene); this._behaviors = []; this._behaviorFactory = new VFXParticleSystemBehaviorFactory(this); + this._emitterFactory = new VFXParticleSystemEmitterFactory(this); // Create proxy array that updates functions when modified this.behaviorConfigs = this._createBehaviorConfigsProxy([]); } + /** + * Create emitter shape based on VFX shape configuration + */ + public createEmitterShape(shape: VFXShape | undefined, cumulativeScale: Vector3, rotationMatrix: Matrix | null): void { + this._emitterFactory.createEmitter(shape, cumulativeScale, rotationMatrix); + } + /** * Get behavior functions (internal use) */ @@ -176,4 +189,156 @@ export class VFXParticleSystem extends ParticleSystem { } } } + + /** + * Configure particle system from VFX config + * This method applies all configuration from VFXParticleEmitterConfig + */ + public configureFromConfig( + config: VFXParticleEmitterConfig, + options?: { + texture?: Texture; + blendMode?: number; + emitterShape?: { shape: VFXShape | undefined; cumulativeScale: Vector3; rotationMatrix: Matrix | null }; + } + ): void { + // Parse values + const emissionRate = config.emissionOverTime !== undefined ? VFXValueUtils.parseConstantValue(config.emissionOverTime) : 10; + const duration = config.duration || 5; + const lifeTime = config.startLife !== undefined ? VFXValueUtils.parseIntervalValue(config.startLife) : { min: 1, max: 1 }; + const speed = config.startSpeed !== undefined ? VFXValueUtils.parseIntervalValue(config.startSpeed) : { min: 1, max: 1 }; + const size = config.startSize !== undefined ? VFXValueUtils.parseIntervalValue(config.startSize) : { min: 1, max: 1 }; + const startColor = config.startColor !== undefined ? VFXValueUtils.parseConstantColor(config.startColor) : new Color4(1, 1, 1, 1); + + // Configure basic properties + this.targetStopDuration = duration; + this.emitRate = emissionRate; + this.manualEmitCount = -1; + this.minLifeTime = lifeTime.min; + this.maxLifeTime = lifeTime.max; + this.minEmitPower = speed.min; + this.maxEmitPower = speed.max; + this.minSize = size.min; + this.maxSize = size.max; + this.color1 = startColor; + this.color2 = startColor; + this.colorDead = new Color4(startColor.r, startColor.g, startColor.b, 0); + + // Configure rotation + if (config.startRotation) { + if (this._isEulerRotation(config.startRotation)) { + if (config.startRotation.angleZ !== undefined) { + const angleZ = VFXValueUtils.parseIntervalValue(config.startRotation.angleZ); + this.minInitialRotation = angleZ.min; + this.maxInitialRotation = angleZ.max; + } + } else { + const rotation = VFXValueUtils.parseIntervalValue(config.startRotation as any); + this.minInitialRotation = rotation.min; + this.maxInitialRotation = rotation.max; + } + } + + // Configure sprite tiles + if (config.uTileCount !== undefined && config.vTileCount !== undefined) { + if (config.uTileCount > 1 || config.vTileCount > 1) { + this.isAnimationSheetEnabled = true; + this.spriteCellWidth = config.uTileCount; + this.spriteCellHeight = config.vTileCount; + + if (config.startTileIndex !== undefined) { + const startTile = VFXValueUtils.parseConstantValue(config.startTileIndex); + this.startSpriteCellID = Math.floor(startTile); + this.endSpriteCellID = Math.floor(startTile); + } + } + } + + // Configure rendering + if (config.renderOrder !== undefined) { + this.renderingGroupId = config.renderOrder; + } + if (config.layers !== undefined) { + this.layerMask = config.layers; + } + + // Apply texture and blend mode + if (options?.texture) { + this.particleTexture = options.texture; + } + if (options?.blendMode !== undefined) { + this.blendMode = options.blendMode; + } + + // Apply emission bursts + if (config.emissionBursts && Array.isArray(config.emissionBursts) && config.emissionBursts.length > 0) { + this._applyEmissionBursts(config.emissionBursts, emissionRate, duration); + } + + // Apply behaviors + if (config.behaviors && Array.isArray(config.behaviors) && config.behaviors.length > 0) { + this.behaviorConfigs.length = 0; + this.behaviorConfigs.push(...config.behaviors); + } + + // Configure world space + if (config.worldSpace !== undefined) { + this.isLocal = !config.worldSpace; + } + + // Configure looping + if (config.looping !== undefined) { + this.targetStopDuration = config.looping ? 0 : duration; + } + + // Configure render mode + if (config.renderMode !== undefined) { + if (config.renderMode === 0) { + this.isBillboardBased = true; + } else if (config.renderMode === 1) { + this.billboardMode = ParticleSystem.BILLBOARDMODE_STRETCHED; + } + } + + // Configure auto destroy + if (config.autoDestroy !== undefined) { + this.disposeOnStop = config.autoDestroy; + } + + // Set emitter shape + if (options?.emitterShape) { + this.createEmitterShape(options.emitterShape.shape, options.emitterShape.cumulativeScale, options.emitterShape.rotationMatrix); + } + } + + /** + * Check if rotation is Euler type + */ + private _isEulerRotation(rotation: any): rotation is { type: "Euler"; angleZ?: any } { + return typeof rotation === "object" && rotation !== null && "type" in rotation && rotation.type === "Euler"; + } + + /** + * Apply emission bursts via emit rate gradients + */ + private _applyEmissionBursts(bursts: VFXEmissionBurst[], baseEmitRate: number, duration: number): void { + for (const burst of bursts) { + if (burst.time === undefined || burst.count === undefined) { + continue; + } + + const burstTime = VFXValueUtils.parseConstantValue(burst.time); + const burstCount = VFXValueUtils.parseConstantValue(burst.count); + const timeRatio = Math.min(Math.max(burstTime / duration, 0), 1); + const windowSize = 0.02; + const burstEmitRate = burstCount / windowSize; + + const beforeTime = Math.max(0, timeRatio - windowSize); + const afterTime = Math.min(1, timeRatio + windowSize); + + this.addEmitRateGradient(beforeTime, baseEmitRate); + this.addEmitRateGradient(timeRatio, burstEmitRate); + this.addEmitRateGradient(afterTime, baseEmitRate); + } + } } diff --git a/editor/src/editor/windows/fx-editor/VFX/systems/VFXSolidParticleSystem.ts b/editor/src/editor/windows/fx-editor/VFX/systems/VFXSolidParticleSystem.ts index 57560ee7c..9c59337e7 100644 --- a/editor/src/editor/windows/fx-editor/VFX/systems/VFXSolidParticleSystem.ts +++ b/editor/src/editor/windows/fx-editor/VFX/systems/VFXSolidParticleSystem.ts @@ -9,6 +9,7 @@ import type { VFXColor } from "../types/colors"; import type { VFXValue } from "../types/values"; import type { VFXRotation } from "../types/rotations"; import { VFXSolidParticleSystemBehaviorFactory } from "../factories/VFXSolidParticleSystemBehaviorFactory"; +import { VFXSolidParticleSystemEmitterFactory } from "../factories/VFXSolidParticleSystemEmitterFactory"; import { VFXValueUtils } from "../utils/valueParser"; /** @@ -34,6 +35,7 @@ export class VFXSolidParticleSystem extends SolidParticleSystem { private _emissionState: EmissionState; private _behaviors: VFXPerSolidParticleBehaviorFunction[]; private _behaviorFactory: VFXSolidParticleSystemBehaviorFactory; + private _emitterFactory: VFXSolidParticleSystemEmitterFactory; private _parent: TransformNode | null; private _vfxTransform: { position: Vector3; rotation: Quaternion; scale: Vector3 } | null; private _logger: VFXLogger | null; @@ -139,6 +141,7 @@ export class VFXSolidParticleSystem extends SolidParticleSystem { this._name = name; this._behaviors = []; this._behaviorFactory = new VFXSolidParticleSystemBehaviorFactory(); + this._emitterFactory = new VFXSolidParticleSystemEmitterFactory(); // Initialize properties from initialConfig this.isLooping = initialConfig.looping !== false; @@ -302,77 +305,12 @@ export class VFXSolidParticleSystem extends SolidParticleSystem { } } - private _initializeSphereShape(particle: SolidParticle, radius: number, arc: number, thickness: number, startSpeed: number): void { - const u = Math.random(); - const v = Math.random(); - const rand = 1 - thickness + Math.random() * thickness; - const theta = u * arc; - const phi = Math.acos(2.0 * v - 1.0); - const sinTheta = Math.sin(theta); - const cosTheta = Math.cos(theta); - const sinPhi = Math.sin(phi); - const cosPhi = Math.cos(phi); - - particle.position.set(sinPhi * cosTheta, sinPhi * sinTheta, cosPhi); - particle.velocity.copyFrom(particle.position); - particle.velocity.scaleInPlace(startSpeed); - particle.position.scaleInPlace(radius * rand); - } - - private _initializeConeShape(particle: SolidParticle, radius: number, arc: number, thickness: number, angle: number, startSpeed: number): void { - const u = Math.random(); - const rand = 1 - thickness + Math.random() * thickness; - const theta = u * arc; - const r = Math.sqrt(rand); - const sinTheta = Math.sin(theta); - const cosTheta = Math.cos(theta); - - particle.position.set(r * cosTheta, r * sinTheta, 0); - const coneAngle = angle * r; - particle.velocity.set(0, 0, Math.cos(coneAngle)); - particle.velocity.addInPlace(particle.position.scale(Math.sin(coneAngle))); - particle.velocity.scaleInPlace(startSpeed); - particle.position.scaleInPlace(radius); - } - - private _initializePointShape(particle: SolidParticle, startSpeed: number): void { - const theta = Math.random() * Math.PI * 2; - const phi = Math.acos(2.0 * Math.random() - 1.0); - const direction = new Vector3(Math.sin(phi) * Math.cos(theta), Math.sin(phi) * Math.sin(theta), Math.cos(phi)); - particle.position.setAll(0); - particle.velocity.copyFrom(direction); - particle.velocity.scaleInPlace(startSpeed); - } - - private _initializeDefaultShape(particle: SolidParticle, startSpeed: number): void { - particle.position.setAll(0); - particle.velocity.set(0, 1, 0); - particle.velocity.scaleInPlace(startSpeed); - } - + /** + * Initialize emitter shape for particle using factory + */ private _initializeEmitterShape(particle: SolidParticle): void { const startSpeed = particle.props?.startSpeed ?? 0; - - if (!this.shape) { - this._initializeDefaultShape(particle, startSpeed); - return; - } - - const shapeType = this.shape.type?.toLowerCase(); - const radius = this.shape.radius ?? 1; - const arc = this.shape.arc ?? Math.PI * 2; - const thickness = this.shape.thickness ?? 1; - const angle = this.shape.angle ?? Math.PI / 6; - - if (shapeType === "sphere") { - this._initializeSphereShape(particle, radius, arc, thickness, startSpeed); - } else if (shapeType === "cone") { - this._initializeConeShape(particle, radius, arc, thickness, angle, startSpeed); - } else if (shapeType === "point") { - this._initializePointShape(particle, startSpeed); - } else { - this._initializeDefaultShape(particle, startSpeed); - } + this._emitterFactory.initializeParticle(particle, this.shape, startSpeed); } private _getEmitterMatrix(): Matrix { diff --git a/editor/src/editor/windows/fx-editor/VFX/types/factories.ts b/editor/src/editor/windows/fx-editor/VFX/types/factories.ts index d0099cc48..ff72d2ede 100644 --- a/editor/src/editor/windows/fx-editor/VFX/types/factories.ts +++ b/editor/src/editor/windows/fx-editor/VFX/types/factories.ts @@ -1,7 +1,4 @@ -import { Nullable, Mesh, ParticleSystem, SolidParticleSystem, PBRMaterial, Color4, Texture, ColorGradient } from "babylonjs"; -import type { VFXValue } from "./values"; -import type { VFXColor } from "./colors"; -import type { VFXGradientKey } from "./gradients"; +import { Nullable, Mesh, ParticleSystem, SolidParticleSystem, PBRMaterial, Texture } from "babylonjs"; import type { VFXEmitterData } from "./emitter"; /** @@ -19,11 +16,3 @@ export interface IVFXGeometryFactory { export interface IVFXEmitterFactory { createEmitter(emitterData: VFXEmitterData): Nullable; } - -export interface IVFXValueParser { - parseConstantValue(value: VFXValue): number; - parseIntervalValue(value: VFXValue): { min: number; max: number }; - parseConstantColor(value: VFXColor): Color4; - parseGradientColorKeys(keys: VFXGradientKey[]): ColorGradient[]; - parseGradientAlphaKeys(keys: VFXGradientKey[]): { gradient: number; factor: number }[]; -} diff --git a/editor/src/editor/windows/fx-editor/VFX/types/index.ts b/editor/src/editor/windows/fx-editor/VFX/types/index.ts index a7a24b695..a0b35d32f 100644 --- a/editor/src/editor/windows/fx-editor/VFX/types/index.ts +++ b/editor/src/editor/windows/fx-editor/VFX/types/index.ts @@ -13,7 +13,7 @@ export type { VFXParseContext } from "./context"; export type { VFXEmitterData } from "./emitter"; // Factory interfaces -export type { IVFXMaterialFactory, IVFXGeometryFactory, IVFXEmitterFactory, IVFXValueParser } from "./factories"; +export type { IVFXMaterialFactory, IVFXGeometryFactory, IVFXEmitterFactory } from "./factories"; // Core VFX types export type { VFXConstantValue, VFXIntervalValue, VFXValue } from "./values"; diff --git a/editor/src/editor/windows/fx-editor/VFX/utils/valueParser.ts b/editor/src/editor/windows/fx-editor/VFX/utils/valueParser.ts index fdd157dbf..c6580e6cf 100644 --- a/editor/src/editor/windows/fx-editor/VFX/utils/valueParser.ts +++ b/editor/src/editor/windows/fx-editor/VFX/utils/valueParser.ts @@ -1,6 +1,7 @@ -import { Color4 } from "babylonjs"; +import { Color4, ColorGradient } from "babylonjs"; import type { VFXValue } from "../types/values"; import type { VFXColor } from "../types/colors"; +import type { VFXGradientKey } from "../types/gradients"; /** * Static utility functions for parsing VFX values @@ -134,5 +135,51 @@ export class VFXValueUtils { return mt3 * p0 + 3 * mt2 * segmentT * p1 + 3 * mt * t2 * p2 + t3 * p3; } -} + /** + * Parse gradient color keys + */ + public static parseGradientColorKeys(keys: VFXGradientKey[]): ColorGradient[] { + const gradients: ColorGradient[] = []; + for (const key of keys) { + const pos = key.pos ?? key.time ?? 0; + if (key.value !== undefined && pos !== undefined) { + let color4: Color4; + if (typeof key.value === "number") { + // Single number - grayscale + color4 = new Color4(key.value, key.value, key.value, 1); + } else if (Array.isArray(key.value)) { + // Array format [r, g, b, a?] + color4 = new Color4(key.value[0] || 0, key.value[1] || 0, key.value[2] || 0, key.value[3] !== undefined ? key.value[3] : 1); + } else { + // Object format { r, g, b, a? } + color4 = new Color4(key.value.r || 0, key.value.g || 0, key.value.b || 0, key.value.a !== undefined ? key.value.a : 1); + } + gradients.push(new ColorGradient(pos, color4)); + } + } + return gradients; + } + + /** + * Parse gradient alpha keys + */ + public static parseGradientAlphaKeys(keys: VFXGradientKey[]): { gradient: number; factor: number }[] { + const gradients: { gradient: number; factor: number }[] = []; + for (const key of keys) { + const pos = key.pos ?? key.time ?? 0; + if (key.value !== undefined && pos !== undefined) { + let factor: number; + if (typeof key.value === "number") { + factor = key.value; + } else if (Array.isArray(key.value)) { + factor = key.value[3] !== undefined ? key.value[3] : 1; + } else { + factor = key.value.a !== undefined ? key.value.a : 1; + } + gradients.push({ gradient: pos, factor }); + } + } + return gradients; + } +} From e6b643b6ea111ae4884f5d3963c277d97d66bf05 Mon Sep 17 00:00:00 2001 From: Mikalai Lazitski Date: Fri, 12 Dec 2025 19:31:43 +0300 Subject: [PATCH 19/62] refactor: remove VFXEmitterFactory and streamline VFX system creation by integrating emitter logic into VFXSystemFactory for improved clarity and performance --- .../VFX/factories/VFXEmitterFactory.ts | 299 ------------------ .../VFX/factories/VFXSystemFactory.ts | 244 ++++++++++++-- .../src/editor/windows/fx-editor/VFX/index.ts | 1 - .../fx-editor/VFX/parsers/VFXDataConverter.ts | 1 + .../fx-editor/VFX/parsers/VFXParser.ts | 5 +- .../VFX/systems/VFXSolidParticleSystem.ts | 27 +- .../windows/fx-editor/VFX/types/factories.ts | 7 +- .../windows/fx-editor/VFX/types/hierarchy.ts | 1 + .../windows/fx-editor/VFX/types/index.ts | 2 +- 9 files changed, 257 insertions(+), 330 deletions(-) delete mode 100644 editor/src/editor/windows/fx-editor/VFX/factories/VFXEmitterFactory.ts diff --git a/editor/src/editor/windows/fx-editor/VFX/factories/VFXEmitterFactory.ts b/editor/src/editor/windows/fx-editor/VFX/factories/VFXEmitterFactory.ts deleted file mode 100644 index e1f4a01cc..000000000 --- a/editor/src/editor/windows/fx-editor/VFX/factories/VFXEmitterFactory.ts +++ /dev/null @@ -1,299 +0,0 @@ -import { Mesh, CreatePlane, Nullable, Color4, Matrix, ParticleSystem, SolidParticleSystem, Constants, Vector3, Quaternion, Texture } from "babylonjs"; -import type { VFXEmitterData } from "../types/emitter"; -import type { VFXParseContext } from "../types/context"; -import { VFXLogger } from "../loggers/VFXLogger"; -import { VFXValueUtils } from "../utils/valueParser"; -import type { IVFXMaterialFactory, IVFXGeometryFactory } from "../types/factories"; -import { VFXSolidParticleSystem } from "../systems/VFXSolidParticleSystem"; -import { VFXParticleSystem } from "../systems/VFXParticleSystem"; -import type { VFXParticleEmitterConfig } from "../types/emitterConfig"; - -/** - * Factory for creating particle emitters (ParticleSystem and SolidParticleSystem) - */ -export class VFXEmitterFactory { - private _logger: VFXLogger; - private _context: VFXParseContext; - private _materialFactory: IVFXMaterialFactory; - private _geometryFactory: IVFXGeometryFactory; - - constructor(context: VFXParseContext, materialFactory: IVFXMaterialFactory, geometryFactory: IVFXGeometryFactory) { - this._context = context; - this._logger = new VFXLogger("[VFXEmitterFactory]"); - this._materialFactory = materialFactory; - this._geometryFactory = geometryFactory; - } - - /** - * Create a particle emitter from emitter data - */ - public createEmitter(emitterData: VFXEmitterData): Nullable { - const { options } = this._context; - - // Use systemType from emitter data (determined during conversion) - const systemType = emitterData.vfxEmitter?.systemType || "base"; - this._logger.log(`Using ${systemType === "solid" ? "SolidParticleSystem" : "ParticleSystem"}`, options); - - if (systemType === "solid") { - return this._createSolidParticleSystem(emitterData); - } else { - return this._createParticleSystem(emitterData); - } - } - - /** - * Create a ParticleSystem (billboard-based particles) - */ - private _createParticleSystem(emitterData: VFXEmitterData): Nullable { - const { name, config } = emitterData; - const { options, scene } = this._context; - - this._logger.log(`Creating ParticleSystem: ${name}`, options); - - // Parse values for capacity calculation - const emissionRate = config.emissionOverTime !== undefined ? VFXValueUtils.parseConstantValue(config.emissionOverTime) : 10; - const duration = config.duration || 5; - const capacity = Math.ceil(emissionRate * duration * 2); - const speed = config.startSpeed !== undefined ? VFXValueUtils.parseIntervalValue(config.startSpeed) : { min: 1, max: 1 }; - const size = config.startSize !== undefined ? VFXValueUtils.parseIntervalValue(config.startSize) : { min: 1, max: 1 }; - const startColor = config.startColor !== undefined ? VFXValueUtils.parseConstantColor(config.startColor) : new Color4(1, 1, 1, 1); - const avgStartSpeed = (speed.min + speed.max) / 2; - const avgStartSize = (size.min + size.max) / 2; - - // Create instance - const particleSystem = new VFXParticleSystem(name, capacity, scene, avgStartSpeed, avgStartSize, startColor); - - // Get texture and blend mode - const texture: Texture | undefined = emitterData.materialId ? this._materialFactory.createTexture(emitterData.materialId) || undefined : undefined; - const blendMode = emitterData.materialId ? this._getBlendModeFromMaterial(emitterData.materialId) : undefined; - - // Extract rotation matrix - const rotationMatrix = this._extractRotationMatrix(emitterData.matrix); - - // Configure from config - particleSystem.configureFromConfig(config, { - texture, - blendMode, - emitterShape: { - shape: config.shape, - cumulativeScale: emitterData.cumulativeScale, - rotationMatrix, - }, - }); - - this._logger.log(`ParticleSystem created: ${name}`, options); - return particleSystem; - } - - /** - * Gets blend mode from material blending value - */ - private _getBlendModeFromMaterial(materialId: string): number | undefined { - const { jsonData } = this._context; - const material = jsonData.materials?.find((m: any) => m.uuid === materialId); - - if (material?.blending === undefined) { - return undefined; - } - - const blendModeMap: Record = { - 0: Constants.ALPHA_DISABLE, // NoBlending - 1: Constants.ALPHA_COMBINE, // NormalBlending - 2: Constants.ALPHA_ADD, // AdditiveBlending - }; - - return blendModeMap[material.blending]; - } - - /** - * Create a SolidParticleSystem (mesh-based particles) - */ - private _createSolidParticleSystem(emitterData: VFXEmitterData): Nullable { - const { name, config } = emitterData; - const { options } = this._context; - - this._logger.log(`Creating SolidParticleSystem: ${name}`, options); - - const capacity = this._calculateSPSCapacity(config); - const vfxTransform = this._getVFXTransform(emitterData); - const sps = this._createSPSInstance(name, config, emitterData, vfxTransform); - - const particleMesh = this._createOrLoadParticleMesh(name, config, emitterData); - if (!particleMesh) { - return null; - } - - this._addShapeToSPS(sps, particleMesh, capacity); - this._configureSPSBillboard(sps, config); - - // Apply behaviors - if (config.behaviors && Array.isArray(config.behaviors) && config.behaviors.length > 0) { - sps.behaviorConfigs.length = 0; - sps.behaviorConfigs.push(...config.behaviors); - } - - particleMesh.dispose(); - - this._logger.log(`SolidParticleSystem created: ${name}`, options); - return sps; - } - - /** - * Calculates capacity for SolidParticleSystem - */ - private _calculateSPSCapacity(config: VFXParticleEmitterConfig): number { - const { options } = this._context; - const emissionRate = config.emissionOverTime !== undefined ? VFXValueUtils.parseConstantValue(config.emissionOverTime) : 10; - const particleLifetime = config.duration || 5; - const isLooping = config.looping !== false; - - if (isLooping) { - const capacity = Math.max(Math.ceil(emissionRate * particleLifetime), 1); - this._logger.log(` Looping system: Emission rate: ${emissionRate} particles/sec, Particle lifetime: ${particleLifetime} sec, Capacity: ${capacity}`, options); - return capacity; - } else { - const capacity = Math.ceil(emissionRate * particleLifetime * 2); - this._logger.log(` Non-looping system: Emission rate: ${emissionRate} particles/sec, Particle lifetime: ${particleLifetime} sec, Capacity: ${capacity}`, options); - return capacity; - } - } - - /** - * Gets VFX transform from emitter data - */ - private _getVFXTransform(emitterData: VFXEmitterData): { position: Vector3; rotation: Quaternion; scale: Vector3 } | null { - const vfxEmitter = emitterData.vfxEmitter; - return vfxEmitter?.transform || null; - } - - /** - * Creates SolidParticleSystem instance - */ - private _createSPSInstance( - name: string, - config: VFXParticleEmitterConfig, - emitterData: VFXEmitterData, - vfxTransform: { position: Vector3; rotation: Quaternion; scale: Vector3 } | null - ): VFXSolidParticleSystem { - const { scene, options } = this._context; - const sps = new VFXSolidParticleSystem(name, scene, config, { - updatable: true, - isPickable: false, - enableDepthSort: false, - particleIntersection: false, - useModelMaterial: true, - parentGroup: emitterData.parentGroup, - vfxTransform, - logger: this._logger, - loaderOptions: options, - }); - // Set parent after creation (will apply to mesh) - if (emitterData.parentGroup) { - sps.parent = emitterData.parentGroup; - } - return sps; - } - - /** - * Creates or loads particle mesh for SPS - */ - private _createOrLoadParticleMesh(name: string, config: VFXParticleEmitterConfig, emitterData: VFXEmitterData): Nullable { - const { scene, options } = this._context; - let particleMesh = this._loadParticleGeometry(config, emitterData, name); - - if (!particleMesh) { - particleMesh = this._createDefaultPlaneMesh(name, scene); - this._applyMaterialToMesh(particleMesh, emitterData.materialId, name); - } else { - this._ensureMaterialApplied(particleMesh, emitterData.materialId, name); - } - - if (!particleMesh) { - this._logger.warn(` Cannot add shape to SPS: particleMesh is null`, options); - } - - return particleMesh; - } - - /** - * Loads particle geometry if specified - */ - private _loadParticleGeometry(config: VFXParticleEmitterConfig, emitterData: VFXEmitterData, name: string): Nullable { - const { options } = this._context; - - if (!config.instancingGeometry) { - return null; - } - - this._logger.log(` Loading geometry: ${config.instancingGeometry}`, options); - const mesh = this._geometryFactory.createMesh(config.instancingGeometry, emitterData.materialId, name + "_shape"); - if (!mesh) { - this._logger.warn(` Failed to load geometry ${config.instancingGeometry}, will create default plane`, options); - } - - return mesh; - } - - /** - * Creates default plane mesh - */ - private _createDefaultPlaneMesh(name: string, scene: any): Mesh { - const { options } = this._context; - this._logger.log(` Creating default plane geometry`, options); - return CreatePlane(name + "_shape", { width: 1, height: 1 }, scene); - } - - /** - * Applies material to mesh - */ - private _applyMaterialToMesh(mesh: Mesh | null, materialId: string | undefined, name: string): void { - if (!mesh || !materialId) { - return; - } - - const material = this._materialFactory.createMaterial(materialId, name); - if (material) { - mesh.material = material; - } - } - - /** - * Ensures material is applied to mesh if missing - */ - private _ensureMaterialApplied(mesh: Mesh, materialId: string | undefined, name: string): void { - if (materialId && !mesh.material) { - this._applyMaterialToMesh(mesh, materialId, name); - } - } - - /** - * Adds shape to SPS - */ - private _addShapeToSPS(sps: SolidParticleSystem, particleMesh: Mesh, capacity: number): void { - const { options } = this._context; - this._logger.log(` Adding shape to SPS: mesh name=${particleMesh.name}, hasMaterial=${!!particleMesh.material}`, options); - sps.addShape(particleMesh, capacity); - } - - /** - * Configures billboard mode for SPS - */ - private _configureSPSBillboard(sps: SolidParticleSystem, config: VFXParticleEmitterConfig): void { - if (config.renderMode === 0 || config.renderMode === 1) { - sps.billboard = true; - } - } - - /** - * Extracts rotation matrix from Three.js matrix array - */ - private _extractRotationMatrix(matrix: number[] | undefined): Matrix | null { - if (!matrix || matrix.length < 16) { - return null; - } - - const mat = Matrix.FromArray(matrix); - mat.transpose(); - return mat.getRotationMatrix(); - } -} diff --git a/editor/src/editor/windows/fx-editor/VFX/factories/VFXSystemFactory.ts b/editor/src/editor/windows/fx-editor/VFX/factories/VFXSystemFactory.ts index 7495b5ec8..c1294c74a 100644 --- a/editor/src/editor/windows/fx-editor/VFX/factories/VFXSystemFactory.ts +++ b/editor/src/editor/windows/fx-editor/VFX/factories/VFXSystemFactory.ts @@ -1,11 +1,12 @@ -import { Nullable, Vector3, TransformNode } from "babylonjs"; +import { Nullable, Vector3, TransformNode, Mesh, CreatePlane, Color4, Matrix, Constants, Texture } from "babylonjs"; import { VFXParticleSystem } from "../systems/VFXParticleSystem"; import { VFXSolidParticleSystem } from "../systems/VFXSolidParticleSystem"; import type { VFXParseContext } from "../types/context"; -import type { VFXEmitterData } from "../types/emitter"; import type { VFXData, VFXGroup, VFXEmitter, VFXTransform } from "../types/hierarchy"; import { VFXLogger } from "../loggers/VFXLogger"; -import type { IVFXEmitterFactory } from "../types/factories"; +import { VFXValueUtils } from "../utils/valueParser"; +import type { IVFXMaterialFactory, IVFXGeometryFactory } from "../types/factories"; +import type { VFXParticleEmitterConfig } from "../types/emitterConfig"; /** * Factory for creating particle systems from VFX data @@ -14,12 +15,14 @@ import type { IVFXEmitterFactory } from "../types/factories"; export class VFXSystemFactory { private _logger: VFXLogger; private _context: VFXParseContext; - private _emitterFactory: IVFXEmitterFactory; + private _materialFactory: IVFXMaterialFactory; + private _geometryFactory: IVFXGeometryFactory; - constructor(context: VFXParseContext, emitterFactory: IVFXEmitterFactory) { + constructor(context: VFXParseContext, materialFactory: IVFXMaterialFactory, geometryFactory: IVFXGeometryFactory) { this._context = context; this._logger = new VFXLogger("[VFXSystemFactory]"); - this._emitterFactory = emitterFactory; + this._materialFactory = materialFactory; + this._geometryFactory = geometryFactory; } /** @@ -127,8 +130,21 @@ export class VFXSystemFactory { this._logEmitterProcessing(vfxEmitter, parentGroup, depth); this._logEmitterConfig(vfxEmitter, depth); - const emitterData = this._buildEmitterData(vfxEmitter, parentGroup, depth); - const particleSystem = this._emitterFactory.createEmitter(emitterData) as VFXParticleSystem | VFXSolidParticleSystem | null; + const cumulativeScale = this._calculateCumulativeScale(parentGroup); + this._logCumulativeScale(cumulativeScale, depth); + + // Use systemType from emitter (determined during conversion) + const systemType = vfxEmitter.systemType || "base"; + const { options } = this._context; + this._logger.log(`Using ${systemType === "solid" ? "SolidParticleSystem" : "ParticleSystem"}`, options); + + let particleSystem: VFXParticleSystem | VFXSolidParticleSystem | null = null; + + if (systemType === "solid") { + particleSystem = this._createSolidParticleSystem(vfxEmitter, parentGroup, cumulativeScale, depth); + } else { + particleSystem = this._createParticleSystemInstance(vfxEmitter, parentGroup, cumulativeScale, depth); + } if (!particleSystem) { this._logWarning(`Failed to create particle system for emitter: ${vfxEmitter.name}`); @@ -159,20 +175,212 @@ export class VFXSystemFactory { } /** - * Build emitter data structure for factory + * Create a ParticleSystem instance */ - private _buildEmitterData(vfxEmitter: VFXEmitter, parentGroup: Nullable, depth: number): VFXEmitterData { - const cumulativeScale = this._calculateCumulativeScale(parentGroup); - this._logCumulativeScale(cumulativeScale, depth); + private _createParticleSystemInstance(vfxEmitter: VFXEmitter, _parentGroup: Nullable, cumulativeScale: Vector3, _depth: number): Nullable { + const { name, config } = vfxEmitter; + const { options, scene } = this._context; + + this._logger.log(`Creating ParticleSystem: ${name}`, options); + + // Parse values for capacity calculation + const emissionRate = config.emissionOverTime !== undefined ? VFXValueUtils.parseConstantValue(config.emissionOverTime) : 10; + const duration = config.duration || 5; + const capacity = Math.ceil(emissionRate * duration * 2); + const speed = config.startSpeed !== undefined ? VFXValueUtils.parseIntervalValue(config.startSpeed) : { min: 1, max: 1 }; + const size = config.startSize !== undefined ? VFXValueUtils.parseIntervalValue(config.startSize) : { min: 1, max: 1 }; + const startColor = config.startColor !== undefined ? VFXValueUtils.parseConstantColor(config.startColor) : new Color4(1, 1, 1, 1); + const avgStartSpeed = (speed.min + speed.max) / 2; + const avgStartSize = (size.min + size.max) / 2; + + // Create instance + const particleSystem = new VFXParticleSystem(name, capacity, scene, avgStartSpeed, avgStartSize, startColor); + + // Get texture and blend mode + const texture: Texture | undefined = vfxEmitter.materialId ? this._materialFactory.createTexture(vfxEmitter.materialId) || undefined : undefined; + const blendMode = vfxEmitter.materialId ? this._getBlendModeFromMaterial(vfxEmitter.materialId) : undefined; + + // Extract rotation matrix from emitter matrix if available + const rotationMatrix = vfxEmitter.matrix ? this._extractRotationMatrix(vfxEmitter.matrix) : null; + + // Configure from config + particleSystem.configureFromConfig(config, { + texture, + blendMode, + emitterShape: { + shape: config.shape, + cumulativeScale, + rotationMatrix, + }, + }); - return { - name: vfxEmitter.name, - config: vfxEmitter.config, - materialId: vfxEmitter.materialId, + this._logger.log(`ParticleSystem created: ${name}`, options); + return particleSystem; + } + + /** + * Create a SolidParticleSystem instance + */ + private _createSolidParticleSystem(vfxEmitter: VFXEmitter, parentGroup: Nullable, _cumulativeScale: Vector3, _depth: number): Nullable { + const { name, config } = vfxEmitter; + const { options, scene } = this._context; + + this._logger.log(`Creating SolidParticleSystem: ${name}`, options); + + // Calculate capacity + const emissionRate = config.emissionOverTime !== undefined ? VFXValueUtils.parseConstantValue(config.emissionOverTime) : 10; + const particleLifetime = config.duration || 5; + const isLooping = config.looping !== false; + const capacity = isLooping ? Math.max(Math.ceil(emissionRate * particleLifetime), 1) : Math.ceil(emissionRate * particleLifetime * 2); + + this._logger.log(` Capacity: ${capacity} (looping: ${isLooping})`, options); + + // Get VFX transform + const vfxTransform = vfxEmitter.transform || null; + + // Create SPS instance + const sps = new VFXSolidParticleSystem(name, scene, config, { + updatable: true, + isPickable: false, + enableDepthSort: false, + particleIntersection: false, + useModelMaterial: true, parentGroup, - cumulativeScale, - vfxEmitter, + vfxTransform, + logger: this._logger, + loaderOptions: options, + }); + + // Set parent after creation (will apply to mesh) + if (parentGroup) { + sps.parent = parentGroup; + } + + // Create or load particle mesh + const particleMesh = this._createOrLoadParticleMesh(name, config, vfxEmitter.materialId); + if (!particleMesh) { + return null; + } + + // Initialize mesh in SPS + sps.initializeMesh(particleMesh, capacity); + + // Apply behaviors + if (config.behaviors && Array.isArray(config.behaviors) && config.behaviors.length > 0) { + sps.behaviorConfigs.length = 0; + sps.behaviorConfigs.push(...config.behaviors); + } + + // Dispose temporary mesh + particleMesh.dispose(); + + this._logger.log(`SolidParticleSystem created: ${name}`, options); + return sps; + } + + /** + * Gets blend mode from material blending value + */ + private _getBlendModeFromMaterial(materialId: string): number | undefined { + const { jsonData } = this._context; + const material = jsonData.materials?.find((m: any) => m.uuid === materialId); + + if (material?.blending === undefined) { + return undefined; + } + + const blendModeMap: Record = { + 0: Constants.ALPHA_DISABLE, // NoBlending + 1: Constants.ALPHA_COMBINE, // NormalBlending + 2: Constants.ALPHA_ADD, // AdditiveBlending }; + + return blendModeMap[material.blending]; + } + + /** + * Creates or loads particle mesh for SPS + */ + private _createOrLoadParticleMesh(name: string, config: VFXParticleEmitterConfig, materialId: string | undefined): Nullable { + const { scene, options } = this._context; + let particleMesh = this._loadParticleGeometry(config, materialId, name); + + if (!particleMesh) { + particleMesh = this._createDefaultPlaneMesh(name, scene); + this._applyMaterialToMesh(particleMesh, materialId, name); + } else { + this._ensureMaterialApplied(particleMesh, materialId, name); + } + + if (!particleMesh) { + this._logger.warn(` Cannot add shape to SPS: particleMesh is null`, options); + } + + return particleMesh; + } + + /** + * Loads particle geometry if specified + */ + private _loadParticleGeometry(config: VFXParticleEmitterConfig, materialId: string | undefined, name: string): Nullable { + const { options } = this._context; + + if (!config.instancingGeometry) { + return null; + } + + this._logger.log(` Loading geometry: ${config.instancingGeometry}`, options); + const mesh = this._geometryFactory.createMesh(config.instancingGeometry, materialId, name + "_shape"); + if (!mesh) { + this._logger.warn(` Failed to load geometry ${config.instancingGeometry}, will create default plane`, options); + } + + return mesh; + } + + /** + * Creates default plane mesh + */ + private _createDefaultPlaneMesh(name: string, scene: any): Mesh { + const { options } = this._context; + this._logger.log(` Creating default plane geometry`, options); + return CreatePlane(name + "_shape", { width: 1, height: 1 }, scene); + } + + /** + * Applies material to mesh + */ + private _applyMaterialToMesh(mesh: Mesh | null, materialId: string | undefined, name: string): void { + if (!mesh || !materialId) { + return; + } + + const material = this._materialFactory.createMaterial(materialId, name); + if (material) { + mesh.material = material; + } + } + + /** + * Ensures material is applied to mesh if missing + */ + private _ensureMaterialApplied(mesh: Mesh, materialId: string | undefined, name: string): void { + if (materialId && !mesh.material) { + this._applyMaterialToMesh(mesh, materialId, name); + } + } + + /** + * Extracts rotation matrix from Three.js matrix array + */ + private _extractRotationMatrix(matrix: number[] | undefined): Matrix | null { + if (!matrix || matrix.length < 16) { + return null; + } + + const mat = Matrix.FromArray(matrix); + mat.transpose(); + return mat.getRotationMatrix(); } /** diff --git a/editor/src/editor/windows/fx-editor/VFX/index.ts b/editor/src/editor/windows/fx-editor/VFX/index.ts index 25a8b4efd..0c405ea7b 100644 --- a/editor/src/editor/windows/fx-editor/VFX/index.ts +++ b/editor/src/editor/windows/fx-editor/VFX/index.ts @@ -3,7 +3,6 @@ export * from "./parsers/VFXParser"; export * from "./parsers/VFXDataConverter"; export * from "./factories/VFXMaterialFactory"; export * from "./factories/VFXGeometryFactory"; -export * from "./factories/VFXEmitterFactory"; export * from "./factories/VFXSystemFactory"; export * from "./systems/VFXSolidParticleSystem"; export * from "./systems/VFXParticleSystem"; diff --git a/editor/src/editor/windows/fx-editor/VFX/parsers/VFXDataConverter.ts b/editor/src/editor/windows/fx-editor/VFX/parsers/VFXDataConverter.ts index 7c8136e37..fb8934a76 100644 --- a/editor/src/editor/windows/fx-editor/VFX/parsers/VFXDataConverter.ts +++ b/editor/src/editor/windows/fx-editor/VFX/parsers/VFXDataConverter.ts @@ -143,6 +143,7 @@ export class VFXDataConverter { materialId: obj.ps.material, parentUuid: parentUuid || undefined, systemType, + matrix: obj.matrix, // Store original matrix for rotation extraction }; emitters.set(emitter.uuid, emitter); diff --git a/editor/src/editor/windows/fx-editor/VFX/parsers/VFXParser.ts b/editor/src/editor/windows/fx-editor/VFX/parsers/VFXParser.ts index 339736560..613ec4309 100644 --- a/editor/src/editor/windows/fx-editor/VFX/parsers/VFXParser.ts +++ b/editor/src/editor/windows/fx-editor/VFX/parsers/VFXParser.ts @@ -5,7 +5,6 @@ import type { VFXParseContext } from "../types/context"; import { VFXLogger } from "../loggers/VFXLogger"; import { VFXMaterialFactory } from "../factories/VFXMaterialFactory"; import { VFXGeometryFactory } from "../factories/VFXGeometryFactory"; -import { VFXEmitterFactory } from "../factories/VFXEmitterFactory"; import { VFXSystemFactory } from "../factories/VFXSystemFactory"; import { VFXDataConverter } from "./VFXDataConverter"; import { VFXParticleSystem } from "../systems/VFXParticleSystem"; @@ -20,7 +19,6 @@ export class VFXParser { private _logger: VFXLogger; private _materialFactory: VFXMaterialFactory; private _geometryFactory: VFXGeometryFactory; - private _emitterFactory: VFXEmitterFactory; private _systemFactory: VFXSystemFactory; constructor(scene: Scene, rootUrl: string, jsonData: QuarksVFXJSON, options?: VFXLoaderOptions) { @@ -36,8 +34,7 @@ export class VFXParser { this._logger = new VFXLogger("[VFXParser]"); this._materialFactory = new VFXMaterialFactory(this._context); this._geometryFactory = new VFXGeometryFactory(this._context, this._materialFactory); - this._emitterFactory = new VFXEmitterFactory(this._context, this._materialFactory, this._geometryFactory); - this._systemFactory = new VFXSystemFactory(this._context, this._emitterFactory); + this._systemFactory = new VFXSystemFactory(this._context, this._materialFactory, this._geometryFactory); } /** diff --git a/editor/src/editor/windows/fx-editor/VFX/systems/VFXSolidParticleSystem.ts b/editor/src/editor/windows/fx-editor/VFX/systems/VFXSolidParticleSystem.ts index 9c59337e7..ccd731864 100644 --- a/editor/src/editor/windows/fx-editor/VFX/systems/VFXSolidParticleSystem.ts +++ b/editor/src/editor/windows/fx-editor/VFX/systems/VFXSolidParticleSystem.ts @@ -1,4 +1,4 @@ -import { Vector3, Quaternion, Matrix, Color4, SolidParticleSystem, SolidParticle, TransformNode } from "babylonjs"; +import { Vector3, Quaternion, Matrix, Color4, SolidParticleSystem, SolidParticle, TransformNode, Mesh } from "babylonjs"; import type { VFXParticleEmitterConfig, VFXEmissionBurst } from "../types/emitterConfig"; import { VFXLogger } from "../loggers/VFXLogger"; import type { VFXLoaderOptions } from "../types/loader"; @@ -93,6 +93,31 @@ export class VFXSolidParticleSystem extends SolidParticleSystem { return this._behaviors; } + /** + * Initialize mesh for SPS + * Adds the mesh as a shape and configures billboard mode + */ + public initializeMesh(particleMesh: Mesh, capacity: number): void { + if (!particleMesh) { + if (this._logger) { + this._logger.warn(`Cannot add shape to SPS: particleMesh is null`, this._options); + } + return; + } + + if (this._logger) { + this._logger.log(`Adding shape to SPS: mesh name=${particleMesh.name}, hasMaterial=${!!particleMesh.material}`, this._options); + } + + // Add shape to SPS + this.addShape(particleMesh, capacity); + + // Configure billboard mode + if (this.renderMode === 0 || this.renderMode === 1) { + this.billboard = true; + } + } + /** * Get emit rate (constant value from emissionOverTime) */ diff --git a/editor/src/editor/windows/fx-editor/VFX/types/factories.ts b/editor/src/editor/windows/fx-editor/VFX/types/factories.ts index ff72d2ede..acbeaf6e0 100644 --- a/editor/src/editor/windows/fx-editor/VFX/types/factories.ts +++ b/editor/src/editor/windows/fx-editor/VFX/types/factories.ts @@ -1,5 +1,4 @@ -import { Nullable, Mesh, ParticleSystem, SolidParticleSystem, PBRMaterial, Texture } from "babylonjs"; -import type { VFXEmitterData } from "./emitter"; +import { Nullable, Mesh, PBRMaterial, Texture } from "babylonjs"; /** * Factory interfaces for dependency injection @@ -12,7 +11,3 @@ export interface IVFXMaterialFactory { export interface IVFXGeometryFactory { createMesh(geometryId: string, materialId: string | undefined, name: string): Nullable; } - -export interface IVFXEmitterFactory { - createEmitter(emitterData: VFXEmitterData): Nullable; -} diff --git a/editor/src/editor/windows/fx-editor/VFX/types/hierarchy.ts b/editor/src/editor/windows/fx-editor/VFX/types/hierarchy.ts index a4bf80409..bc59c4337 100644 --- a/editor/src/editor/windows/fx-editor/VFX/types/hierarchy.ts +++ b/editor/src/editor/windows/fx-editor/VFX/types/hierarchy.ts @@ -31,6 +31,7 @@ export interface VFXEmitter { materialId?: string; parentUuid?: string; systemType: "solid" | "base"; // Determined from renderMode: 2 = solid, otherwise base + matrix?: number[]; // Original Three.js matrix array for rotation extraction } /** diff --git a/editor/src/editor/windows/fx-editor/VFX/types/index.ts b/editor/src/editor/windows/fx-editor/VFX/types/index.ts index a0b35d32f..fc0b3426b 100644 --- a/editor/src/editor/windows/fx-editor/VFX/types/index.ts +++ b/editor/src/editor/windows/fx-editor/VFX/types/index.ts @@ -13,7 +13,7 @@ export type { VFXParseContext } from "./context"; export type { VFXEmitterData } from "./emitter"; // Factory interfaces -export type { IVFXMaterialFactory, IVFXGeometryFactory, IVFXEmitterFactory } from "./factories"; +export type { IVFXMaterialFactory, IVFXGeometryFactory } from "./factories"; // Core VFX types export type { VFXConstantValue, VFXIntervalValue, VFXValue } from "./values"; From 4d56c703582a5f2779fd23f6f4de433a20373749 Mon Sep 17 00:00:00 2001 From: Mikalai Lazitski Date: Sat, 13 Dec 2025 16:55:24 +0300 Subject: [PATCH 20/62] refactor: enhance VFX system functionality by introducing capacity and matrix utility classes, improving particle system initialization and configuration for better performance and clarity --- .../editor/windows/fx-editor/VFX/VFXEffect.ts | 49 ++--- .../VFX/factories/VFXGeometryFactory.ts | 58 ++++++ .../VFX/factories/VFXMaterialFactory.ts | 20 ++ .../VFX/factories/VFXSystemFactory.ts | 175 ++---------------- .../src/editor/windows/fx-editor/VFX/index.ts | 2 + .../VFX/systems/VFXParticleSystem.ts | 29 ++- .../VFX/systems/VFXSolidParticleSystem.ts | 19 +- .../windows/fx-editor/VFX/types/factories.ts | 2 + .../fx-editor/VFX/utils/capacityCalculator.ts | 34 ++++ .../fx-editor/VFX/utils/matrixUtils.ts | 22 +++ 10 files changed, 220 insertions(+), 190 deletions(-) create mode 100644 editor/src/editor/windows/fx-editor/VFX/utils/capacityCalculator.ts create mode 100644 editor/src/editor/windows/fx-editor/VFX/utils/matrixUtils.ts diff --git a/editor/src/editor/windows/fx-editor/VFX/VFXEffect.ts b/editor/src/editor/windows/fx-editor/VFX/VFXEffect.ts index 6b160f9c8..7e73a3aaa 100644 --- a/editor/src/editor/windows/fx-editor/VFX/VFXEffect.ts +++ b/editor/src/editor/windows/fx-editor/VFX/VFXEffect.ts @@ -32,10 +32,24 @@ export interface VFXEffectNode { */ export class VFXEffect implements IDisposable { /** All particle systems in this effect */ - public readonly systems: (VFXParticleSystem | VFXSolidParticleSystem)[] = []; + private _systems: (VFXParticleSystem | VFXSolidParticleSystem)[] = []; /** Root node of the effect hierarchy */ - public readonly root: VFXEffectNode | null = null; + private _root: VFXEffectNode | null = null; + + /** + * Get all particle systems in this effect + */ + public get systems(): ReadonlyArray { + return this._systems; + } + + /** + * Get root node of the effect hierarchy + */ + public get root(): VFXEffectNode | null { + return this._root; + } /** Map of systems by name for quick lookup */ private readonly _systemsByName = new Map(); @@ -92,19 +106,7 @@ export class VFXEffect implements IDisposable { * @returns A VFXEffect containing all particle systems */ public static Parse(jsonData: QuarksVFXJSON, scene: Scene, rootUrl: string = "", options?: VFXLoaderOptions): VFXEffect { - const parser = new VFXParser(scene, rootUrl, jsonData, options); - const particleSystems = parser.parse(); - const context = parser.getContext(); - const vfxData = context.vfxData; - const groupNodesMap = context.groupNodesMap; - - const effect = new VFXEffect(); - effect.systems.push(...particleSystems); - if (vfxData && groupNodesMap) { - effect._buildHierarchy(vfxData, groupNodesMap, particleSystems); - } - - return effect; + return new VFXEffect(jsonData, scene, rootUrl, options); } /** @@ -122,7 +124,7 @@ export class VFXEffect implements IDisposable { const vfxData = context.vfxData; const groupNodesMap = context.groupNodesMap; - this.systems.push(...particleSystems); + this._systems.push(...particleSystems); if (vfxData && groupNodesMap) { this._buildHierarchy(vfxData, groupNodesMap, particleSystems); } @@ -138,9 +140,7 @@ export class VFXEffect implements IDisposable { } // Create nodes from hierarchy - const rootNode = this._buildNodeFromHierarchy(vfxData.root, null, groupNodesMap, systems); - // Store root (we can't assign to readonly, so we'll use a workaround) - (this as any).root = rootNode; + this._root = this._buildNodeFromHierarchy(vfxData.root, null, groupNodesMap, systems); } /** @@ -274,7 +274,7 @@ export class VFXEffect implements IDisposable { */ private _collectSystemsInGroup(group: TransformNode, systems: (VFXParticleSystem | VFXSolidParticleSystem)[]): void { // Step 1: Find systems that have this group as direct parent - for (const system of this.systems) { + for (const system of this._systems) { const mesh = (system as any).mesh || (system as any).emitter; if (mesh && mesh.parent === group) { systems.push(system); @@ -339,7 +339,7 @@ export class VFXEffect implements IDisposable { * Start all particle systems */ public start(): void { - for (const system of this.systems) { + for (const system of this._systems) { system.start(); } } @@ -348,7 +348,7 @@ export class VFXEffect implements IDisposable { * Stop all particle systems */ public stop(): void { - for (const system of this.systems) { + for (const system of this._systems) { system.stop(); } } @@ -357,10 +357,11 @@ export class VFXEffect implements IDisposable { * Dispose all resources */ public dispose(): void { - for (const system of this.systems) { + for (const system of this._systems) { system.dispose(); } - this.systems.length = 0; + this._systems = []; + this._root = null; this._systemsByName.clear(); this._systemsByUuid.clear(); this._groupsByName.clear(); diff --git a/editor/src/editor/windows/fx-editor/VFX/factories/VFXGeometryFactory.ts b/editor/src/editor/windows/fx-editor/VFX/factories/VFXGeometryFactory.ts index 7e8c68fb2..62886e567 100644 --- a/editor/src/editor/windows/fx-editor/VFX/factories/VFXGeometryFactory.ts +++ b/editor/src/editor/windows/fx-editor/VFX/factories/VFXGeometryFactory.ts @@ -43,6 +43,64 @@ export class VFXGeometryFactory implements IVFXGeometryFactory { return mesh; } + /** + * Create or load particle mesh for SPS + * Tries to load geometry if specified, otherwise creates default plane + */ + public createParticleMesh(config: { instancingGeometry?: string }, materialId: string | undefined, name: string, scene: any): Nullable { + const { options } = this._context; + let particleMesh = this._loadParticleGeometry(config, materialId, name); + + if (!particleMesh) { + particleMesh = this._createDefaultPlaneMesh(name, scene); + this._applyMaterial(particleMesh, materialId, name); + } else { + this._ensureMaterialApplied(particleMesh, materialId, name); + } + + if (!particleMesh) { + this._logger.warn(` Cannot create particle mesh: particleMesh is null`, options); + } + + return particleMesh; + } + + /** + * Loads particle geometry if specified + */ + private _loadParticleGeometry(config: { instancingGeometry?: string }, materialId: string | undefined, name: string): Nullable { + if (!config.instancingGeometry) { + return null; + } + + const { options } = this._context; + this._logger.log(` Loading geometry: ${config.instancingGeometry}`, options); + const mesh = this.createMesh(config.instancingGeometry, materialId, name + "_shape"); + if (!mesh && this._logger) { + this._logger.warn(` Failed to load geometry ${config.instancingGeometry}, will create default plane`, options); + } + + return mesh; + } + + /** + * Creates default plane mesh + */ + private _createDefaultPlaneMesh(name: string, scene: any): Mesh { + const { options } = this._context; + this._logger.log(` Creating default plane geometry`, options); + return CreatePlane(name + "_shape", { width: 1, height: 1 }, scene); + } + + /** + * Ensures material is applied to mesh if missing + */ + private _ensureMaterialApplied(mesh: Mesh, materialId: string | undefined, name: string): void { + if (materialId && !mesh.material) { + this._applyMaterial(mesh, materialId, name); + } + } + /** * Finds geometry by UUID */ diff --git a/editor/src/editor/windows/fx-editor/VFX/factories/VFXMaterialFactory.ts b/editor/src/editor/windows/fx-editor/VFX/factories/VFXMaterialFactory.ts index 92f780545..3b5a60ef4 100644 --- a/editor/src/editor/windows/fx-editor/VFX/factories/VFXMaterialFactory.ts +++ b/editor/src/editor/windows/fx-editor/VFX/factories/VFXMaterialFactory.ts @@ -30,6 +30,26 @@ export class VFXMaterialFactory implements IVFXMaterialFactory { return this._createTextureFromData(textureUrl, texture); } + /** + * Get blend mode from material blending value + */ + public getBlendMode(materialId: string): number | undefined { + const { jsonData } = this._context; + const material = jsonData.materials?.find((m: any) => m.uuid === materialId); + + if (material?.blending === undefined) { + return undefined; + } + + const blendModeMap: Record = { + 0: Constants.ALPHA_DISABLE, // NoBlending + 1: Constants.ALPHA_COMBINE, // NormalBlending + 2: Constants.ALPHA_ADD, // AdditiveBlending + }; + + return blendModeMap[material.blending]; + } + /** * Resolves material, texture, and image data from material ID */ diff --git a/editor/src/editor/windows/fx-editor/VFX/factories/VFXSystemFactory.ts b/editor/src/editor/windows/fx-editor/VFX/factories/VFXSystemFactory.ts index c1294c74a..d57d9c77d 100644 --- a/editor/src/editor/windows/fx-editor/VFX/factories/VFXSystemFactory.ts +++ b/editor/src/editor/windows/fx-editor/VFX/factories/VFXSystemFactory.ts @@ -1,12 +1,11 @@ -import { Nullable, Vector3, TransformNode, Mesh, CreatePlane, Color4, Matrix, Constants, Texture } from "babylonjs"; +import { Nullable, Vector3, TransformNode, Texture } from "babylonjs"; import { VFXParticleSystem } from "../systems/VFXParticleSystem"; import { VFXSolidParticleSystem } from "../systems/VFXSolidParticleSystem"; import type { VFXParseContext } from "../types/context"; import type { VFXData, VFXGroup, VFXEmitter, VFXTransform } from "../types/hierarchy"; import { VFXLogger } from "../loggers/VFXLogger"; -import { VFXValueUtils } from "../utils/valueParser"; +import { VFXMatrixUtils } from "../utils/matrixUtils"; import type { IVFXMaterialFactory, IVFXGeometryFactory } from "../types/factories"; -import type { VFXParticleEmitterConfig } from "../types/emitterConfig"; /** * Factory for creating particle systems from VFX data @@ -141,7 +140,7 @@ export class VFXSystemFactory { let particleSystem: VFXParticleSystem | VFXSolidParticleSystem | null = null; if (systemType === "solid") { - particleSystem = this._createSolidParticleSystem(vfxEmitter, parentGroup, cumulativeScale, depth); + particleSystem = this._createSolidParticleSystem(vfxEmitter, parentGroup); } else { particleSystem = this._createParticleSystemInstance(vfxEmitter, parentGroup, cumulativeScale, depth); } @@ -183,28 +182,15 @@ export class VFXSystemFactory { this._logger.log(`Creating ParticleSystem: ${name}`, options); - // Parse values for capacity calculation - const emissionRate = config.emissionOverTime !== undefined ? VFXValueUtils.parseConstantValue(config.emissionOverTime) : 10; - const duration = config.duration || 5; - const capacity = Math.ceil(emissionRate * duration * 2); - const speed = config.startSpeed !== undefined ? VFXValueUtils.parseIntervalValue(config.startSpeed) : { min: 1, max: 1 }; - const size = config.startSize !== undefined ? VFXValueUtils.parseIntervalValue(config.startSize) : { min: 1, max: 1 }; - const startColor = config.startColor !== undefined ? VFXValueUtils.parseConstantColor(config.startColor) : new Color4(1, 1, 1, 1); - const avgStartSpeed = (speed.min + speed.max) / 2; - const avgStartSize = (size.min + size.max) / 2; - - // Create instance - const particleSystem = new VFXParticleSystem(name, capacity, scene, avgStartSpeed, avgStartSize, startColor); - // Get texture and blend mode const texture: Texture | undefined = vfxEmitter.materialId ? this._materialFactory.createTexture(vfxEmitter.materialId) || undefined : undefined; - const blendMode = vfxEmitter.materialId ? this._getBlendModeFromMaterial(vfxEmitter.materialId) : undefined; + const blendMode = vfxEmitter.materialId ? this._materialFactory.getBlendMode(vfxEmitter.materialId) : undefined; // Extract rotation matrix from emitter matrix if available - const rotationMatrix = vfxEmitter.matrix ? this._extractRotationMatrix(vfxEmitter.matrix) : null; + const rotationMatrix = vfxEmitter.matrix ? VFXMatrixUtils.extractRotationMatrix(vfxEmitter.matrix) : null; - // Configure from config - particleSystem.configureFromConfig(config, { + // Create instance - all configuration happens in constructor + const particleSystem = new VFXParticleSystem(name, scene, config, { texture, blendMode, emitterShape: { @@ -221,24 +207,22 @@ export class VFXSystemFactory { /** * Create a SolidParticleSystem instance */ - private _createSolidParticleSystem(vfxEmitter: VFXEmitter, parentGroup: Nullable, _cumulativeScale: Vector3, _depth: number): Nullable { + private _createSolidParticleSystem(vfxEmitter: VFXEmitter, parentGroup: Nullable): Nullable { const { name, config } = vfxEmitter; const { options, scene } = this._context; this._logger.log(`Creating SolidParticleSystem: ${name}`, options); - // Calculate capacity - const emissionRate = config.emissionOverTime !== undefined ? VFXValueUtils.parseConstantValue(config.emissionOverTime) : 10; - const particleLifetime = config.duration || 5; - const isLooping = config.looping !== false; - const capacity = isLooping ? Math.max(Math.ceil(emissionRate * particleLifetime), 1) : Math.ceil(emissionRate * particleLifetime * 2); - - this._logger.log(` Capacity: ${capacity} (looping: ${isLooping})`, options); - // Get VFX transform const vfxTransform = vfxEmitter.transform || null; - // Create SPS instance + // Create or load particle mesh + const particleMesh = this._geometryFactory.createParticleMesh(config, vfxEmitter.materialId, name, scene); + if (!particleMesh) { + return null; + } + + // Create SPS instance - mesh initialization and capacity calculation happen in constructor const sps = new VFXSolidParticleSystem(name, scene, config, { updatable: true, isPickable: false, @@ -249,140 +233,13 @@ export class VFXSystemFactory { vfxTransform, logger: this._logger, loaderOptions: options, + particleMesh, }); - // Set parent after creation (will apply to mesh) - if (parentGroup) { - sps.parent = parentGroup; - } - - // Create or load particle mesh - const particleMesh = this._createOrLoadParticleMesh(name, config, vfxEmitter.materialId); - if (!particleMesh) { - return null; - } - - // Initialize mesh in SPS - sps.initializeMesh(particleMesh, capacity); - - // Apply behaviors - if (config.behaviors && Array.isArray(config.behaviors) && config.behaviors.length > 0) { - sps.behaviorConfigs.length = 0; - sps.behaviorConfigs.push(...config.behaviors); - } - - // Dispose temporary mesh - particleMesh.dispose(); - this._logger.log(`SolidParticleSystem created: ${name}`, options); return sps; } - /** - * Gets blend mode from material blending value - */ - private _getBlendModeFromMaterial(materialId: string): number | undefined { - const { jsonData } = this._context; - const material = jsonData.materials?.find((m: any) => m.uuid === materialId); - - if (material?.blending === undefined) { - return undefined; - } - - const blendModeMap: Record = { - 0: Constants.ALPHA_DISABLE, // NoBlending - 1: Constants.ALPHA_COMBINE, // NormalBlending - 2: Constants.ALPHA_ADD, // AdditiveBlending - }; - - return blendModeMap[material.blending]; - } - - /** - * Creates or loads particle mesh for SPS - */ - private _createOrLoadParticleMesh(name: string, config: VFXParticleEmitterConfig, materialId: string | undefined): Nullable { - const { scene, options } = this._context; - let particleMesh = this._loadParticleGeometry(config, materialId, name); - - if (!particleMesh) { - particleMesh = this._createDefaultPlaneMesh(name, scene); - this._applyMaterialToMesh(particleMesh, materialId, name); - } else { - this._ensureMaterialApplied(particleMesh, materialId, name); - } - - if (!particleMesh) { - this._logger.warn(` Cannot add shape to SPS: particleMesh is null`, options); - } - - return particleMesh; - } - - /** - * Loads particle geometry if specified - */ - private _loadParticleGeometry(config: VFXParticleEmitterConfig, materialId: string | undefined, name: string): Nullable { - const { options } = this._context; - - if (!config.instancingGeometry) { - return null; - } - - this._logger.log(` Loading geometry: ${config.instancingGeometry}`, options); - const mesh = this._geometryFactory.createMesh(config.instancingGeometry, materialId, name + "_shape"); - if (!mesh) { - this._logger.warn(` Failed to load geometry ${config.instancingGeometry}, will create default plane`, options); - } - - return mesh; - } - - /** - * Creates default plane mesh - */ - private _createDefaultPlaneMesh(name: string, scene: any): Mesh { - const { options } = this._context; - this._logger.log(` Creating default plane geometry`, options); - return CreatePlane(name + "_shape", { width: 1, height: 1 }, scene); - } - - /** - * Applies material to mesh - */ - private _applyMaterialToMesh(mesh: Mesh | null, materialId: string | undefined, name: string): void { - if (!mesh || !materialId) { - return; - } - - const material = this._materialFactory.createMaterial(materialId, name); - if (material) { - mesh.material = material; - } - } - - /** - * Ensures material is applied to mesh if missing - */ - private _ensureMaterialApplied(mesh: Mesh, materialId: string | undefined, name: string): void { - if (materialId && !mesh.material) { - this._applyMaterialToMesh(mesh, materialId, name); - } - } - - /** - * Extracts rotation matrix from Three.js matrix array - */ - private _extractRotationMatrix(matrix: number[] | undefined): Matrix | null { - if (!matrix || matrix.length < 16) { - return null; - } - - const mat = Matrix.FromArray(matrix); - mat.transpose(); - return mat.getRotationMatrix(); - } - /** * Calculate cumulative scale from parent groups */ diff --git a/editor/src/editor/windows/fx-editor/VFX/index.ts b/editor/src/editor/windows/fx-editor/VFX/index.ts index 0c405ea7b..3011d238c 100644 --- a/editor/src/editor/windows/fx-editor/VFX/index.ts +++ b/editor/src/editor/windows/fx-editor/VFX/index.ts @@ -4,6 +4,8 @@ export * from "./parsers/VFXDataConverter"; export * from "./factories/VFXMaterialFactory"; export * from "./factories/VFXGeometryFactory"; export * from "./factories/VFXSystemFactory"; +export * from "./utils/capacityCalculator"; +export * from "./utils/matrixUtils"; export * from "./systems/VFXSolidParticleSystem"; export * from "./systems/VFXParticleSystem"; export * from "./factories/VFXParticleSystemBehaviorFactory"; diff --git a/editor/src/editor/windows/fx-editor/VFX/systems/VFXParticleSystem.ts b/editor/src/editor/windows/fx-editor/VFX/systems/VFXParticleSystem.ts index 63f42712b..d3ce8a01f 100644 --- a/editor/src/editor/windows/fx-editor/VFX/systems/VFXParticleSystem.ts +++ b/editor/src/editor/windows/fx-editor/VFX/systems/VFXParticleSystem.ts @@ -16,6 +16,7 @@ import type { VFXParticleEmitterConfig, VFXEmissionBurst } from "../types/emitte import { VFXParticleSystemBehaviorFactory } from "../factories/VFXParticleSystemBehaviorFactory"; import { VFXParticleSystemEmitterFactory } from "../factories/VFXParticleSystemEmitterFactory"; import { VFXValueUtils } from "../utils/valueParser"; +import { VFXCapacityCalculator } from "../utils/capacityCalculator"; import { applyColorOverLifePS, applySizeOverLifePS, @@ -40,14 +41,34 @@ export class VFXParticleSystem extends ParticleSystem { private _emitterFactory: VFXParticleSystemEmitterFactory; public readonly behaviorConfigs: VFXBehavior[]; - constructor(name: string, capacity: number, scene: Scene, _avgStartSpeed: number, _avgStartSize: number, _startColor: Color4) { + constructor( + name: string, + scene: Scene, + config: VFXParticleEmitterConfig, + options?: { + texture?: Texture; + blendMode?: number; + emitterShape?: { + shape: VFXShape | undefined; + cumulativeScale: Vector3; + rotationMatrix: Matrix | null; + }; + } + ) { + // Calculate capacity + const duration = config.duration || 5; + const capacity = VFXCapacityCalculator.calculateForParticleSystem(config.emissionOverTime, duration); + super(name, capacity, scene); this._behaviors = []; this._behaviorFactory = new VFXParticleSystemBehaviorFactory(this); this._emitterFactory = new VFXParticleSystemEmitterFactory(this); // Create proxy array that updates functions when modified - this.behaviorConfigs = this._createBehaviorConfigsProxy([]); + this.behaviorConfigs = this._createBehaviorConfigsProxy(config.behaviors || []); + + // Configure from config + this._configureFromConfig(config, options); } /** @@ -191,10 +212,10 @@ export class VFXParticleSystem extends ParticleSystem { } /** - * Configure particle system from VFX config + * Configure particle system from VFX config (internal use) * This method applies all configuration from VFXParticleEmitterConfig */ - public configureFromConfig( + private _configureFromConfig( config: VFXParticleEmitterConfig, options?: { texture?: Texture; diff --git a/editor/src/editor/windows/fx-editor/VFX/systems/VFXSolidParticleSystem.ts b/editor/src/editor/windows/fx-editor/VFX/systems/VFXSolidParticleSystem.ts index ccd731864..b9a9fb951 100644 --- a/editor/src/editor/windows/fx-editor/VFX/systems/VFXSolidParticleSystem.ts +++ b/editor/src/editor/windows/fx-editor/VFX/systems/VFXSolidParticleSystem.ts @@ -11,6 +11,7 @@ import type { VFXRotation } from "../types/rotations"; import { VFXSolidParticleSystemBehaviorFactory } from "../factories/VFXSolidParticleSystemBehaviorFactory"; import { VFXSolidParticleSystemEmitterFactory } from "../factories/VFXSolidParticleSystemEmitterFactory"; import { VFXValueUtils } from "../utils/valueParser"; +import { VFXCapacityCalculator } from "../utils/capacityCalculator"; /** * Emission state matching three.quarks EmissionState structure @@ -94,10 +95,10 @@ export class VFXSolidParticleSystem extends SolidParticleSystem { } /** - * Initialize mesh for SPS + * Initialize mesh for SPS (internal use) * Adds the mesh as a shape and configures billboard mode */ - public initializeMesh(particleMesh: Mesh, capacity: number): void { + private _initializeMesh(particleMesh: Mesh): void { if (!particleMesh) { if (this._logger) { this._logger.warn(`Cannot add shape to SPS: particleMesh is null`, this._options); @@ -105,8 +106,11 @@ export class VFXSolidParticleSystem extends SolidParticleSystem { return; } + // Calculate capacity from config + const capacity = VFXCapacityCalculator.calculateForSolidParticleSystem(this.emissionOverTime, this.duration, this.isLooping); + if (this._logger) { - this._logger.log(`Adding shape to SPS: mesh name=${particleMesh.name}, hasMaterial=${!!particleMesh.material}`, this._options); + this._logger.log(`Adding shape to SPS: mesh name=${particleMesh.name}, hasMaterial=${!!particleMesh.material}, capacity=${capacity}`, this._options); } // Add shape to SPS @@ -116,6 +120,9 @@ export class VFXSolidParticleSystem extends SolidParticleSystem { if (this.renderMode === 0 || this.renderMode === 1) { this.billboard = true; } + + // Dispose temporary mesh after adding to SPS + particleMesh.dispose(); } /** @@ -159,6 +166,7 @@ export class VFXSolidParticleSystem extends SolidParticleSystem { vfxTransform?: { position: Vector3; rotation: Quaternion; scale: Vector3 } | null; logger?: VFXLogger | null; loaderOptions?: VFXLoaderOptions; + particleMesh?: Mesh | null; // Pre-created mesh for initialization } ) { super(name, scene, options); @@ -225,6 +233,11 @@ export class VFXSolidParticleSystem extends SolidParticleSystem { burstParticleCount: 0, isBursting: false, }; + + // Initialize mesh if provided + if (options?.particleMesh) { + this._initializeMesh(options.particleMesh); + } } private _findDeadParticle(): SolidParticle | null { diff --git a/editor/src/editor/windows/fx-editor/VFX/types/factories.ts b/editor/src/editor/windows/fx-editor/VFX/types/factories.ts index acbeaf6e0..f695d274c 100644 --- a/editor/src/editor/windows/fx-editor/VFX/types/factories.ts +++ b/editor/src/editor/windows/fx-editor/VFX/types/factories.ts @@ -6,8 +6,10 @@ import { Nullable, Mesh, PBRMaterial, Texture } from "babylonjs"; export interface IVFXMaterialFactory { createMaterial(materialId: string, name: string): Nullable; createTexture(materialId: string): Nullable; + getBlendMode(materialId: string): number | undefined; } export interface IVFXGeometryFactory { createMesh(geometryId: string, materialId: string | undefined, name: string): Nullable; + createParticleMesh(config: { instancingGeometry?: string }, materialId: string | undefined, name: string, scene: any): Nullable; } diff --git a/editor/src/editor/windows/fx-editor/VFX/utils/capacityCalculator.ts b/editor/src/editor/windows/fx-editor/VFX/utils/capacityCalculator.ts new file mode 100644 index 000000000..2d8360515 --- /dev/null +++ b/editor/src/editor/windows/fx-editor/VFX/utils/capacityCalculator.ts @@ -0,0 +1,34 @@ +import { VFXValueUtils } from "./valueParser"; +import type { VFXValue } from "../types/values"; + +/** + * Utility for calculating particle system capacity + */ +export class VFXCapacityCalculator { + /** + * Calculate capacity for ParticleSystem + * Formula: emissionRate * duration * 2 (for non-looping systems) + */ + public static calculateForParticleSystem(emissionOverTime: VFXValue | undefined, duration: number): number { + const emissionRate = emissionOverTime !== undefined ? VFXValueUtils.parseConstantValue(emissionOverTime) : 10; + return Math.ceil(emissionRate * duration * 2); + } + + /** + * Calculate capacity for SolidParticleSystem + * Formula depends on looping: + * - Looping: max(emissionRate * particleLifetime, 1) + * - Non-looping: emissionRate * particleLifetime * 2 + */ + public static calculateForSolidParticleSystem(emissionOverTime: VFXValue | undefined, duration: number, isLooping: boolean): number { + const emissionRate = emissionOverTime !== undefined ? VFXValueUtils.parseConstantValue(emissionOverTime) : 10; + const particleLifetime = duration || 5; + + if (isLooping) { + return Math.max(Math.ceil(emissionRate * particleLifetime), 1); + } else { + return Math.ceil(emissionRate * particleLifetime * 2); + } + } +} + diff --git a/editor/src/editor/windows/fx-editor/VFX/utils/matrixUtils.ts b/editor/src/editor/windows/fx-editor/VFX/utils/matrixUtils.ts new file mode 100644 index 000000000..15872016d --- /dev/null +++ b/editor/src/editor/windows/fx-editor/VFX/utils/matrixUtils.ts @@ -0,0 +1,22 @@ +import { Matrix } from "babylonjs"; + +/** + * Utility functions for matrix operations + */ +export class VFXMatrixUtils { + /** + * Extracts rotation matrix from Three.js matrix array + * @param matrix Three.js matrix array (16 elements) + * @returns Rotation matrix or null if invalid + */ + public static extractRotationMatrix(matrix: number[] | undefined): Matrix | null { + if (!matrix || matrix.length < 16) { + return null; + } + + const mat = Matrix.FromArray(matrix); + mat.transpose(); + return mat.getRotationMatrix(); + } +} + From f7f0f56473d6549cddf12f9d78cfe672d9514bd2 Mon Sep 17 00:00:00 2001 From: Mikalai Lazitski Date: Sat, 13 Dec 2025 23:18:31 +0300 Subject: [PATCH 21/62] refactor: update VFX factories to utilize VFXData for improved resource management and streamline geometry and material creation processes --- .../VFX/factories/VFXGeometryFactory.ts | 168 ++++-------- .../VFX/factories/VFXMaterialFactory.ts | 256 ++++++------------ .../VFX/factories/VFXSystemFactory.ts | 142 ++++------ .../fx-editor/VFX/loggers/VFXLogger.ts | 14 +- .../fx-editor/VFX/parsers/VFXDataConverter.ts | 242 ++++++++++++++++- .../fx-editor/VFX/parsers/VFXParser.ts | 60 ++-- .../windows/fx-editor/VFX/types/factories.ts | 6 +- .../windows/fx-editor/VFX/types/hierarchy.ts | 8 +- .../windows/fx-editor/VFX/types/index.ts | 1 + .../windows/fx-editor/VFX/types/resources.ts | 85 ++++++ 10 files changed, 546 insertions(+), 436 deletions(-) create mode 100644 editor/src/editor/windows/fx-editor/VFX/types/resources.ts diff --git a/editor/src/editor/windows/fx-editor/VFX/factories/VFXGeometryFactory.ts b/editor/src/editor/windows/fx-editor/VFX/factories/VFXGeometryFactory.ts index 62886e567..aebfe04f8 100644 --- a/editor/src/editor/windows/fx-editor/VFX/factories/VFXGeometryFactory.ts +++ b/editor/src/editor/windows/fx-editor/VFX/factories/VFXGeometryFactory.ts @@ -1,45 +1,42 @@ -import { Mesh, VertexData, CreatePlane, Nullable } from "babylonjs"; +import { Mesh, VertexData, CreatePlane, Nullable, Scene } from "babylonjs"; import type { IVFXGeometryFactory } from "../types/factories"; -import type { VFXParseContext } from "../types/context"; import { VFXLogger } from "../loggers/VFXLogger"; -import { VFXMaterialFactory } from "./VFXMaterialFactory"; -import type { QuarksGeometry } from "../types/quarksTypes"; +import type { VFXData } from "../types/hierarchy"; +import type { VFXGeometry } from "../types/resources"; +import type { VFXLoaderOptions } from "../types/loader"; /** * Factory for creating meshes from Three.js geometry data */ export class VFXGeometryFactory implements IVFXGeometryFactory { private _logger: VFXLogger; - private _context: VFXParseContext; - private _materialFactory: VFXMaterialFactory; + private _vfxData: VFXData; - constructor(context: VFXParseContext, materialFactory: VFXMaterialFactory) { - this._context = context; - this._logger = new VFXLogger("[VFXGeometryFactory]"); - this._materialFactory = materialFactory; + constructor(vfxData: VFXData, options: VFXLoaderOptions) { + this._vfxData = vfxData; + this._logger = new VFXLogger("[VFXGeometryFactory]", options); } /** - * Create a mesh from geometry ID with material applied + * Create a mesh from geometry ID */ - public createMesh(geometryId: string, materialId: string | undefined, name: string): Nullable { - const { options } = this._context; - this._logger.log(`Creating mesh from geometry ID: ${geometryId}, name: ${name}`, options); + public createMesh(geometryId: string, name: string, scene: Scene): Nullable { + this._logger.log(`Creating mesh from geometry ID: ${geometryId}, name: ${name}`); const geometryData = this._findGeometry(geometryId); if (!geometryData) { return null; } - this._logGeometryInfo(geometryData, geometryId); + const geometryName = geometryData.type || geometryId; + this._logger.log(`Found geometry: ${geometryName} (type: ${geometryData.type})`); - const mesh = this._createMeshFromGeometry(geometryData, name); + const mesh = this._createMeshFromGeometry(geometryData, name, scene); if (!mesh) { - this._logger.warn(`Failed to create mesh from geometry ${geometryId}`, options); + this._logger.warn(`Failed to create mesh from geometry ${geometryId}`); return null; } - this._applyMaterial(mesh, materialId, name); return mesh; } @@ -47,19 +44,15 @@ export class VFXGeometryFactory implements IVFXGeometryFactory { * Create or load particle mesh for SPS * Tries to load geometry if specified, otherwise creates default plane */ - public createParticleMesh(config: { instancingGeometry?: string }, materialId: string | undefined, name: string, scene: any): Nullable { - const { options } = this._context; - let particleMesh = this._loadParticleGeometry(config, materialId, name); + public createParticleMesh(config: { instancingGeometry?: string }, name: string, scene: Scene): Nullable { + let particleMesh = this._loadParticleGeometry(config, name, scene); if (!particleMesh) { particleMesh = this._createDefaultPlaneMesh(name, scene); - this._applyMaterial(particleMesh, materialId, name); - } else { - this._ensureMaterialApplied(particleMesh, materialId, name); } if (!particleMesh) { - this._logger.warn(` Cannot create particle mesh: particleMesh is null`, options); + this._logger.warn(` Cannot create particle mesh: particleMesh is null`); } return particleMesh; @@ -68,16 +61,15 @@ export class VFXGeometryFactory implements IVFXGeometryFactory { /** * Loads particle geometry if specified */ - private _loadParticleGeometry(config: { instancingGeometry?: string }, materialId: string | undefined, name: string): Nullable { + private _loadParticleGeometry(config: { instancingGeometry?: string }, name: string, scene: Scene): Nullable { if (!config.instancingGeometry) { return null; } - const { options } = this._context; - this._logger.log(` Loading geometry: ${config.instancingGeometry}`, options); - const mesh = this.createMesh(config.instancingGeometry, materialId, name + "_shape"); - if (!mesh && this._logger) { - this._logger.warn(` Failed to load geometry ${config.instancingGeometry}, will create default plane`, options); + this._logger.log(` Loading geometry: ${config.instancingGeometry}`); + const mesh = this.createMesh(config.instancingGeometry, name + "_shape", scene); + if (!mesh) { + this._logger.warn(` Failed to load geometry ${config.instancingGeometry}, will create default plane`); } return mesh; @@ -86,115 +78,74 @@ export class VFXGeometryFactory implements IVFXGeometryFactory { /** * Creates default plane mesh */ - private _createDefaultPlaneMesh(name: string, scene: any): Mesh { - const { options } = this._context; - this._logger.log(` Creating default plane geometry`, options); + private _createDefaultPlaneMesh(name: string, scene: Scene): Mesh { + this._logger.log(` Creating default plane geometry`); return CreatePlane(name + "_shape", { width: 1, height: 1 }, scene); } - /** - * Ensures material is applied to mesh if missing - */ - private _ensureMaterialApplied(mesh: Mesh, materialId: string | undefined, name: string): void { - if (materialId && !mesh.material) { - this._applyMaterial(mesh, materialId, name); - } - } - /** * Finds geometry by UUID */ - private _findGeometry(geometryId: string): QuarksGeometry | null { - const { jsonData, options } = this._context; - - if (!jsonData.geometries) { - this._logger.warn("No geometries data available", options); + private _findGeometry(geometryId: string): VFXGeometry | null { + if (!this._vfxData.geometries || this._vfxData.geometries.length === 0) { + this._logger.warn("No geometries data available"); return null; } - const geometry = jsonData.geometries.find((g) => g.uuid === geometryId); + const geometry = this._vfxData.geometries.find((g) => g.uuid === geometryId); if (!geometry) { - this._logger.warn(`Geometry not found: ${geometryId}`, options); + this._logger.warn(`Geometry not found: ${geometryId}`); return null; } return geometry; } - /** - * Logs geometry information - */ - private _logGeometryInfo(geometryData: QuarksGeometry, geometryId: string): void { - const { options } = this._context; - const geometryName = geometryData.type || geometryId; - this._logger.log(`Found geometry: ${geometryName} (type: ${geometryData.type})`, options); - } - - /** - * Applies material to mesh if provided - */ - private _applyMaterial(mesh: Mesh, materialId: string | undefined, name: string): void { - if (!materialId) { - return; - } - - const { options } = this._context; - const material = this._materialFactory.createMaterial(materialId, name); - if (material) { - mesh.material = material; - this._logger.log(`Applied material to mesh: ${name}`, options); - } - } - /** * Creates mesh from geometry data based on type */ - private _createMeshFromGeometry(geometryData: QuarksGeometry, name: string): Nullable { - const { options } = this._context; - this._logger.log(`createMeshFromGeometry: type=${geometryData.type}, name=${name}`, options); + private _createMeshFromGeometry(geometryData: VFXGeometry, name: string, scene: Scene): Nullable { + this._logger.log(`createMeshFromGeometry: type=${geometryData.type}, name=${name}`); - const geometryTypeHandlers: Record Nullable> = { - PlaneGeometry: (data, meshName) => this._createPlaneGeometry(data, meshName), - BufferGeometry: (data, meshName) => this._createBufferGeometry(data, meshName), + const geometryTypeHandlers: Record Nullable> = { + PlaneGeometry: (data, meshName, scene) => this._createPlaneGeometry(data, meshName, scene), + BufferGeometry: (data, meshName, scene) => this._createBufferGeometry(data, meshName, scene), }; const handler = geometryTypeHandlers[geometryData.type]; if (!handler) { - this._logger.warn(`Unsupported geometry type: ${geometryData.type}`, options); + this._logger.warn(`Unsupported geometry type: ${geometryData.type}`); return null; } - return handler(geometryData, name); + return handler(geometryData, name, scene); } /** * Creates plane geometry mesh */ - private _createPlaneGeometry(geometryData: QuarksGeometry, name: string): Nullable { - const { scene, options } = this._context; - const width = this._getNumericProperty(geometryData, "width", 1); - const height = this._getNumericProperty(geometryData, "height", 1); + private _createPlaneGeometry(geometryData: VFXGeometry, name: string, scene: Scene): Nullable { + const width = geometryData.width ?? 1; + const height = geometryData.height ?? 1; - this._logger.log(`Creating PlaneGeometry: width=${width}, height=${height}`, options); + this._logger.log(`Creating PlaneGeometry: width=${width}, height=${height}`); const mesh = CreatePlane(name, { width, height }, scene); if (mesh) { - this._logger.log(`PlaneGeometry created successfully`, options); + this._logger.log(`PlaneGeometry created successfully`); } else { - this._logger.warn(`Failed to create PlaneGeometry`, options); + this._logger.warn(`Failed to create PlaneGeometry`); } return mesh; } /** - * Creates buffer geometry mesh + * Creates buffer geometry mesh (already converted to left-handed) */ - private _createBufferGeometry(geometryData: QuarksGeometry, name: string): Nullable { - const { scene, options } = this._context; - + private _createBufferGeometry(geometryData: VFXGeometry, name: string, scene: Scene): Nullable { if (!geometryData.data?.attributes) { - this._logger.warn("BufferGeometry missing data or attributes", options); + this._logger.warn("BufferGeometry missing data or attributes"); return null; } @@ -205,17 +156,15 @@ export class VFXGeometryFactory implements IVFXGeometryFactory { const mesh = new Mesh(name, scene); vertexData.applyToMesh(mesh); - this._convertToLeftHanded(mesh); + // Geometry is already converted to left-handed in VFXDataConverter return mesh; } /** - * Creates VertexData from BufferGeometry attributes + * Creates VertexData from BufferGeometry attributes (already converted to left-handed) */ - private _createVertexDataFromAttributes(geometryData: QuarksGeometry): Nullable { - const { options } = this._context; - + private _createVertexDataFromAttributes(geometryData: VFXGeometry): Nullable { if (!geometryData.data?.attributes) { return null; } @@ -223,7 +172,7 @@ export class VFXGeometryFactory implements IVFXGeometryFactory { const attrs = geometryData.data.attributes; const positions = attrs.position; if (!positions?.array) { - this._logger.warn("BufferGeometry missing position attribute", options); + this._logger.warn("BufferGeometry missing position attribute"); return null; } @@ -260,21 +209,4 @@ export class VFXGeometryFactory implements IVFXGeometryFactory { const vertexCount = positionsLength / 3; return Array.from({ length: vertexCount }, (_, i) => i); } - - /** - * Converts mesh geometry from right-handed (Three.js) to left-handed (Babylon.js) coordinate system - */ - private _convertToLeftHanded(mesh: Mesh): void { - if (mesh.geometry) { - mesh.geometry.toLeftHanded(); - } - } - - /** - * Gets numeric property from geometry data with fallback - */ - private _getNumericProperty(geometryData: QuarksGeometry, property: string, defaultValue: number): number { - const value = (geometryData as any)[property]; - return typeof value === "number" ? value : defaultValue; - } } diff --git a/editor/src/editor/windows/fx-editor/VFX/factories/VFXMaterialFactory.ts b/editor/src/editor/windows/fx-editor/VFX/factories/VFXMaterialFactory.ts index 3b5a60ef4..797e4df47 100644 --- a/editor/src/editor/windows/fx-editor/VFX/factories/VFXMaterialFactory.ts +++ b/editor/src/editor/windows/fx-editor/VFX/factories/VFXMaterialFactory.ts @@ -1,19 +1,24 @@ -import { Nullable, Color3, Texture, PBRMaterial, Material, Constants, Tools } from "babylonjs"; +import { Nullable, Texture, PBRMaterial, Material, Constants, Tools, Scene, Color3 } from "babylonjs"; import type { IVFXMaterialFactory } from "../types/factories"; -import type { VFXParseContext } from "../types/context"; import { VFXLogger } from "../loggers/VFXLogger"; -import type { QuarksTexture, QuarksMaterial, QuarksImage } from "../types/quarksTypes"; +import type { VFXLoaderOptions } from "../types/loader"; +import type { VFXData } from "../types/hierarchy"; +import type { VFXMaterial, VFXTexture, VFXImage } from "../types/resources"; /** * Factory for creating materials and textures from Three.js JSON data */ export class VFXMaterialFactory implements IVFXMaterialFactory { private _logger: VFXLogger; - private _context: VFXParseContext; - - constructor(context: VFXParseContext) { - this._context = context; - this._logger = new VFXLogger("[VFXMaterialFactory]"); + private _scene: Scene; + private _vfxData: VFXData; + private _rootUrl: string; + + constructor(scene: Scene, vfxData: VFXData, rootUrl: string, options: VFXLoaderOptions) { + this._scene = scene; + this._vfxData = vfxData; + this._rootUrl = rootUrl; + this._logger = new VFXLogger("[VFXMaterialFactory]", options); } /** @@ -34,8 +39,7 @@ export class VFXMaterialFactory implements IVFXMaterialFactory { * Get blend mode from material blending value */ public getBlendMode(materialId: string): number | undefined { - const { jsonData } = this._context; - const material = jsonData.materials?.find((m: any) => m.uuid === materialId); + const material = this._vfxData.materials?.find((m: any) => m.uuid === materialId); if (material?.blending === undefined) { return undefined; @@ -53,11 +57,9 @@ export class VFXMaterialFactory implements IVFXMaterialFactory { /** * Resolves material, texture, and image data from material ID */ - private _resolveTextureData(materialId: string): { material: QuarksMaterial; texture: QuarksTexture; image: QuarksImage } | null { - const { options } = this._context; - + private _resolveTextureData(materialId: string): { material: VFXMaterial; texture: VFXTexture; image: VFXImage } | null { if (!this._hasRequiredData()) { - this._logger.warn(`Missing materials/textures/images data for material ${materialId}`, options); + this._logger.warn(`Missing materials/textures/images data for material ${materialId}`); return null; } @@ -83,18 +85,16 @@ export class VFXMaterialFactory implements IVFXMaterialFactory { * Checks if required JSON data is available */ private _hasRequiredData(): boolean { - const { jsonData } = this._context; - return !!(jsonData.materials && jsonData.textures && jsonData.images); + return !!(this._vfxData.materials && this._vfxData.textures && this._vfxData.images); } /** * Finds material by UUID */ - private _findMaterial(materialId: string): QuarksMaterial | null { - const { jsonData, options } = this._context; - const material = jsonData.materials?.find((m) => m.uuid === materialId); + private _findMaterial(materialId: string): VFXMaterial | null { + const material = this._vfxData.materials?.find((m) => m.uuid === materialId); if (!material) { - this._logger.warn(`Material not found: ${materialId}`, options); + this._logger.warn(`Material not found: ${materialId}`); return null; } return material; @@ -103,11 +103,10 @@ export class VFXMaterialFactory implements IVFXMaterialFactory { /** * Finds texture by UUID */ - private _findTexture(textureId: string): QuarksTexture | null { - const { jsonData, options } = this._context; - const texture = jsonData.textures?.find((t) => t.uuid === textureId); + private _findTexture(textureId: string): VFXTexture | null { + const texture = this._vfxData.textures?.find((t) => t.uuid === textureId); if (!texture) { - this._logger.warn(`Texture not found: ${textureId}`, options); + this._logger.warn(`Texture not found: ${textureId}`); return null; } return texture; @@ -116,11 +115,10 @@ export class VFXMaterialFactory implements IVFXMaterialFactory { /** * Finds image by UUID */ - private _findImage(imageId: string): QuarksImage | null { - const { jsonData, options } = this._context; - const image = jsonData.images?.find((img) => img.uuid === imageId); + private _findImage(imageId: string): VFXImage | null { + const image = this._vfxData.images?.find((img) => img.uuid === imageId); if (!image) { - this._logger.warn(`Image not found: ${imageId}`, options); + this._logger.warn(`Image not found: ${imageId}`); return null; } return image; @@ -129,84 +127,51 @@ export class VFXMaterialFactory implements IVFXMaterialFactory { /** * Builds texture URL from image data */ - private _buildTextureUrl(image: QuarksImage): string { - const { rootUrl } = this._context; + private _buildTextureUrl(image: VFXImage): string { if (!image.url) { return ""; } const isBase64 = image.url.startsWith("data:"); - return isBase64 ? image.url : Tools.GetAssetUrl(rootUrl + image.url); + return isBase64 ? image.url : Tools.GetAssetUrl(this._rootUrl + image.url); } /** - * Parses sampling mode from Three.js texture filters + * Applies texture properties from VFX texture data to Babylon.js texture */ - private _parseSamplingMode(texture: QuarksTexture): number { - // Three.js filter constants: - // 1006 = LinearFilter (BILINEAR) - // 1007 = NearestMipmapLinearFilter - // 1008 = LinearMipmapLinearFilter (TRILINEAR) - // 1009 = LinearMipmapNearestFilter - - if (texture.minFilter !== undefined) { - if (texture.minFilter === 1008 || texture.minFilter === 1009) { - return Texture.TRILINEAR_SAMPLINGMODE; - } - if (texture.minFilter === 1007 || texture.minFilter === 1006) { - return Texture.BILINEAR_SAMPLINGMODE; - } - return Texture.NEAREST_SAMPLINGMODE; + private _applyTextureProperties(babylonTexture: Texture, texture: VFXTexture): void { + if (texture.wrapU !== undefined) { + babylonTexture.wrapU = texture.wrapU; } - - if (texture.magFilter !== undefined) { - return texture.magFilter === 1006 ? Texture.BILINEAR_SAMPLINGMODE : Texture.NEAREST_SAMPLINGMODE; + if (texture.wrapV !== undefined) { + babylonTexture.wrapV = texture.wrapV; } - - return Texture.TRILINEAR_SAMPLINGMODE; - } - - /** - * Applies texture properties from Three.js JSON to Babylon.js texture - */ - private _applyTextureProperties(babylonTexture: Texture, texture: QuarksTexture): void { - // Wrap mode: Three.js 1000=Repeat, 1001=Clamp, 1002=Mirror - if (texture.wrap && Array.isArray(texture.wrap)) { - const wrapModeMap: Record = { - 1000: Texture.WRAP_ADDRESSMODE, - 1001: Texture.CLAMP_ADDRESSMODE, - 1002: Texture.MIRROR_ADDRESSMODE, - }; - babylonTexture.wrapU = wrapModeMap[texture.wrap[0]] ?? Texture.WRAP_ADDRESSMODE; - babylonTexture.wrapV = wrapModeMap[texture.wrap[1]] ?? Texture.WRAP_ADDRESSMODE; + if (texture.uScale !== undefined) { + babylonTexture.uScale = texture.uScale; } - - if (texture.repeat && Array.isArray(texture.repeat)) { - babylonTexture.uScale = texture.repeat[0] || 1; - babylonTexture.vScale = texture.repeat[1] || 1; + if (texture.vScale !== undefined) { + babylonTexture.vScale = texture.vScale; } - - if (texture.offset && Array.isArray(texture.offset)) { - babylonTexture.uOffset = texture.offset[0] || 0; - babylonTexture.vOffset = texture.offset[1] || 0; + if (texture.uOffset !== undefined) { + babylonTexture.uOffset = texture.uOffset; } - - if (typeof texture.channel === "number") { - babylonTexture.coordinatesIndex = texture.channel; + if (texture.vOffset !== undefined) { + babylonTexture.vOffset = texture.vOffset; } - - if (texture.rotation !== undefined) { - babylonTexture.uAng = texture.rotation; + if (texture.coordinatesIndex !== undefined) { + babylonTexture.coordinatesIndex = texture.coordinatesIndex; + } + if (texture.uAng !== undefined) { + babylonTexture.uAng = texture.uAng; } } /** - * Creates Babylon.js texture from texture data + * Creates Babylon.js texture from VFX texture data */ - private _createTextureFromData(textureUrl: string, texture: QuarksTexture): Texture { - const { scene } = this._context; - const samplingMode = this._parseSamplingMode(texture); + private _createTextureFromData(textureUrl: string, texture: VFXTexture): Texture { + const samplingMode = texture.samplingMode ?? Texture.TRILINEAR_SAMPLINGMODE; - const babylonTexture = new Texture(textureUrl, scene, { + const babylonTexture = new Texture(textureUrl, this._scene, { noMipmap: !texture.generateMipmaps, invertY: texture.flipY !== false, samplingMode, @@ -220,8 +185,7 @@ export class VFXMaterialFactory implements IVFXMaterialFactory { * Create a material with texture from material ID */ public createMaterial(materialId: string, name: string): Nullable { - const { options } = this._context; - this._logger.log(`Creating material for ID: ${materialId}, name: ${name}`, options); + this._logger.log(`Creating material for ID: ${materialId}, name: ${name}`); const textureData = this._resolveTextureData(materialId); if (!textureData) { @@ -231,85 +195,27 @@ export class VFXMaterialFactory implements IVFXMaterialFactory { const { material, texture, image } = textureData; const materialType = material.type || "MeshStandardMaterial"; - this._logMaterialInfo(material, texture, image, materialType); + this._logger.log(`Found material: type=${materialType}, uuid=${material.uuid}, transparent=${material.transparent}, blending=${material.blending}`); + this._logger.log(`Found texture: ${JSON.stringify({ uuid: texture.uuid, image: texture.image })}`); + const imageInfo = image.url ? (image.url.split("/").pop() || image.url).substring(0, 50) : "unknown"; + this._logger.log(`Found image: file: ${imageInfo}`); const textureUrl = this._buildTextureUrl(image); const babylonTexture = this._createTextureFromData(textureUrl, texture); - const materialColor = this._parseMaterialColor(material); + const materialColor = material.color || new Color3(1, 1, 1); if (materialType === "MeshBasicMaterial") { return this._createUnlitMaterial(name, material, babylonTexture, materialColor); } - return new PBRMaterial(name + "_material", this._context.scene); - } - - /** - * Logs material, texture, and image information - */ - private _logMaterialInfo(material: QuarksMaterial, texture: QuarksTexture, image: QuarksImage, materialType: string): void { - const { options } = this._context; - this._logger.log(`Found material: type=${materialType}, uuid=${material.uuid}, transparent=${material.transparent}, blending=${material.blending}`, options); - this._logger.log(`Found texture: ${JSON.stringify({ uuid: texture.uuid, image: texture.image })}`, options); - - const imageInfo = this._formatImageInfo(image); - this._logger.log(`Found image: ${imageInfo}`, options); - } - - /** - * Formats image information for logging - */ - private _formatImageInfo(image: QuarksImage): string { - if (!image.url) { - return "unknown"; - } - - const urlParts = image.url.split("/"); - let filename = urlParts[urlParts.length - 1] || image.url; - if (filename.length > 50) { - filename = filename.substring(0, 20) + "..."; - } - return `file: ${filename}`; - } - - /** - * Parses material color from Three.js format - */ - private _parseMaterialColor(material: QuarksMaterial): Color3 { - const { options } = this._context; - - if (material.color === undefined) { - return new Color3(1, 1, 1); - } - - const colorHex = this._parseColorHex(material.color); - const r = ((colorHex >> 16) & 0xff) / 255; - const g = ((colorHex >> 8) & 0xff) / 255; - const b = (colorHex & 0xff) / 255; - - this._logger.log(`Parsed material color: R=${r.toFixed(2)}, G=${g.toFixed(2)}, B=${b.toFixed(2)}`, options); - return new Color3(r, g, b); - } - - /** - * Parses color hex value from various formats - */ - private _parseColorHex(color: number | string): number { - if (typeof color === "number") { - return color; - } - if (typeof color === "string") { - return parseInt(color.replace("#", ""), 16); - } - return 0xffffff; + return new PBRMaterial(name + "_material", this._scene); } /** * Creates unlit material (MeshBasicMaterial equivalent) */ - private _createUnlitMaterial(name: string, material: QuarksMaterial, texture: Texture, color: Color3): PBRMaterial { - const { scene, options } = this._context; - const unlitMaterial = new PBRMaterial(name + "_material", scene); + private _createUnlitMaterial(name: string, material: VFXMaterial, texture: Texture, color: Color3): PBRMaterial { + const unlitMaterial = new PBRMaterial(name + "_material", this._scene); unlitMaterial.unlit = true; unlitMaterial.albedoColor = color; @@ -320,8 +226,8 @@ export class VFXMaterialFactory implements IVFXMaterialFactory { this._applySideSettings(unlitMaterial, material); this._applyBlendMode(unlitMaterial, material); - this._logger.log(`Using MeshBasicMaterial: PBRMaterial with unlit=true, albedoTexture`, options); - this._logger.log(`Material created successfully: ${name}_material`, options); + this._logger.log(`Using MeshBasicMaterial: PBRMaterial with unlit=true, albedoTexture`); + this._logger.log(`Material created successfully: ${name}_material`); return unlitMaterial; } @@ -329,15 +235,13 @@ export class VFXMaterialFactory implements IVFXMaterialFactory { /** * Applies transparency settings to material */ - private _applyTransparency(material: PBRMaterial, quarksMaterial: QuarksMaterial, texture: Texture): void { - const { options } = this._context; - - if (quarksMaterial.transparent) { + private _applyTransparency(material: PBRMaterial, vfxMaterial: VFXMaterial, texture: Texture): void { + if (vfxMaterial.transparent) { material.transparencyMode = Material.MATERIAL_ALPHABLEND; material.needDepthPrePass = false; texture.hasAlpha = true; material.useAlphaFromAlbedoTexture = true; - this._logger.log(`Material is transparent (transparencyMode: ALPHABLEND, alphaMode: COMBINE)`, options); + this._logger.log(`Material is transparent (transparencyMode: ALPHABLEND, alphaMode: COMBINE)`); } else { material.transparencyMode = Material.MATERIAL_OPAQUE; material.alpha = 1.0; @@ -347,12 +251,10 @@ export class VFXMaterialFactory implements IVFXMaterialFactory { /** * Applies depth write settings to material */ - private _applyDepthWrite(material: PBRMaterial, quarksMaterial: QuarksMaterial): void { - const { options } = this._context; - - if (quarksMaterial.depthWrite !== undefined) { - material.disableDepthWrite = !quarksMaterial.depthWrite; - this._logger.log(`Set disableDepthWrite: ${!quarksMaterial.depthWrite}`, options); + private _applyDepthWrite(material: PBRMaterial, vfxMaterial: VFXMaterial): void { + if (vfxMaterial.depthWrite !== undefined) { + material.disableDepthWrite = !vfxMaterial.depthWrite; + this._logger.log(`Set disableDepthWrite: ${!vfxMaterial.depthWrite}`); } else { material.disableDepthWrite = true; } @@ -361,24 +263,20 @@ export class VFXMaterialFactory implements IVFXMaterialFactory { /** * Applies side orientation settings to material */ - private _applySideSettings(material: PBRMaterial, quarksMaterial: QuarksMaterial): void { - const { options } = this._context; - + private _applySideSettings(material: PBRMaterial, vfxMaterial: VFXMaterial): void { material.backFaceCulling = false; - if (quarksMaterial.side !== undefined) { - material.sideOrientation = quarksMaterial.side; - this._logger.log(`Set sideOrientation: ${quarksMaterial.side}`, options); + if (vfxMaterial.side !== undefined) { + material.sideOrientation = vfxMaterial.side; + this._logger.log(`Set sideOrientation: ${vfxMaterial.side}`); } } /** * Applies blend mode to material */ - private _applyBlendMode(material: PBRMaterial, quarksMaterial: QuarksMaterial): void { - const { options } = this._context; - - if (quarksMaterial.blending === undefined) { + private _applyBlendMode(material: PBRMaterial, vfxMaterial: VFXMaterial): void { + if (vfxMaterial.blending === undefined) { return; } @@ -388,7 +286,7 @@ export class VFXMaterialFactory implements IVFXMaterialFactory { 2: Constants.ALPHA_ADD, // AdditiveBlending }; - const alphaMode = blendModeMap[quarksMaterial.blending]; + const alphaMode = blendModeMap[vfxMaterial.blending]; if (alphaMode !== undefined) { material.alphaMode = alphaMode; const modeNames: Record = { @@ -396,7 +294,7 @@ export class VFXMaterialFactory implements IVFXMaterialFactory { 1: "NORMAL", 2: "ADDITIVE", }; - this._logger.log(`Set blend mode: ${modeNames[quarksMaterial.blending]}`, options); + this._logger.log(`Set blend mode: ${modeNames[vfxMaterial.blending]}`); } } } diff --git a/editor/src/editor/windows/fx-editor/VFX/factories/VFXSystemFactory.ts b/editor/src/editor/windows/fx-editor/VFX/factories/VFXSystemFactory.ts index d57d9c77d..5ddfd5b41 100644 --- a/editor/src/editor/windows/fx-editor/VFX/factories/VFXSystemFactory.ts +++ b/editor/src/editor/windows/fx-editor/VFX/factories/VFXSystemFactory.ts @@ -1,11 +1,11 @@ -import { Nullable, Vector3, TransformNode, Texture } from "babylonjs"; +import { Nullable, Vector3, TransformNode, Texture, Scene } from "babylonjs"; import { VFXParticleSystem } from "../systems/VFXParticleSystem"; import { VFXSolidParticleSystem } from "../systems/VFXSolidParticleSystem"; -import type { VFXParseContext } from "../types/context"; import type { VFXData, VFXGroup, VFXEmitter, VFXTransform } from "../types/hierarchy"; import { VFXLogger } from "../loggers/VFXLogger"; import { VFXMatrixUtils } from "../utils/matrixUtils"; import type { IVFXMaterialFactory, IVFXGeometryFactory } from "../types/factories"; +import type { VFXLoaderOptions } from "../types/loader"; /** * Factory for creating particle systems from VFX data @@ -13,13 +13,17 @@ import type { IVFXMaterialFactory, IVFXGeometryFactory } from "../types/factorie */ export class VFXSystemFactory { private _logger: VFXLogger; - private _context: VFXParseContext; + private _scene: Scene; + private _options: VFXLoaderOptions; + private _groupNodesMap: Map; private _materialFactory: IVFXMaterialFactory; private _geometryFactory: IVFXGeometryFactory; - constructor(context: VFXParseContext, materialFactory: IVFXMaterialFactory, geometryFactory: IVFXGeometryFactory) { - this._context = context; - this._logger = new VFXLogger("[VFXSystemFactory]"); + constructor(scene: Scene, options: VFXLoaderOptions, groupNodesMap: Map, materialFactory: IVFXMaterialFactory, geometryFactory: IVFXGeometryFactory) { + this._scene = scene; + this._options = options; + this._groupNodesMap = groupNodesMap; + this._logger = new VFXLogger("[VFXSystemFactory]", options); this._materialFactory = materialFactory; this._geometryFactory = geometryFactory; } @@ -30,11 +34,11 @@ export class VFXSystemFactory { */ public createSystems(vfxData: VFXData): (VFXParticleSystem | VFXSolidParticleSystem)[] { if (!vfxData.root) { - this._logWarning("No root object found in VFX data"); + this._logger.warn("No root object found in VFX data"); return []; } - this._logInfo("Processing hierarchy: creating nodes, setting parents, and applying transformations"); + this._logger.log("Processing hierarchy: creating nodes, setting parents, and applying transformations"); const particleSystems: (VFXParticleSystem | VFXSolidParticleSystem)[] = []; this._processVFXObject(vfxData.root, null, 0, particleSystems, vfxData); return particleSystems; @@ -51,7 +55,7 @@ export class VFXSystemFactory { particleSystems: (VFXParticleSystem | VFXSolidParticleSystem)[], vfxData: VFXData ): void { - this._logObjectProcessing(vfxObj.name, depth); + this._logger.log(`${" ".repeat(depth)}Processing object: ${vfxObj.name}`); if (this._isGroup(vfxObj)) { this._processGroup(vfxObj, parentGroup, depth, particleSystems, vfxData); @@ -98,7 +102,7 @@ export class VFXSystemFactory { return; } - this._logChildrenProcessing(children.length, depth); + this._logger.log(`${" ".repeat(depth)}Processing ${children.length} children`); children.forEach((child) => { this._processVFXObject(child, parentGroup, depth + 1, particleSystems, vfxData); }); @@ -108,17 +112,16 @@ export class VFXSystemFactory { * Create a TransformNode for a VFX Group */ private _createGroupNode(vfxGroup: VFXGroup, parentGroup: Nullable, depth: number): TransformNode { - const { scene } = this._context; - const groupNode = new TransformNode(vfxGroup.name, scene); + const groupNode = new TransformNode(vfxGroup.name, this._scene); groupNode.id = vfxGroup.uuid; this._applyTransform(groupNode, vfxGroup.transform, depth); this._setParent(groupNode, parentGroup, depth); - // Store in context for potential future reference - this._context.groupNodesMap.set(vfxGroup.uuid, groupNode); + // Store in map for potential future reference + this._groupNodesMap.set(vfxGroup.uuid, groupNode); - this._logGroupCreation(vfxGroup.name, depth); + this._logger.log(`${" ".repeat(depth)}Created group node: ${vfxGroup.name}`); return groupNode; } @@ -126,16 +129,19 @@ export class VFXSystemFactory { * Create a particle system from a VFX Emitter */ private _createParticleSystem(vfxEmitter: VFXEmitter, parentGroup: Nullable, depth: number): Nullable { - this._logEmitterProcessing(vfxEmitter, parentGroup, depth); - this._logEmitterConfig(vfxEmitter, depth); + const indent = " ".repeat(depth); + const parentName = parentGroup ? parentGroup.name : "none"; + this._logger.log(`${indent}Processing emitter: ${vfxEmitter.name} (parent: ${parentName})`); + + const config = vfxEmitter.config; + this._logger.log(`${indent} Config: duration=${config.duration}, looping=${config.looping}, systemType=${vfxEmitter.systemType}`); const cumulativeScale = this._calculateCumulativeScale(parentGroup); - this._logCumulativeScale(cumulativeScale, depth); + this._logger.log(`${indent}Cumulative scale: (${cumulativeScale.x.toFixed(2)}, ${cumulativeScale.y.toFixed(2)}, ${cumulativeScale.z.toFixed(2)})`); // Use systemType from emitter (determined during conversion) const systemType = vfxEmitter.systemType || "base"; - const { options } = this._context; - this._logger.log(`Using ${systemType === "solid" ? "SolidParticleSystem" : "ParticleSystem"}`, options); + this._logger.log(`Using ${systemType === "solid" ? "SolidParticleSystem" : "ParticleSystem"}`); let particleSystem: VFXParticleSystem | VFXSolidParticleSystem | null = null; @@ -146,7 +152,7 @@ export class VFXSystemFactory { } if (!particleSystem) { - this._logWarning(`Failed to create particle system for emitter: ${vfxEmitter.name}`); + this._logger.warn(`Failed to create particle system for emitter: ${vfxEmitter.name}`); return null; } @@ -169,7 +175,7 @@ export class VFXSystemFactory { // Handle prewarm this._handlePrewarm(particleSystem, vfxEmitter.config.prewarm); - this._logParticleSystemCreation(vfxEmitter.name, depth); + this._logger.log(`${indent}Created particle system: ${vfxEmitter.name}`); return particleSystem; } @@ -178,9 +184,8 @@ export class VFXSystemFactory { */ private _createParticleSystemInstance(vfxEmitter: VFXEmitter, _parentGroup: Nullable, cumulativeScale: Vector3, _depth: number): Nullable { const { name, config } = vfxEmitter; - const { options, scene } = this._context; - this._logger.log(`Creating ParticleSystem: ${name}`, options); + this._logger.log(`Creating ParticleSystem: ${name}`); // Get texture and blend mode const texture: Texture | undefined = vfxEmitter.materialId ? this._materialFactory.createTexture(vfxEmitter.materialId) || undefined : undefined; @@ -190,7 +195,7 @@ export class VFXSystemFactory { const rotationMatrix = vfxEmitter.matrix ? VFXMatrixUtils.extractRotationMatrix(vfxEmitter.matrix) : null; // Create instance - all configuration happens in constructor - const particleSystem = new VFXParticleSystem(name, scene, config, { + const particleSystem = new VFXParticleSystem(name, this._scene, config, { texture, blendMode, emitterShape: { @@ -200,7 +205,7 @@ export class VFXSystemFactory { }, }); - this._logger.log(`ParticleSystem created: ${name}`, options); + this._logger.log(`ParticleSystem created: ${name}`); return particleSystem; } @@ -209,21 +214,28 @@ export class VFXSystemFactory { */ private _createSolidParticleSystem(vfxEmitter: VFXEmitter, parentGroup: Nullable): Nullable { const { name, config } = vfxEmitter; - const { options, scene } = this._context; - this._logger.log(`Creating SolidParticleSystem: ${name}`, options); + this._logger.log(`Creating SolidParticleSystem: ${name}`); // Get VFX transform const vfxTransform = vfxEmitter.transform || null; // Create or load particle mesh - const particleMesh = this._geometryFactory.createParticleMesh(config, vfxEmitter.materialId, name, scene); + const particleMesh = this._geometryFactory.createParticleMesh(config, name, this._scene); if (!particleMesh) { return null; } + // Apply material if provided + if (vfxEmitter.materialId) { + const material = this._materialFactory.createMaterial(vfxEmitter.materialId, name); + if (material) { + particleMesh.material = material; + } + } + // Create SPS instance - mesh initialization and capacity calculation happen in constructor - const sps = new VFXSolidParticleSystem(name, scene, config, { + const sps = new VFXSolidParticleSystem(name, this._scene, config, { updatable: true, isPickable: false, enableDepthSort: false, @@ -232,11 +244,11 @@ export class VFXSystemFactory { parentGroup, vfxTransform, logger: this._logger, - loaderOptions: options, + loaderOptions: this._options, particleMesh, }); - this._logger.log(`SolidParticleSystem created: ${name}`, options); + this._logger.log(`SolidParticleSystem created: ${name}`); return sps; } @@ -271,67 +283,15 @@ export class VFXSystemFactory { return "children" in vfxObj; } - // Logging helpers - private _getIndent(depth: number): string { - return " ".repeat(depth); - } - - private _logInfo(message: string): void { - this._logger.log(message, this._context.options); - } - - private _logWarning(message: string): void { - this._logger.warn(message, this._context.options); - } - - private _logObjectProcessing(name: string, depth: number): void { - const indent = this._getIndent(depth); - this._logger.log(`${indent}Processing object: ${name}`, this._context.options); - } - - private _logGroupCreation(name: string, depth: number): void { - const indent = this._getIndent(depth); - this._logger.log(`${indent}Created group node: ${name}`, this._context.options); - } - - private _logChildrenProcessing(count: number, depth: number): void { - const indent = this._getIndent(depth); - this._logger.log(`${indent}Processing ${count} children`, this._context.options); - } - - private _logEmitterProcessing(vfxEmitter: VFXEmitter, parentGroup: Nullable, depth: number): void { - const indent = this._getIndent(depth); - const parentName = parentGroup ? parentGroup.name : "none"; - this._logger.log(`${indent}Processing emitter: ${vfxEmitter.name} (parent: ${parentName})`, this._context.options); - } - - private _logEmitterConfig(vfxEmitter: VFXEmitter, depth: number): void { - const indent = this._getIndent(depth); - const config = vfxEmitter.config; - this._logger.log(`${indent} Config: duration=${config.duration}, looping=${config.looping}, systemType=${vfxEmitter.systemType}`, this._context.options); - } - - private _logParticleSystemCreation(name: string, depth: number): void { - const indent = this._getIndent(depth); - this._logger.log(`${indent}Created particle system: ${name}`, this._context.options); - } - - private _logCumulativeScale(scale: Vector3, depth: number): void { - const indent = this._getIndent(depth); - this._logger.log(`${indent}Cumulative scale: (${scale.x.toFixed(2)}, ${scale.y.toFixed(2)}, ${scale.z.toFixed(2)})`, this._context.options); - } - /** * Apply transform to a node */ private _applyTransform(node: TransformNode, transform: VFXTransform, depth: number): void { if (!transform) { - this._logWarning(`Transform is undefined for node: ${node.name}`); + this._logger.warn(`Transform is undefined for node: ${node.name}`); return; } - const indent = this._getIndent(depth); - if (transform.position && node.position) { node.position.copyFrom(transform.position); } @@ -345,9 +305,9 @@ export class VFXSystemFactory { } if (transform.position && transform.scale) { + const indent = " ".repeat(depth); this._logger.log( - `${indent}Applied transform: pos=(${transform.position.x.toFixed(2)}, ${transform.position.y.toFixed(2)}, ${transform.position.z.toFixed(2)}), scale=(${transform.scale.x.toFixed(2)}, ${transform.scale.y.toFixed(2)}, ${transform.scale.z.toFixed(2)})`, - this._context.options + `${indent}Applied transform: pos=(${transform.position.x.toFixed(2)}, ${transform.position.y.toFixed(2)}, ${transform.position.z.toFixed(2)}), scale=(${transform.scale.x.toFixed(2)}, ${transform.scale.y.toFixed(2)}, ${transform.scale.z.toFixed(2)})` ); } } @@ -363,11 +323,11 @@ export class VFXSystemFactory { // Check if node has setParent method (TransformNode, AbstractMesh, etc.) if (typeof node.setParent === "function") { node.setParent(parent, false, true); - const indent = this._getIndent(depth); - this._logger.log(`${indent}Set parent: ${node.name || "unknown"} -> ${parent.name}`, this._context.options); + const indent = " ".repeat(depth); + this._logger.log(`${indent}Set parent: ${node.name || "unknown"} -> ${parent.name}`); } else { - const indent = this._getIndent(depth); - this._logger.warn(`${indent}Node does not support setParent: ${node.constructor?.name || "unknown"}`, this._context.options); + const indent = " ".repeat(depth); + this._logger.warn(`${indent}Node does not support setParent: ${node.constructor?.name || "unknown"}`); } } } diff --git a/editor/src/editor/windows/fx-editor/VFX/loggers/VFXLogger.ts b/editor/src/editor/windows/fx-editor/VFX/loggers/VFXLogger.ts index 6e1c48405..8e78d699e 100644 --- a/editor/src/editor/windows/fx-editor/VFX/loggers/VFXLogger.ts +++ b/editor/src/editor/windows/fx-editor/VFX/loggers/VFXLogger.ts @@ -6,16 +6,18 @@ import type { VFXLoaderOptions } from "../types"; */ export class VFXLogger { private _prefix: string; + private _options?: VFXLoaderOptions; - constructor(prefix: string = "[VFX]") { + constructor(prefix: string = "[VFX]", options?: VFXLoaderOptions) { this._prefix = prefix; + this._options = options; } /** * Log a message if verbose mode is enabled */ - public log(message: string, options?: VFXLoaderOptions): void { - if (options?.verbose) { + public log(message: string): void { + if (this._options?.verbose) { Logger.Log(`${this._prefix} ${message}`); } } @@ -23,8 +25,8 @@ export class VFXLogger { /** * Log a warning if verbose or validate mode is enabled */ - public warn(message: string, options?: VFXLoaderOptions): void { - if (options?.verbose || options?.validate) { + public warn(message: string): void { + if (this._options?.verbose || this._options?.validate) { Logger.Warn(`${this._prefix} ${message}`); } } @@ -32,7 +34,7 @@ export class VFXLogger { /** * Log an error */ - public error(message: string, _options?: VFXLoaderOptions): void { + public error(message: string): void { Logger.Error(`${this._prefix} ${message}`); } } diff --git a/editor/src/editor/windows/fx-editor/VFX/parsers/VFXDataConverter.ts b/editor/src/editor/windows/fx-editor/VFX/parsers/VFXDataConverter.ts index fb8934a76..47285a0f8 100644 --- a/editor/src/editor/windows/fx-editor/VFX/parsers/VFXDataConverter.ts +++ b/editor/src/editor/windows/fx-editor/VFX/parsers/VFXDataConverter.ts @@ -1,6 +1,6 @@ -import { Vector3, Matrix, Quaternion } from "babylonjs"; +import { Vector3, Matrix, Quaternion, Color3, Texture } from "babylonjs"; import type { VFXLoaderOptions } from "../types/loader"; -import type { QuarksVFXJSON } from "../types/quarksTypes"; +import type { QuarksVFXJSON, QuarksMaterial, QuarksTexture, QuarksImage, QuarksGeometry } from "../types/quarksTypes"; import type { QuarksObject, QuarksParticleEmitterConfig, @@ -24,6 +24,7 @@ import type { QuarksOrbitOverLifeBehavior, } from "../types/quarksTypes"; import type { VFXTransform, VFXGroup, VFXEmitter, VFXData } from "../types/hierarchy"; +import type { VFXMaterial, VFXTexture, VFXImage, VFXGeometry, VFXGeometryData } from "../types/resources"; import type { VFXParticleEmitterConfig } from "../types/emitterConfig"; import type { VFXBehavior, @@ -48,18 +49,16 @@ import { VFXLogger } from "../loggers/VFXLogger"; */ export class VFXDataConverter { private _logger: VFXLogger; - private _options?: VFXLoaderOptions; constructor(options?: VFXLoaderOptions) { - this._logger = new VFXLogger("[VFXDataConverter]"); - this._options = options; + this._logger = new VFXLogger("[VFXDataConverter]", options); } /** * Convert Quarks/Three.js VFX JSON to Babylon.js VFX format */ public convert(quarksVFXData: QuarksVFXJSON): VFXData { - this._logger.log("=== Converting Quarks VFX to Babylon.js VFX format ===", this._options); + this._logger.log("=== Converting Quarks VFX to Babylon.js VFX format ==="); const groups = new Map(); const emitters = new Map(); @@ -70,12 +69,24 @@ export class VFXDataConverter { root = this._convertObject(quarksVFXData.object, null, groups, emitters, 0); } - this._logger.log(`=== Conversion complete. Groups: ${groups.size}, Emitters: ${emitters.size} ===`, this._options); + // Convert all resources + const materials = this._convertMaterials(quarksVFXData.materials || []); + const textures = this._convertTextures(quarksVFXData.textures || []); + const images = this._convertImages(quarksVFXData.images || []); + const geometries = this._convertGeometries(quarksVFXData.geometries || []); + + this._logger.log( + `=== Conversion complete. Groups: ${groups.size}, Emitters: ${emitters.size}, Materials: ${materials.length}, Textures: ${textures.length}, Images: ${images.length}, Geometries: ${geometries.length} ===` + ); return { root, groups, emitters, + materials, + textures, + images, + geometries, }; } @@ -90,16 +101,15 @@ export class VFXDataConverter { depth: number ): VFXGroup | VFXEmitter | null { const indent = " ".repeat(depth); - const options = this._options; if (!obj || typeof obj !== "object") { return null; } - this._logger.log(`${indent}Converting object: ${obj.type || "unknown"} (name: ${obj.name || "unnamed"})`, options); + this._logger.log(`${indent}Converting object: ${obj.type || "unknown"} (name: ${obj.name || "unnamed"})`); // Convert transform from right-handed to left-handed - const transform = this._convertTransform(obj.matrix, obj.position, obj.rotation, obj.scale, options); + const transform = this._convertTransform(obj.matrix, obj.position, obj.rotation, obj.scale); if (obj.type === "Group") { const group: VFXGroup = { @@ -126,7 +136,7 @@ export class VFXDataConverter { } groups.set(group.uuid, group); - this._logger.log(`${indent}Converted Group: ${group.name} (uuid: ${group.uuid})`, options); + this._logger.log(`${indent}Converted Group: ${group.name} (uuid: ${group.uuid})`); return group; } else if (obj.type === "ParticleEmitter" && obj.ps) { // Convert emitter config from Quarks to VFX format @@ -147,7 +157,7 @@ export class VFXDataConverter { }; emitters.set(emitter.uuid, emitter); - this._logger.log(`${indent}Converted Emitter: ${emitter.name} (uuid: ${emitter.uuid}, systemType: ${systemType})`, options); + this._logger.log(`${indent}Converted Emitter: ${emitter.name} (uuid: ${emitter.uuid}, systemType: ${systemType})`); return emitter; } @@ -158,7 +168,7 @@ export class VFXDataConverter { * Convert transform from Quarks/Three.js (right-handed) to Babylon.js VFX (left-handed) * This is the ONLY place where handedness conversion happens */ - private _convertTransform(matrixArray?: number[], positionArray?: number[], rotationArray?: number[], scaleArray?: number[], _options?: VFXLoaderOptions): VFXTransform { + private _convertTransform(matrixArray?: number[], positionArray?: number[], rotationArray?: number[], scaleArray?: number[]): VFXTransform { const position = Vector3.Zero(); const rotation = Quaternion.Identity(); const scale = Vector3.One(); @@ -572,4 +582,210 @@ export class VFXDataConverter { return quarksBehavior as VFXBehavior; } } + + /** + * Convert Quarks materials to VFX materials + */ + private _convertMaterials(quarksMaterials: QuarksMaterial[]): VFXMaterial[] { + return quarksMaterials.map((quarks) => { + const vfx: VFXMaterial = { + uuid: quarks.uuid, + type: quarks.type, + transparent: quarks.transparent, + depthWrite: quarks.depthWrite, + side: quarks.side, + map: quarks.map, + }; + + // Convert color from hex to Color3 + if (quarks.color !== undefined) { + const colorHex = typeof quarks.color === "number" ? quarks.color : parseInt(String(quarks.color).replace("#", ""), 16) || 0xffffff; + const r = ((colorHex >> 16) & 0xff) / 255; + const g = ((colorHex >> 8) & 0xff) / 255; + const b = (colorHex & 0xff) / 255; + vfx.color = new Color3(r, g, b); + } + + // Convert blending mode (Three.js → Babylon.js) + if (quarks.blending !== undefined) { + const blendModeMap: Record = { + 0: 0, // NoBlending → ALPHA_DISABLE + 1: 1, // NormalBlending → ALPHA_COMBINE + 2: 2, // AdditiveBlending → ALPHA_ADD + }; + vfx.blending = blendModeMap[quarks.blending] ?? quarks.blending; + } + + return vfx; + }); + } + + /** + * Convert Quarks textures to VFX textures + */ + private _convertTextures(quarksTextures: QuarksTexture[]): VFXTexture[] { + return quarksTextures.map((quarks) => { + const vfx: VFXTexture = { + uuid: quarks.uuid, + image: quarks.image, + generateMipmaps: quarks.generateMipmaps, + flipY: quarks.flipY, + }; + + // Convert wrap mode (Three.js → Babylon.js) + if (quarks.wrap && Array.isArray(quarks.wrap)) { + const wrapModeMap: Record = { + 1000: Texture.WRAP_ADDRESSMODE, // RepeatWrapping + 1001: Texture.CLAMP_ADDRESSMODE, // ClampToEdgeWrapping + 1002: Texture.MIRROR_ADDRESSMODE, // MirroredRepeatWrapping + }; + vfx.wrapU = wrapModeMap[quarks.wrap[0]] ?? Texture.WRAP_ADDRESSMODE; + vfx.wrapV = wrapModeMap[quarks.wrap[1]] ?? Texture.WRAP_ADDRESSMODE; + } + + // Convert repeat to scale + if (quarks.repeat && Array.isArray(quarks.repeat)) { + vfx.uScale = quarks.repeat[0] || 1; + vfx.vScale = quarks.repeat[1] || 1; + } + + // Convert offset + if (quarks.offset && Array.isArray(quarks.offset)) { + vfx.uOffset = quarks.offset[0] || 0; + vfx.vOffset = quarks.offset[1] || 0; + } + + // Convert rotation + if (quarks.rotation !== undefined) { + vfx.uAng = quarks.rotation; + } + + // Convert channel + if (typeof quarks.channel === "number") { + vfx.coordinatesIndex = quarks.channel; + } + + // Convert sampling mode (Three.js filters → Babylon.js sampling mode) + if (quarks.minFilter !== undefined) { + if (quarks.minFilter === 1008 || quarks.minFilter === 1009) { + vfx.samplingMode = Texture.TRILINEAR_SAMPLINGMODE; + } else if (quarks.minFilter === 1007 || quarks.minFilter === 1006) { + vfx.samplingMode = Texture.BILINEAR_SAMPLINGMODE; + } else { + vfx.samplingMode = Texture.NEAREST_SAMPLINGMODE; + } + } else if (quarks.magFilter !== undefined) { + vfx.samplingMode = quarks.magFilter === 1006 ? Texture.BILINEAR_SAMPLINGMODE : Texture.NEAREST_SAMPLINGMODE; + } else { + vfx.samplingMode = Texture.TRILINEAR_SAMPLINGMODE; + } + + return vfx; + }); + } + + /** + * Convert Quarks images to VFX images (normalize URLs) + */ + private _convertImages(quarksImages: QuarksImage[]): VFXImage[] { + return quarksImages.map((quarks) => ({ + uuid: quarks.uuid, + url: quarks.url || "", + })); + } + + /** + * Convert Quarks geometries to VFX geometries (convert to left-handed) + */ + private _convertGeometries(quarksGeometries: QuarksGeometry[]): VFXGeometry[] { + return quarksGeometries.map((quarks) => { + if (quarks.type === "PlaneGeometry") { + // PlaneGeometry - simple properties + const vfx: VFXGeometry = { + uuid: quarks.uuid, + type: "PlaneGeometry", + width: (quarks as any).width ?? 1, + height: (quarks as any).height ?? 1, + }; + return vfx; + } else if (quarks.type === "BufferGeometry") { + // BufferGeometry - convert attributes to left-handed + const vfx: VFXGeometry = { + uuid: quarks.uuid, + type: "BufferGeometry", + }; + + if (quarks.data?.attributes) { + const attributes: VFXGeometryData["attributes"] = {}; + const quarksAttrs = quarks.data.attributes; + + // Convert position (right-hand → left-hand: flip Z) + if (quarksAttrs.position) { + const positions = Array.from(quarksAttrs.position.array); + // Flip Z coordinate for left-handed system + for (let i = 2; i < positions.length; i += 3) { + positions[i] = -positions[i]; + } + attributes.position = { + array: positions, + itemSize: quarksAttrs.position.itemSize, + }; + } + + // Convert normal (right-hand → left-hand: flip Z) + if (quarksAttrs.normal) { + const normals = Array.from(quarksAttrs.normal.array); + for (let i = 2; i < normals.length; i += 3) { + normals[i] = -normals[i]; + } + attributes.normal = { + array: normals, + itemSize: quarksAttrs.normal.itemSize, + }; + } + + // UV and color - no conversion needed + if (quarksAttrs.uv) { + attributes.uv = { + array: Array.from(quarksAttrs.uv.array), + itemSize: quarksAttrs.uv.itemSize, + }; + } + + if (quarksAttrs.color) { + attributes.color = { + array: Array.from(quarksAttrs.color.array), + itemSize: quarksAttrs.color.itemSize, + }; + } + + vfx.data = { + attributes, + }; + + // Convert indices (reverse winding order for left-handed) + if (quarks.data.index) { + const indices = Array.from(quarks.data.index.array); + // Reverse winding: swap every 2nd and 3rd index in each triangle + for (let i = 0; i < indices.length; i += 3) { + const temp = indices[i + 1]; + indices[i + 1] = indices[i + 2]; + indices[i + 2] = temp; + } + vfx.data.index = { + array: indices, + }; + } + } + + return vfx; + } + + // Unknown geometry type - return as-is + return { + uuid: quarks.uuid, + type: quarks.type as "PlaneGeometry" | "BufferGeometry", + }; + }); + } } diff --git a/editor/src/editor/windows/fx-editor/VFX/parsers/VFXParser.ts b/editor/src/editor/windows/fx-editor/VFX/parsers/VFXParser.ts index 613ec4309..21bf73dc3 100644 --- a/editor/src/editor/windows/fx-editor/VFX/parsers/VFXParser.ts +++ b/editor/src/editor/windows/fx-editor/VFX/parsers/VFXParser.ts @@ -2,6 +2,7 @@ import { Scene, TransformNode } from "babylonjs"; import type { QuarksVFXJSON } from "../types/quarksTypes"; import type { VFXLoaderOptions } from "../types/loader"; import type { VFXParseContext } from "../types/context"; +import type { VFXData } from "../types/hierarchy"; import { VFXLogger } from "../loggers/VFXLogger"; import { VFXMaterialFactory } from "../factories/VFXMaterialFactory"; import { VFXGeometryFactory } from "../factories/VFXGeometryFactory"; @@ -31,59 +32,68 @@ export class VFXParser { groupNodesMap: new Map(), }; - this._logger = new VFXLogger("[VFXParser]"); - this._materialFactory = new VFXMaterialFactory(this._context); - this._geometryFactory = new VFXGeometryFactory(this._context, this._materialFactory); - this._systemFactory = new VFXSystemFactory(this._context, this._materialFactory, this._geometryFactory); + this._logger = new VFXLogger("[VFXParser]", opts); + + // Convert Quarks JSON to VFXData first + const dataConverter = new VFXDataConverter(opts); + const vfxData = dataConverter.convert(jsonData); + this._context.vfxData = vfxData; + + // Create factories with VFXData instead of QuarksVFXJSON + this._materialFactory = new VFXMaterialFactory(scene, vfxData, rootUrl, opts); + this._geometryFactory = new VFXGeometryFactory(vfxData, opts); + this._systemFactory = new VFXSystemFactory(scene, opts, this._context.groupNodesMap, this._materialFactory, this._geometryFactory); } /** * Parse the JSON data and create particle systems */ public parse(): (VFXParticleSystem | VFXSolidParticleSystem)[] { - const { jsonData, options } = this._context; - this._logger.log("=== Starting Particle System Parsing ===", options); + const { options, vfxData } = this._context; + this._logger.log("=== Starting Particle System Parsing ==="); + + if (!vfxData) { + this._logger.warn("VFXData is missing"); + return []; + } if (options.validate) { - this._validateJSONStructure(jsonData, options); + this._validateJSONStructure(vfxData); } - const dataConverter = new VFXDataConverter(options); - const vfxData = dataConverter.convert(jsonData); - this._context.vfxData = vfxData; const particleSystems = this._systemFactory.createSystems(vfxData); - this._logger.log(`=== Parsing complete. Created ${particleSystems.length} particle system(s) ===`, options); + this._logger.log(`=== Parsing complete. Created ${particleSystems.length} particle system(s) ===`); return particleSystems; } /** - * Validate JSON structure + * Validate VFX data structure */ - private _validateJSONStructure(jsonData: QuarksVFXJSON, options: VFXLoaderOptions): void { - this._logger.log("Validating JSON structure...", options); + private _validateJSONStructure(vfxData: VFXData): void { + this._logger.log("Validating VFX data structure..."); - if (!jsonData.object) { - this._logger.warn("JSON missing 'object' property", options); + if (!vfxData.root) { + this._logger.warn("VFX data missing 'root' property"); } - if (!jsonData.materials || jsonData.materials.length === 0) { - this._logger.warn("JSON has no materials", options); + if (!vfxData.materials || vfxData.materials.length === 0) { + this._logger.warn("VFX data has no materials"); } - if (!jsonData.textures || jsonData.textures.length === 0) { - this._logger.warn("JSON has no textures", options); + if (!vfxData.textures || vfxData.textures.length === 0) { + this._logger.warn("VFX data has no textures"); } - if (!jsonData.images || jsonData.images.length === 0) { - this._logger.warn("JSON has no images", options); + if (!vfxData.images || vfxData.images.length === 0) { + this._logger.warn("VFX data has no images"); } - if (!jsonData.geometries || jsonData.geometries.length === 0) { - this._logger.warn("JSON has no geometries", options); + if (!vfxData.geometries || vfxData.geometries.length === 0) { + this._logger.warn("VFX data has no geometries"); } - this._logger.log("Validation complete", options); + this._logger.log("Validation complete"); } /** diff --git a/editor/src/editor/windows/fx-editor/VFX/types/factories.ts b/editor/src/editor/windows/fx-editor/VFX/types/factories.ts index f695d274c..43f886e95 100644 --- a/editor/src/editor/windows/fx-editor/VFX/types/factories.ts +++ b/editor/src/editor/windows/fx-editor/VFX/types/factories.ts @@ -1,4 +1,4 @@ -import { Nullable, Mesh, PBRMaterial, Texture } from "babylonjs"; +import { Nullable, Mesh, PBRMaterial, Texture, Scene } from "babylonjs"; /** * Factory interfaces for dependency injection @@ -10,6 +10,6 @@ export interface IVFXMaterialFactory { } export interface IVFXGeometryFactory { - createMesh(geometryId: string, materialId: string | undefined, name: string): Nullable; - createParticleMesh(config: { instancingGeometry?: string }, materialId: string | undefined, name: string, scene: any): Nullable; + createMesh(geometryId: string, name: string, scene: Scene): Nullable; + createParticleMesh(config: { instancingGeometry?: string }, name: string, scene: Scene): Nullable; } diff --git a/editor/src/editor/windows/fx-editor/VFX/types/hierarchy.ts b/editor/src/editor/windows/fx-editor/VFX/types/hierarchy.ts index bc59c4337..6627f4377 100644 --- a/editor/src/editor/windows/fx-editor/VFX/types/hierarchy.ts +++ b/editor/src/editor/windows/fx-editor/VFX/types/hierarchy.ts @@ -1,5 +1,6 @@ import { Vector3, Quaternion } from "babylonjs"; import type { VFXParticleEmitterConfig } from "./emitterConfig"; +import type { VFXMaterial, VFXTexture, VFXImage, VFXGeometry } from "./resources"; /** * VFX transform (converted from Quarks, left-handed coordinate system) @@ -36,10 +37,15 @@ export interface VFXEmitter { /** * VFX data (converted from Quarks) - * Contains the converted VFX structure with groups and emitters + * Contains the converted VFX structure with groups, emitters, and resources */ export interface VFXData { root: VFXGroup | VFXEmitter | null; groups: Map; emitters: Map; + // Resources (converted from Quarks, ready for Babylon.js) + materials: VFXMaterial[]; + textures: VFXTexture[]; + images: VFXImage[]; + geometries: VFXGeometry[]; } diff --git a/editor/src/editor/windows/fx-editor/VFX/types/index.ts b/editor/src/editor/windows/fx-editor/VFX/types/index.ts index fc0b3426b..11cd8ec53 100644 --- a/editor/src/editor/windows/fx-editor/VFX/types/index.ts +++ b/editor/src/editor/windows/fx-editor/VFX/types/index.ts @@ -38,5 +38,6 @@ export type { } from "./behaviors"; export type { VFXEmissionBurst, VFXParticleEmitterConfig } from "./emitterConfig"; export type { VFXTransform, VFXGroup, VFXEmitter, VFXData } from "./hierarchy"; +export type { VFXMaterial, VFXTexture, VFXImage, VFXGeometry } from "./resources"; export type { QuarksVFXJSON } from "./quarksTypes"; export type { VFXPerParticleContext, VFXPerParticleBehaviorFunction, VFXPerSolidParticleBehaviorFunction, VFXSystemBehaviorFunction } from "./VFXBehaviorFunction"; diff --git a/editor/src/editor/windows/fx-editor/VFX/types/resources.ts b/editor/src/editor/windows/fx-editor/VFX/types/resources.ts new file mode 100644 index 000000000..3408a2092 --- /dev/null +++ b/editor/src/editor/windows/fx-editor/VFX/types/resources.ts @@ -0,0 +1,85 @@ +import { Color3, Texture } from "babylonjs"; + +/** + * VFX Material (converted from Quarks, ready for Babylon.js) + */ +export interface VFXMaterial { + uuid: string; + type?: string; + color?: Color3; // Converted from hex/array to Color3 + opacity?: number; + transparent?: boolean; + depthWrite?: boolean; + side?: number; + blending?: number; // Converted to Babylon.js constants + map?: string; // Texture UUID reference +} + +/** + * VFX Texture (converted from Quarks, ready for Babylon.js) + */ +export interface VFXTexture { + uuid: string; + image?: string; // Image UUID reference + wrapU?: number; // Converted to Babylon.js wrap mode + wrapV?: number; // Converted to Babylon.js wrap mode + uScale?: number; // From repeat[0] + vScale?: number; // From repeat[1] + uOffset?: number; // From offset[0] + vOffset?: number; // From offset[1] + uAng?: number; // From rotation + coordinatesIndex?: number; // From channel + samplingMode?: number; // Converted from Three.js filters to Babylon.js sampling mode + generateMipmaps?: boolean; + flipY?: boolean; +} + +/** + * VFX Image (converted from Quarks, normalized URL) + */ +export interface VFXImage { + uuid: string; + url: string; // Normalized URL (ready for use) +} + +/** + * VFX Geometry Attribute Data + */ +export interface VFXGeometryAttribute { + array: number[]; + itemSize?: number; +} + +/** + * VFX Geometry Index Data + */ +export interface VFXGeometryIndex { + array: number[]; +} + +/** + * VFX Geometry Data (converted from Quarks, left-handed coordinate system) + */ +export interface VFXGeometryData { + attributes: { + position?: VFXGeometryAttribute; + normal?: VFXGeometryAttribute; + uv?: VFXGeometryAttribute; + color?: VFXGeometryAttribute; + }; + index?: VFXGeometryIndex; +} + +/** + * VFX Geometry (converted from Quarks, ready for Babylon.js) + */ +export interface VFXGeometry { + uuid: string; + type: "PlaneGeometry" | "BufferGeometry"; + // For PlaneGeometry + width?: number; + height?: number; + // For BufferGeometry (already converted to left-handed) + data?: VFXGeometryData; +} + From 5aaa41abbdb842f404cde0bbaec28e394962b8a1 Mon Sep 17 00:00:00 2001 From: Mikalai Lazitski Date: Sun, 14 Dec 2025 08:04:53 +0300 Subject: [PATCH 22/62] refactor: streamline VFX behavior implementations by removing unused factories and enhancing gradient systems for improved performance and clarity --- .../fx-editor/VFX/behaviors/colorOverLife.ts | 73 +++-- .../VFX/behaviors/limitSpeedOverLife.ts | 32 ++ .../VFX/behaviors/rotationOverLife.ts | 111 +++++-- .../fx-editor/VFX/behaviors/sizeOverLife.ts | 50 ++- .../fx-editor/VFX/behaviors/speedOverLife.ts | 60 ++-- .../VFXParticleSystemBehaviorFactory.ts | 65 ---- .../VFXSolidParticleSystemBehaviorFactory.ts | 132 -------- .../src/editor/windows/fx-editor/VFX/index.ts | 2 - .../VFX/systems/VFXParticleSystem.ts | 64 +++- .../VFX/systems/VFXSolidParticleSystem.ts | 296 ++++++++++++++++-- .../VFX/types/VFXBehaviorFunction.ts | 8 +- .../fx-editor/VFX/utils/gradientSystem.ts | 100 ++++++ 12 files changed, 646 insertions(+), 347 deletions(-) delete mode 100644 editor/src/editor/windows/fx-editor/VFX/factories/VFXParticleSystemBehaviorFactory.ts delete mode 100644 editor/src/editor/windows/fx-editor/VFX/factories/VFXSolidParticleSystemBehaviorFactory.ts create mode 100644 editor/src/editor/windows/fx-editor/VFX/utils/gradientSystem.ts diff --git a/editor/src/editor/windows/fx-editor/VFX/behaviors/colorOverLife.ts b/editor/src/editor/windows/fx-editor/VFX/behaviors/colorOverLife.ts index a289205c9..e571c643f 100644 --- a/editor/src/editor/windows/fx-editor/VFX/behaviors/colorOverLife.ts +++ b/editor/src/editor/windows/fx-editor/VFX/behaviors/colorOverLife.ts @@ -1,6 +1,6 @@ -import { Color4, ParticleSystem, SolidParticle } from "babylonjs"; +import { Color4, ParticleSystem } from "babylonjs"; import type { VFXColorOverLifeBehavior } from "../types/behaviors"; -import { extractColorFromValue, extractAlphaFromValue, interpolateColorKeys, interpolateGradientKeys } from "./utils"; +import { extractColorFromValue, extractAlphaFromValue } from "./utils"; /** * Apply ColorOverLife behavior to ParticleSystem @@ -38,42 +38,51 @@ export function applyColorOverLifePS(particleSystem: ParticleSystem, behavior: V } /** - * Apply ColorOverLife behavior to SolidParticle - * Gets lifeRatio from particle (age / lifeTime) + * Apply ColorOverLife behavior to SolidParticleSystem + * Adds color gradients to the system (similar to ParticleSystem native gradients) */ -export function applyColorOverLifeSPS(particle: SolidParticle, behavior: VFXColorOverLifeBehavior): void { - if (!behavior.color || !particle.color || particle.lifeTime <= 0) { +export function applyColorOverLifeSPS(system: any, behavior: VFXColorOverLifeBehavior): void { + if (!behavior.color) { return; } - // Get lifeRatio from particle - const lifeRatio = particle.age / particle.lifeTime; - - const colorKeys = behavior.color.color?.keys ?? behavior.color.keys; - if (!colorKeys || !Array.isArray(colorKeys)) { - return; - } - - const interpolatedColor = interpolateColorKeys(colorKeys, lifeRatio); - const startColor = particle.props?.startColor; - - if (startColor) { - // Multiply with startColor (matching three.quarks behavior) - particle.color.r = interpolatedColor.r * startColor.r; - particle.color.g = interpolatedColor.g * startColor.g; - particle.color.b = interpolatedColor.b * startColor.b; - } else { - particle.color.r = interpolatedColor.r; - particle.color.g = interpolatedColor.g; - particle.color.b = interpolatedColor.b; + // Add color gradients from keys + if (behavior.color.color && behavior.color.color.keys) { + const colorKeys = behavior.color.color.keys; + for (const key of colorKeys) { + if (key.value !== undefined && key.pos !== undefined) { + const color = extractColorFromValue(key.value); + const alpha = extractAlphaFromValue(key.value); + system.addColorGradient(key.pos, new Color4(color.r, color.g, color.b, alpha)); + } + } + } else if (behavior.color.keys) { + const colorKeys = behavior.color.keys; + for (const key of colorKeys) { + if (key.value !== undefined && key.pos !== undefined) { + const color = extractColorFromValue(key.value); + const alpha = extractAlphaFromValue(key.value); + system.addColorGradient(key.pos, new Color4(color.r, color.g, color.b, alpha)); + } + } } - // Apply alpha if specified - if (behavior.color.alpha?.keys) { + // Update alpha for existing gradients if alpha keys are specified + if (behavior.color.alpha && behavior.color.alpha.keys) { const alphaKeys = behavior.color.alpha.keys; - const alpha = interpolateGradientKeys(alphaKeys, lifeRatio, extractAlphaFromValue); - particle.color.a = alpha; - } else { - particle.color.a = interpolatedColor.a; + for (const key of alphaKeys) { + if (key.value !== undefined) { + const pos = key.pos ?? key.time ?? 0; + const alpha = extractAlphaFromValue(key.value); + // Get existing gradients and update alpha + const gradients = system._colorGradients.getGradients(); + const existingGradient = gradients.find((g: any) => Math.abs(g.gradient - pos) < 0.001); + if (existingGradient) { + existingGradient.value.a = alpha; + } else { + system.addColorGradient(pos, new Color4(1, 1, 1, alpha)); + } + } + } } } diff --git a/editor/src/editor/windows/fx-editor/VFX/behaviors/limitSpeedOverLife.ts b/editor/src/editor/windows/fx-editor/VFX/behaviors/limitSpeedOverLife.ts index d9f7a7ccb..493aa32bf 100644 --- a/editor/src/editor/windows/fx-editor/VFX/behaviors/limitSpeedOverLife.ts +++ b/editor/src/editor/windows/fx-editor/VFX/behaviors/limitSpeedOverLife.ts @@ -33,3 +33,35 @@ export function applyLimitSpeedOverLifePS(particleSystem: ParticleSystem, behavi } } } + +/** + * Apply LimitSpeedOverLife behavior to SolidParticleSystem + * Adds limit velocity gradients to the system (similar to ParticleSystem native gradients) + */ +export function applyLimitSpeedOverLifeSPS(system: any, behavior: VFXLimitSpeedOverLifeBehavior): void { + if (behavior.dampen !== undefined) { + const dampen = VFXValueUtils.parseConstantValue(behavior.dampen); + system.limitVelocityDamping = dampen; + } + + if (behavior.maxSpeed !== undefined) { + const speedLimit = VFXValueUtils.parseConstantValue(behavior.maxSpeed); + system.addLimitVelocityGradient(0, speedLimit); + system.addLimitVelocityGradient(1, speedLimit); + } else if (behavior.speed !== undefined) { + if (typeof behavior.speed === "object" && behavior.speed !== null && "keys" in behavior.speed && behavior.speed.keys && Array.isArray(behavior.speed.keys)) { + for (const key of behavior.speed.keys) { + const pos = key.pos ?? key.time ?? 0; + const val = key.value; + if (val !== undefined && pos !== undefined) { + const numVal = extractNumberFromValue(val); + system.addLimitVelocityGradient(pos, numVal); + } + } + } else if (typeof behavior.speed === "number" || (typeof behavior.speed === "object" && behavior.speed !== null && "type" in behavior.speed)) { + const speedLimit = VFXValueUtils.parseConstantValue(behavior.speed); + system.addLimitVelocityGradient(0, speedLimit); + system.addLimitVelocityGradient(1, speedLimit); + } + } +} diff --git a/editor/src/editor/windows/fx-editor/VFX/behaviors/rotationOverLife.ts b/editor/src/editor/windows/fx-editor/VFX/behaviors/rotationOverLife.ts index 0be4f3215..7170620ed 100644 --- a/editor/src/editor/windows/fx-editor/VFX/behaviors/rotationOverLife.ts +++ b/editor/src/editor/windows/fx-editor/VFX/behaviors/rotationOverLife.ts @@ -1,37 +1,108 @@ -import { ParticleSystem, SolidParticle } from "babylonjs"; +import { ParticleSystem } from "babylonjs"; import type { VFXRotationOverLifeBehavior } from "../types/behaviors"; import { VFXValueUtils } from "../utils/valueParser"; +import { extractNumberFromValue } from "./utils"; /** * Apply RotationOverLife behavior to ParticleSystem + * Uses addAngularSpeedGradient for gradient support (Babylon.js native) */ export function applyRotationOverLifePS(particleSystem: ParticleSystem, behavior: VFXRotationOverLifeBehavior): void { - if (behavior.angularVelocity) { + if (!behavior.angularVelocity) { + return; + } + + // Check if angularVelocity has gradient keys + if ( + typeof behavior.angularVelocity === "object" && + behavior.angularVelocity !== null && + "keys" in behavior.angularVelocity && + Array.isArray(behavior.angularVelocity.keys) && + behavior.angularVelocity.keys.length > 0 + ) { + // Use gradient for keys + for (const key of behavior.angularVelocity.keys) { + const pos = key.pos ?? key.time ?? 0; + const val = key.value; + if (val !== undefined && pos !== undefined) { + const numVal = extractNumberFromValue(val); + particleSystem.addAngularSpeedGradient(pos, numVal); + } + } + } else if ( + typeof behavior.angularVelocity === "object" && + behavior.angularVelocity !== null && + "functions" in behavior.angularVelocity && + Array.isArray(behavior.angularVelocity.functions) && + behavior.angularVelocity.functions.length > 0 + ) { + // Use gradient for functions + for (const func of behavior.angularVelocity.functions) { + if (func.function && func.start !== undefined) { + const startSpeed = func.function.p0 || 0; + const endSpeed = func.function.p3 !== undefined ? func.function.p3 : startSpeed; + particleSystem.addAngularSpeedGradient(func.start, startSpeed); + if (func.function.p3 !== undefined) { + particleSystem.addAngularSpeedGradient(Math.min(func.start + 0.5, 1), endSpeed); + } + } + } + } else { + // Fallback to interval (min/max) - use gradient with min at 0 and max at 1 const angularVel = VFXValueUtils.parseIntervalValue(behavior.angularVelocity); - particleSystem.minAngularSpeed = angularVel.min; - particleSystem.maxAngularSpeed = angularVel.max; + particleSystem.addAngularSpeedGradient(0, angularVel.min); + particleSystem.addAngularSpeedGradient(1, angularVel.max); } } /** - * Apply RotationOverLife behavior to SolidParticle - * Gets lifeRatio from particle (age / lifeTime) and updateSpeed from system + * Apply RotationOverLife behavior to SolidParticleSystem + * Adds angular speed gradients to the system (similar to ParticleSystem native gradients) */ -export function applyRotationOverLifeSPS(particle: SolidParticle, behavior: VFXRotationOverLifeBehavior): void { - if (!behavior.angularVelocity || particle.lifeTime <= 0) { +export function applyRotationOverLifeSPS(system: any, behavior: VFXRotationOverLifeBehavior): void { + if (!behavior.angularVelocity) { return; } - // Get lifeRatio from particle - const lifeRatio = particle.age / particle.lifeTime; - - // Get updateSpeed from system (stored in particle.props or use default) - const updateSpeed = (particle as any).system?.updateSpeed ?? 0.016; - - const angularVel = VFXValueUtils.parseIntervalValue(behavior.angularVelocity); - const angularSpeed = angularVel.min + (angularVel.max - angularVel.min) * lifeRatio; - - // Apply rotation around Z axis (2D rotation) - // SolidParticle uses rotation.z for 2D rotation - particle.rotation.z += angularSpeed * updateSpeed; + // Check if angularVelocity has gradient keys + if ( + typeof behavior.angularVelocity === "object" && + behavior.angularVelocity !== null && + "keys" in behavior.angularVelocity && + Array.isArray(behavior.angularVelocity.keys) && + behavior.angularVelocity.keys.length > 0 + ) { + // Use gradient for keys + for (const key of behavior.angularVelocity.keys) { + const pos = key.pos ?? key.time ?? 0; + const val = key.value; + if (val !== undefined && pos !== undefined) { + const numVal = extractNumberFromValue(val); + system.addAngularSpeedGradient(pos, numVal); + } + } + } else if ( + typeof behavior.angularVelocity === "object" && + behavior.angularVelocity !== null && + "functions" in behavior.angularVelocity && + Array.isArray(behavior.angularVelocity.functions) && + behavior.angularVelocity.functions.length > 0 + ) { + // Use gradient for functions + for (const func of behavior.angularVelocity.functions) { + if (func.function && func.start !== undefined) { + const startSpeed = func.function.p0 || 0; + const endSpeed = func.function.p3 !== undefined ? func.function.p3 : startSpeed; + system.addAngularSpeedGradient(func.start, startSpeed); + if (func.function.p3 !== undefined) { + system.addAngularSpeedGradient(Math.min(func.start + 0.5, 1), endSpeed); + } + } + } + } else { + // Fallback to interval (min/max) - use gradient with min at 0 and max at 1 + const angularVel = VFXValueUtils.parseIntervalValue(behavior.angularVelocity); + system.addAngularSpeedGradient(0, angularVel.min); + system.addAngularSpeedGradient(1, angularVel.max); + } } diff --git a/editor/src/editor/windows/fx-editor/VFX/behaviors/sizeOverLife.ts b/editor/src/editor/windows/fx-editor/VFX/behaviors/sizeOverLife.ts index 0aa7490f8..dd0e8376c 100644 --- a/editor/src/editor/windows/fx-editor/VFX/behaviors/sizeOverLife.ts +++ b/editor/src/editor/windows/fx-editor/VFX/behaviors/sizeOverLife.ts @@ -1,6 +1,6 @@ -import { ParticleSystem, SolidParticle } from "babylonjs"; +import { ParticleSystem } from "babylonjs"; import type { VFXSizeOverLifeBehavior } from "../types/behaviors"; -import { extractNumberFromValue, interpolateGradientKeys } from "./utils"; +import { extractNumberFromValue } from "./utils"; /** * Apply SizeOverLife behavior to ParticleSystem @@ -29,34 +29,32 @@ export function applySizeOverLifePS(particleSystem: ParticleSystem, behavior: VF } /** - * Apply SizeOverLife behavior to SolidParticle - * Gets lifeRatio from particle (age / lifeTime) + * Apply SizeOverLife behavior to SolidParticleSystem + * Adds size gradients to the system (similar to ParticleSystem native gradients) */ -export function applySizeOverLifeSPS(particle: SolidParticle, behavior: VFXSizeOverLifeBehavior): void { - if (!behavior.size || particle.lifeTime <= 0) { +export function applySizeOverLifeSPS(system: any, behavior: VFXSizeOverLifeBehavior): void { + if (!behavior.size) { return; } - // Get lifeRatio from particle - const lifeRatio = particle.age / particle.lifeTime; - - let sizeMultiplier = 1; - - if (behavior.size.keys && Array.isArray(behavior.size.keys)) { - sizeMultiplier = interpolateGradientKeys(behavior.size.keys, lifeRatio, extractNumberFromValue); - } else if (behavior.size.functions && Array.isArray(behavior.size.functions)) { - // Handle functions (simplified - use first function) - const func = behavior.size.functions[0]; - if (func && func.function && func.start !== undefined) { - const startSize = func.function.p0 || 1; - const endSize = func.function.p3 !== undefined ? func.function.p3 : startSize; - const t = Math.max(0, Math.min(1, (lifeRatio - func.start) / 0.5)); - sizeMultiplier = startSize + (endSize - startSize) * t; + if (behavior.size.functions) { + const functions = behavior.size.functions; + for (const func of functions) { + if (func.function && func.start !== undefined) { + const startSize = func.function.p0 || 1; + const endSize = func.function.p3 !== undefined ? func.function.p3 : startSize; + system.addSizeGradient(func.start, startSize); + if (func.function.p3 !== undefined) { + system.addSizeGradient(Math.min(func.start + 0.5, 1), endSize); + } + } + } + } else if (behavior.size.keys) { + for (const key of behavior.size.keys) { + if (key.value !== undefined && key.pos !== undefined) { + const size = extractNumberFromValue(key.value); + system.addSizeGradient(key.pos, size); + } } } - - // Multiply startSize by the gradient value (matching three.quarks behavior) - const startSize = particle.props?.startSize ?? 1; - const newSize = startSize * sizeMultiplier; - particle.scaling.setAll(newSize); } diff --git a/editor/src/editor/windows/fx-editor/VFX/behaviors/speedOverLife.ts b/editor/src/editor/windows/fx-editor/VFX/behaviors/speedOverLife.ts index c189e7f12..6f94f58a5 100644 --- a/editor/src/editor/windows/fx-editor/VFX/behaviors/speedOverLife.ts +++ b/editor/src/editor/windows/fx-editor/VFX/behaviors/speedOverLife.ts @@ -1,6 +1,6 @@ -import { SolidParticle, ParticleSystem } from "babylonjs"; +import { ParticleSystem } from "babylonjs"; import type { VFXSpeedOverLifeBehavior } from "../types/behaviors"; -import { extractNumberFromValue, interpolateGradientKeys } from "./utils"; +import { extractNumberFromValue } from "./utils"; import { VFXValueUtils } from "../utils/valueParser"; /** @@ -43,21 +43,23 @@ export function applySpeedOverLifePS(particleSystem: ParticleSystem, behavior: V } /** - * Apply SpeedOverLife behavior to SolidParticle - * Gets lifeRatio from particle (age / lifeTime) + * Apply SpeedOverLife behavior to SolidParticleSystem + * Adds velocity gradients to the system (similar to ParticleSystem native gradients) */ -export function applySpeedOverLifeSPS(particle: SolidParticle, behavior: VFXSpeedOverLifeBehavior): void { - if (!behavior.speed || particle.lifeTime <= 0) { +export function applySpeedOverLifeSPS(system: any, behavior: VFXSpeedOverLifeBehavior): void { + if (!behavior.speed) { return; } - // Get lifeRatio from particle - const lifeRatio = particle.age / particle.lifeTime; - - let speedMultiplier = 1; - if (typeof behavior.speed === "object" && behavior.speed !== null && "keys" in behavior.speed && behavior.speed.keys && Array.isArray(behavior.speed.keys)) { - speedMultiplier = interpolateGradientKeys(behavior.speed.keys, lifeRatio, extractNumberFromValue); + for (const key of behavior.speed.keys) { + const pos = key.pos ?? key.time ?? 0; + const val = key.value; + if (val !== undefined && pos !== undefined) { + const numVal = extractNumberFromValue(val); + system.addVelocityGradient(pos, numVal); + } + } } else if ( typeof behavior.speed === "object" && behavior.speed !== null && @@ -65,31 +67,19 @@ export function applySpeedOverLifeSPS(particle: SolidParticle, behavior: VFXSpee behavior.speed.functions && Array.isArray(behavior.speed.functions) ) { - // Handle functions (simplified - use first function) - const func = behavior.speed.functions[0]; - if (func && func.function && func.start !== undefined) { - const startSpeed = func.function.p0 || 1; - const endSpeed = func.function.p3 !== undefined ? func.function.p3 : startSpeed; - const t = Math.max(0, Math.min(1, (lifeRatio - func.start) / 0.5)); - speedMultiplier = startSpeed + (endSpeed - startSpeed) * t; + for (const func of behavior.speed.functions) { + if (func.function && func.start !== undefined) { + const startSpeed = func.function.p0 || 1; + const endSpeed = func.function.p3 !== undefined ? func.function.p3 : startSpeed; + system.addVelocityGradient(func.start, startSpeed); + if (func.function.p3 !== undefined) { + system.addVelocityGradient(Math.min(func.start + 0.5, 1), endSpeed); + } + } } } else if (typeof behavior.speed === "number" || (typeof behavior.speed === "object" && behavior.speed !== null && "type" in behavior.speed)) { const speedValue = VFXValueUtils.parseIntervalValue(behavior.speed); - speedMultiplier = speedValue.min + (speedValue.max - speedValue.min) * lifeRatio; - } - - // Apply speed modifier to velocity - const startSpeed = particle.props?.startSpeed ?? 1; - const speedModifier = particle.props?.speedModifier ?? 1; - const newSpeedModifier = speedModifier * speedMultiplier; - particle.props = particle.props || {}; - particle.props.speedModifier = newSpeedModifier; - - // Update velocity magnitude - const velocityLength = Math.sqrt(particle.velocity.x * particle.velocity.x + particle.velocity.y * particle.velocity.y + particle.velocity.z * particle.velocity.z); - if (velocityLength > 0) { - const newLength = startSpeed * newSpeedModifier; - const scale = newLength / velocityLength; - particle.velocity.scaleInPlace(scale); + system.addVelocityGradient(0, speedValue.min); + system.addVelocityGradient(1, speedValue.max); } } diff --git a/editor/src/editor/windows/fx-editor/VFX/factories/VFXParticleSystemBehaviorFactory.ts b/editor/src/editor/windows/fx-editor/VFX/factories/VFXParticleSystemBehaviorFactory.ts deleted file mode 100644 index 8f9b3ee28..000000000 --- a/editor/src/editor/windows/fx-editor/VFX/factories/VFXParticleSystemBehaviorFactory.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { Particle } from "babylonjs"; -import type { VFXPerParticleBehaviorFunction } from "../types/VFXBehaviorFunction"; -import type { VFXBehavior, VFXColorBySpeedBehavior, VFXSizeBySpeedBehavior, VFXRotationBySpeedBehavior, VFXOrbitOverLifeBehavior } from "../types/behaviors"; -import { applyColorBySpeedPS, applySizeBySpeedPS, applyRotationBySpeedPS, applyOrbitOverLifePS } from "../behaviors"; -import type { ParticleSystem } from "babylonjs"; - -/** - * Behavior factory for VFXParticleSystem - * Creates behavior functions from configurations - */ -export class VFXParticleSystemBehaviorFactory { - private _particleSystem: ParticleSystem; - - constructor(particleSystem: ParticleSystem) { - this._particleSystem = particleSystem; - } - - /** - * Create behavior functions from configurations - * Behaviors receive only particle and behavior config - all data comes from particle - */ - public createBehaviorFunctions(behaviors: VFXBehavior[]): VFXPerParticleBehaviorFunction[] { - const functions: VFXPerParticleBehaviorFunction[] = []; - - for (const behavior of behaviors) { - switch (behavior.type) { - case "ColorBySpeed": { - const b = behavior as VFXColorBySpeedBehavior; - functions.push((particle: Particle, _behaviorConfig: VFXBehavior) => { - applyColorBySpeedPS(particle, b); - }); - break; - } - - case "SizeBySpeed": { - const b = behavior as VFXSizeBySpeedBehavior; - functions.push((particle: Particle, _behaviorConfig: VFXBehavior) => { - applySizeBySpeedPS(particle, b); - }); - break; - } - - case "RotationBySpeed": { - const b = behavior as VFXRotationBySpeedBehavior; - functions.push((particle: Particle, _behaviorConfig: VFXBehavior) => { - // Store reference to system in particle for behaviors that need it - (particle as any).particleSystem = this._particleSystem; - applyRotationBySpeedPS(particle, b); - }); - break; - } - - case "OrbitOverLife": { - const b = behavior as VFXOrbitOverLifeBehavior; - functions.push((particle: Particle, _behaviorConfig: VFXBehavior) => { - applyOrbitOverLifePS(particle, b); - }); - break; - } - } - } - - return functions; - } -} diff --git a/editor/src/editor/windows/fx-editor/VFX/factories/VFXSolidParticleSystemBehaviorFactory.ts b/editor/src/editor/windows/fx-editor/VFX/factories/VFXSolidParticleSystemBehaviorFactory.ts deleted file mode 100644 index 90c37f448..000000000 --- a/editor/src/editor/windows/fx-editor/VFX/factories/VFXSolidParticleSystemBehaviorFactory.ts +++ /dev/null @@ -1,132 +0,0 @@ -import { SolidParticle } from "babylonjs"; -import type { VFXPerSolidParticleBehaviorFunction } from "../types/VFXBehaviorFunction"; -import type { - VFXBehavior, - VFXColorOverLifeBehavior, - VFXSizeOverLifeBehavior, - VFXRotationOverLifeBehavior, - VFXForceOverLifeBehavior, - VFXSpeedOverLifeBehavior, - VFXColorBySpeedBehavior, - VFXSizeBySpeedBehavior, - VFXRotationBySpeedBehavior, - VFXOrbitOverLifeBehavior, -} from "../types/behaviors"; -import { VFXValueUtils } from "../utils/valueParser"; -import { - applyColorOverLifeSPS, - applySizeOverLifeSPS, - applyRotationOverLifeSPS, - applySpeedOverLifeSPS, - applyColorBySpeedSPS, - applySizeBySpeedSPS, - applyRotationBySpeedSPS, - applyOrbitOverLifeSPS, -} from "../behaviors"; - -/** - * Behavior factory for VFXSolidParticleSystem - * Creates behavior functions from configurations - */ -export class VFXSolidParticleSystemBehaviorFactory { - /** - * Create behavior functions from configurations - * Behaviors receive only particle and behavior config - all data comes from particle - */ - public createBehaviorFunctions(behaviors: VFXBehavior[]): VFXPerSolidParticleBehaviorFunction[] { - const functions: VFXPerSolidParticleBehaviorFunction[] = []; - - for (const behavior of behaviors) { - switch (behavior.type) { - case "ColorOverLife": { - const b = behavior as VFXColorOverLifeBehavior; - functions.push((particle: SolidParticle, _behaviorConfig: VFXBehavior) => { - applyColorOverLifeSPS(particle, b); - }); - break; - } - - case "SizeOverLife": { - const b = behavior as VFXSizeOverLifeBehavior; - functions.push((particle: SolidParticle, _behaviorConfig: VFXBehavior) => { - applySizeOverLifeSPS(particle, b); - }); - break; - } - - case "RotationOverLife": - case "Rotation3DOverLife": { - const b = behavior as VFXRotationOverLifeBehavior; - functions.push((particle: SolidParticle, _behaviorConfig: VFXBehavior) => { - applyRotationOverLifeSPS(particle, b); - }); - break; - } - - case "ForceOverLife": - case "ApplyForce": { - const b = behavior as VFXForceOverLifeBehavior; - functions.push((particle: SolidParticle, _behaviorConfig: VFXBehavior) => { - // Get updateSpeed from system (stored in particle.props or use default) - const updateSpeed = (particle as any).system?.updateSpeed ?? 0.016; - - const forceX = b.x ?? b.force?.x; - const forceY = b.y ?? b.force?.y; - const forceZ = b.z ?? b.force?.z; - if (forceX !== undefined || forceY !== undefined || forceZ !== undefined) { - const fx = forceX !== undefined ? VFXValueUtils.parseConstantValue(forceX) : 0; - const fy = forceY !== undefined ? VFXValueUtils.parseConstantValue(forceY) : 0; - const fz = forceZ !== undefined ? VFXValueUtils.parseConstantValue(forceZ) : 0; - particle.velocity.x += fx * updateSpeed; - particle.velocity.y += fy * updateSpeed; - particle.velocity.z += fz * updateSpeed; - } - }); - break; - } - - case "SpeedOverLife": { - const b = behavior as VFXSpeedOverLifeBehavior; - functions.push((particle: SolidParticle, _behaviorConfig: VFXBehavior) => { - applySpeedOverLifeSPS(particle, b); - }); - break; - } - - case "ColorBySpeed": { - const b = behavior as VFXColorBySpeedBehavior; - functions.push((particle: SolidParticle, _behaviorConfig: VFXBehavior) => { - applyColorBySpeedSPS(particle, b); - }); - break; - } - - case "SizeBySpeed": { - const b = behavior as VFXSizeBySpeedBehavior; - functions.push((particle: SolidParticle, _behaviorConfig: VFXBehavior) => { - applySizeBySpeedSPS(particle, b); - }); - break; - } - - case "RotationBySpeed": { - const b = behavior as VFXRotationBySpeedBehavior; - functions.push((particle: SolidParticle, _behaviorConfig: VFXBehavior) => { - applyRotationBySpeedSPS(particle, b); - }); - break; - } - - case "OrbitOverLife": { - const b = behavior as VFXOrbitOverLifeBehavior; - functions.push((particle: SolidParticle, _behaviorConfig: VFXBehavior) => { - applyOrbitOverLifeSPS(particle, b); - }); - break; - } - } - } - - return functions; - } -} diff --git a/editor/src/editor/windows/fx-editor/VFX/index.ts b/editor/src/editor/windows/fx-editor/VFX/index.ts index 3011d238c..7d79789c6 100644 --- a/editor/src/editor/windows/fx-editor/VFX/index.ts +++ b/editor/src/editor/windows/fx-editor/VFX/index.ts @@ -8,8 +8,6 @@ export * from "./utils/capacityCalculator"; export * from "./utils/matrixUtils"; export * from "./systems/VFXSolidParticleSystem"; export * from "./systems/VFXParticleSystem"; -export * from "./factories/VFXParticleSystemBehaviorFactory"; -export * from "./factories/VFXSolidParticleSystemBehaviorFactory"; export * from "./loggers/VFXLogger"; export * from "./VFXEffect"; export * from "./utils/valueParser"; diff --git a/editor/src/editor/windows/fx-editor/VFX/systems/VFXParticleSystem.ts b/editor/src/editor/windows/fx-editor/VFX/systems/VFXParticleSystem.ts index d3ce8a01f..5e2b383cb 100644 --- a/editor/src/editor/windows/fx-editor/VFX/systems/VFXParticleSystem.ts +++ b/editor/src/editor/windows/fx-editor/VFX/systems/VFXParticleSystem.ts @@ -10,10 +10,14 @@ import type { VFXSpeedOverLifeBehavior, VFXFrameOverLifeBehavior, VFXLimitSpeedOverLifeBehavior, + VFXColorBySpeedBehavior, + VFXSizeBySpeedBehavior, + VFXRotationBySpeedBehavior, + VFXOrbitOverLifeBehavior, } from "../types/behaviors"; +import type { Particle } from "babylonjs"; import type { VFXShape } from "../types/shapes"; import type { VFXParticleEmitterConfig, VFXEmissionBurst } from "../types/emitterConfig"; -import { VFXParticleSystemBehaviorFactory } from "../factories/VFXParticleSystemBehaviorFactory"; import { VFXParticleSystemEmitterFactory } from "../factories/VFXParticleSystemEmitterFactory"; import { VFXValueUtils } from "../utils/valueParser"; import { VFXCapacityCalculator } from "../utils/capacityCalculator"; @@ -26,6 +30,10 @@ import { applySpeedOverLifePS, applyFrameOverLifePS, applyLimitSpeedOverLifePS, + applyColorBySpeedPS, + applySizeBySpeedPS, + applyRotationBySpeedPS, + applyOrbitOverLifePS, } from "../behaviors"; /** @@ -37,7 +45,6 @@ export class VFXParticleSystem extends ParticleSystem { public startSpeed: number; public startColor: Color4; private _behaviors: VFXPerParticleBehaviorFunction[]; - private _behaviorFactory: VFXParticleSystemBehaviorFactory; private _emitterFactory: VFXParticleSystemEmitterFactory; public readonly behaviorConfigs: VFXBehavior[]; @@ -61,7 +68,6 @@ export class VFXParticleSystem extends ParticleSystem { super(name, capacity, scene); this._behaviors = []; - this._behaviorFactory = new VFXParticleSystemBehaviorFactory(this); this._emitterFactory = new VFXParticleSystemEmitterFactory(this); // Create proxy array that updates functions when modified @@ -166,8 +172,56 @@ export class VFXParticleSystem extends ParticleSystem { // Apply system-level behaviors (gradients, etc.) - these configure the ParticleSystem once this._applySystemLevelBehaviors(); - // Create per-particle behavior functions - this._behaviors = this._behaviorFactory.createBehaviorFunctions(this.behaviorConfigs); + // Create per-particle behavior functions (BySpeed, OrbitOverLife, etc.) + this._behaviors = this._createPerParticleBehaviorFunctions(this.behaviorConfigs); + } + + /** + * Create per-particle behavior functions from configurations + * Only creates functions for behaviors that depend on particle properties (speed, orbit) + */ + private _createPerParticleBehaviorFunctions(behaviors: VFXBehavior[]): VFXPerParticleBehaviorFunction[] { + const functions: VFXPerParticleBehaviorFunction[] = []; + + for (const behavior of behaviors) { + switch (behavior.type) { + case "ColorBySpeed": { + const b = behavior as VFXColorBySpeedBehavior; + functions.push((particle: Particle) => { + applyColorBySpeedPS(particle, b); + }); + break; + } + + case "SizeBySpeed": { + const b = behavior as VFXSizeBySpeedBehavior; + functions.push((particle: Particle) => { + applySizeBySpeedPS(particle, b); + }); + break; + } + + case "RotationBySpeed": { + const b = behavior as VFXRotationBySpeedBehavior; + functions.push((particle: Particle) => { + // Store reference to system in particle for behaviors that need it + (particle as any).particleSystem = this; + applyRotationBySpeedPS(particle, b); + }); + break; + } + + case "OrbitOverLife": { + const b = behavior as VFXOrbitOverLifeBehavior; + functions.push((particle: Particle) => { + applyOrbitOverLifePS(particle, b); + }); + break; + } + } + } + + return functions; } /** diff --git a/editor/src/editor/windows/fx-editor/VFX/systems/VFXSolidParticleSystem.ts b/editor/src/editor/windows/fx-editor/VFX/systems/VFXSolidParticleSystem.ts index b9a9fb951..b99b7a6eb 100644 --- a/editor/src/editor/windows/fx-editor/VFX/systems/VFXSolidParticleSystem.ts +++ b/editor/src/editor/windows/fx-editor/VFX/systems/VFXSolidParticleSystem.ts @@ -3,15 +3,23 @@ import type { VFXParticleEmitterConfig, VFXEmissionBurst } from "../types/emitte import { VFXLogger } from "../loggers/VFXLogger"; import type { VFXLoaderOptions } from "../types/loader"; import type { VFXPerSolidParticleBehaviorFunction } from "../types/VFXBehaviorFunction"; -import type { VFXBehavior } from "../types/behaviors"; +import type { + VFXBehavior, + VFXForceOverLifeBehavior, + VFXColorBySpeedBehavior, + VFXSizeBySpeedBehavior, + VFXRotationBySpeedBehavior, + VFXOrbitOverLifeBehavior, +} from "../types/behaviors"; import type { VFXShape } from "../types/shapes"; import type { VFXColor } from "../types/colors"; import type { VFXValue } from "../types/values"; import type { VFXRotation } from "../types/rotations"; -import { VFXSolidParticleSystemBehaviorFactory } from "../factories/VFXSolidParticleSystemBehaviorFactory"; import { VFXSolidParticleSystemEmitterFactory } from "../factories/VFXSolidParticleSystemEmitterFactory"; import { VFXValueUtils } from "../utils/valueParser"; import { VFXCapacityCalculator } from "../utils/capacityCalculator"; +import { ColorGradientSystem, NumberGradientSystem } from "../utils/gradientSystem"; +import { applyColorBySpeedSPS, applySizeBySpeedSPS, applyRotationBySpeedSPS, applyOrbitOverLifeSPS } from "../behaviors"; /** * Emission state matching three.quarks EmissionState structure @@ -35,15 +43,21 @@ interface EmissionState { export class VFXSolidParticleSystem extends SolidParticleSystem { private _emissionState: EmissionState; private _behaviors: VFXPerSolidParticleBehaviorFunction[]; - private _behaviorFactory: VFXSolidParticleSystemBehaviorFactory; private _emitterFactory: VFXSolidParticleSystemEmitterFactory; private _parent: TransformNode | null; private _vfxTransform: { position: Vector3; rotation: Quaternion; scale: Vector3 } | null; private _logger: VFXLogger | null; - private _options: VFXLoaderOptions | undefined; private _name: string; private _emitEnded: boolean; + // Gradient systems for "OverLife" behaviors (similar to ParticleSystem native gradients) + private _colorGradients: ColorGradientSystem; + private _sizeGradients: NumberGradientSystem; + private _velocityGradients: NumberGradientSystem; + private _angularSpeedGradients: NumberGradientSystem; + private _limitVelocityGradients: NumberGradientSystem; + private _limitVelocityDamping: number; + // Properties moved from config public isLooping: boolean; public duration: number; @@ -94,6 +108,52 @@ export class VFXSolidParticleSystem extends SolidParticleSystem { return this._behaviors; } + /** + * Add color gradient (for ColorOverLife behavior) + */ + public addColorGradient(gradient: number, color: Color4): void { + this._colorGradients.addGradient(gradient, color); + } + + /** + * Add size gradient (for SizeOverLife behavior) + */ + public addSizeGradient(gradient: number, size: number): void { + this._sizeGradients.addGradient(gradient, size); + } + + /** + * Add velocity gradient (for SpeedOverLife behavior) + */ + public addVelocityGradient(gradient: number, velocity: number): void { + this._velocityGradients.addGradient(gradient, velocity); + } + + /** + * Add angular speed gradient (for RotationOverLife behavior) + */ + public addAngularSpeedGradient(gradient: number, angularSpeed: number): void { + this._angularSpeedGradients.addGradient(gradient, angularSpeed); + } + + /** + * Add limit velocity gradient (for LimitSpeedOverLife behavior) + */ + public addLimitVelocityGradient(gradient: number, limit: number): void { + this._limitVelocityGradients.addGradient(gradient, limit); + } + + /** + * Set limit velocity damping (for LimitSpeedOverLife behavior) + */ + public set limitVelocityDamping(value: number) { + this._limitVelocityDamping = value; + } + + public get limitVelocityDamping(): number { + return this._limitVelocityDamping; + } + /** * Initialize mesh for SPS (internal use) * Adds the mesh as a shape and configures billboard mode @@ -101,7 +161,7 @@ export class VFXSolidParticleSystem extends SolidParticleSystem { private _initializeMesh(particleMesh: Mesh): void { if (!particleMesh) { if (this._logger) { - this._logger.warn(`Cannot add shape to SPS: particleMesh is null`, this._options); + this._logger.warn(`Cannot add shape to SPS: particleMesh is null`); } return; } @@ -110,7 +170,7 @@ export class VFXSolidParticleSystem extends SolidParticleSystem { const capacity = VFXCapacityCalculator.calculateForSolidParticleSystem(this.emissionOverTime, this.duration, this.isLooping); if (this._logger) { - this._logger.log(`Adding shape to SPS: mesh name=${particleMesh.name}, hasMaterial=${!!particleMesh.material}, capacity=${capacity}`, this._options); + this._logger.log(`Adding shape to SPS: mesh name=${particleMesh.name}, hasMaterial=${!!particleMesh.material}, capacity=${capacity}`); } // Add shape to SPS @@ -173,7 +233,6 @@ export class VFXSolidParticleSystem extends SolidParticleSystem { this._name = name; this._behaviors = []; - this._behaviorFactory = new VFXSolidParticleSystemBehaviorFactory(); this._emitterFactory = new VFXSolidParticleSystemEmitterFactory(); // Initialize properties from initialConfig @@ -205,6 +264,14 @@ export class VFXSolidParticleSystem extends SolidParticleSystem { this.softNearFade = initialConfig.softNearFade; this.worldSpace = initialConfig.worldSpace || false; + // Initialize gradient systems + this._colorGradients = new ColorGradientSystem(); + this._sizeGradients = new NumberGradientSystem(); + this._velocityGradients = new NumberGradientSystem(); + this._angularSpeedGradients = new NumberGradientSystem(); + this._limitVelocityGradients = new NumberGradientSystem(); + this._limitVelocityDamping = 0.1; + // Create proxy array for behavior configs this.behaviorConfigs = this._createBehaviorConfigsProxy(initialConfig.behaviors || []); @@ -214,7 +281,6 @@ export class VFXSolidParticleSystem extends SolidParticleSystem { this._parent = options?.parentGroup ?? null; this._vfxTransform = options?.vfxTransform ?? null; this._logger = options?.logger ?? null; - this._options = options?.loaderOptions; this._emitEnded = false; this._normalMatrix = new Matrix(); this._tempVec = Vector3.Zero(); @@ -444,37 +510,37 @@ export class VFXSolidParticleSystem extends SolidParticleSystem { private _setupMeshProperties(): void { if (!this.mesh) { if (this._logger) { - this._logger.warn(` SPS mesh is null in initParticles!`, this._options); + this._logger.warn(` SPS mesh is null in initParticles!`); } return; } if (this._logger) { - this._logger.log(` initParticles called for SPS: ${this._name}`, this._options); - this._logger.log(` SPS mesh exists: ${this.mesh.name}`, this._options); + this._logger.log(` initParticles called for SPS: ${this._name}`); + this._logger.log(` SPS mesh exists: ${this.mesh.name}`); } if (this.renderOrder !== undefined) { this.mesh.renderingGroupId = this.renderOrder; if (this._logger) { - this._logger.log(` Set SPS mesh renderingGroupId: ${this.renderOrder}`, this._options); + this._logger.log(` Set SPS mesh renderingGroupId: ${this.renderOrder}`); } } if (this.layers !== undefined) { this.mesh.layerMask = this.layers; if (this._logger) { - this._logger.log(` Set SPS mesh layerMask: ${this.layers}`, this._options); + this._logger.log(` Set SPS mesh layerMask: ${this.layers}`); } } if (this._parent) { this.mesh.setParent(this._parent, false, true); if (this._logger) { - this._logger.log(` Set SPS mesh parent to: ${this._parent.name}`, this._options); + this._logger.log(` Set SPS mesh parent to: ${this._parent.name}`); } } else if (this._logger) { - this._logger.log(` No parent group to set for SPS mesh`, this._options); + this._logger.log(` No parent group to set for SPS mesh`); } if (this._vfxTransform) { @@ -485,12 +551,11 @@ export class VFXSolidParticleSystem extends SolidParticleSystem { if (this._logger) { const rot = this.mesh.rotationQuaternion; this._logger.log( - ` Applied VFX transform to SPS mesh: pos=(${this._vfxTransform.position.x.toFixed(2)}, ${this._vfxTransform.position.y.toFixed(2)}, ${this._vfxTransform.position.z.toFixed(2)}), rot=(${rot ? rot.x.toFixed(4) : 0}, ${rot ? rot.y.toFixed(4) : 0}, ${rot ? rot.z.toFixed(4) : 0}, ${rot ? rot.w.toFixed(4) : 1}), scale=(${this._vfxTransform.scale.x.toFixed(2)}, ${this._vfxTransform.scale.y.toFixed(2)}, ${this._vfxTransform.scale.z.toFixed(2)})`, - this._options + ` Applied VFX transform to SPS mesh: pos=(${this._vfxTransform.position.x.toFixed(2)}, ${this._vfxTransform.position.y.toFixed(2)}, ${this._vfxTransform.position.z.toFixed(2)}), rot=(${rot ? rot.x.toFixed(4) : 0}, ${rot ? rot.y.toFixed(4) : 0}, ${rot ? rot.z.toFixed(4) : 0}, ${rot ? rot.w.toFixed(4) : 1}), scale=(${this._vfxTransform.scale.x.toFixed(2)}, ${this._vfxTransform.scale.y.toFixed(2)}, ${this._vfxTransform.scale.z.toFixed(2)})` ); } } else if (this._logger) { - this._logger.log(` No VFX transform to apply to SPS mesh`, this._options); + this._logger.log(` No VFX transform to apply to SPS mesh`); } } @@ -610,8 +675,132 @@ export class VFXSolidParticleSystem extends SolidParticleSystem { * Update behavior functions from configs * Internal method, called automatically when configs change */ + /** + * Update behavior functions from configs + * Applies both system-level behaviors (gradients) and per-particle behaviors + */ private _updateBehaviorFunctions(): void { - this._behaviors = this._behaviorFactory.createBehaviorFunctions(this.behaviorConfigs); + // Clear all gradients + this._colorGradients.clear(); + this._sizeGradients.clear(); + this._velocityGradients.clear(); + this._angularSpeedGradients.clear(); + this._limitVelocityGradients.clear(); + + // Apply system-level behaviors (gradients) - these configure the system once + this._applySystemLevelBehaviors(); + + // Create per-particle behavior functions (BySpeed, OrbitOverLife, ForceOverLife, etc.) + this._behaviors = this._createPerParticleBehaviorFunctions(this.behaviorConfigs); + } + + /** + * Create per-particle behavior functions from configurations + * Only creates functions for behaviors that depend on particle properties (speed, orbit, force) + * "OverLife" behaviors are handled by gradients (system-level) + */ + private _createPerParticleBehaviorFunctions(behaviors: VFXBehavior[]): VFXPerSolidParticleBehaviorFunction[] { + const functions: VFXPerSolidParticleBehaviorFunction[] = []; + + for (const behavior of behaviors) { + switch (behavior.type) { + case "ForceOverLife": + case "ApplyForce": { + const b = behavior as VFXForceOverLifeBehavior; + functions.push((particle: SolidParticle) => { + // Get updateSpeed from system (stored in particle.props or use default) + const updateSpeed = (particle as any).system?.updateSpeed ?? 0.016; + + const forceX = b.x ?? b.force?.x; + const forceY = b.y ?? b.force?.y; + const forceZ = b.z ?? b.force?.z; + if (forceX !== undefined || forceY !== undefined || forceZ !== undefined) { + const fx = forceX !== undefined ? VFXValueUtils.parseConstantValue(forceX) : 0; + const fy = forceY !== undefined ? VFXValueUtils.parseConstantValue(forceY) : 0; + const fz = forceZ !== undefined ? VFXValueUtils.parseConstantValue(forceZ) : 0; + particle.velocity.x += fx * updateSpeed; + particle.velocity.y += fy * updateSpeed; + particle.velocity.z += fz * updateSpeed; + } + }); + break; + } + + case "ColorBySpeed": { + const b = behavior as VFXColorBySpeedBehavior; + functions.push((particle: SolidParticle) => { + applyColorBySpeedSPS(particle, b); + }); + break; + } + + case "SizeBySpeed": { + const b = behavior as VFXSizeBySpeedBehavior; + functions.push((particle: SolidParticle) => { + applySizeBySpeedSPS(particle, b); + }); + break; + } + + case "RotationBySpeed": { + const b = behavior as VFXRotationBySpeedBehavior; + functions.push((particle: SolidParticle) => { + applyRotationBySpeedSPS(particle, b); + }); + break; + } + + case "OrbitOverLife": { + const b = behavior as VFXOrbitOverLifeBehavior; + functions.push((particle: SolidParticle) => { + applyOrbitOverLifeSPS(particle, b); + }); + break; + } + } + } + + return functions; + } + + /** + * Apply system-level behaviors (gradients) to SolidParticleSystem + * These are applied once when behaviors change, not per-particle + * Similar to ParticleSystem native gradients + */ + private _applySystemLevelBehaviors(): void { + // Import behaviors dynamically to avoid circular dependencies + const behaviors = require("../behaviors"); + const applyColorOverLifeSPS = behaviors.applyColorOverLifeSPS; + const applySizeOverLifeSPS = behaviors.applySizeOverLifeSPS; + const applyRotationOverLifeSPS = behaviors.applyRotationOverLifeSPS; + const applySpeedOverLifeSPS = behaviors.applySpeedOverLifeSPS; + const applyLimitSpeedOverLifeSPS = behaviors.applyLimitSpeedOverLifeSPS; + + for (const behavior of this.behaviorConfigs) { + if (!behavior.type) { + continue; + } + + switch (behavior.type) { + case "ColorOverLife": + applyColorOverLifeSPS(this, behavior as any); + break; + case "SizeOverLife": + applySizeOverLifeSPS(this, behavior as any); + break; + case "RotationOverLife": + case "Rotation3DOverLife": + applyRotationOverLifeSPS(this, behavior as any); + break; + case "SpeedOverLife": + applySpeedOverLifeSPS(this, behavior as any); + break; + case "LimitSpeedOverLife": + applyLimitSpeedOverLifeSPS(this, behavior as any); + break; + } + } } public override beforeUpdateParticles(start?: number, stop?: number, update?: boolean): void { @@ -649,21 +838,76 @@ export class VFXSolidParticleSystem extends SolidParticleSystem { return particle; } + // Calculate lifeRatio for gradient interpolation + const lifeRatio = particle.lifeTime > 0 ? particle.age / particle.lifeTime : 0; + + // Apply "OverLife" gradients (similar to ParticleSystem native gradients) + this._applyGradients(particle, lifeRatio); + // Store reference to system in particle for behaviors that need it (particle as any).system = this; - // Apply behaviors - they receive only particle and behavior config - // All data (lifeRatio, speed, etc.) comes from particle itself - // Behavior config is stored in closure by factory, so we pass it from behaviorConfigs - for (let i = 0; i < this._behaviors.length && i < this.behaviorConfigs.length; i++) { - const behaviorFn = this._behaviors[i]; - const behaviorConfig = this.behaviorConfigs[i]; - behaviorFn(particle, behaviorConfig); + // Apply per-particle behaviors (BySpeed, OrbitOverLife, etc.) + // These behaviors don't use gradients as they depend on particle properties, not lifeRatio + // Behavior config is captured in closure, so we only need to pass particle + for (const behaviorFn of this._behaviors) { + behaviorFn(particle); } + // Apply velocity with speed modifier const speedModifier = particle.props?.speedModifier ?? 1.0; particle.position.addInPlace(particle.velocity.scale(this.updateSpeed * speedModifier)); return particle; } + + /** + * Apply gradients to particle based on lifeRatio + */ + private _applyGradients(particle: SolidParticle, lifeRatio: number): void { + // Apply color gradient + const color = this._colorGradients.getValue(lifeRatio); + if (color && particle.color) { + const startColor = particle.props?.startColor; + if (startColor) { + // Multiply with startColor (matching ParticleSystem behavior) + particle.color.r = color.r * startColor.r; + particle.color.g = color.g * startColor.g; + particle.color.b = color.b * startColor.b; + particle.color.a = color.a * startColor.a; + } else { + particle.color.copyFrom(color); + } + } + + // Apply size gradient + const size = this._sizeGradients.getValue(lifeRatio); + if (size !== null && particle.props?.startSize !== undefined) { + const newSize = particle.props.startSize * size; + particle.scaling.setAll(newSize); + } + + // Apply velocity gradient (speed modifier) + const velocity = this._velocityGradients.getValue(lifeRatio); + if (velocity !== null) { + particle.props = particle.props || {}; + particle.props.speedModifier = velocity; + } + + // Apply angular speed gradient + const angularSpeed = this._angularSpeedGradients.getValue(lifeRatio); + if (angularSpeed !== null) { + particle.rotation.z += angularSpeed * this.updateSpeed; + } + + // Apply limit velocity + const limitVelocity = this._limitVelocityGradients.getValue(lifeRatio); + if (limitVelocity !== null && this._limitVelocityDamping > 0) { + const currentSpeed = Math.sqrt(particle.velocity.x * particle.velocity.x + particle.velocity.y * particle.velocity.y + particle.velocity.z * particle.velocity.z); + if (currentSpeed > limitVelocity) { + const scale = limitVelocity / currentSpeed; + particle.velocity.scaleInPlace(scale * this._limitVelocityDamping); + } + } + } } diff --git a/editor/src/editor/windows/fx-editor/VFX/types/VFXBehaviorFunction.ts b/editor/src/editor/windows/fx-editor/VFX/types/VFXBehaviorFunction.ts index 2c93499b2..2cebe48ec 100644 --- a/editor/src/editor/windows/fx-editor/VFX/types/VFXBehaviorFunction.ts +++ b/editor/src/editor/windows/fx-editor/VFX/types/VFXBehaviorFunction.ts @@ -3,15 +3,15 @@ import type { VFXBehavior } from "./behaviors"; /** * Per-particle behavior function for ParticleSystem - * Takes only particle and behavior config - all data comes from particle + * Behavior config is captured in closure, only particle is needed */ -export type VFXPerParticleBehaviorFunction = (particle: Particle, behavior: VFXBehavior) => void; +export type VFXPerParticleBehaviorFunction = (particle: Particle) => void; /** * Per-particle behavior function for SolidParticleSystem - * Takes only particle and behavior config - all data comes from particle + * Behavior config is captured in closure, only particle is needed */ -export type VFXPerSolidParticleBehaviorFunction = (particle: SolidParticle, behavior: VFXBehavior) => void; +export type VFXPerSolidParticleBehaviorFunction = (particle: SolidParticle) => void; /** * System-level behavior function (applied once during initialization) diff --git a/editor/src/editor/windows/fx-editor/VFX/utils/gradientSystem.ts b/editor/src/editor/windows/fx-editor/VFX/utils/gradientSystem.ts new file mode 100644 index 000000000..7bf149576 --- /dev/null +++ b/editor/src/editor/windows/fx-editor/VFX/utils/gradientSystem.ts @@ -0,0 +1,100 @@ +import { Color4 } from "babylonjs"; + +/** + * Generic gradient system for storing and interpolating gradient values + * Similar to Babylon.js native gradients but for SolidParticleSystem + */ +export class GradientSystem { + private gradients: Array<{ gradient: number; value: T }>; + + constructor() { + this.gradients = []; + } + + /** + * Add a gradient point + */ + public addGradient(gradient: number, value: T): void { + // Insert in sorted order + const index = this.gradients.findIndex((g) => g.gradient > gradient); + if (index === -1) { + this.gradients.push({ gradient, value }); + } else { + this.gradients.splice(index, 0, { gradient, value }); + } + } + + /** + * Get interpolated value at given gradient position (0-1) + */ + public getValue(gradient: number): T | null { + if (this.gradients.length === 0) { + return null; + } + + if (this.gradients.length === 1) { + return this.gradients[0].value; + } + + // Clamp gradient to [0, 1] + const clampedGradient = Math.max(0, Math.min(1, gradient)); + + // Find the two gradients to interpolate between + for (let i = 0; i < this.gradients.length - 1; i++) { + const g1 = this.gradients[i]; + const g2 = this.gradients[i + 1]; + + if (clampedGradient >= g1.gradient && clampedGradient <= g2.gradient) { + const t = g2.gradient - g1.gradient !== 0 ? (clampedGradient - g1.gradient) / (g2.gradient - g1.gradient) : 0; + return this.interpolate(g1.value, g2.value, t); + } + } + + // Clamp to first or last gradient + if (clampedGradient <= this.gradients[0].gradient) { + return this.gradients[0].value; + } + return this.gradients[this.gradients.length - 1].value; + } + + /** + * Clear all gradients + */ + public clear(): void { + this.gradients = []; + } + + /** + * Get all gradients (for debugging) + */ + public getGradients(): Array<{ gradient: number; value: T }> { + return [...this.gradients]; + } + + /** + * Interpolate between two values (to be overridden by subclasses) + */ + protected interpolate(value1: T, value2: T, t: number): T { + // Default implementation - should be overridden + return value1; + } +} + +/** + * Color gradient system for Color4 + */ +export class ColorGradientSystem extends GradientSystem { + protected interpolate(value1: Color4, value2: Color4, t: number): Color4 { + return new Color4(value1.r + (value2.r - value1.r) * t, value1.g + (value2.g - value1.g) * t, value1.b + (value2.b - value1.b) * t, value1.a + (value2.a - value1.a) * t); + } +} + +/** + * Number gradient system + */ +export class NumberGradientSystem extends GradientSystem { + protected interpolate(value1: number, value2: number, t: number): number { + return value1 + (value2 - value1) * t; + } +} + From fb2b1a22d0eb274ef468c558dcad24fe4a001e83 Mon Sep 17 00:00:00 2001 From: Mikalai Lazitski Date: Sun, 14 Dec 2025 08:47:35 +0300 Subject: [PATCH 23/62] refactor: enhance VFX parsing and system integration by implementing error handling, improving data conversion processes, and introducing a unified system interface for better performance and clarity --- .../editor/windows/fx-editor/VFX/VFXEffect.ts | 125 +++++----- .../VFX/behaviors/rotationBySpeed.ts | 6 +- .../VFX/factories/VFXSystemFactory.ts | 93 +++++--- .../fx-editor/VFX/parsers/VFXDataConverter.ts | 43 +++- .../fx-editor/VFX/parsers/VFXParser.ts | 70 +++--- .../VFX/systems/VFXParticleSystem.ts | 8 +- .../VFX/systems/VFXSolidParticleSystem.ts | 216 ++++++++++++------ .../windows/fx-editor/VFX/types/index.ts | 5 +- .../windows/fx-editor/VFX/types/system.ts | 54 +++++ .../fx-editor/VFX/utils/valueParser.ts | 4 +- 10 files changed, 421 insertions(+), 203 deletions(-) create mode 100644 editor/src/editor/windows/fx-editor/VFX/types/system.ts diff --git a/editor/src/editor/windows/fx-editor/VFX/VFXEffect.ts b/editor/src/editor/windows/fx-editor/VFX/VFXEffect.ts index 7e73a3aaa..bdea3f404 100644 --- a/editor/src/editor/windows/fx-editor/VFX/VFXEffect.ts +++ b/editor/src/editor/windows/fx-editor/VFX/VFXEffect.ts @@ -5,6 +5,7 @@ import { VFXParser } from "./parsers/VFXParser"; import type { VFXParticleSystem } from "./systems/VFXParticleSystem"; import type { VFXSolidParticleSystem } from "./systems/VFXSolidParticleSystem"; import type { VFXGroup, VFXEmitter, VFXData } from "./types/hierarchy"; +import { isVFXSystem } from "./types/system"; /** * VFX Effect Node - represents either a particle system or a group @@ -119,28 +120,31 @@ export class VFXEffect implements IDisposable { constructor(jsonData?: QuarksVFXJSON, scene?: Scene, rootUrl: string = "", options?: VFXLoaderOptions) { if (jsonData && scene) { const parser = new VFXParser(scene, rootUrl, jsonData, options); - const particleSystems = parser.parse(); - const context = parser.getContext(); - const vfxData = context.vfxData; - const groupNodesMap = context.groupNodesMap; - - this._systems.push(...particleSystems); - if (vfxData && groupNodesMap) { - this._buildHierarchy(vfxData, groupNodesMap, particleSystems); + const parseResult = parser.parse(); + + this._systems.push(...parseResult.systems); + if (parseResult.vfxData && parseResult.groupNodesMap) { + this._buildHierarchy(parseResult.vfxData, parseResult.groupNodesMap, parseResult.systems); } } } /** * Build hierarchy from VFX data and group nodes map + * Handles errors gracefully and continues building partial hierarchy if errors occur */ private _buildHierarchy(vfxData: VFXData, groupNodesMap: Map, systems: (VFXParticleSystem | VFXSolidParticleSystem)[]): void { if (!vfxData || !vfxData.root) { return; } - // Create nodes from hierarchy - this._root = this._buildNodeFromHierarchy(vfxData.root, null, groupNodesMap, systems); + try { + // Create nodes from hierarchy + this._root = this._buildNodeFromHierarchy(vfxData.root, null, groupNodesMap, systems); + } catch (error) { + // Log error but don't throw - effect can still work with partial hierarchy + console.error(`Failed to build VFX hierarchy: ${error instanceof Error ? error.message : String(error)}`); + } } /** @@ -156,55 +160,66 @@ export class VFXEffect implements IDisposable { return null; } - const node: VFXEffectNode = { - name: obj.name, - uuid: obj.uuid, - parent: parent || undefined, - children: [], - type: "config" in obj ? "particle" : "group", - }; - - if (node.type === "particle") { - // Find system by name - const emitter = obj as VFXEmitter; - const system = systems.find((s) => s.name === emitter.name); - if (system) { - node.system = system; - this._systemsByName.set(emitter.name, system); - if (emitter.uuid) { - this._systemsByUuid.set(emitter.uuid, system); + try { + const node: VFXEffectNode = { + name: obj.name, + uuid: obj.uuid, + parent: parent || undefined, + children: [], + type: "config" in obj ? "particle" : "group", + }; + + if (node.type === "particle") { + // Find system by name + const emitter = obj as VFXEmitter; + const system = systems.find((s) => s.name === emitter.name); + if (system) { + node.system = system; + this._systemsByName.set(emitter.name, system); + if (emitter.uuid) { + this._systemsByUuid.set(emitter.uuid, system); + } } - } - } else { - // Find group TransformNode - const group = obj as VFXGroup; - const groupNode = group.uuid ? groupNodesMap.get(group.uuid) : null; - if (groupNode) { - node.group = groupNode; - this._groupsByName.set(group.name, groupNode); - if (group.uuid) { - this._groupsByUuid.set(group.uuid, groupNode); + } else { + // Find group TransformNode + const group = obj as VFXGroup; + const groupNode = group.uuid ? groupNodesMap.get(group.uuid) : null; + if (groupNode) { + node.group = groupNode; + this._groupsByName.set(group.name, groupNode); + if (group.uuid) { + this._groupsByUuid.set(group.uuid, groupNode); + } } } - } - // Process children - if ("children" in obj && obj.children) { - for (const child of obj.children) { - const childNode = this._buildNodeFromHierarchy(child, node, groupNodesMap, systems); - if (childNode) { - node.children.push(childNode); + // Process children with error handling + if ("children" in obj && obj.children) { + for (const child of obj.children) { + try { + const childNode = this._buildNodeFromHierarchy(child, node, groupNodesMap, systems); + if (childNode) { + node.children.push(childNode); + } + } catch (error) { + // Log error but continue processing other children + console.warn(`Failed to build child node ${child.name}: ${error instanceof Error ? error.message : String(error)}`); + } } } - } - // Store node - if (obj.uuid) { - this._nodes.set(obj.uuid, node); - } - this._nodes.set(obj.name, node); + // Store node + if (obj.uuid) { + this._nodes.set(obj.uuid, node); + } + this._nodes.set(obj.name, node); - return node; + return node; + } catch (error) { + // Log error but return null to continue building other parts of hierarchy + console.error(`Failed to build node ${obj.name}: ${error instanceof Error ? error.message : String(error)}`); + return null; + } } /** @@ -275,9 +290,11 @@ export class VFXEffect implements IDisposable { private _collectSystemsInGroup(group: TransformNode, systems: (VFXParticleSystem | VFXSolidParticleSystem)[]): void { // Step 1: Find systems that have this group as direct parent for (const system of this._systems) { - const mesh = (system as any).mesh || (system as any).emitter; - if (mesh && mesh.parent === group) { - systems.push(system); + if (isVFXSystem(system)) { + const parentNode = system.getParentNode(); + if (parentNode && parentNode.parent === group) { + systems.push(system); + } } } diff --git a/editor/src/editor/windows/fx-editor/VFX/behaviors/rotationBySpeed.ts b/editor/src/editor/windows/fx-editor/VFX/behaviors/rotationBySpeed.ts index 8ca493fdb..12b1843e4 100644 --- a/editor/src/editor/windows/fx-editor/VFX/behaviors/rotationBySpeed.ts +++ b/editor/src/editor/windows/fx-editor/VFX/behaviors/rotationBySpeed.ts @@ -16,7 +16,8 @@ export function applyRotationBySpeedPS(particle: Particle, behavior: VFXRotation const currentSpeed = Vector3.Distance(Vector3.Zero(), particle.direction); // Get updateSpeed from system (stored in particle or use default) - const updateSpeed = (particle as any).particleSystem?.updateSpeed ?? 0.016; + const particleWithSystem = particle as ParticleWithSystem; + const updateSpeed = particleWithSystem.particleSystem?.updateSpeed ?? 0.016; // angularVelocity can be VFXValue (constant/interval) or object with keys let angularSpeed = 0; @@ -52,7 +53,8 @@ export function applyRotationBySpeedSPS(particle: SolidParticle, behavior: VFXRo const currentSpeed = Math.sqrt(particle.velocity.x * particle.velocity.x + particle.velocity.y * particle.velocity.y + particle.velocity.z * particle.velocity.z); // Get updateSpeed from system (stored in particle.props or use default) - const updateSpeed = (particle as any).system?.updateSpeed ?? 0.016; + const particleWithSystem = particle as SolidParticleWithSystem; + const updateSpeed = particleWithSystem.system?.updateSpeed ?? 0.016; // angularVelocity can be VFXValue (constant/interval) or object with keys let angularSpeed = 0; diff --git a/editor/src/editor/windows/fx-editor/VFX/factories/VFXSystemFactory.ts b/editor/src/editor/windows/fx-editor/VFX/factories/VFXSystemFactory.ts index 5ddfd5b41..1cbbac605 100644 --- a/editor/src/editor/windows/fx-editor/VFX/factories/VFXSystemFactory.ts +++ b/editor/src/editor/windows/fx-editor/VFX/factories/VFXSystemFactory.ts @@ -133,50 +133,75 @@ export class VFXSystemFactory { const parentName = parentGroup ? parentGroup.name : "none"; this._logger.log(`${indent}Processing emitter: ${vfxEmitter.name} (parent: ${parentName})`); - const config = vfxEmitter.config; - this._logger.log(`${indent} Config: duration=${config.duration}, looping=${config.looping}, systemType=${vfxEmitter.systemType}`); + try { + const config = vfxEmitter.config; + if (!config) { + this._logger.warn(`${indent}Emitter ${vfxEmitter.name} has no config, skipping`); + return null; + } - const cumulativeScale = this._calculateCumulativeScale(parentGroup); - this._logger.log(`${indent}Cumulative scale: (${cumulativeScale.x.toFixed(2)}, ${cumulativeScale.y.toFixed(2)}, ${cumulativeScale.z.toFixed(2)})`); + this._logger.log(`${indent} Config: duration=${config.duration}, looping=${config.looping}, systemType=${vfxEmitter.systemType}`); - // Use systemType from emitter (determined during conversion) - const systemType = vfxEmitter.systemType || "base"; - this._logger.log(`Using ${systemType === "solid" ? "SolidParticleSystem" : "ParticleSystem"}`); + const cumulativeScale = this._calculateCumulativeScale(parentGroup); + this._logger.log(`${indent}Cumulative scale: (${cumulativeScale.x.toFixed(2)}, ${cumulativeScale.y.toFixed(2)}, ${cumulativeScale.z.toFixed(2)})`); - let particleSystem: VFXParticleSystem | VFXSolidParticleSystem | null = null; + // Use systemType from emitter (determined during conversion) + const systemType = vfxEmitter.systemType || "base"; + this._logger.log(`Using ${systemType === "solid" ? "SolidParticleSystem" : "ParticleSystem"}`); - if (systemType === "solid") { - particleSystem = this._createSolidParticleSystem(vfxEmitter, parentGroup); - } else { - particleSystem = this._createParticleSystemInstance(vfxEmitter, parentGroup, cumulativeScale, depth); - } + let particleSystem: VFXParticleSystem | VFXSolidParticleSystem | null = null; - if (!particleSystem) { - this._logger.warn(`Failed to create particle system for emitter: ${vfxEmitter.name}`); - return null; - } + try { + if (systemType === "solid") { + particleSystem = this._createSolidParticleSystem(vfxEmitter, parentGroup); + } else { + particleSystem = this._createParticleSystemInstance(vfxEmitter, parentGroup, cumulativeScale, depth); + } + } catch (error) { + this._logger.error(`${indent}Failed to create ${systemType} system for emitter ${vfxEmitter.name}: ${error instanceof Error ? error.message : String(error)}`); + return null; + } - // Apply transform to particle system - if (particleSystem instanceof VFXSolidParticleSystem) { - // For SPS, transform is applied to the mesh - if (particleSystem.mesh) { - this._applyTransform(particleSystem.mesh, vfxEmitter.transform, depth); - this._setParent(particleSystem.mesh, parentGroup, depth); + if (!particleSystem) { + this._logger.warn(`${indent}Failed to create particle system for emitter: ${vfxEmitter.name}`); + return null; } - } else if (particleSystem instanceof VFXParticleSystem) { - // For PS, transform is applied to the emitter mesh - const emitter = (particleSystem as any).emitter; - if (emitter) { - this._applyTransform(emitter, vfxEmitter.transform, depth); - this._setParent(emitter, parentGroup, depth); + + // Apply transform to particle system + try { + if (particleSystem instanceof VFXSolidParticleSystem) { + // For SPS, transform is applied to the mesh + if (particleSystem.mesh) { + this._applyTransform(particleSystem.mesh, vfxEmitter.transform, depth); + this._setParent(particleSystem.mesh, parentGroup, depth); + } + } else if (particleSystem instanceof VFXParticleSystem) { + // For PS, transform is applied to the emitter mesh + const emitter = particleSystem.getParentNode(); + if (emitter) { + this._applyTransform(emitter, vfxEmitter.transform, depth); + this._setParent(emitter, parentGroup, depth); + } + } + } catch (error) { + this._logger.warn(`${indent}Failed to apply transform to system ${vfxEmitter.name}: ${error instanceof Error ? error.message : String(error)}`); + // Continue - system is created, just transform failed } - } - // Handle prewarm - this._handlePrewarm(particleSystem, vfxEmitter.config.prewarm); + // Handle prewarm + try { + this._handlePrewarm(particleSystem, vfxEmitter.config.prewarm); + } catch (error) { + this._logger.warn(`${indent}Failed to handle prewarm for system ${vfxEmitter.name}: ${error instanceof Error ? error.message : String(error)}`); + // Continue - prewarm is optional + } - this._logger.log(`${indent}Created particle system: ${vfxEmitter.name}`); - return particleSystem; + this._logger.log(`${indent}Created particle system: ${vfxEmitter.name}`); + return particleSystem; + } catch (error) { + this._logger.error(`${indent}Unexpected error creating particle system ${vfxEmitter.name}: ${error instanceof Error ? error.message : String(error)}`); + return null; + } } /** diff --git a/editor/src/editor/windows/fx-editor/VFX/parsers/VFXDataConverter.ts b/editor/src/editor/windows/fx-editor/VFX/parsers/VFXDataConverter.ts index 47285a0f8..8e2aa7adc 100644 --- a/editor/src/editor/windows/fx-editor/VFX/parsers/VFXDataConverter.ts +++ b/editor/src/editor/windows/fx-editor/VFX/parsers/VFXDataConverter.ts @@ -56,6 +56,7 @@ export class VFXDataConverter { /** * Convert Quarks/Three.js VFX JSON to Babylon.js VFX format + * Handles errors gracefully and returns partial data if conversion fails */ public convert(quarksVFXData: QuarksVFXJSON): VFXData { this._logger.log("=== Converting Quarks VFX to Babylon.js VFX format ==="); @@ -65,15 +66,43 @@ export class VFXDataConverter { let root: VFXGroup | VFXEmitter | null = null; - if (quarksVFXData.object) { - root = this._convertObject(quarksVFXData.object, null, groups, emitters, 0); + try { + if (quarksVFXData.object) { + root = this._convertObject(quarksVFXData.object, null, groups, emitters, 0); + } + } catch (error) { + this._logger.error(`Failed to convert root object: ${error instanceof Error ? error.message : String(error)}`); } - // Convert all resources - const materials = this._convertMaterials(quarksVFXData.materials || []); - const textures = this._convertTextures(quarksVFXData.textures || []); - const images = this._convertImages(quarksVFXData.images || []); - const geometries = this._convertGeometries(quarksVFXData.geometries || []); + // Convert all resources with error handling + let materials: VFXMaterial[] = []; + let textures: VFXTexture[] = []; + let images: VFXImage[] = []; + let geometries: VFXGeometry[] = []; + + try { + materials = this._convertMaterials(quarksVFXData.materials || []); + } catch (error) { + this._logger.error(`Failed to convert materials: ${error instanceof Error ? error.message : String(error)}`); + } + + try { + textures = this._convertTextures(quarksVFXData.textures || []); + } catch (error) { + this._logger.error(`Failed to convert textures: ${error instanceof Error ? error.message : String(error)}`); + } + + try { + images = this._convertImages(quarksVFXData.images || []); + } catch (error) { + this._logger.error(`Failed to convert images: ${error instanceof Error ? error.message : String(error)}`); + } + + try { + geometries = this._convertGeometries(quarksVFXData.geometries || []); + } catch (error) { + this._logger.error(`Failed to convert geometries: ${error instanceof Error ? error.message : String(error)}`); + } this._logger.log( `=== Conversion complete. Groups: ${groups.size}, Emitters: ${emitters.size}, Materials: ${materials.length}, Textures: ${textures.length}, Images: ${images.length}, Geometries: ${geometries.length} ===` diff --git a/editor/src/editor/windows/fx-editor/VFX/parsers/VFXParser.ts b/editor/src/editor/windows/fx-editor/VFX/parsers/VFXParser.ts index 21bf73dc3..cfb90dfed 100644 --- a/editor/src/editor/windows/fx-editor/VFX/parsers/VFXParser.ts +++ b/editor/src/editor/windows/fx-editor/VFX/parsers/VFXParser.ts @@ -1,7 +1,6 @@ import { Scene, TransformNode } from "babylonjs"; import type { QuarksVFXJSON } from "../types/quarksTypes"; import type { VFXLoaderOptions } from "../types/loader"; -import type { VFXParseContext } from "../types/context"; import type { VFXData } from "../types/hierarchy"; import { VFXLogger } from "../loggers/VFXLogger"; import { VFXMaterialFactory } from "../factories/VFXMaterialFactory"; @@ -11,60 +10,76 @@ import { VFXDataConverter } from "./VFXDataConverter"; import { VFXParticleSystem } from "../systems/VFXParticleSystem"; import { VFXSolidParticleSystem } from "../systems/VFXSolidParticleSystem"; +/** + * Result of parsing VFX JSON + */ +export interface VFXParseResult { + /** Created particle systems */ + systems: (VFXParticleSystem | VFXSolidParticleSystem)[]; + /** Converted VFX data */ + vfxData: VFXData; + /** Map of group UUIDs to TransformNodes */ + groupNodesMap: Map; +} + /** * Main parser for Three.js particle JSON files * Orchestrates the parsing process using modular components */ export class VFXParser { - private _context: VFXParseContext; private _logger: VFXLogger; private _materialFactory: VFXMaterialFactory; private _geometryFactory: VFXGeometryFactory; private _systemFactory: VFXSystemFactory; + private _vfxData: VFXData; + private _groupNodesMap: Map; + private _options: VFXLoaderOptions; constructor(scene: Scene, rootUrl: string, jsonData: QuarksVFXJSON, options?: VFXLoaderOptions) { const opts = options || {}; - this._context = { - scene, - rootUrl, - jsonData, - options: opts, - groupNodesMap: new Map(), - }; + this._options = opts; + this._groupNodesMap = new Map(); this._logger = new VFXLogger("[VFXParser]", opts); // Convert Quarks JSON to VFXData first const dataConverter = new VFXDataConverter(opts); - const vfxData = dataConverter.convert(jsonData); - this._context.vfxData = vfxData; + this._vfxData = dataConverter.convert(jsonData); // Create factories with VFXData instead of QuarksVFXJSON - this._materialFactory = new VFXMaterialFactory(scene, vfxData, rootUrl, opts); - this._geometryFactory = new VFXGeometryFactory(vfxData, opts); - this._systemFactory = new VFXSystemFactory(scene, opts, this._context.groupNodesMap, this._materialFactory, this._geometryFactory); + this._materialFactory = new VFXMaterialFactory(scene, this._vfxData, rootUrl, opts); + this._geometryFactory = new VFXGeometryFactory(this._vfxData, opts); + this._systemFactory = new VFXSystemFactory(scene, opts, this._groupNodesMap, this._materialFactory, this._geometryFactory); } /** * Parse the JSON data and create particle systems + * Returns all necessary data for building the effect hierarchy */ - public parse(): (VFXParticleSystem | VFXSolidParticleSystem)[] { - const { options, vfxData } = this._context; + public parse(): VFXParseResult { this._logger.log("=== Starting Particle System Parsing ==="); - if (!vfxData) { + if (!this._vfxData) { this._logger.warn("VFXData is missing"); - return []; + return { + systems: [], + vfxData: this._vfxData, + groupNodesMap: this._groupNodesMap, + }; } - if (options.validate) { - this._validateJSONStructure(vfxData); + if (this._options.validate) { + this._validateJSONStructure(this._vfxData); } - const particleSystems = this._systemFactory.createSystems(vfxData); + const particleSystems = this._systemFactory.createSystems(this._vfxData); this._logger.log(`=== Parsing complete. Created ${particleSystems.length} particle system(s) ===`); - return particleSystems; + return { + systems: particleSystems, + vfxData: this._vfxData, + groupNodesMap: this._groupNodesMap, + }; } /** @@ -97,21 +112,14 @@ export class VFXParser { } /** - * Get the parse context (for use by other components) - */ - public getContext(): VFXParseContext { - return this._context; - } - - /** - * Get the material factory + * Get the material factory (for advanced use cases) */ public getMaterialFactory(): VFXMaterialFactory { return this._materialFactory; } /** - * Get the geometry factory + * Get the geometry factory (for advanced use cases) */ public getGeometryFactory(): VFXGeometryFactory { return this._geometryFactory; diff --git a/editor/src/editor/windows/fx-editor/VFX/systems/VFXParticleSystem.ts b/editor/src/editor/windows/fx-editor/VFX/systems/VFXParticleSystem.ts index 5e2b383cb..e343e4797 100644 --- a/editor/src/editor/windows/fx-editor/VFX/systems/VFXParticleSystem.ts +++ b/editor/src/editor/windows/fx-editor/VFX/systems/VFXParticleSystem.ts @@ -1,5 +1,6 @@ -import { Color4, ParticleSystem, Scene, Vector3, Matrix, Texture } from "babylonjs"; +import { Color4, ParticleSystem, Scene, Vector3, Matrix, Texture, AbstractMesh } from "babylonjs"; import type { VFXPerParticleBehaviorFunction } from "../types/VFXBehaviorFunction"; +import type { IVFXSystem, ParticleWithSystem } from "../types/system"; import type { VFXBehavior, VFXColorOverLifeBehavior, @@ -40,7 +41,7 @@ import { * Extended ParticleSystem with VFX behaviors support * Fully self-contained, no dependencies on parsers or factories */ -export class VFXParticleSystem extends ParticleSystem { +export class VFXParticleSystem extends ParticleSystem implements IVFXSystem { public startSize: number; public startSpeed: number; public startColor: Color4; @@ -205,7 +206,8 @@ export class VFXParticleSystem extends ParticleSystem { const b = behavior as VFXRotationBySpeedBehavior; functions.push((particle: Particle) => { // Store reference to system in particle for behaviors that need it - (particle as any).particleSystem = this; + const particleWithSystem = particle as ParticleWithSystem; + particleWithSystem.particleSystem = this; applyRotationBySpeedPS(particle, b); }); break; diff --git a/editor/src/editor/windows/fx-editor/VFX/systems/VFXSolidParticleSystem.ts b/editor/src/editor/windows/fx-editor/VFX/systems/VFXSolidParticleSystem.ts index b99b7a6eb..939fa67ad 100644 --- a/editor/src/editor/windows/fx-editor/VFX/systems/VFXSolidParticleSystem.ts +++ b/editor/src/editor/windows/fx-editor/VFX/systems/VFXSolidParticleSystem.ts @@ -1,8 +1,9 @@ -import { Vector3, Quaternion, Matrix, Color4, SolidParticleSystem, SolidParticle, TransformNode, Mesh } from "babylonjs"; +import { Vector3, Quaternion, Matrix, Color4, SolidParticleSystem, SolidParticle, TransformNode, Mesh, AbstractMesh } from "babylonjs"; import type { VFXParticleEmitterConfig, VFXEmissionBurst } from "../types/emitterConfig"; import { VFXLogger } from "../loggers/VFXLogger"; import type { VFXLoaderOptions } from "../types/loader"; import type { VFXPerSolidParticleBehaviorFunction } from "../types/VFXBehaviorFunction"; +import type { IVFXSystem, SolidParticleWithSystem } from "../types/system"; import type { VFXBehavior, VFXForceOverLifeBehavior, @@ -40,7 +41,7 @@ interface EmissionState { * Extended SolidParticleSystem implementing three.quarks Mesh renderMode (renderMode = 2) logic * This class replicates the exact behavior of three.quarks ParticleSystem with renderMode = Mesh */ -export class VFXSolidParticleSystem extends SolidParticleSystem { +export class VFXSolidParticleSystem extends SolidParticleSystem implements IVFXSystem { private _emissionState: EmissionState; private _behaviors: VFXPerSolidParticleBehaviorFunction[]; private _emitterFactory: VFXSolidParticleSystemEmitterFactory; @@ -101,6 +102,14 @@ export class VFXSolidParticleSystem extends SolidParticleSystem { } } + /** + * Get the parent node (mesh) for hierarchy operations + * Implements IVFXSystem interface + */ + public getParentNode(): AbstractMesh | TransformNode | null { + return this.mesh || null; + } + /** * Get behavior functions (internal use) */ @@ -306,84 +315,126 @@ export class VFXSolidParticleSystem extends SolidParticleSystem { } } + /** + * Find a dead particle for recycling + * Оптимизировано: кешируем particles и nbParticles + */ private _findDeadParticle(): SolidParticle | null { - for (let j = 0; j < this.nbParticles; j++) { - if (!this.particles[j].alive) { - return this.particles[j]; + const particles = this.particles; + const nbParticles = this.nbParticles; + for (let j = 0; j < nbParticles; j++) { + if (!particles[j].alive) { + return particles[j]; } } return null; } + /** + * Reset particle to initial state for recycling + * Оптимизировано: используем прямые присваивания вместо setAll где возможно + */ private _resetParticle(particle: SolidParticle): void { particle.age = 0; particle.alive = true; particle.isVisible = true; + particle._stillInvisible = false; // Сбрасываем флаг невидимости particle.position.setAll(0); particle.velocity.setAll(0); particle.rotation.setAll(0); particle.scaling.setAll(1); + + // Оптимизация: создаем color только если его нет if (particle.color) { particle.color.set(1, 1, 1, 1); } else { particle.color = new Color4(1, 1, 1, 1); } - if (!particle.props) { - particle.props = {}; - } - particle.props.speedModifier = 1.0; + // Оптимизация: создаем props только если его нет + const props = particle.props || (particle.props = {}); + props.speedModifier = 1.0; } + /** + * Initialize particle color + * Оптимизировано: кешируем props и избегаем лишних созданий объектов + */ private _initializeParticleColor(particle: SolidParticle): void { - if (!particle.color) { - particle.color = new Color4(1, 1, 1, 1); - } - + const props = particle.props!; + if (this.startColor !== undefined) { const startColor = VFXValueUtils.parseConstantColor(this.startColor); - particle.props!.startColor = startColor.clone(); - particle.color.copyFrom(startColor); + props.startColor = startColor.clone(); + if (particle.color) { + particle.color.copyFrom(startColor); + } else { + particle.color = startColor.clone(); + } } else { - const defaultColor = new Color4(1, 1, 1, 1); - particle.props!.startColor = defaultColor.clone(); - particle.color.copyFrom(defaultColor); + // Используем один объект для всех частиц без цвета (оптимизация памяти) + if (!particle.color) { + particle.color = new Color4(1, 1, 1, 1); + } else { + particle.color.set(1, 1, 1, 1); + } + props.startColor = particle.color.clone(); } } - private _initializeParticleSpeed(particle: SolidParticle): void { + /** + * Initialize particle speed + * Оптимизировано: normalizedTime передается как параметр (вычисляется один раз в _spawn) + */ + private _initializeParticleSpeed(particle: SolidParticle, normalizedTime: number): void { + const props = particle.props!; if (this.startSpeed !== undefined) { - const normalizedTime = this._emissionState.time / this.duration; - particle.props!.startSpeed = VFXValueUtils.parseValue(this.startSpeed, normalizedTime); + props.startSpeed = VFXValueUtils.parseValue(this.startSpeed, normalizedTime); } else { - particle.props!.startSpeed = 0; + props.startSpeed = 0; } } - private _initializeParticleLife(particle: SolidParticle): void { + /** + * Initialize particle lifetime + * Оптимизировано: normalizedTime передается как параметр (вычисляется один раз в _spawn) + */ + private _initializeParticleLife(particle: SolidParticle, normalizedTime: number): void { if (this.startLife !== undefined) { - const normalizedTime = this._emissionState.time / this.duration; particle.lifeTime = VFXValueUtils.parseValue(this.startLife, normalizedTime); } else { particle.lifeTime = 1; } } - private _initializeParticleSize(particle: SolidParticle): void { + /** + * Initialize particle size + * Оптимизировано: normalizedTime передается как параметр (вычисляется один раз в _spawn) + */ + private _initializeParticleSize(particle: SolidParticle, normalizedTime: number): void { + const props = particle.props!; if (this.startSize !== undefined) { - const normalizedTime = this._emissionState.time / this.duration; const sizeValue = VFXValueUtils.parseValue(this.startSize, normalizedTime); - particle.props!.startSize = sizeValue; + props.startSize = sizeValue; particle.scaling.setAll(sizeValue); } else { - particle.props!.startSize = 1; + props.startSize = 1; particle.scaling.setAll(1); } } + /** + * Spawn particles from dead pool + * Оптимизировано: вычисляем матрицу эмиттера один раз для всех частиц + */ private _spawn(count: number): void { + if (count <= 0) { + return; + } + const emissionState = this._emissionState; + // Вычисляем матрицу эмиттера один раз для всех частиц const emitterMatrix = this._getEmitterMatrix(); const translation = this._tempVec; const quaternion = this._tempQuat; @@ -391,20 +442,27 @@ export class VFXSolidParticleSystem extends SolidParticleSystem { emitterMatrix.decompose(scale, quaternion, translation); emitterMatrix.toNormalMatrix(this._normalMatrix); + // Кешируем normalizedTime один раз для всех частиц в этом спавне + const normalizedTime = this.duration > 0 ? this._emissionState.time / this.duration : 0; + for (let i = 0; i < count; i++) { emissionState.burstParticleIndex = i; const particle = this._findDeadParticle(); if (!particle) { - continue; + // Логируем только один раз для избежания спама + if (i === 0 && this._logger) { + this._logger.warn(`No dead particles available for spawning. Capacity may be insufficient.`); + } + break; // Нет смысла продолжать, если нет мертвых частиц } + // Вызываем методы напрямую (быстрее, чем bind) this._resetParticle(particle); this._initializeParticleColor(particle); - this._initializeParticleSpeed(particle); - this._initializeParticleLife(particle); - this._initializeParticleSize(particle); - + this._initializeParticleSpeed(particle, normalizedTime); + this._initializeParticleLife(particle, normalizedTime); + this._initializeParticleSize(particle, normalizedTime); this._initializeEmitterShape(particle); } } @@ -441,7 +499,7 @@ export class VFXSolidParticleSystem extends SolidParticleSystem { private _spawnFromWaitEmiting(): void { const emissionState = this._emissionState; - const totalSpawn = Math.ceil(emissionState.waitEmiting); + const totalSpawn = Math.floor(emissionState.waitEmiting); if (totalSpawn > 0) { this._spawn(totalSpawn); emissionState.waitEmiting -= totalSpawn; @@ -495,12 +553,14 @@ export class VFXSolidParticleSystem extends SolidParticleSystem { } private _emit(delta: number): void { - this._handleEmissionLooping(); - this._spawnFromWaitEmiting(); - this._spawnBursts(); + // Сначала накапливаем эмиссию для текущего кадра this._accumulateEmission(delta); - this._emissionState.time += delta; + // Потом спавним частицы из накопленного waitEmiting + this._spawnFromWaitEmiting(); + + // Спавним bursts + this._spawnBursts(); } private _getBurstTime(burst: VFXEmissionBurst): number { @@ -709,7 +769,8 @@ export class VFXSolidParticleSystem extends SolidParticleSystem { const b = behavior as VFXForceOverLifeBehavior; functions.push((particle: SolidParticle) => { // Get updateSpeed from system (stored in particle.props or use default) - const updateSpeed = (particle as any).system?.updateSpeed ?? 0.016; + const particleWithSystem = particle as SolidParticleWithSystem; + const updateSpeed = particleWithSystem.system?.updateSpeed ?? 0.016; const forceX = b.x ?? b.force?.x; const forceY = b.y ?? b.force?.y; @@ -812,32 +873,41 @@ export class VFXSolidParticleSystem extends SolidParticleSystem { const deltaTime = this._scaledUpdateSpeed || 0.016; - this._emit(deltaTime); + // Сначала увеличиваем время this._emissionState.time += deltaTime; + + // Потом эмиттим (внутри _emit будет накопление и спавн) + this._emit(deltaTime); + + // В конце обрабатываем looping (теперь time уже увеличен) + this._handleEmissionLooping(); } private _updateParticle(particle: SolidParticle): SolidParticle { + // Ранний выход для мертвых частиц - базовый класс пропустит их обработку (continue) if (!particle.alive) { particle.isVisible = false; - if (this._positions32 && particle._model) { + // Обнуляем позиции только если частица еще не была помечена как невидимая + // Базовый класс обнуляет позиции для невидимых, но живых частиц в блоке else. + // Для мертвых частиц базовый класс делает continue до блока else, + // поэтому нам нужно обнулить позиции здесь, но только один раз. + if (!particle._stillInvisible && this._positions32 && particle._model) { const shape = particle._model._shape; const startIdx = particle._pos; - for (let pt = 0; pt < shape.length; pt++) { + const positions32 = this._positions32; + // Оптимизированное обнуление: используем один цикл с прямым доступом + for (let pt = 0, len = shape.length; pt < len; pt++) { const idx = startIdx + pt * 3; - this._positions32[idx] = 0; - this._positions32[idx + 1] = 0; - this._positions32[idx + 2] = 0; + positions32[idx] = positions32[idx + 1] = positions32[idx + 2] = 0; } + particle._stillInvisible = true; // Помечаем как невидимую для оптимизации } return particle; } - if (particle.age < 0) { - return particle; - } - + // Базовый класс уже обновил particle.age и проверил lifetime перед вызовом updateParticle // Calculate lifeRatio for gradient interpolation const lifeRatio = particle.lifeTime > 0 ? particle.age / particle.lifeTime : 0; @@ -845,36 +915,48 @@ export class VFXSolidParticleSystem extends SolidParticleSystem { this._applyGradients(particle, lifeRatio); // Store reference to system in particle for behaviors that need it - (particle as any).system = this; + // Используем type assertion только один раз для оптимизации + const particleWithSystem = particle as SolidParticleWithSystem; + particleWithSystem.system = this; // Apply per-particle behaviors (BySpeed, OrbitOverLife, etc.) // These behaviors don't use gradients as they depend on particle properties, not lifeRatio // Behavior config is captured in closure, so we only need to pass particle - for (const behaviorFn of this._behaviors) { - behaviorFn(particle); + const behaviors = this._behaviors; + for (let i = 0, len = behaviors.length; i < len; i++) { + behaviors[i](particle); } // Apply velocity with speed modifier - const speedModifier = particle.props?.speedModifier ?? 1.0; - particle.position.addInPlace(particle.velocity.scale(this.updateSpeed * speedModifier)); + // Оптимизация: кешируем props и используем прямое обращение + const props = particle.props; + const speedModifier = props?.speedModifier ?? 1.0; + const updateSpeed = this.updateSpeed; + particle.position.addInPlace(particle.velocity.scale(updateSpeed * speedModifier)); return particle; } /** * Apply gradients to particle based on lifeRatio + * Оптимизировано для производительности: кешируем props и updateSpeed */ private _applyGradients(particle: SolidParticle, lifeRatio: number): void { + // Кешируем props и updateSpeed для избежания повторных обращений + const props = particle.props || (particle.props = {}); + const updateSpeed = this.updateSpeed; + // Apply color gradient const color = this._colorGradients.getValue(lifeRatio); if (color && particle.color) { - const startColor = particle.props?.startColor; + const startColor = props.startColor; if (startColor) { // Multiply with startColor (matching ParticleSystem behavior) - particle.color.r = color.r * startColor.r; - particle.color.g = color.g * startColor.g; - particle.color.b = color.b * startColor.b; - particle.color.a = color.a * startColor.a; + const pColor = particle.color; + pColor.r = color.r * startColor.r; + pColor.g = color.g * startColor.g; + pColor.b = color.b * startColor.b; + pColor.a = color.a * startColor.a; } else { particle.color.copyFrom(color); } @@ -882,31 +964,29 @@ export class VFXSolidParticleSystem extends SolidParticleSystem { // Apply size gradient const size = this._sizeGradients.getValue(lifeRatio); - if (size !== null && particle.props?.startSize !== undefined) { - const newSize = particle.props.startSize * size; - particle.scaling.setAll(newSize); + if (size !== null && props.startSize !== undefined) { + particle.scaling.setAll(props.startSize * size); } // Apply velocity gradient (speed modifier) const velocity = this._velocityGradients.getValue(lifeRatio); if (velocity !== null) { - particle.props = particle.props || {}; - particle.props.speedModifier = velocity; + props.speedModifier = velocity; } // Apply angular speed gradient const angularSpeed = this._angularSpeedGradients.getValue(lifeRatio); if (angularSpeed !== null) { - particle.rotation.z += angularSpeed * this.updateSpeed; + particle.rotation.z += angularSpeed * updateSpeed; } // Apply limit velocity const limitVelocity = this._limitVelocityGradients.getValue(lifeRatio); if (limitVelocity !== null && this._limitVelocityDamping > 0) { - const currentSpeed = Math.sqrt(particle.velocity.x * particle.velocity.x + particle.velocity.y * particle.velocity.y + particle.velocity.z * particle.velocity.z); + const vel = particle.velocity; + const currentSpeed = Math.sqrt(vel.x * vel.x + vel.y * vel.y + vel.z * vel.z); if (currentSpeed > limitVelocity) { - const scale = limitVelocity / currentSpeed; - particle.velocity.scaleInPlace(scale * this._limitVelocityDamping); + vel.scaleInPlace((limitVelocity / currentSpeed) * this._limitVelocityDamping); } } } diff --git a/editor/src/editor/windows/fx-editor/VFX/types/index.ts b/editor/src/editor/windows/fx-editor/VFX/types/index.ts index 11cd8ec53..8afbb464e 100644 --- a/editor/src/editor/windows/fx-editor/VFX/types/index.ts +++ b/editor/src/editor/windows/fx-editor/VFX/types/index.ts @@ -5,9 +5,8 @@ * Import types directly from their specific modules for better tree-shaking. */ -// Loader and context types +// Loader types export type { VFXLoaderOptions } from "./loader"; -export type { VFXParseContext } from "./context"; // Emitter types export type { VFXEmitterData } from "./emitter"; @@ -41,3 +40,5 @@ export type { VFXTransform, VFXGroup, VFXEmitter, VFXData } from "./hierarchy"; export type { VFXMaterial, VFXTexture, VFXImage, VFXGeometry } from "./resources"; export type { QuarksVFXJSON } from "./quarksTypes"; export type { VFXPerParticleContext, VFXPerParticleBehaviorFunction, VFXPerSolidParticleBehaviorFunction, VFXSystemBehaviorFunction } from "./VFXBehaviorFunction"; +export type { IVFXSystem, ParticleWithSystem, SolidParticleWithSystem } from "./system"; +export { isVFXSystem } from "./system"; diff --git a/editor/src/editor/windows/fx-editor/VFX/types/system.ts b/editor/src/editor/windows/fx-editor/VFX/types/system.ts new file mode 100644 index 000000000..c8e4c9a84 --- /dev/null +++ b/editor/src/editor/windows/fx-editor/VFX/types/system.ts @@ -0,0 +1,54 @@ +import { TransformNode, AbstractMesh, Particle, SolidParticle } from "babylonjs"; +import type { VFXParticleSystem } from "../systems/VFXParticleSystem"; +import type { VFXSolidParticleSystem } from "../systems/VFXSolidParticleSystem"; + +/** + * Common interface for all VFX particle systems + * Provides type-safe access to common properties and methods + */ +export interface IVFXSystem { + /** System name */ + name: string; + /** Get the parent node (mesh or emitter) for hierarchy operations */ + getParentNode(): AbstractMesh | TransformNode | null; + /** Start the particle system */ + start(): void; + /** Stop the particle system */ + stop(): void; + /** Dispose the particle system */ + dispose(): void; +} + +/** + * Extended Particle type with system reference + * Used for behaviors that need access to the particle system + * Uses intersection type to add custom property without conflicting with base type + */ +export type ParticleWithSystem = Particle & { + particleSystem?: VFXParticleSystem; +}; + +/** + * Extended SolidParticle type with system reference + * Used for behaviors that need access to the solid particle system + * Uses intersection type to add custom property without conflicting with base type + */ +export type SolidParticleWithSystem = SolidParticle & { + system?: VFXSolidParticleSystem; +}; + +/** + * Type guard to check if a system implements IVFXSystem + */ +export function isVFXSystem(system: unknown): system is IVFXSystem { + return ( + typeof system === "object" && + system !== null && + "getParentNode" in system && + typeof (system as IVFXSystem).getParentNode === "function" && + "start" in system && + typeof (system as IVFXSystem).start === "function" && + "stop" in system && + typeof (system as IVFXSystem).stop === "function" + ); +} diff --git a/editor/src/editor/windows/fx-editor/VFX/utils/valueParser.ts b/editor/src/editor/windows/fx-editor/VFX/utils/valueParser.ts index c6580e6cf..d671dbfb6 100644 --- a/editor/src/editor/windows/fx-editor/VFX/utils/valueParser.ts +++ b/editor/src/editor/windows/fx-editor/VFX/utils/valueParser.ts @@ -1,5 +1,5 @@ import { Color4, ColorGradient } from "babylonjs"; -import type { VFXValue } from "../types/values"; +import type { VFXPiecewiseBezier, VFXValue } from "../types/values"; import type { VFXColor } from "../types/colors"; import type { VFXGradientKey } from "../types/gradients"; @@ -83,7 +83,7 @@ export class VFXValueUtils { /** * Evaluate PiecewiseBezier at normalized time t (0-1) */ - private static _evaluatePiecewiseBezier(bezier: any, t: number): number { + private static _evaluatePiecewiseBezier(bezier: VFXPiecewiseBezier, t: number): number { if (!bezier.functions || bezier.functions.length === 0) { return 0; } From 12d0b70acc94c1e38566877433696bbd177ed2d3 Mon Sep 17 00:00:00 2001 From: Mikalai Lazitski Date: Sun, 14 Dec 2025 08:47:57 +0300 Subject: [PATCH 24/62] refactor: clean up whitespace in VFXSolidParticleSystem for improved code readability --- .../windows/fx-editor/VFX/systems/VFXSolidParticleSystem.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/editor/src/editor/windows/fx-editor/VFX/systems/VFXSolidParticleSystem.ts b/editor/src/editor/windows/fx-editor/VFX/systems/VFXSolidParticleSystem.ts index 939fa67ad..e07b64191 100644 --- a/editor/src/editor/windows/fx-editor/VFX/systems/VFXSolidParticleSystem.ts +++ b/editor/src/editor/windows/fx-editor/VFX/systems/VFXSolidParticleSystem.ts @@ -343,7 +343,7 @@ export class VFXSolidParticleSystem extends SolidParticleSystem implements IVFXS particle.velocity.setAll(0); particle.rotation.setAll(0); particle.scaling.setAll(1); - + // Оптимизация: создаем color только если его нет if (particle.color) { particle.color.set(1, 1, 1, 1); @@ -362,7 +362,7 @@ export class VFXSolidParticleSystem extends SolidParticleSystem implements IVFXS */ private _initializeParticleColor(particle: SolidParticle): void { const props = particle.props!; - + if (this.startColor !== undefined) { const startColor = VFXValueUtils.parseConstantColor(this.startColor); props.startColor = startColor.clone(); From 64415bca22b5bfa8b91cb9eb31078a9b47a0d31c Mon Sep 17 00:00:00 2001 From: Mikalai Lazitski Date: Sun, 14 Dec 2025 12:32:50 +0300 Subject: [PATCH 25/62] refactor: remove trailing whitespace in VFX files for improved code cleanliness --- .../VFXSolidParticleSystemEmitterFactory.ts | 1 - .../fx-editor/VFX/systems/VFXParticleSystem.ts | 12 +++++++++++- .../editor/windows/fx-editor/VFX/types/resources.ts | 1 - .../fx-editor/VFX/utils/capacityCalculator.ts | 1 - .../windows/fx-editor/VFX/utils/gradientSystem.ts | 1 - .../windows/fx-editor/VFX/utils/matrixUtils.ts | 1 - .../fx-editor/properties/particle-initialization.tsx | 4 +--- 7 files changed, 12 insertions(+), 9 deletions(-) diff --git a/editor/src/editor/windows/fx-editor/VFX/factories/VFXSolidParticleSystemEmitterFactory.ts b/editor/src/editor/windows/fx-editor/VFX/factories/VFXSolidParticleSystemEmitterFactory.ts index 360124cd0..7e78f313f 100644 --- a/editor/src/editor/windows/fx-editor/VFX/factories/VFXSolidParticleSystemEmitterFactory.ts +++ b/editor/src/editor/windows/fx-editor/VFX/factories/VFXSolidParticleSystemEmitterFactory.ts @@ -97,4 +97,3 @@ export class VFXSolidParticleSystemEmitterFactory { particle.velocity.scaleInPlace(startSpeed); } } - diff --git a/editor/src/editor/windows/fx-editor/VFX/systems/VFXParticleSystem.ts b/editor/src/editor/windows/fx-editor/VFX/systems/VFXParticleSystem.ts index e343e4797..7093530a1 100644 --- a/editor/src/editor/windows/fx-editor/VFX/systems/VFXParticleSystem.ts +++ b/editor/src/editor/windows/fx-editor/VFX/systems/VFXParticleSystem.ts @@ -1,4 +1,4 @@ -import { Color4, ParticleSystem, Scene, Vector3, Matrix, Texture, AbstractMesh } from "babylonjs"; +import { Color4, ParticleSystem, Scene, Vector3, Matrix, Texture, AbstractMesh, TransformNode } from "babylonjs"; import type { VFXPerParticleBehaviorFunction } from "../types/VFXBehaviorFunction"; import type { IVFXSystem, ParticleWithSystem } from "../types/system"; import type { @@ -78,6 +78,16 @@ export class VFXParticleSystem extends ParticleSystem implements IVFXSystem { this._configureFromConfig(config, options); } + /** + * Get the parent node (emitter) for hierarchy operations + * Required by IVFXSystem interface + */ + public getParentNode(): AbstractMesh | TransformNode | null { + // ParticleSystem.emitter can be AbstractMesh, Vector3, or null + // Return emitter if it's an AbstractMesh, otherwise null + return this.emitter instanceof AbstractMesh ? this.emitter : null; + } + /** * Create emitter shape based on VFX shape configuration */ diff --git a/editor/src/editor/windows/fx-editor/VFX/types/resources.ts b/editor/src/editor/windows/fx-editor/VFX/types/resources.ts index 3408a2092..81109ea4f 100644 --- a/editor/src/editor/windows/fx-editor/VFX/types/resources.ts +++ b/editor/src/editor/windows/fx-editor/VFX/types/resources.ts @@ -82,4 +82,3 @@ export interface VFXGeometry { // For BufferGeometry (already converted to left-handed) data?: VFXGeometryData; } - diff --git a/editor/src/editor/windows/fx-editor/VFX/utils/capacityCalculator.ts b/editor/src/editor/windows/fx-editor/VFX/utils/capacityCalculator.ts index 2d8360515..74c27c560 100644 --- a/editor/src/editor/windows/fx-editor/VFX/utils/capacityCalculator.ts +++ b/editor/src/editor/windows/fx-editor/VFX/utils/capacityCalculator.ts @@ -31,4 +31,3 @@ export class VFXCapacityCalculator { } } } - diff --git a/editor/src/editor/windows/fx-editor/VFX/utils/gradientSystem.ts b/editor/src/editor/windows/fx-editor/VFX/utils/gradientSystem.ts index 7bf149576..7f452bbb5 100644 --- a/editor/src/editor/windows/fx-editor/VFX/utils/gradientSystem.ts +++ b/editor/src/editor/windows/fx-editor/VFX/utils/gradientSystem.ts @@ -97,4 +97,3 @@ export class NumberGradientSystem extends GradientSystem { return value1 + (value2 - value1) * t; } } - diff --git a/editor/src/editor/windows/fx-editor/VFX/utils/matrixUtils.ts b/editor/src/editor/windows/fx-editor/VFX/utils/matrixUtils.ts index 15872016d..d437d04c4 100644 --- a/editor/src/editor/windows/fx-editor/VFX/utils/matrixUtils.ts +++ b/editor/src/editor/windows/fx-editor/VFX/utils/matrixUtils.ts @@ -19,4 +19,3 @@ export class VFXMatrixUtils { return mat.getRotationMatrix(); } } - diff --git a/editor/src/editor/windows/fx-editor/properties/particle-initialization.tsx b/editor/src/editor/windows/fx-editor/properties/particle-initialization.tsx index 29c3c51ad..7274461cc 100644 --- a/editor/src/editor/windows/fx-editor/properties/particle-initialization.tsx +++ b/editor/src/editor/windows/fx-editor/properties/particle-initialization.tsx @@ -62,9 +62,7 @@ export function FXEditorParticleInitializationProperties(props: IFXEditorParticl // For now, show that properties exist but need proper VFXValue editors return ( <> -
- Initialization properties are stored as VFXValue. Full editor support coming soon. -
+
Initialization properties are stored as VFXValue. Full editor support coming soon.
{/* TODO: Add VFXValue editors for startLife, startSize, startSpeed, startColor */} ); From a5e93a138ec92a75ce658c4122390a5c34d68c59 Mon Sep 17 00:00:00 2001 From: Mikalai Lazitski Date: Sun, 14 Dec 2025 13:13:45 +0300 Subject: [PATCH 26/62] refactor: enhance ColorOverLife behavior by improving key interpolation and ensuring vertex colors and alpha support in VFXSolidParticleSystem for better visual fidelity --- .../fx-editor/VFX/behaviors/colorOverLife.ts | 157 ++++++++++++++---- .../VFX/systems/VFXSolidParticleSystem.ts | 48 +++++- 2 files changed, 166 insertions(+), 39 deletions(-) diff --git a/editor/src/editor/windows/fx-editor/VFX/behaviors/colorOverLife.ts b/editor/src/editor/windows/fx-editor/VFX/behaviors/colorOverLife.ts index e571c643f..8cac9f9a9 100644 --- a/editor/src/editor/windows/fx-editor/VFX/behaviors/colorOverLife.ts +++ b/editor/src/editor/windows/fx-editor/VFX/behaviors/colorOverLife.ts @@ -40,49 +40,144 @@ export function applyColorOverLifePS(particleSystem: ParticleSystem, behavior: V /** * Apply ColorOverLife behavior to SolidParticleSystem * Adds color gradients to the system (similar to ParticleSystem native gradients) + * Properly combines color and alpha keys even when they have different positions */ export function applyColorOverLifeSPS(system: any, behavior: VFXColorOverLifeBehavior): void { if (!behavior.color) { return; } - // Add color gradients from keys - if (behavior.color.color && behavior.color.color.keys) { - const colorKeys = behavior.color.color.keys; - for (const key of colorKeys) { - if (key.value !== undefined && key.pos !== undefined) { - const color = extractColorFromValue(key.value); - const alpha = extractAlphaFromValue(key.value); - system.addColorGradient(key.pos, new Color4(color.r, color.g, color.b, alpha)); + // Collect all unique positions from both color and alpha keys + const allPositions = new Set(); + + // Get color keys + const colorKeys = behavior.color.color?.keys || behavior.color.keys || []; + for (const key of colorKeys) { + if (key.pos !== undefined) { + allPositions.add(key.pos); + } + } + + // Get alpha keys + const alphaKeys = behavior.color.alpha?.keys || []; + for (const key of alphaKeys) { + const pos = key.pos ?? key.time ?? 0; + allPositions.add(pos); + } + + // If no keys found, return + if (allPositions.size === 0) { + return; + } + + // Sort positions + const sortedPositions = Array.from(allPositions).sort((a, b) => a - b); + + // For each position, compute color and alpha separately + for (const pos of sortedPositions) { + // Find color for this position (interpolate if needed) + let color = { r: 1, g: 1, b: 1 }; + if (colorKeys.length > 0) { + // Find the color key at this position or interpolate + const exactColorKey = colorKeys.find((k) => k.pos !== undefined && Math.abs(k.pos - pos) < 0.001); + if (exactColorKey && exactColorKey.value !== undefined) { + color = extractColorFromValue(exactColorKey.value); + } else { + // Interpolate color from surrounding keys + color = interpolateColorFromKeys(colorKeys, pos); } } - } else if (behavior.color.keys) { - const colorKeys = behavior.color.keys; - for (const key of colorKeys) { - if (key.value !== undefined && key.pos !== undefined) { - const color = extractColorFromValue(key.value); - const alpha = extractAlphaFromValue(key.value); - system.addColorGradient(key.pos, new Color4(color.r, color.g, color.b, alpha)); + + // Find alpha for this position (interpolate if needed) + let alpha = 1; + if (alphaKeys.length > 0) { + const exactAlphaKey = alphaKeys.find((k) => { + const kPos = k.pos ?? k.time ?? 0; + return Math.abs(kPos - pos) < 0.001; + }); + if (exactAlphaKey && exactAlphaKey.value !== undefined) { + alpha = extractAlphaFromValue(exactAlphaKey.value); + } else { + // Interpolate alpha from surrounding keys + alpha = interpolateAlphaFromKeys(alphaKeys, pos); + } + } else if (colorKeys.length > 0) { + // If no alpha keys, try to get alpha from color keys + const exactColorKey = colorKeys.find((k) => k.pos !== undefined && Math.abs(k.pos - pos) < 0.001); + if (exactColorKey && exactColorKey.value !== undefined) { + alpha = extractAlphaFromValue(exactColorKey.value); } } + + // Add gradient with combined color and alpha + system.addColorGradient(pos, new Color4(color.r, color.g, color.b, alpha)); } +} - // Update alpha for existing gradients if alpha keys are specified - if (behavior.color.alpha && behavior.color.alpha.keys) { - const alphaKeys = behavior.color.alpha.keys; - for (const key of alphaKeys) { - if (key.value !== undefined) { - const pos = key.pos ?? key.time ?? 0; - const alpha = extractAlphaFromValue(key.value); - // Get existing gradients and update alpha - const gradients = system._colorGradients.getGradients(); - const existingGradient = gradients.find((g: any) => Math.abs(g.gradient - pos) < 0.001); - if (existingGradient) { - existingGradient.value.a = alpha; - } else { - system.addColorGradient(pos, new Color4(1, 1, 1, alpha)); - } - } +/** + * Interpolate color from gradient keys at given position + */ +function interpolateColorFromKeys(keys: any[], pos: number): { r: number; g: number; b: number } { + if (keys.length === 0) { + return { r: 1, g: 1, b: 1 }; + } + + if (keys.length === 1) { + return extractColorFromValue(keys[0].value); + } + + // Find surrounding keys + for (let i = 0; i < keys.length - 1; i++) { + const pos1 = keys[i].pos ?? 0; + const pos2 = keys[i + 1].pos ?? 1; + + if (pos >= pos1 && pos <= pos2) { + const t = pos2 - pos1 !== 0 ? (pos - pos1) / (pos2 - pos1) : 0; + const c1 = extractColorFromValue(keys[i].value); + const c2 = extractColorFromValue(keys[i + 1].value); + return { + r: c1.r + (c2.r - c1.r) * t, + g: c1.g + (c2.g - c1.g) * t, + b: c1.b + (c2.b - c1.b) * t, + }; + } + } + + // Clamp to first or last + if (pos <= (keys[0].pos ?? 0)) { + return extractColorFromValue(keys[0].value); + } + return extractColorFromValue(keys[keys.length - 1].value); +} + +/** + * Interpolate alpha from gradient keys at given position + */ +function interpolateAlphaFromKeys(keys: any[], pos: number): number { + if (keys.length === 0) { + return 1; + } + + if (keys.length === 1) { + return extractAlphaFromValue(keys[0].value); + } + + // Find surrounding keys + for (let i = 0; i < keys.length - 1; i++) { + const pos1 = keys[i].pos ?? keys[i].time ?? 0; + const pos2 = keys[i + 1].pos ?? keys[i + 1].time ?? 1; + + if (pos >= pos1 && pos <= pos2) { + const t = pos2 - pos1 !== 0 ? (pos - pos1) / (pos2 - pos1) : 0; + const a1 = extractAlphaFromValue(keys[i].value); + const a2 = extractAlphaFromValue(keys[i + 1].value); + return a1 + (a2 - a1) * t; } } + + // Clamp to first or last + if (pos <= (keys[0].pos ?? keys[0].time ?? 0)) { + return extractAlphaFromValue(keys[0].value); + } + return extractAlphaFromValue(keys[keys.length - 1].value); } diff --git a/editor/src/editor/windows/fx-editor/VFX/systems/VFXSolidParticleSystem.ts b/editor/src/editor/windows/fx-editor/VFX/systems/VFXSolidParticleSystem.ts index e07b64191..2e25e2bc2 100644 --- a/editor/src/editor/windows/fx-editor/VFX/systems/VFXSolidParticleSystem.ts +++ b/editor/src/editor/windows/fx-editor/VFX/systems/VFXSolidParticleSystem.ts @@ -190,6 +190,10 @@ export class VFXSolidParticleSystem extends SolidParticleSystem implements IVFXS this.billboard = true; } + // Enable vertex colors and alpha for particle color support + // This must be done after addShape but before buildMesh + // The mesh will be created in buildMesh, so we'll set it there + // Dispose temporary mesh after adding to SPS particleMesh.dispose(); } @@ -567,6 +571,25 @@ export class VFXSolidParticleSystem extends SolidParticleSystem implements IVFXS return VFXValueUtils.parseConstantValue(burst.time); } + /** + * Override buildMesh to enable vertex colors and alpha + * This is required for ColorOverLife behavior to work visually + */ + public override buildMesh(): Mesh { + const mesh = super.buildMesh(); + + // Enable vertex colors and alpha for particle color support + // This is required for ColorOverLife behavior to work + if (mesh) { + mesh.hasVertexAlpha = true; + if (this._logger) { + this._logger.log(`Enabled hasVertexAlpha for SPS mesh: ${mesh.name}`); + } + } + + return mesh; + } + private _setupMeshProperties(): void { if (!this.mesh) { if (this._logger) { @@ -575,6 +598,14 @@ export class VFXSolidParticleSystem extends SolidParticleSystem implements IVFXS return; } + // Ensure vertex alpha is enabled (in case mesh was already built) + if (!this.mesh.hasVertexAlpha) { + this.mesh.hasVertexAlpha = true; + if (this._logger) { + this._logger.log(`Enabled hasVertexAlpha for existing SPS mesh: ${this.mesh.name}`); + } + } + if (this._logger) { this._logger.log(` initParticles called for SPS: ${this._name}`); this._logger.log(` SPS mesh exists: ${this.mesh.name}`); @@ -949,16 +980,17 @@ export class VFXSolidParticleSystem extends SolidParticleSystem implements IVFXS // Apply color gradient const color = this._colorGradients.getValue(lifeRatio); if (color && particle.color) { + // Always apply gradient color directly to particle.color + // The base class will apply this to vertex colors if _computeParticleColor is enabled + particle.color.copyFrom(color); + + // Multiply with startColor if it exists (matching ParticleSystem behavior) const startColor = props.startColor; if (startColor) { - // Multiply with startColor (matching ParticleSystem behavior) - const pColor = particle.color; - pColor.r = color.r * startColor.r; - pColor.g = color.g * startColor.g; - pColor.b = color.b * startColor.b; - pColor.a = color.a * startColor.a; - } else { - particle.color.copyFrom(color); + particle.color.r *= startColor.r; + particle.color.g *= startColor.g; + particle.color.b *= startColor.b; + particle.color.a *= startColor.a; } } From 33f9ab28994c916b9466123ea7f84ff9c370749a Mon Sep 17 00:00:00 2001 From: Mikalai Lazitski Date: Sun, 14 Dec 2025 16:18:04 +0300 Subject: [PATCH 27/62] refactor: enhance VFX behavior and properties by implementing comprehensive rotation and color editors, improving initialization properties handling, and introducing new VFX value types for better flexibility and clarity --- .../fx-editor/VFX/behaviors/sizeOverLife.ts | 19 +- .../fx-editor/VFX/parsers/VFXDataConverter.ts | 1 + .../VFX/systems/VFXSolidParticleSystem.ts | 366 +++++++++++++++ .../windows/fx-editor/VFX/types/colors.ts | 34 +- .../windows/fx-editor/VFX/types/rotations.ts | 15 +- .../properties/particle-initialization.tsx | 265 +++++++++-- .../fx-editor/properties/vfx-color-editor.tsx | 425 ++++++++++++++++++ .../properties/vfx-rotation-editor.tsx | 318 +++++++++++++ .../fx-editor/properties/vfx-value-editor.tsx | 312 +++++++++++++ 9 files changed, 1700 insertions(+), 55 deletions(-) create mode 100644 editor/src/editor/windows/fx-editor/properties/vfx-color-editor.tsx create mode 100644 editor/src/editor/windows/fx-editor/properties/vfx-rotation-editor.tsx create mode 100644 editor/src/editor/windows/fx-editor/properties/vfx-value-editor.tsx diff --git a/editor/src/editor/windows/fx-editor/VFX/behaviors/sizeOverLife.ts b/editor/src/editor/windows/fx-editor/VFX/behaviors/sizeOverLife.ts index dd0e8376c..22b7142a7 100644 --- a/editor/src/editor/windows/fx-editor/VFX/behaviors/sizeOverLife.ts +++ b/editor/src/editor/windows/fx-editor/VFX/behaviors/sizeOverLife.ts @@ -4,25 +4,32 @@ import { extractNumberFromValue } from "./utils"; /** * Apply SizeOverLife behavior to ParticleSystem + * In Quarks, SizeOverLife values are multipliers relative to initial particle size + * In Babylon.js, sizeGradients are absolute values, so we multiply by average initial size */ export function applySizeOverLifePS(particleSystem: ParticleSystem, behavior: VFXSizeOverLifeBehavior): void { + // Get average initial size from minSize/maxSize to use as base for multipliers + const avgInitialSize = (particleSystem.minSize + particleSystem.maxSize) / 2; + if (behavior.size && behavior.size.functions) { const functions = behavior.size.functions; for (const func of functions) { if (func.function && func.start !== undefined) { - const startSize = func.function.p0 || 1; - const endSize = func.function.p3 !== undefined ? func.function.p3 : startSize; - particleSystem.addSizeGradient(func.start, startSize); + // Values from Quarks are multipliers, convert to absolute values + const startSizeMultiplier = func.function.p0 || 1; + const endSizeMultiplier = func.function.p3 !== undefined ? func.function.p3 : startSizeMultiplier; + particleSystem.addSizeGradient(func.start, startSizeMultiplier * avgInitialSize); if (func.function.p3 !== undefined) { - particleSystem.addSizeGradient(func.start + 0.5, endSize); + particleSystem.addSizeGradient(func.start + 0.5, endSizeMultiplier * avgInitialSize); } } } } else if (behavior.size && behavior.size.keys) { for (const key of behavior.size.keys) { if (key.value !== undefined && key.pos !== undefined) { - const size = extractNumberFromValue(key.value); - particleSystem.addSizeGradient(key.pos, size); + // Values from Quarks are multipliers, convert to absolute values + const sizeMultiplier = extractNumberFromValue(key.value); + particleSystem.addSizeGradient(key.pos, sizeMultiplier * avgInitialSize); } } } diff --git a/editor/src/editor/windows/fx-editor/VFX/parsers/VFXDataConverter.ts b/editor/src/editor/windows/fx-editor/VFX/parsers/VFXDataConverter.ts index 8e2aa7adc..7c08c027f 100644 --- a/editor/src/editor/windows/fx-editor/VFX/parsers/VFXDataConverter.ts +++ b/editor/src/editor/windows/fx-editor/VFX/parsers/VFXDataConverter.ts @@ -395,6 +395,7 @@ export class VFXDataConverter { angleX: quarksRotation.angleX !== undefined ? this._convertValue(quarksRotation.angleX) : undefined, angleY: quarksRotation.angleY !== undefined ? this._convertValue(quarksRotation.angleY) : undefined, angleZ: quarksRotation.angleZ !== undefined ? this._convertValue(quarksRotation.angleZ) : undefined, + order: (quarksRotation as any).order || "xyz", // Default to xyz if not specified }; } return this._convertValue(quarksRotation as QuarksValue); diff --git a/editor/src/editor/windows/fx-editor/VFX/systems/VFXSolidParticleSystem.ts b/editor/src/editor/windows/fx-editor/VFX/systems/VFXSolidParticleSystem.ts index 2e25e2bc2..e52572918 100644 --- a/editor/src/editor/windows/fx-editor/VFX/systems/VFXSolidParticleSystem.ts +++ b/editor/src/editor/windows/fx-editor/VFX/systems/VFXSolidParticleSystem.ts @@ -102,6 +102,291 @@ export class VFXSolidParticleSystem extends SolidParticleSystem implements IVFXS } } + /** + * Get/set minSize (compatible with VFXParticleSystem API) + * Works with startSize VFXValue under the hood + */ + public get minSize(): number { + if (!this.startSize) { + return 1; + } + return VFXValueUtils.parseIntervalValue(this.startSize).min; + } + public set minSize(value: number) { + if (!this.startSize) { + this.startSize = { type: "IntervalValue", min: value, max: value }; + return; + } + if (typeof this.startSize === "number") { + this.startSize = { type: "IntervalValue", min: value, max: this.startSize }; + return; + } + if (this.startSize.type === "ConstantValue") { + this.startSize = { type: "IntervalValue", min: value, max: this.startSize.value }; + return; + } + if (this.startSize.type === "IntervalValue") { + this.startSize.min = value; + return; + } + // For PiecewiseBezier, convert to IntervalValue + this.startSize = { type: "IntervalValue", min: value, max: value }; + } + + /** + * Get/set maxSize (compatible with VFXParticleSystem API) + * Works with startSize VFXValue under the hood + */ + public get maxSize(): number { + if (!this.startSize) { + return 1; + } + return VFXValueUtils.parseIntervalValue(this.startSize).max; + } + public set maxSize(value: number) { + if (!this.startSize) { + this.startSize = { type: "IntervalValue", min: value, max: value }; + return; + } + if (typeof this.startSize === "number") { + this.startSize = { type: "IntervalValue", min: this.startSize, max: value }; + return; + } + if (this.startSize.type === "ConstantValue") { + this.startSize = { type: "IntervalValue", min: this.startSize.value, max: value }; + return; + } + if (this.startSize.type === "IntervalValue") { + this.startSize.max = value; + return; + } + // For PiecewiseBezier, convert to IntervalValue + this.startSize = { type: "IntervalValue", min: value, max: value }; + } + + /** + * Get/set minLifeTime (compatible with VFXParticleSystem API) + * Works with startLife VFXValue under the hood + */ + public get minLifeTime(): number { + if (!this.startLife) { + return 1; + } + return VFXValueUtils.parseIntervalValue(this.startLife).min; + } + public set minLifeTime(value: number) { + if (!this.startLife) { + this.startLife = { type: "IntervalValue", min: value, max: value }; + return; + } + if (typeof this.startLife === "number") { + this.startLife = { type: "IntervalValue", min: value, max: this.startLife }; + return; + } + if (this.startLife.type === "ConstantValue") { + this.startLife = { type: "IntervalValue", min: value, max: this.startLife.value }; + return; + } + if (this.startLife.type === "IntervalValue") { + this.startLife.min = value; + return; + } + // For PiecewiseBezier, convert to IntervalValue + this.startLife = { type: "IntervalValue", min: value, max: value }; + } + + /** + * Get/set maxLifeTime (compatible with VFXParticleSystem API) + * Works with startLife VFXValue under the hood + */ + public get maxLifeTime(): number { + if (!this.startLife) { + return 1; + } + return VFXValueUtils.parseIntervalValue(this.startLife).max; + } + public set maxLifeTime(value: number) { + if (!this.startLife) { + this.startLife = { type: "IntervalValue", min: value, max: value }; + return; + } + if (typeof this.startLife === "number") { + this.startLife = { type: "IntervalValue", min: this.startLife, max: value }; + return; + } + if (this.startLife.type === "ConstantValue") { + this.startLife = { type: "IntervalValue", min: this.startLife.value, max: value }; + return; + } + if (this.startLife.type === "IntervalValue") { + this.startLife.max = value; + return; + } + // For PiecewiseBezier, convert to IntervalValue + this.startLife = { type: "IntervalValue", min: value, max: value }; + } + + /** + * Get/set minEmitPower (compatible with VFXParticleSystem API) + * Works with startSpeed VFXValue under the hood + */ + public get minEmitPower(): number { + if (!this.startSpeed) { + return 1; + } + return VFXValueUtils.parseIntervalValue(this.startSpeed).min; + } + public set minEmitPower(value: number) { + if (!this.startSpeed) { + this.startSpeed = { type: "IntervalValue", min: value, max: value }; + return; + } + if (typeof this.startSpeed === "number") { + this.startSpeed = { type: "IntervalValue", min: value, max: this.startSpeed }; + return; + } + if (this.startSpeed.type === "ConstantValue") { + this.startSpeed = { type: "IntervalValue", min: value, max: this.startSpeed.value }; + return; + } + if (this.startSpeed.type === "IntervalValue") { + this.startSpeed.min = value; + return; + } + // For PiecewiseBezier, convert to IntervalValue + this.startSpeed = { type: "IntervalValue", min: value, max: value }; + } + + /** + * Get/set maxEmitPower (compatible with VFXParticleSystem API) + * Works with startSpeed VFXValue under the hood + */ + public get maxEmitPower(): number { + if (!this.startSpeed) { + return 1; + } + return VFXValueUtils.parseIntervalValue(this.startSpeed).max; + } + public set maxEmitPower(value: number) { + if (!this.startSpeed) { + this.startSpeed = { type: "IntervalValue", min: value, max: value }; + return; + } + if (typeof this.startSpeed === "number") { + this.startSpeed = { type: "IntervalValue", min: this.startSpeed, max: value }; + return; + } + if (this.startSpeed.type === "ConstantValue") { + this.startSpeed = { type: "IntervalValue", min: this.startSpeed.value, max: value }; + return; + } + if (this.startSpeed.type === "IntervalValue") { + this.startSpeed.max = value; + return; + } + // For PiecewiseBezier, convert to IntervalValue + this.startSpeed = { type: "IntervalValue", min: value, max: value }; + } + + /** + * Get/set color1 (compatible with VFXParticleSystem API) + * Works with startColor VFXColor under the hood + */ + public get color1(): Color4 { + if (!this.startColor) { + return new Color4(1, 1, 1, 1); + } + return VFXValueUtils.parseConstantColor(this.startColor); + } + public set color1(value: Color4) { + this.startColor = { + type: "ConstantColor", + value: [value.r, value.g, value.b, value.a], + }; + } + + /** + * Get/set minInitialRotation (compatible with VFXParticleSystem API) + * Works with startRotation VFXRotation under the hood (uses angleZ) + */ + public get minInitialRotation(): number { + if (!this.startRotation) { + return 0; + } + // Handle Euler rotation with angleZ + if (typeof this.startRotation === "object" && "type" in this.startRotation && this.startRotation.type === "Euler") { + if (this.startRotation.angleZ) { + return VFXValueUtils.parseIntervalValue(this.startRotation.angleZ).min; + } + return 0; + } + // Handle simple VFXValue rotation + if (typeof this.startRotation === "object" && "type" in this.startRotation) { + return VFXValueUtils.parseIntervalValue(this.startRotation as any).min; + } + return typeof this.startRotation === "number" ? this.startRotation : 0; + } + public set minInitialRotation(value: number) { + if (!this.startRotation) { + this.startRotation = { type: "Euler", angleZ: { type: "IntervalValue", min: value, max: value } }; + return; + } + // Handle Euler rotation + if (typeof this.startRotation === "object" && "type" in this.startRotation && this.startRotation.type === "Euler") { + if (!this.startRotation.angleZ) { + this.startRotation.angleZ = { type: "IntervalValue", min: value, max: value }; + } else { + const currentMax = VFXValueUtils.parseIntervalValue(this.startRotation.angleZ).max; + this.startRotation.angleZ = { type: "IntervalValue", min: value, max: currentMax }; + } + return; + } + // Convert to Euler rotation + const currentMax = this.maxInitialRotation; + this.startRotation = { type: "Euler", angleZ: { type: "IntervalValue", min: value, max: currentMax } }; + } + + /** + * Get/set maxInitialRotation (compatible with VFXParticleSystem API) + * Works with startRotation VFXRotation under the hood (uses angleZ) + */ + public get maxInitialRotation(): number { + if (!this.startRotation) { + return 0; + } + // Handle Euler rotation with angleZ + if (typeof this.startRotation === "object" && "type" in this.startRotation && this.startRotation.type === "Euler") { + if (this.startRotation.angleZ) { + return VFXValueUtils.parseIntervalValue(this.startRotation.angleZ).max; + } + return 0; + } + // Handle simple VFXValue rotation + if (typeof this.startRotation === "object" && "type" in this.startRotation) { + return VFXValueUtils.parseIntervalValue(this.startRotation as any).max; + } + return typeof this.startRotation === "number" ? this.startRotation : 0; + } + public set maxInitialRotation(value: number) { + if (!this.startRotation) { + this.startRotation = { type: "Euler", angleZ: { type: "IntervalValue", min: value, max: value } }; + return; + } + // Handle Euler rotation + if (typeof this.startRotation === "object" && "type" in this.startRotation && this.startRotation.type === "Euler") { + if (!this.startRotation.angleZ) { + this.startRotation.angleZ = { type: "IntervalValue", min: value, max: value }; + } else { + const currentMin = VFXValueUtils.parseIntervalValue(this.startRotation.angleZ).min; + this.startRotation.angleZ = { type: "IntervalValue", min: currentMin, max: value }; + } + return; + } + // Convert to Euler rotation + const currentMin = this.minInitialRotation; + this.startRotation = { type: "Euler", angleZ: { type: "IntervalValue", min: currentMin, max: value } }; + } + /** * Get the parent node (mesh) for hierarchy operations * Implements IVFXSystem interface @@ -427,6 +712,86 @@ export class VFXSolidParticleSystem extends SolidParticleSystem implements IVFXS } } + /** + * Initialize particle rotation + * Supports Euler, AxisAngle, and RandomQuat rotation types + */ + private _initializeParticleRotation(particle: SolidParticle, normalizedTime: number): void { + if (!this.startRotation) { + particle.rotation.setAll(0); + return; + } + + // Handle simple VFXValue (treat as angleZ for backward compatibility) + if (typeof this.startRotation === "number" || (typeof this.startRotation === "object" && "type" in this.startRotation && (this.startRotation.type === "ConstantValue" || this.startRotation.type === "IntervalValue" || this.startRotation.type === "PiecewiseBezier"))) { + const angleZ = VFXValueUtils.parseValue(this.startRotation as VFXValue, normalizedTime); + particle.rotation.set(0, 0, angleZ); + return; + } + + // Handle Euler rotation + if (this.startRotation.type === "Euler") { + const angleX = this.startRotation.angleX ? VFXValueUtils.parseValue(this.startRotation.angleX, normalizedTime) : 0; + const angleY = this.startRotation.angleY ? VFXValueUtils.parseValue(this.startRotation.angleY, normalizedTime) : 0; + const angleZ = this.startRotation.angleZ ? VFXValueUtils.parseValue(this.startRotation.angleZ, normalizedTime) : 0; + const order = this.startRotation.order || "xyz"; + + // Convert Euler angles to quaternion based on order + let quat: Quaternion; + if (order === "xyz") { + // XYZ order: apply X, then Y, then Z + quat = Quaternion.RotationYawPitchRoll(angleY, angleX, angleZ); + } else { + // ZYX order: apply Z, then Y, then X + const quatZ = Quaternion.RotationAxis(Vector3.Forward(), angleZ); + const quatY = Quaternion.RotationAxis(Vector3.Up(), angleY); + const quatX = Quaternion.RotationAxis(Vector3.Right(), angleX); + quat = quatZ.multiply(quatY).multiply(quatX); + } + // Convert quaternion to Euler for particle.rotation (Vector3) + const euler = quat.toEulerAngles(); + particle.rotation.set(euler.x, euler.y, euler.z); + return; + } + + // Handle AxisAngle rotation + if (this.startRotation.type === "AxisAngle") { + const axisX = this.startRotation.x ? VFXValueUtils.parseValue(this.startRotation.x, normalizedTime) : 0; + const axisY = this.startRotation.y ? VFXValueUtils.parseValue(this.startRotation.y, normalizedTime) : 0; + const axisZ = this.startRotation.z ? VFXValueUtils.parseValue(this.startRotation.z, normalizedTime) : 1; + const angle = this.startRotation.angle ? VFXValueUtils.parseValue(this.startRotation.angle, normalizedTime) : 0; + + const axis = new Vector3(axisX, axisY, axisZ); + axis.normalize(); + const quat = Quaternion.RotationAxis(axis, angle); + const euler = quat.toEulerAngles(); + particle.rotation.set(euler.x, euler.y, euler.z); + return; + } + + // Handle RandomQuat rotation + if (this.startRotation.type === "RandomQuat") { + // Generate random quaternion (uniform distribution on unit sphere) + const u1 = Math.random(); + const u2 = Math.random(); + const u3 = Math.random(); + const sqrt1MinusU1 = Math.sqrt(1 - u1); + const sqrtU1 = Math.sqrt(u1); + const quat = new Quaternion( + sqrt1MinusU1 * Math.sin(2 * Math.PI * u2), + sqrt1MinusU1 * Math.cos(2 * Math.PI * u2), + sqrtU1 * Math.sin(2 * Math.PI * u3), + sqrtU1 * Math.cos(2 * Math.PI * u3) + ); + const euler = quat.toEulerAngles(); + particle.rotation.set(euler.x, euler.y, euler.z); + return; + } + + // Fallback: no rotation + particle.rotation.setAll(0); + } + /** * Spawn particles from dead pool * Оптимизировано: вычисляем матрицу эмиттера один раз для всех частиц @@ -467,6 +832,7 @@ export class VFXSolidParticleSystem extends SolidParticleSystem implements IVFXS this._initializeParticleSpeed(particle, normalizedTime); this._initializeParticleLife(particle, normalizedTime); this._initializeParticleSize(particle, normalizedTime); + this._initializeParticleRotation(particle, normalizedTime); this._initializeEmitterShape(particle); } } diff --git a/editor/src/editor/windows/fx-editor/VFX/types/colors.ts b/editor/src/editor/windows/fx-editor/VFX/types/colors.ts index 0e15e144e..02b189afe 100644 --- a/editor/src/editor/windows/fx-editor/VFX/types/colors.ts +++ b/editor/src/editor/windows/fx-editor/VFX/types/colors.ts @@ -1,3 +1,5 @@ +import type { VFXGradientKey } from "./gradients"; + /** * VFX color types (converted from Quarks) */ @@ -6,4 +8,34 @@ export interface VFXConstantColor { value: [number, number, number, number]; // RGBA } -export type VFXColor = VFXConstantColor | [number, number, number, number] | string; +export interface VFXColorRange { + type: "ColorRange"; + colorA: [number, number, number, number]; // RGBA + colorB: [number, number, number, number]; // RGBA +} + +export interface VFXGradientColor { + type: "Gradient"; + colorKeys: VFXGradientKey[]; + alphaKeys?: VFXGradientKey[]; +} + +export interface VFXRandomColor { + type: "RandomColor"; + colorA: [number, number, number, number]; // RGBA + colorB: [number, number, number, number]; // RGBA +} + +export interface VFXRandomColorBetweenGradient { + type: "RandomColorBetweenGradient"; + gradient1: { + colorKeys: VFXGradientKey[]; + alphaKeys?: VFXGradientKey[]; + }; + gradient2: { + colorKeys: VFXGradientKey[]; + alphaKeys?: VFXGradientKey[]; + }; +} + +export type VFXColor = VFXConstantColor | VFXColorRange | VFXGradientColor | VFXRandomColor | VFXRandomColorBetweenGradient | [number, number, number, number] | string; diff --git a/editor/src/editor/windows/fx-editor/VFX/types/rotations.ts b/editor/src/editor/windows/fx-editor/VFX/types/rotations.ts index 703c3b2f8..2eca7d7e7 100644 --- a/editor/src/editor/windows/fx-editor/VFX/types/rotations.ts +++ b/editor/src/editor/windows/fx-editor/VFX/types/rotations.ts @@ -8,6 +8,19 @@ export interface VFXEulerRotation { angleX?: VFXValue; angleY?: VFXValue; angleZ?: VFXValue; + order?: "xyz" | "zyx"; } -export type VFXRotation = VFXEulerRotation | VFXValue; +export interface VFXAxisAngleRotation { + type: "AxisAngle"; + x?: VFXValue; + y?: VFXValue; + z?: VFXValue; + angle?: VFXValue; +} + +export interface VFXRandomQuatRotation { + type: "RandomQuat"; +} + +export type VFXRotation = VFXEulerRotation | VFXAxisAngleRotation | VFXRandomQuatRotation | VFXValue; diff --git a/editor/src/editor/windows/fx-editor/properties/particle-initialization.tsx b/editor/src/editor/windows/fx-editor/properties/particle-initialization.tsx index 7274461cc..e0abf1d87 100644 --- a/editor/src/editor/windows/fx-editor/properties/particle-initialization.tsx +++ b/editor/src/editor/windows/fx-editor/properties/particle-initialization.tsx @@ -1,11 +1,16 @@ import { ReactNode } from "react"; import { EditorInspectorBlockField } from "../../../layout/inspector/fields/block"; -import { EditorInspectorNumberField } from "../../../layout/inspector/fields/number"; -import { EditorInspectorColorField } from "../../../layout/inspector/fields/color"; import type { VFXEffectNode } from "../VFX"; import { VFXParticleSystem, VFXSolidParticleSystem } from "../VFX"; +import { VFXValueEditor, type IVec3Function } from "./vfx-value-editor"; +import { VFXColorEditor } from "./vfx-color-editor"; +import { VFXRotationEditor } from "./vfx-rotation-editor"; +import type { VFXValue } from "../VFX/types/values"; +import type { VFXColor } from "../VFX/types/colors"; +import type { VFXRotation } from "../VFX/types/rotations"; +import { VFXValueUtils } from "../VFX/utils/valueParser"; export interface IFXEditorParticleInitializationPropertiesProps { nodeData: VFXEffectNode; @@ -22,51 +27,217 @@ export function FXEditorParticleInitializationProperties(props: IFXEditorParticl const system = nodeData.system; - // For VFXParticleSystem, show initialization properties - if (system instanceof VFXParticleSystem) { - return ( - <> - -
Life Time
-
- - -
-
- -
Size
-
- - -
-
- -
Speed (Emit Power)
-
- - -
-
- - {system instanceof VFXParticleSystem && system.startColor && ( - - )} - {/* TODO: Add rotation properties */} - - ); - } + // Helper to get/set startLife as VFXValue for both systems + const getStartLife = (): VFXValue | undefined => { + if (system instanceof VFXSolidParticleSystem) { + return system.startLife; + } + // For VFXParticleSystem, convert minLifeTime/maxLifeTime to IntervalValue + if (system instanceof VFXParticleSystem) { + return { type: "IntervalValue", min: system.minLifeTime, max: system.maxLifeTime }; + } + return undefined; + }; - // For VFXSolidParticleSystem, initialization properties are VFXValue format - // TODO: Add proper editors for VFXValue (ConstantValue, IntervalValue, etc.) - if (system instanceof VFXSolidParticleSystem) { - // For now, show that properties exist but need proper VFXValue editors - return ( - <> -
Initialization properties are stored as VFXValue. Full editor support coming soon.
- {/* TODO: Add VFXValue editors for startLife, startSize, startSpeed, startColor */} - - ); - } + const setStartLife = (value: VFXValue): void => { + if (system instanceof VFXSolidParticleSystem) { + system.startLife = value; + } else if (system instanceof VFXParticleSystem) { + const interval = VFXValueUtils.parseIntervalValue(value); + system.minLifeTime = interval.min; + system.maxLifeTime = interval.max; + } + onChange(); + }; + + // Helper to get/set startSize as VFXValue | IVec3Function for both systems + const getStartSize = (): VFXValue | IVec3Function | undefined => { + if (system instanceof VFXSolidParticleSystem) { + return system.startSize; + } + // For VFXParticleSystem, convert minSize/maxSize to IntervalValue + if (system instanceof VFXParticleSystem) { + return { type: "IntervalValue", min: system.minSize, max: system.maxSize }; + } + return undefined; + }; + + const setStartSize = (value: VFXValue | IVec3Function): void => { + if (system instanceof VFXSolidParticleSystem) { + // For Vec3Function, we need to handle it differently - but VFXSolidParticleSystem doesn't support Vec3Function yet + // For now, convert Vec3Function to a single value + if (typeof value === "object" && "type" in value && value.type === "Vec3Function") { + const x = VFXValueUtils.parseConstantValue(value.x); + const y = VFXValueUtils.parseConstantValue(value.y); + const z = VFXValueUtils.parseConstantValue(value.z); + const avg = (x + y + z) / 3; + system.startSize = { type: "ConstantValue", value: avg }; + } else { + system.startSize = value as VFXValue; + } + } else if (system instanceof VFXParticleSystem) { + if (typeof value === "object" && "type" in value && value.type === "Vec3Function") { + // For Vec3Function, use average of x, y, z + const x = VFXValueUtils.parseConstantValue(value.x); + const y = VFXValueUtils.parseConstantValue(value.y); + const z = VFXValueUtils.parseConstantValue(value.z); + const avg = (x + y + z) / 3; + system.minSize = avg; + system.maxSize = avg; + } else { + const interval = VFXValueUtils.parseIntervalValue(value as VFXValue); + system.minSize = interval.min; + system.maxSize = interval.max; + } + } + onChange(); + }; + + // Helper to get/set startSpeed as VFXValue for both systems + const getStartSpeed = (): VFXValue | undefined => { + if (system instanceof VFXSolidParticleSystem) { + return system.startSpeed; + } + // For VFXParticleSystem, convert minEmitPower/maxEmitPower to IntervalValue + if (system instanceof VFXParticleSystem) { + return { type: "IntervalValue", min: system.minEmitPower, max: system.maxEmitPower }; + } + return undefined; + }; + + const setStartSpeed = (value: VFXValue): void => { + if (system instanceof VFXSolidParticleSystem) { + system.startSpeed = value; + } else if (system instanceof VFXParticleSystem) { + const interval = VFXValueUtils.parseIntervalValue(value); + system.minEmitPower = interval.min; + system.maxEmitPower = interval.max; + } + onChange(); + }; + + // Helper to get/set startColor as VFXColor for both systems + const getStartColor = (): VFXColor | undefined => { + if (system instanceof VFXSolidParticleSystem) { + return system.startColor; + } + // For VFXParticleSystem, convert Color4 to ConstantColor + if (system instanceof VFXParticleSystem && system.color1) { + return { type: "ConstantColor", value: [system.color1.r, system.color1.g, system.color1.b, system.color1.a] }; + } + return undefined; + }; - return null; + const setStartColor = (value: VFXColor): void => { + if (system instanceof VFXSolidParticleSystem) { + system.startColor = value; + } else if (system instanceof VFXParticleSystem) { + const color = VFXValueUtils.parseConstantColor(value); + system.color1 = color; + } + onChange(); + }; + + // Helper to get/set startRotation as VFXRotation for both systems + const getStartRotation = (): VFXRotation | undefined => { + if (system instanceof VFXSolidParticleSystem) { + return system.startRotation; + } + // For VFXParticleSystem, convert minInitialRotation/maxInitialRotation to Euler with angleZ + if (system instanceof VFXParticleSystem) { + return { + type: "Euler", + angleZ: { type: "IntervalValue", min: system.minInitialRotation, max: system.maxInitialRotation }, + order: "xyz", + }; + } + return undefined; + }; + + const setStartRotation = (value: VFXRotation): void => { + if (system instanceof VFXSolidParticleSystem) { + system.startRotation = value; + } else if (system instanceof VFXParticleSystem) { + // Extract angleZ from rotation for VFXParticleSystem + if (typeof value === "object" && "type" in value && value.type === "Euler" && value.angleZ) { + const interval = VFXValueUtils.parseIntervalValue(value.angleZ); + system.minInitialRotation = interval.min; + system.maxInitialRotation = interval.max; + } else if ( + typeof value === "number" || + (typeof value === "object" && "type" in value && (value.type === "ConstantValue" || value.type === "IntervalValue" || value.type === "PiecewiseBezier")) + ) { + const interval = VFXValueUtils.parseIntervalValue(value as VFXValue); + system.minInitialRotation = interval.min; + system.maxInitialRotation = interval.max; + } + } + onChange(); + }; + + return ( + <> + +
Start Life
+ +
+ + +
Start Size
+ +
+ + +
Start Speed
+ +
+ + +
Start Color
+ +
+ + +
Start Rotation
+ {system instanceof VFXSolidParticleSystem ? ( + + ) : ( + (() => { + // For VFXParticleSystem, extract angleZ from rotation + const rotation = getStartRotation(); + const angleZ = + rotation && typeof rotation === "object" && "type" in rotation && rotation.type === "Euler" && rotation.angleZ + ? rotation.angleZ + : rotation && + (typeof rotation === "number" || + (typeof rotation === "object" && + "type" in rotation && + (rotation.type === "ConstantValue" || rotation.type === "IntervalValue" || rotation.type === "PiecewiseBezier"))) + ? (rotation as VFXValue) + : { type: "IntervalValue" as const, min: 0, max: 0 }; + return ( + { + setStartRotation({ + type: "Euler", + angleZ: newAngleZ as VFXValue, + order: "xyz", + }); + }} + availableTypes={["ConstantValue", "IntervalValue", "PiecewiseBezier"]} + step={0.1} + /> + ); + })() + )} +
+ + ); } diff --git a/editor/src/editor/windows/fx-editor/properties/vfx-color-editor.tsx b/editor/src/editor/windows/fx-editor/properties/vfx-color-editor.tsx new file mode 100644 index 000000000..fbde88784 --- /dev/null +++ b/editor/src/editor/windows/fx-editor/properties/vfx-color-editor.tsx @@ -0,0 +1,425 @@ +import { ReactNode } from "react"; +import { Color4, Vector3, Color3 } from "babylonjs"; + +import { EditorInspectorColorField } from "../../../layout/inspector/fields/color"; +import { EditorInspectorListField } from "../../../layout/inspector/fields/list"; +import { EditorInspectorBlockField } from "../../../layout/inspector/fields/block"; +import { EditorInspectorNumberField } from "../../../layout/inspector/fields/number"; + +import { Button } from "../../../../ui/shadcn/ui/button"; +import { AiOutlinePlus, AiOutlineClose } from "react-icons/ai"; +import { Slider } from "../../../../ui/shadcn/ui/slider"; + +import type { VFXColor, VFXConstantColor, VFXColorRange, VFXGradientColor, VFXRandomColor, VFXRandomColorBetweenGradient } from "../VFX/types/colors"; +import type { VFXGradientKey } from "../VFX/types/gradients"; +import { VFXValueUtils } from "../VFX/utils/valueParser"; + +export type VFXColorType = "ConstantColor" | "ColorRange" | "Gradient" | "RandomColor" | "RandomColorBetweenGradient"; + +export interface IVFXColorEditorProps { + value: VFXColor | undefined; + onChange: (newValue: VFXColor) => void; + label?: string; +} + +/** + * Editor for VFXColor (ConstantColor, ColorRange, Gradient, RandomColor, RandomColorBetweenGradient) + * Works directly with VFXColor types, not wrappers + */ +export function VFXColorEditor(props: IVFXColorEditorProps): ReactNode { + const { value, onChange, label } = props; + + // Determine current type from value + let currentType: VFXColorType = "ConstantColor"; + if (value) { + if (typeof value === "string" || Array.isArray(value)) { + currentType = "ConstantColor"; + } else if ("type" in value) { + if (value.type === "ConstantColor") { + currentType = "ConstantColor"; + } else if (value.type === "ColorRange") { + currentType = "ColorRange"; + } else if (value.type === "Gradient") { + currentType = "Gradient"; + } else if (value.type === "RandomColor") { + currentType = "RandomColor"; + } else if (value.type === "RandomColorBetweenGradient") { + currentType = "RandomColorBetweenGradient"; + } + } + } + + const typeItems = [ + { text: "Color", value: "ConstantColor" }, + { text: "Color Range", value: "ColorRange" }, + { text: "Gradient", value: "Gradient" }, + { text: "Random Color", value: "RandomColor" }, + { text: "Random Between Gradient", value: "RandomColorBetweenGradient" }, + ]; + + // Wrapper object for EditorInspectorListField + const wrapper = { + get type() { + return currentType; + }, + set type(newType: VFXColorType) { + currentType = newType; + // Convert value to new type + let newValue: VFXColor; + const currentColor = value ? VFXValueUtils.parseConstantColor(value) : new Color4(1, 1, 1, 1); + if (newType === "ConstantColor") { + newValue = { type: "ConstantColor", value: [currentColor.r, currentColor.g, currentColor.b, currentColor.a] }; + } else if (newType === "ColorRange") { + newValue = { + type: "ColorRange", + colorA: [currentColor.r, currentColor.g, currentColor.b, currentColor.a], + colorB: [1, 1, 1, 1], + }; + } else if (newType === "Gradient") { + newValue = { + type: "Gradient", + colorKeys: [ + { pos: 0, value: [currentColor.r, currentColor.g, currentColor.b, currentColor.a] }, + { pos: 1, value: [1, 1, 1, 1] }, + ], + alphaKeys: [ + { pos: 0, value: currentColor.a }, + { pos: 1, value: 1 }, + ], + }; + } else if (newType === "RandomColor") { + newValue = { + type: "RandomColor", + colorA: [currentColor.r, currentColor.g, currentColor.b, currentColor.a], + colorB: [1, 1, 1, 1], + }; + } else { + // RandomColorBetweenGradient + newValue = { + type: "RandomColorBetweenGradient", + gradient1: { + colorKeys: [ + { pos: 0, value: [currentColor.r, currentColor.g, currentColor.b, currentColor.a] }, + { pos: 1, value: [1, 1, 1, 1] }, + ], + alphaKeys: [ + { pos: 0, value: currentColor.a }, + { pos: 1, value: 1 }, + ], + }, + gradient2: { + colorKeys: [ + { pos: 0, value: [1, 0, 0, 1] }, + { pos: 1, value: [0, 1, 0, 1] }, + ], + alphaKeys: [ + { pos: 0, value: 1 }, + { pos: 1, value: 1 }, + ], + }, + }; + } + onChange(newValue); + }, + }; + + return ( + <> + { + // Type change is handled by setter + }} + /> + + {currentType === "ConstantColor" && ( + <> + {(() => { + const constantColor = value ? VFXValueUtils.parseConstantColor(value) : new Color4(1, 1, 1, 1); + const wrapperColor = { + get color() { + return constantColor; + }, + set color(newColor: Color4) { + onChange({ type: "ConstantColor", value: [newColor.r, newColor.g, newColor.b, newColor.a] }); + }, + }; + return {}} />; + })()} + + )} + + {currentType === "ColorRange" && ( + <> + {(() => { + const colorRange = value && typeof value === "object" && "type" in value && value.type === "ColorRange" ? value : null; + const colorA = colorRange ? new Color4(colorRange.colorA[0], colorRange.colorA[1], colorRange.colorA[2], colorRange.colorA[3]) : new Color4(0, 0, 0, 1); + const colorB = colorRange ? new Color4(colorRange.colorB[0], colorRange.colorB[1], colorRange.colorB[2], colorRange.colorB[3]) : new Color4(1, 1, 1, 1); + const wrapperRange = { + get colorA() { + return colorA; + }, + set colorA(newColor: Color4) { + const currentB = colorRange ? colorRange.colorB : [1, 1, 1, 1]; + onChange({ type: "ColorRange", colorA: [newColor.r, newColor.g, newColor.b, newColor.a], colorB: currentB }); + }, + get colorB() { + return colorB; + }, + set colorB(newColor: Color4) { + const currentA = colorRange ? colorRange.colorA : [0, 0, 0, 1]; + onChange({ type: "ColorRange", colorA: currentA, colorB: [newColor.r, newColor.g, newColor.b, newColor.a] }); + }, + }; + return ( + <> + {}} /> + {}} /> + + ); + })()} + + )} + + {currentType === "Gradient" && } + + {currentType === "RandomColor" && ( + <> + {(() => { + const randomColor = value && typeof value === "object" && "type" in value && value.type === "RandomColor" ? value : null; + const colorA = randomColor ? new Color4(randomColor.colorA[0], randomColor.colorA[1], randomColor.colorA[2], randomColor.colorA[3]) : new Color4(0, 0, 0, 1); + const colorB = randomColor ? new Color4(randomColor.colorB[0], randomColor.colorB[1], randomColor.colorB[2], randomColor.colorB[3]) : new Color4(1, 1, 1, 1); + const wrapperRandom = { + get colorA() { + return colorA; + }, + set colorA(newColor: Color4) { + const currentB = randomColor ? randomColor.colorB : [1, 1, 1, 1]; + onChange({ type: "RandomColor", colorA: [newColor.r, newColor.g, newColor.b, newColor.a], colorB: currentB }); + }, + get colorB() { + return colorB; + }, + set colorB(newColor: Color4) { + const currentA = randomColor ? randomColor.colorA : [0, 0, 0, 1]; + onChange({ type: "RandomColor", colorA: currentA, colorB: [newColor.r, newColor.g, newColor.b, newColor.a] }); + }, + }; + return ( + <> + {}} /> + {}} /> + + ); + })()} + + )} + + {currentType === "RandomColorBetweenGradient" && ( + <> + {(() => { + const randomGradient = value && typeof value === "object" && "type" in value && value.type === "RandomColorBetweenGradient" ? value : null; + return ( + <> + +
Gradient 1
+ { + if (randomGradient) { + onChange({ + type: "RandomColorBetweenGradient", + gradient1: { + colorKeys: newGradient.type === "Gradient" ? newGradient.colorKeys : [], + alphaKeys: newGradient.type === "Gradient" ? newGradient.alphaKeys : [], + }, + gradient2: randomGradient.gradient2, + }); + } + }} + /> +
+ +
Gradient 2
+ { + if (randomGradient) { + onChange({ + type: "RandomColorBetweenGradient", + gradient1: randomGradient.gradient1, + gradient2: { + colorKeys: newGradient.type === "Gradient" ? newGradient.colorKeys : [], + alphaKeys: newGradient.type === "Gradient" ? newGradient.alphaKeys : [], + }, + }); + } + }} + /> +
+ + ); + })()} + + )} + + ); +} + +interface IGradientEditorProps { + value: VFXGradientColor | null; + onChange: (newValue: VFXGradientColor) => void; +} + +function GradientEditor(props: IGradientEditorProps): ReactNode { + const { value, onChange } = props; + + // Initialize gradient data + const colorKeys = value?.colorKeys || [ + { pos: 0, value: [0, 0, 0, 1] }, + { pos: 1, value: [1, 1, 1, 1] }, + ]; + const alphaKeys = value?.alphaKeys || [ + { pos: 0, value: 1 }, + { pos: 1, value: 1 }, + ]; + + const updateGradient = (newColorKeys: VFXGradientKey[], newAlphaKeys?: VFXGradientKey[]) => { + onChange({ + type: "Gradient", + colorKeys: newColorKeys, + alphaKeys: newAlphaKeys || alphaKeys, + }); + }; + + return ( +
+
Color Keys
+ {colorKeys.map((key, index) => { + // Convert value to Color3 for color picker + const colorValue = Array.isArray(key.value) ? key.value : typeof key.value === "number" ? [key.value, key.value, key.value] : [key.value?.r || 0, key.value?.g || 0, key.value?.b || 0]; + const color3 = new Color3(colorValue[0], colorValue[1], colorValue[2]); + const alpha = Array.isArray(key.value) && key.value.length > 3 ? key.value[3] : 1; + + return ( + +
+
+ { + const newColorKeys = [...colorKeys]; + newColorKeys[index] = { ...key, value: [color.r, color.g, color.b, alpha] }; + updateGradient(newColorKeys); + }} + /> +
+
+ { + const newColorKeys = [...colorKeys]; + newColorKeys[index] = { ...key, pos: vals[0] }; + updateGradient(newColorKeys); + }} + /> +
+ +
+
+ ); + })} + + +
Alpha Keys
+ {alphaKeys.map((key, index) => ( + +
+
+ {(() => { + const alphaValue = typeof key.value === "number" ? key.value : Array.isArray(key.value) ? key.value[3] || 1 : 1; + const wrapperAlpha = { + get value() { + return alphaValue; + }, + set value(newVal: number) { + const newAlphaKeys = [...alphaKeys]; + newAlphaKeys[index] = { ...key, value: newVal }; + updateGradient(colorKeys, newAlphaKeys); + }, + }; + return {}} />; + })()} +
+
+ { + const newAlphaKeys = [...alphaKeys]; + newAlphaKeys[index] = { ...key, pos: vals[0] }; + updateGradient(colorKeys, newAlphaKeys); + }} + /> +
+ +
+
+ ))} + +
+ ); +} + diff --git a/editor/src/editor/windows/fx-editor/properties/vfx-rotation-editor.tsx b/editor/src/editor/windows/fx-editor/properties/vfx-rotation-editor.tsx new file mode 100644 index 000000000..5b079fe59 --- /dev/null +++ b/editor/src/editor/windows/fx-editor/properties/vfx-rotation-editor.tsx @@ -0,0 +1,318 @@ +import { ReactNode } from "react"; + +import { EditorInspectorListField } from "../../../layout/inspector/fields/list"; +import { EditorInspectorBlockField } from "../../../layout/inspector/fields/block"; + +import type { VFXRotation, VFXEulerRotation, VFXAxisAngleRotation, VFXRandomQuatRotation } from "../VFX/types/rotations"; +import { VFXValueEditor } from "./vfx-value-editor"; +import { VFXValueUtils } from "../VFX/utils/valueParser"; +import type { VFXValue } from "../VFX/types/values"; + +export type VFXRotationType = "Euler" | "AxisAngle" | "RandomQuat"; + +export interface IVFXRotationEditorProps { + value: VFXRotation | undefined; + onChange: (newValue: VFXRotation) => void; + label?: string; +} + +/** + * Editor for VFXRotation (Euler, AxisAngle, RandomQuat) + * Works directly with VFXRotation types, not wrappers + */ +export function VFXRotationEditor(props: IVFXRotationEditorProps): ReactNode { + const { value, onChange, label } = props; + + // Determine current type from value + let currentType: VFXRotationType = "Euler"; + if (value) { + if (typeof value === "number" || (typeof value === "object" && "type" in value && (value.type === "ConstantValue" || value.type === "IntervalValue" || value.type === "PiecewiseBezier"))) { + // Simple VFXValue - convert to Euler + currentType = "Euler"; + } else if (typeof value === "object" && "type" in value) { + if (value.type === "Euler") { + currentType = "Euler"; + } else if (value.type === "AxisAngle") { + currentType = "AxisAngle"; + } else if (value.type === "RandomQuat") { + currentType = "RandomQuat"; + } + } + } + + const typeItems = [ + { text: "Euler", value: "Euler" }, + { text: "Axis Angle", value: "AxisAngle" }, + { text: "Random Quat", value: "RandomQuat" }, + ]; + + // Wrapper object for EditorInspectorListField + const wrapper = { + get type() { + return currentType; + }, + set type(newType: VFXRotationType) { + currentType = newType; + // Convert value to new type + let newValue: VFXRotation; + if (newType === "Euler") { + // Convert current value to Euler + if (value && typeof value === "object" && "type" in value && value.type === "Euler") { + newValue = value; + } else { + const angleZ = value ? (typeof value === "number" ? value : VFXValueUtils.parseConstantValue(value as VFXValue)) : 0; + newValue = { + type: "Euler", + angleZ: { type: "ConstantValue", value: angleZ }, + order: "xyz", + }; + } + } else if (newType === "AxisAngle") { + // Convert to AxisAngle + const angle = value ? (typeof value === "number" ? value : VFXValueUtils.parseConstantValue(value as VFXValue)) : 0; + newValue = { + type: "AxisAngle", + x: { type: "ConstantValue", value: 0 }, + y: { type: "ConstantValue", value: 0 }, + z: { type: "ConstantValue", value: 1 }, + angle: { type: "ConstantValue", value: angle }, + }; + } else { + // RandomQuat + newValue = { type: "RandomQuat" }; + } + onChange(newValue); + }, + }; + + return ( + <> + { + // Type change is handled by setter + }} + /> + + {currentType === "Euler" && ( + <> + {(() => { + const eulerValue = value && typeof value === "object" && "type" in value && value.type === "Euler" ? value : null; + const angleX = eulerValue?.angleX || { type: "ConstantValue" as const, value: 0 }; + const angleY = eulerValue?.angleY || { type: "ConstantValue" as const, value: 0 }; + const angleZ = eulerValue?.angleZ || { type: "ConstantValue" as const, value: 0 }; + const order = eulerValue?.order || "xyz"; + + const orderWrapper = { + get order() { + return order; + }, + set order(newOrder: "xyz" | "zyx") { + if (eulerValue) { + onChange({ ...eulerValue, order: newOrder }); + } else { + onChange({ + type: "Euler", + angleX, + angleY, + angleZ, + order: newOrder, + }); + } + }, + }; + + return ( + <> + {}} + /> + +
Angle X
+ { + if (eulerValue) { + onChange({ ...eulerValue, angleX: newAngleX as VFXValue }); + } else { + onChange({ + type: "Euler", + angleX: newAngleX as VFXValue, + angleY, + angleZ, + order, + }); + } + }} + availableTypes={["ConstantValue", "IntervalValue", "PiecewiseBezier"]} + step={0.1} + /> +
+ +
Angle Y
+ { + if (eulerValue) { + onChange({ ...eulerValue, angleY: newAngleY as VFXValue }); + } else { + onChange({ + type: "Euler", + angleX, + angleY: newAngleY as VFXValue, + angleZ, + order, + }); + } + }} + availableTypes={["ConstantValue", "IntervalValue", "PiecewiseBezier"]} + step={0.1} + /> +
+ +
Angle Z
+ { + if (eulerValue) { + onChange({ ...eulerValue, angleZ: newAngleZ as VFXValue }); + } else { + onChange({ + type: "Euler", + angleX, + angleY, + angleZ: newAngleZ as VFXValue, + order, + }); + } + }} + availableTypes={["ConstantValue", "IntervalValue", "PiecewiseBezier"]} + step={0.1} + /> +
+ + ); + })()} + + )} + + {currentType === "AxisAngle" && ( + <> + {(() => { + const axisAngleValue = value && typeof value === "object" && "type" in value && value.type === "AxisAngle" ? value : null; + const x = axisAngleValue?.x || { type: "ConstantValue" as const, value: 0 }; + const y = axisAngleValue?.y || { type: "ConstantValue" as const, value: 0 }; + const z = axisAngleValue?.z || { type: "ConstantValue" as const, value: 1 }; + const angle = axisAngleValue?.angle || { type: "ConstantValue" as const, value: 0 }; + + return ( + <> + +
Axis X
+ { + if (axisAngleValue) { + onChange({ ...axisAngleValue, x: newX as VFXValue }); + } else { + onChange({ + type: "AxisAngle", + x: newX as VFXValue, + y, + z, + angle, + }); + } + }} + availableTypes={["ConstantValue", "IntervalValue", "PiecewiseBezier"]} + step={0.1} + /> +
+ +
Axis Y
+ { + if (axisAngleValue) { + onChange({ ...axisAngleValue, y: newY as VFXValue }); + } else { + onChange({ + type: "AxisAngle", + x, + y: newY as VFXValue, + z, + angle, + }); + } + }} + availableTypes={["ConstantValue", "IntervalValue", "PiecewiseBezier"]} + step={0.1} + /> +
+ +
Axis Z
+ { + if (axisAngleValue) { + onChange({ ...axisAngleValue, z: newZ as VFXValue }); + } else { + onChange({ + type: "AxisAngle", + x, + y, + z: newZ as VFXValue, + angle, + }); + } + }} + availableTypes={["ConstantValue", "IntervalValue", "PiecewiseBezier"]} + step={0.1} + /> +
+ +
Angle
+ { + if (axisAngleValue) { + onChange({ ...axisAngleValue, angle: newAngle as VFXValue }); + } else { + onChange({ + type: "AxisAngle", + x, + y, + z, + angle: newAngle as VFXValue, + }); + } + }} + availableTypes={["ConstantValue", "IntervalValue", "PiecewiseBezier"]} + step={0.1} + /> +
+ + ); + })()} + + )} + + {currentType === "RandomQuat" && ( + <> +
Random quaternion rotation will be applied to each particle
+ + )} + + ); +} + diff --git a/editor/src/editor/windows/fx-editor/properties/vfx-value-editor.tsx b/editor/src/editor/windows/fx-editor/properties/vfx-value-editor.tsx new file mode 100644 index 000000000..1852ce825 --- /dev/null +++ b/editor/src/editor/windows/fx-editor/properties/vfx-value-editor.tsx @@ -0,0 +1,312 @@ +import { ReactNode } from "react"; + +import { EditorInspectorNumberField } from "../../../layout/inspector/fields/number"; +import { EditorInspectorListField } from "../../../layout/inspector/fields/list"; +import { EditorInspectorBlockField } from "../../../layout/inspector/fields/block"; + +import type { VFXValue, VFXConstantValue, VFXIntervalValue, VFXPiecewiseBezier } from "../VFX/types/values"; +import { BezierEditor } from "./behaviors/bezier-editor"; +import { VFXValueUtils } from "../VFX/utils/valueParser"; + +export type VFXValueType = "ConstantValue" | "IntervalValue" | "PiecewiseBezier" | "Vec3Function"; + +export interface IVec3Function { + type: "Vec3Function"; + x: VFXValue; + y: VFXValue; + z: VFXValue; +} + +export interface IVFXValueEditorProps { + value: VFXValue | IVec3Function | undefined; + onChange: (newValue: VFXValue | IVec3Function) => void; + label?: string; + availableTypes?: VFXValueType[]; + min?: number; + step?: number; +} + +/** + * Editor for VFXValue (ConstantValue, IntervalValue, PiecewiseBezier, Vec3Function) + * Works directly with VFXValue types, not wrappers + */ +export function VFXValueEditor(props: IVFXValueEditorProps): ReactNode { + const { value, onChange, label, availableTypes, min, step } = props; + + const types = availableTypes || ["ConstantValue", "IntervalValue", "PiecewiseBezier"]; + + // Determine current type from value + let currentType: VFXValueType = "ConstantValue"; + if (value) { + if (typeof value === "number") { + currentType = "ConstantValue"; + } else if ("type" in value) { + if (value.type === "Vec3Function") { + currentType = "Vec3Function"; + } else if (value.type === "ConstantValue") { + currentType = "ConstantValue"; + } else if (value.type === "IntervalValue") { + currentType = "IntervalValue"; + } else if (value.type === "PiecewiseBezier") { + currentType = "PiecewiseBezier"; + } + } + } + + const typeItems = types.map((type) => ({ + text: type, + value: type, + })); + + // Wrapper object for EditorInspectorListField + const wrapper = { + get type() { + return currentType; + }, + set type(newType: VFXValueType) { + currentType = newType; + // Convert value to new type + let newValue: VFXValue | IVec3Function; + if (newType === "ConstantValue") { + const currentValue = value && typeof value !== "number" && "type" in value && value.type !== "Vec3Function" ? VFXValueUtils.parseConstantValue(value as VFXValue) : typeof value === "number" ? value : 1; + newValue = { type: "ConstantValue", value: currentValue }; + } else if (newType === "IntervalValue") { + const interval = value && typeof value !== "number" && "type" in value && value.type !== "Vec3Function" ? VFXValueUtils.parseIntervalValue(value as VFXValue) : { min: 0, max: 1 }; + newValue = { type: "IntervalValue", min: interval.min, max: interval.max }; + } else if (newType === "Vec3Function") { + const currentValue = value && typeof value !== "number" && "type" in value && value.type !== "Vec3Function" ? VFXValueUtils.parseConstantValue(value as VFXValue) : typeof value === "number" ? value : 1; + newValue = { + type: "Vec3Function", + x: { type: "ConstantValue", value: currentValue }, + y: { type: "ConstantValue", value: currentValue }, + z: { type: "ConstantValue", value: currentValue }, + }; + } else { + // PiecewiseBezier - convert from current value + const currentValue = value && typeof value !== "number" && "type" in value && value.type !== "Vec3Function" ? VFXValueUtils.parseConstantValue(value as VFXValue) : typeof value === "number" ? value : 1; + newValue = { + type: "PiecewiseBezier", + functions: [ + { + function: { p0: currentValue, p1: currentValue, p2: currentValue, p3: currentValue }, + start: 0, + }, + ], + }; + } + onChange(newValue); + }, + }; + + return ( + <> + { + // Type change is handled by setter + }} + /> + + {currentType === "ConstantValue" && ( + <> + {(() => { + const constantValue = value && typeof value !== "number" && "type" in value && value.type !== "Vec3Function" ? VFXValueUtils.parseConstantValue(value as VFXValue) : typeof value === "number" ? value : 1; + const wrapperValue = { + get value() { + return constantValue; + }, + set value(newVal: number) { + onChange({ type: "ConstantValue", value: newVal }); + }, + }; + return ( + { + // Value change is handled by setter + }} + /> + ); + })()} + + )} + + {currentType === "IntervalValue" && ( + <> + {(() => { + const interval = value && typeof value !== "number" && "type" in value && value.type !== "Vec3Function" ? VFXValueUtils.parseIntervalValue(value as VFXValue) : { min: 0, max: 1 }; + const wrapperInterval = { + get min() { + return interval.min; + }, + set min(newMin: number) { + const currentMax = value && typeof value !== "number" && "type" in value && value.type === "IntervalValue" ? value.max : interval.max; + onChange({ type: "IntervalValue", min: newMin, max: currentMax }); + }, + get max() { + return interval.max; + }, + set max(newMax: number) { + const currentMin = value && typeof value !== "number" && "type" in value && value.type === "IntervalValue" ? value.min : interval.min; + onChange({ type: "IntervalValue", min: currentMin, max: newMax }); + }, + }; + return ( + +
{label ? "Range" : ""}
+
+ { + // Value change is handled by setter + }} + /> + { + // Value change is handled by setter + }} + /> +
+
+ ); + })()} + + )} + + {currentType === "PiecewiseBezier" && ( + <> + {(() => { + // Convert VFXValue to wrapper format for BezierEditor + const bezierValue = value && typeof value !== "number" && "type" in value && value.type === "PiecewiseBezier" ? value : null; + const wrapperBezier = { + get functionType() { + return "PiecewiseBezier"; + }, + set functionType(_: string) {}, + get data() { + if (!bezierValue || bezierValue.functions.length === 0) { + return { + function: { p0: 1, p1: 1.0 / 3, p2: (1.0 / 3) * 2, p3: 1 }, + }; + } + // Use first function for editing + return { + function: bezierValue.functions[0].function, + }; + }, + set data(newData: any) { + // Update first function or create new + if (!bezierValue) { + onChange({ + type: "PiecewiseBezier", + functions: [ + { + function: newData.function || { p0: 1, p1: 1.0 / 3, p2: (1.0 / 3) * 2, p3: 1 }, + start: 0, + }, + ], + }); + } else { + const newFunctions = [...bezierValue.functions]; + newFunctions[0] = { + ...newFunctions[0], + function: newData.function || newFunctions[0].function, + }; + onChange({ + type: "PiecewiseBezier", + functions: newFunctions, + }); + } + }, + }; + return {}} />; + })()} + + )} + + {currentType === "Vec3Function" && ( + <> + {(() => { + const vec3Value = value && typeof value !== "number" && "type" in value && value.type === "Vec3Function" ? value : null; + const currentX = vec3Value ? vec3Value.x : { type: "ConstantValue" as const, value: 1 }; + const currentY = vec3Value ? vec3Value.y : { type: "ConstantValue" as const, value: 1 }; + const currentZ = vec3Value ? vec3Value.z : { type: "ConstantValue" as const, value: 1 }; + return ( + <> + +
X
+ { + onChange({ + type: "Vec3Function", + x: newX as VFXValue, + y: currentY, + z: currentZ, + }); + }} + availableTypes={["ConstantValue", "IntervalValue", "PiecewiseBezier"]} + min={min} + step={step} + /> +
+ +
Y
+ { + onChange({ + type: "Vec3Function", + x: currentX, + y: newY as VFXValue, + z: currentZ, + }); + }} + availableTypes={["ConstantValue", "IntervalValue", "PiecewiseBezier"]} + min={min} + step={step} + /> +
+ +
Z
+ { + onChange({ + type: "Vec3Function", + x: currentX, + y: currentY, + z: newZ as VFXValue, + }); + }} + availableTypes={["ConstantValue", "IntervalValue", "PiecewiseBezier"]} + min={min} + step={step} + /> +
+ + ); + })()} + + )} + + ); +} From 23d54db43f2b14abe5a8f0e3adc2da4f822545ac Mon Sep 17 00:00:00 2001 From: Mikalai Lazitski Date: Sun, 14 Dec 2025 20:57:21 +0300 Subject: [PATCH 28/62] refactor: enhance VFX system by introducing a new VFXEmitterFactory for improved emitter creation, integrating emitter logic into particle systems, and refining emitter configuration handling for better performance and clarity --- .../editor/windows/fx-editor/VFX/VFXEffect.ts | 32 +++- .../VFX/factories/VFXEmitterFactory.ts | 175 ++++++++++++++++++ .../VFXParticleSystemEmitterFactory.ts | 147 --------------- .../VFXSolidParticleSystemEmitterFactory.ts | 99 ---------- .../VFX/factories/VFXSystemFactory.ts | 14 +- .../fx-editor/VFX/parsers/VFXDataConverter.ts | 54 +++++- .../VFX/systems/VFXParticleSystem.ts | 33 +--- .../VFX/systems/VFXSolidParticleSystem.ts | 104 +++++++++-- .../fx-editor/VFX/types/emitterConfig.ts | 5 +- .../windows/fx-editor/VFX/types/emitters.ts | 92 +++++++++ .../windows/fx-editor/VFX/types/index.ts | 4 +- editor/src/editor/windows/fx-editor/graph.tsx | 12 ++ .../src/editor/windows/fx-editor/layout.tsx | 1 + .../src/editor/windows/fx-editor/preview.tsx | 42 ++++- .../fx-editor/properties/vfx-color-editor.tsx | 45 ++++- 15 files changed, 538 insertions(+), 321 deletions(-) create mode 100644 editor/src/editor/windows/fx-editor/VFX/factories/VFXEmitterFactory.ts delete mode 100644 editor/src/editor/windows/fx-editor/VFX/factories/VFXParticleSystemEmitterFactory.ts delete mode 100644 editor/src/editor/windows/fx-editor/VFX/factories/VFXSolidParticleSystemEmitterFactory.ts create mode 100644 editor/src/editor/windows/fx-editor/VFX/types/emitters.ts diff --git a/editor/src/editor/windows/fx-editor/VFX/VFXEffect.ts b/editor/src/editor/windows/fx-editor/VFX/VFXEffect.ts index bdea3f404..78a034303 100644 --- a/editor/src/editor/windows/fx-editor/VFX/VFXEffect.ts +++ b/editor/src/editor/windows/fx-editor/VFX/VFXEffect.ts @@ -2,8 +2,8 @@ import { Scene, Tools, IDisposable, TransformNode } from "babylonjs"; import type { QuarksVFXJSON } from "./types/quarksTypes"; import type { VFXLoaderOptions } from "./types/loader"; import { VFXParser } from "./parsers/VFXParser"; -import type { VFXParticleSystem } from "./systems/VFXParticleSystem"; -import type { VFXSolidParticleSystem } from "./systems/VFXSolidParticleSystem"; +import { VFXParticleSystem } from "./systems/VFXParticleSystem"; +import { VFXSolidParticleSystem } from "./systems/VFXSolidParticleSystem"; import type { VFXGroup, VFXEmitter, VFXData } from "./types/hierarchy"; import { isVFXSystem } from "./types/system"; @@ -370,6 +370,34 @@ export class VFXEffect implements IDisposable { } } + /** + * Reset all particle systems (stop and clear particles) + */ + public reset(): void { + for (const system of this._systems) { + system.reset(); + } + } + + /** + * Check if any system is started + */ + public isStarted(): boolean { + for (const system of this._systems) { + if (system instanceof VFXParticleSystem) { + if ((system as any).isStarted && (system as any).isStarted()) { + return true; + } + } else if (system instanceof VFXSolidParticleSystem) { + // Check internal _started flag for SPS + if ((system as any)._started && !(system as any)._stopped) { + return true; + } + } + } + return false; + } + /** * Dispose all resources */ diff --git a/editor/src/editor/windows/fx-editor/VFX/factories/VFXEmitterFactory.ts b/editor/src/editor/windows/fx-editor/VFX/factories/VFXEmitterFactory.ts new file mode 100644 index 000000000..831b13e1e --- /dev/null +++ b/editor/src/editor/windows/fx-editor/VFX/factories/VFXEmitterFactory.ts @@ -0,0 +1,175 @@ +import { ParticleSystem, Vector3, Matrix } from "babylonjs"; +import type { VFXShape } from "../types/shapes"; +import type { VFXSolidParticleSystem } from "../systems/VFXSolidParticleSystem"; + +/** + * Factory for creating emitters for particle systems + * Handles both ParticleSystem and SolidParticleSystem emitter creation + */ +export class VFXEmitterFactory { + /** + * Create emitter for ParticleSystem + * Applies emitter shape to the particle system + */ + public createParticleSystemEmitter(particleSystem: ParticleSystem, shape: VFXShape | undefined, cumulativeScale: Vector3, rotationMatrix: Matrix | null): void { + if (!shape || !shape.type) { + this._createPointEmitter(particleSystem, Vector3.Zero(), Vector3.Zero()); + return; + } + + const shapeType = shape.type.toLowerCase(); + const shapeHandlers: Record void> = { + cone: this._createConeEmitter.bind(this, particleSystem), + sphere: this._createSphereEmitter.bind(this, particleSystem), + point: this._createPointEmitter.bind(this, particleSystem), + box: this._createBoxEmitter.bind(this, particleSystem), + hemisphere: this._createHemisphereEmitter.bind(this, particleSystem), + cylinder: this._createCylinderEmitter.bind(this, particleSystem), + }; + + const handler = shapeHandlers[shapeType]; + if (handler) { + handler(shape, cumulativeScale, rotationMatrix); + } else { + this._createDefaultPointEmitter(particleSystem, rotationMatrix); + } + } + + /** + * Create emitter for SolidParticleSystem + * Creates emitter using system's create*Emitter methods (similar to ParticleSystem) + */ + public createSolidParticleSystemEmitter(sps: VFXSolidParticleSystem, shape: VFXShape | undefined): void { + if (!shape || !shape.type) { + sps.createPointEmitter(); + return; + } + + const shapeType = shape.type.toLowerCase(); + const radius = shape.radius ?? 1; + const arc = shape.arc ?? Math.PI * 2; + const thickness = shape.thickness ?? 1; + const angle = shape.angle ?? Math.PI / 6; + + switch (shapeType) { + case "sphere": + sps.createSphereEmitter(radius, arc, thickness); + break; + case "cone": + sps.createConeEmitter(radius, arc, thickness, angle); + break; + case "point": + sps.createPointEmitter(); + break; + default: + sps.createPointEmitter(); + break; + } + } + + /** + * Applies rotation to default direction vector + */ + private _applyRotationToDirection(defaultDir: Vector3, rotationMatrix: Matrix | null): Vector3 { + if (!rotationMatrix) { + return defaultDir; + } + + const rotatedDir = Vector3.Zero(); + Vector3.TransformNormalToRef(defaultDir, rotationMatrix, rotatedDir); + return rotatedDir; + } + + /** + * Creates cone emitter for ParticleSystem + */ + private _createConeEmitter(particleSystem: ParticleSystem, shape: VFXShape, scale: Vector3, rotationMatrix: Matrix | null): void { + const radius = ((shape as any).radius || 1) * ((scale.x + scale.z) / 2); + const angle = (shape as any).angle !== undefined ? (shape as any).angle : Math.PI / 4; + const defaultDir = new Vector3(0, 1, 0); + const rotatedDir = this._applyRotationToDirection(defaultDir, rotationMatrix); + + if (rotationMatrix) { + particleSystem.createDirectedConeEmitter(radius, angle, rotatedDir, rotatedDir); + } else { + particleSystem.createConeEmitter(radius, angle); + } + } + + /** + * Creates sphere emitter for ParticleSystem + */ + private _createSphereEmitter(particleSystem: ParticleSystem, shape: VFXShape, scale: Vector3, rotationMatrix: Matrix | null): void { + const radius = ((shape as any).radius || 1) * ((scale.x + scale.y + scale.z) / 3); + const defaultDir = new Vector3(0, 1, 0); + const rotatedDir = this._applyRotationToDirection(defaultDir, rotationMatrix); + + if (rotationMatrix) { + particleSystem.createDirectedSphereEmitter(radius, rotatedDir, rotatedDir); + } else { + particleSystem.createSphereEmitter(radius); + } + } + + /** + * Creates point emitter for ParticleSystem + */ + private _createPointEmitter(particleSystem: ParticleSystem, direction: Vector3, minDirection: Vector3): void { + particleSystem.createPointEmitter(direction, minDirection); + } + + /** + * Creates box emitter for ParticleSystem + */ + private _createBoxEmitter(particleSystem: ParticleSystem, shape: VFXShape, scale: Vector3, rotationMatrix: Matrix | null): void { + const boxSize = ((shape as any).size || [1, 1, 1]).map((s: number, i: number) => s * [scale.x, scale.y, scale.z][i]); + const minBox = new Vector3(-boxSize[0] / 2, -boxSize[1] / 2, -boxSize[2] / 2); + const maxBox = new Vector3(boxSize[0] / 2, boxSize[1] / 2, boxSize[2] / 2); + const defaultDir = new Vector3(0, 1, 0); + const rotatedDir = this._applyRotationToDirection(defaultDir, rotationMatrix); + + if (rotationMatrix) { + particleSystem.createBoxEmitter(rotatedDir, rotatedDir, minBox, maxBox); + } else { + particleSystem.createBoxEmitter(Vector3.Zero(), Vector3.Zero(), minBox, maxBox); + } + } + + /** + * Creates hemisphere emitter for ParticleSystem + */ + private _createHemisphereEmitter(particleSystem: ParticleSystem, shape: VFXShape, scale: Vector3, _rotationMatrix: Matrix | null): void { + const radius = ((shape as any).radius || 1) * ((scale.x + scale.y + scale.z) / 3); + particleSystem.createHemisphericEmitter(radius); + } + + /** + * Creates cylinder emitter for ParticleSystem + */ + private _createCylinderEmitter(particleSystem: ParticleSystem, shape: VFXShape, scale: Vector3, rotationMatrix: Matrix | null): void { + const radius = ((shape as any).radius || 1) * ((scale.x + scale.z) / 2); + const height = ((shape as any).height || 1) * scale.y; + const defaultDir = new Vector3(0, 1, 0); + const rotatedDir = this._applyRotationToDirection(defaultDir, rotationMatrix); + + if (rotationMatrix) { + particleSystem.createDirectedCylinderEmitter(radius, height, 1, rotatedDir, rotatedDir); + } else { + particleSystem.createCylinderEmitter(radius, height); + } + } + + /** + * Creates default point emitter for ParticleSystem + */ + private _createDefaultPointEmitter(particleSystem: ParticleSystem, rotationMatrix: Matrix | null): void { + const defaultDir = new Vector3(0, 1, 0); + const rotatedDir = this._applyRotationToDirection(defaultDir, rotationMatrix); + + if (rotationMatrix) { + this._createPointEmitter(particleSystem, rotatedDir, rotatedDir); + } else { + this._createPointEmitter(particleSystem, Vector3.Zero(), Vector3.Zero()); + } + } +} diff --git a/editor/src/editor/windows/fx-editor/VFX/factories/VFXParticleSystemEmitterFactory.ts b/editor/src/editor/windows/fx-editor/VFX/factories/VFXParticleSystemEmitterFactory.ts deleted file mode 100644 index 2362cac60..000000000 --- a/editor/src/editor/windows/fx-editor/VFX/factories/VFXParticleSystemEmitterFactory.ts +++ /dev/null @@ -1,147 +0,0 @@ -import { ParticleSystem, Vector3, Matrix } from "babylonjs"; -import type { VFXShape } from "../types/shapes"; - -/** - * Factory for creating shape emitters for ParticleSystem - * Creates emitters based on VFX shape configuration - */ -export class VFXParticleSystemEmitterFactory { - private _particleSystem: ParticleSystem; - - constructor(particleSystem: ParticleSystem) { - this._particleSystem = particleSystem; - } - - /** - * Create emitter shape based on VFX shape configuration - */ - public createEmitter(shape: VFXShape | undefined, cumulativeScale: Vector3, rotationMatrix: Matrix | null): void { - if (!shape || !shape.type) { - this._createPointEmitter(Vector3.Zero(), Vector3.Zero()); - return; - } - - const shapeType = shape.type.toLowerCase(); - const shapeHandlers: Record void> = { - cone: this._createConeEmitter.bind(this), - sphere: this._createSphereEmitter.bind(this), - point: this._createPointEmitter.bind(this), - box: this._createBoxEmitter.bind(this), - hemisphere: this._createHemisphereEmitter.bind(this), - cylinder: this._createCylinderEmitter.bind(this), - }; - - const handler = shapeHandlers[shapeType]; - if (handler) { - handler(shape, cumulativeScale, rotationMatrix); - } else { - this._createDefaultPointEmitter(rotationMatrix); - } - } - - /** - * Applies rotation to default direction vector - */ - private _applyRotationToDirection(defaultDir: Vector3, rotationMatrix: Matrix | null): Vector3 { - if (!rotationMatrix) { - return defaultDir; - } - - const rotatedDir = Vector3.Zero(); - Vector3.TransformNormalToRef(defaultDir, rotationMatrix, rotatedDir); - return rotatedDir; - } - - /** - * Creates cone emitter - */ - private _createConeEmitter(shape: VFXShape, scale: Vector3, rotationMatrix: Matrix | null): void { - const radius = ((shape as any).radius || 1) * ((scale.x + scale.z) / 2); - const angle = (shape as any).angle !== undefined ? (shape as any).angle : Math.PI / 4; - const defaultDir = new Vector3(0, 1, 0); - const rotatedDir = this._applyRotationToDirection(defaultDir, rotationMatrix); - - if (rotationMatrix) { - this._particleSystem.createDirectedConeEmitter(radius, angle, rotatedDir, rotatedDir); - } else { - this._particleSystem.createConeEmitter(radius, angle); - } - } - - /** - * Creates sphere emitter - */ - private _createSphereEmitter(shape: VFXShape, scale: Vector3, rotationMatrix: Matrix | null): void { - const radius = ((shape as any).radius || 1) * ((scale.x + scale.y + scale.z) / 3); - const defaultDir = new Vector3(0, 1, 0); - const rotatedDir = this._applyRotationToDirection(defaultDir, rotationMatrix); - - if (rotationMatrix) { - this._particleSystem.createDirectedSphereEmitter(radius, rotatedDir, rotatedDir); - } else { - this._particleSystem.createSphereEmitter(radius); - } - } - - /** - * Creates point emitter - */ - private _createPointEmitter(direction: Vector3, minDirection: Vector3): void { - this._particleSystem.createPointEmitter(direction, minDirection); - } - - /** - * Creates box emitter - */ - private _createBoxEmitter(shape: VFXShape, scale: Vector3, rotationMatrix: Matrix | null): void { - const boxSize = ((shape as any).size || [1, 1, 1]).map((s: number, i: number) => s * [scale.x, scale.y, scale.z][i]); - const minBox = new Vector3(-boxSize[0] / 2, -boxSize[1] / 2, -boxSize[2] / 2); - const maxBox = new Vector3(boxSize[0] / 2, boxSize[1] / 2, boxSize[2] / 2); - const defaultDir = new Vector3(0, 1, 0); - const rotatedDir = this._applyRotationToDirection(defaultDir, rotationMatrix); - - if (rotationMatrix) { - this._particleSystem.createBoxEmitter(rotatedDir, rotatedDir, minBox, maxBox); - } else { - this._particleSystem.createBoxEmitter(Vector3.Zero(), Vector3.Zero(), minBox, maxBox); - } - } - - /** - * Creates hemisphere emitter - */ - private _createHemisphereEmitter(shape: VFXShape, scale: Vector3, _rotationMatrix: Matrix | null): void { - const radius = ((shape as any).radius || 1) * ((scale.x + scale.y + scale.z) / 3); - this._particleSystem.createHemisphericEmitter(radius); - } - - /** - * Creates cylinder emitter - */ - private _createCylinderEmitter(shape: VFXShape, scale: Vector3, rotationMatrix: Matrix | null): void { - const radius = ((shape as any).radius || 1) * ((scale.x + scale.z) / 2); - const height = ((shape as any).height || 1) * scale.y; - const defaultDir = new Vector3(0, 1, 0); - const rotatedDir = this._applyRotationToDirection(defaultDir, rotationMatrix); - - if (rotationMatrix) { - this._particleSystem.createDirectedCylinderEmitter(radius, height, 1, rotatedDir, rotatedDir); - } else { - this._particleSystem.createCylinderEmitter(radius, height); - } - } - - /** - * Creates default point emitter - */ - private _createDefaultPointEmitter(rotationMatrix: Matrix | null): void { - const defaultDir = new Vector3(0, 1, 0); - const rotatedDir = this._applyRotationToDirection(defaultDir, rotationMatrix); - - if (rotationMatrix) { - this._createPointEmitter(rotatedDir, rotatedDir); - } else { - this._createPointEmitter(Vector3.Zero(), Vector3.Zero()); - } - } -} diff --git a/editor/src/editor/windows/fx-editor/VFX/factories/VFXSolidParticleSystemEmitterFactory.ts b/editor/src/editor/windows/fx-editor/VFX/factories/VFXSolidParticleSystemEmitterFactory.ts deleted file mode 100644 index 7e78f313f..000000000 --- a/editor/src/editor/windows/fx-editor/VFX/factories/VFXSolidParticleSystemEmitterFactory.ts +++ /dev/null @@ -1,99 +0,0 @@ -import { SolidParticle, Vector3 } from "babylonjs"; -import type { VFXShape } from "../types/shapes"; - -/** - * Factory for initializing particle positions and velocities based on emitter shape for SolidParticleSystem - * This is used during particle initialization, not emitter creation - */ -export class VFXSolidParticleSystemEmitterFactory { - /** - * Initialize particle position and velocity based on emitter shape - */ - public initializeParticle(particle: SolidParticle, shape: VFXShape | undefined, startSpeed: number): void { - if (!shape) { - this._initializeDefaultShape(particle, startSpeed); - return; - } - - const shapeType = shape.type?.toLowerCase(); - const radius = shape.radius ?? 1; - const arc = shape.arc ?? Math.PI * 2; - const thickness = shape.thickness ?? 1; - const angle = shape.angle ?? Math.PI / 6; - - switch (shapeType) { - case "sphere": - this._initializeSphereShape(particle, radius, arc, thickness, startSpeed); - break; - case "cone": - this._initializeConeShape(particle, radius, arc, thickness, angle, startSpeed); - break; - case "point": - this._initializePointShape(particle, startSpeed); - break; - default: - this._initializeDefaultShape(particle, startSpeed); - break; - } - } - - /** - * Initialize default shape (point emitter) - */ - private _initializeDefaultShape(particle: SolidParticle, startSpeed: number): void { - particle.position.setAll(0); - particle.velocity.set(0, 1, 0); - particle.velocity.scaleInPlace(startSpeed); - } - - /** - * Initialize sphere shape - */ - private _initializeSphereShape(particle: SolidParticle, radius: number, arc: number, thickness: number, startSpeed: number): void { - const u = Math.random(); - const v = Math.random(); - const rand = 1 - thickness + Math.random() * thickness; - const theta = u * arc; - const phi = Math.acos(2.0 * v - 1.0); - const sinTheta = Math.sin(theta); - const cosTheta = Math.cos(theta); - const sinPhi = Math.sin(phi); - const cosPhi = Math.cos(phi); - - particle.position.set(sinPhi * cosTheta, sinPhi * sinTheta, cosPhi); - particle.velocity.copyFrom(particle.position); - particle.velocity.scaleInPlace(startSpeed); - particle.position.scaleInPlace(radius * rand); - } - - /** - * Initialize cone shape - */ - private _initializeConeShape(particle: SolidParticle, radius: number, arc: number, thickness: number, angle: number, startSpeed: number): void { - const u = Math.random(); - const rand = 1 - thickness + Math.random() * thickness; - const theta = u * arc; - const r = Math.sqrt(rand); - const sinTheta = Math.sin(theta); - const cosTheta = Math.cos(theta); - - particle.position.set(r * cosTheta, r * sinTheta, 0); - const coneAngle = angle * r; - particle.velocity.set(0, 0, Math.cos(coneAngle)); - particle.velocity.addInPlace(particle.position.scale(Math.sin(coneAngle))); - particle.velocity.scaleInPlace(startSpeed); - particle.position.scaleInPlace(radius); - } - - /** - * Initialize point shape - */ - private _initializePointShape(particle: SolidParticle, startSpeed: number): void { - const theta = Math.random() * Math.PI * 2; - const phi = Math.acos(2.0 * Math.random() - 1.0); - const direction = new Vector3(Math.sin(phi) * Math.cos(theta), Math.sin(phi) * Math.sin(theta), Math.cos(phi)); - particle.position.setAll(0); - particle.velocity.copyFrom(direction); - particle.velocity.scaleInPlace(startSpeed); - } -} diff --git a/editor/src/editor/windows/fx-editor/VFX/factories/VFXSystemFactory.ts b/editor/src/editor/windows/fx-editor/VFX/factories/VFXSystemFactory.ts index 1cbbac605..78f9ffc70 100644 --- a/editor/src/editor/windows/fx-editor/VFX/factories/VFXSystemFactory.ts +++ b/editor/src/editor/windows/fx-editor/VFX/factories/VFXSystemFactory.ts @@ -4,6 +4,7 @@ import { VFXSolidParticleSystem } from "../systems/VFXSolidParticleSystem"; import type { VFXData, VFXGroup, VFXEmitter, VFXTransform } from "../types/hierarchy"; import { VFXLogger } from "../loggers/VFXLogger"; import { VFXMatrixUtils } from "../utils/matrixUtils"; +import { VFXEmitterFactory } from "./VFXEmitterFactory"; import type { IVFXMaterialFactory, IVFXGeometryFactory } from "../types/factories"; import type { VFXLoaderOptions } from "../types/loader"; @@ -18,6 +19,7 @@ export class VFXSystemFactory { private _groupNodesMap: Map; private _materialFactory: IVFXMaterialFactory; private _geometryFactory: IVFXGeometryFactory; + private _emitterFactory: VFXEmitterFactory; constructor(scene: Scene, options: VFXLoaderOptions, groupNodesMap: Map, materialFactory: IVFXMaterialFactory, geometryFactory: IVFXGeometryFactory) { this._scene = scene; @@ -26,6 +28,7 @@ export class VFXSystemFactory { this._logger = new VFXLogger("[VFXSystemFactory]", options); this._materialFactory = materialFactory; this._geometryFactory = geometryFactory; + this._emitterFactory = new VFXEmitterFactory(); } /** @@ -223,13 +226,11 @@ export class VFXSystemFactory { const particleSystem = new VFXParticleSystem(name, this._scene, config, { texture, blendMode, - emitterShape: { - shape: config.shape, - cumulativeScale, - rotationMatrix, - }, }); + // Create emitter using factory + this._emitterFactory.createParticleSystemEmitter(particleSystem, config.shape, cumulativeScale, rotationMatrix); + this._logger.log(`ParticleSystem created: ${name}`); return particleSystem; } @@ -273,6 +274,9 @@ export class VFXSystemFactory { particleMesh, }); + // Create emitter using factory (similar to ParticleSystem) + this._emitterFactory.createSolidParticleSystemEmitter(sps, config.shape); + this._logger.log(`SolidParticleSystem created: ${name}`); return sps; } diff --git a/editor/src/editor/windows/fx-editor/VFX/parsers/VFXDataConverter.ts b/editor/src/editor/windows/fx-editor/VFX/parsers/VFXDataConverter.ts index 7c08c027f..2e06d55ad 100644 --- a/editor/src/editor/windows/fx-editor/VFX/parsers/VFXDataConverter.ts +++ b/editor/src/editor/windows/fx-editor/VFX/parsers/VFXDataConverter.ts @@ -1,4 +1,4 @@ -import { Vector3, Matrix, Quaternion, Color3, Texture } from "babylonjs"; +import { Vector3, Matrix, Quaternion, Color3, Texture, ParticleSystem } from "babylonjs"; import type { VFXLoaderOptions } from "../types/loader"; import type { QuarksVFXJSON, QuarksMaterial, QuarksTexture, QuarksImage, QuarksGeometry } from "../types/quarksTypes"; import type { @@ -171,9 +171,6 @@ export class VFXDataConverter { // Convert emitter config from Quarks to VFX format const vfxConfig = this._convertEmitterConfig(obj.ps); - // Determine system type based on renderMode: 2 = solid, otherwise base - const systemType: "solid" | "base" = vfxConfig.renderMode === 2 ? "solid" : "base"; - const emitter: VFXEmitter = { uuid: obj.uuid || `emitter_${emitters.size}`, name: obj.name || "ParticleEmitter", @@ -181,12 +178,12 @@ export class VFXDataConverter { config: vfxConfig, materialId: obj.ps.material, parentUuid: parentUuid || undefined, - systemType, + systemType: vfxConfig.systemType, // systemType is set in _convertEmitterConfig matrix: obj.matrix, // Store original matrix for rotation extraction }; emitters.set(emitter.uuid, emitter); - this._logger.log(`${indent}Converted Emitter: ${emitter.name} (uuid: ${emitter.uuid}, systemType: ${systemType})`); + this._logger.log(`${indent}Converted Emitter: ${emitter.name} (uuid: ${emitter.uuid}, systemType: ${vfxConfig.systemType})`); return emitter; } @@ -252,6 +249,9 @@ export class VFXDataConverter { * Convert emitter config from Quarks to VFX format */ private _convertEmitterConfig(quarksConfig: QuarksParticleEmitterConfig): VFXParticleEmitterConfig { + // Determine system type based on renderMode: 2 = solid, otherwise base + const systemType: "solid" | "base" = quarksConfig.renderMode === 2 ? "solid" : "base"; + const vfxConfig: VFXParticleEmitterConfig = { version: quarksConfig.version, autoDestroy: quarksConfig.autoDestroy, @@ -261,7 +261,7 @@ export class VFXDataConverter { onlyUsedByOther: quarksConfig.onlyUsedByOther, instancingGeometry: quarksConfig.instancingGeometry, renderOrder: quarksConfig.renderOrder, - renderMode: quarksConfig.renderMode, + systemType, rendererEmitterSettings: quarksConfig.rendererEmitterSettings, material: quarksConfig.material, layers: quarksConfig.layers, @@ -318,6 +318,46 @@ export class VFXDataConverter { vfxConfig.behaviors = quarksConfig.behaviors.map((behavior) => this._convertBehavior(behavior)); } + // Convert renderMode to systemType, billboardMode and isBillboardBased + // Quarks RenderMode: + // 0 = BillBoard → systemType = "base", isBillboardBased = true, billboardMode = ALL (default) + // 1 = StretchedBillBoard → systemType = "base", isBillboardBased = true, billboardMode = STRETCHED + // 2 = Mesh → systemType = "solid", isBillboardBased = false (always) + // 3 = Trail → systemType = "base", isBillboardBased = true, billboardMode = ALL (not directly supported, treat as billboard) + // 4 = HorizontalBillBoard → systemType = "base", isBillboardBased = true, billboardMode = Y + // 5 = VerticalBillBoard → systemType = "base", isBillboardBased = true, billboardMode = Y (same as horizontal) + if (quarksConfig.renderMode !== undefined) { + if (quarksConfig.renderMode === 0) { + // BillBoard + vfxConfig.isBillboardBased = true; + vfxConfig.billboardMode = ParticleSystem.BILLBOARDMODE_ALL; + } else if (quarksConfig.renderMode === 1) { + // StretchedBillBoard + vfxConfig.isBillboardBased = true; + vfxConfig.billboardMode = ParticleSystem.BILLBOARDMODE_STRETCHED; + } else if (quarksConfig.renderMode === 2) { + // Mesh (SolidParticleSystem) - always false + vfxConfig.isBillboardBased = false; + // billboardMode not applicable for mesh + } else if (quarksConfig.renderMode === 3) { + // Trail - not directly supported, treat as billboard + vfxConfig.isBillboardBased = true; + vfxConfig.billboardMode = ParticleSystem.BILLBOARDMODE_ALL; + } else if (quarksConfig.renderMode === 4 || quarksConfig.renderMode === 5) { + // HorizontalBillBoard or VerticalBillBoard + vfxConfig.isBillboardBased = true; + vfxConfig.billboardMode = ParticleSystem.BILLBOARDMODE_Y; + } else { + // Unknown renderMode, default to billboard + vfxConfig.isBillboardBased = true; + vfxConfig.billboardMode = ParticleSystem.BILLBOARDMODE_ALL; + } + } else { + // Default: billboard mode + vfxConfig.isBillboardBased = true; + vfxConfig.billboardMode = ParticleSystem.BILLBOARDMODE_ALL; + } + return vfxConfig; } diff --git a/editor/src/editor/windows/fx-editor/VFX/systems/VFXParticleSystem.ts b/editor/src/editor/windows/fx-editor/VFX/systems/VFXParticleSystem.ts index 7093530a1..0b4b3a726 100644 --- a/editor/src/editor/windows/fx-editor/VFX/systems/VFXParticleSystem.ts +++ b/editor/src/editor/windows/fx-editor/VFX/systems/VFXParticleSystem.ts @@ -19,7 +19,6 @@ import type { import type { Particle } from "babylonjs"; import type { VFXShape } from "../types/shapes"; import type { VFXParticleEmitterConfig, VFXEmissionBurst } from "../types/emitterConfig"; -import { VFXParticleSystemEmitterFactory } from "../factories/VFXParticleSystemEmitterFactory"; import { VFXValueUtils } from "../utils/valueParser"; import { VFXCapacityCalculator } from "../utils/capacityCalculator"; import { @@ -46,7 +45,6 @@ export class VFXParticleSystem extends ParticleSystem implements IVFXSystem { public startSpeed: number; public startColor: Color4; private _behaviors: VFXPerParticleBehaviorFunction[]; - private _emitterFactory: VFXParticleSystemEmitterFactory; public readonly behaviorConfigs: VFXBehavior[]; constructor( @@ -56,11 +54,6 @@ export class VFXParticleSystem extends ParticleSystem implements IVFXSystem { options?: { texture?: Texture; blendMode?: number; - emitterShape?: { - shape: VFXShape | undefined; - cumulativeScale: Vector3; - rotationMatrix: Matrix | null; - }; } ) { // Calculate capacity @@ -69,7 +62,6 @@ export class VFXParticleSystem extends ParticleSystem implements IVFXSystem { super(name, capacity, scene); this._behaviors = []; - this._emitterFactory = new VFXParticleSystemEmitterFactory(this); // Create proxy array that updates functions when modified this.behaviorConfigs = this._createBehaviorConfigsProxy(config.behaviors || []); @@ -88,13 +80,6 @@ export class VFXParticleSystem extends ParticleSystem implements IVFXSystem { return this.emitter instanceof AbstractMesh ? this.emitter : null; } - /** - * Create emitter shape based on VFX shape configuration - */ - public createEmitterShape(shape: VFXShape | undefined, cumulativeScale: Vector3, rotationMatrix: Matrix | null): void { - this._emitterFactory.createEmitter(shape, cumulativeScale, rotationMatrix); - } - /** * Get behavior functions (internal use) */ @@ -378,13 +363,12 @@ export class VFXParticleSystem extends ParticleSystem implements IVFXSystem { this.targetStopDuration = config.looping ? 0 : duration; } - // Configure render mode - if (config.renderMode !== undefined) { - if (config.renderMode === 0) { - this.isBillboardBased = true; - } else if (config.renderMode === 1) { - this.billboardMode = ParticleSystem.BILLBOARDMODE_STRETCHED; - } + // Configure billboard mode (converted from renderMode in VFXDataConverter) + if (config.isBillboardBased !== undefined) { + this.isBillboardBased = config.isBillboardBased; + } + if (config.billboardMode !== undefined) { + this.billboardMode = config.billboardMode; } // Configure auto destroy @@ -392,10 +376,7 @@ export class VFXParticleSystem extends ParticleSystem implements IVFXSystem { this.disposeOnStop = config.autoDestroy; } - // Set emitter shape - if (options?.emitterShape) { - this.createEmitterShape(options.emitterShape.shape, options.emitterShape.cumulativeScale, options.emitterShape.rotationMatrix); - } + // Emitter shape is created in VFXSystemFactory after system creation } /** diff --git a/editor/src/editor/windows/fx-editor/VFX/systems/VFXSolidParticleSystem.ts b/editor/src/editor/windows/fx-editor/VFX/systems/VFXSolidParticleSystem.ts index e52572918..9703e802e 100644 --- a/editor/src/editor/windows/fx-editor/VFX/systems/VFXSolidParticleSystem.ts +++ b/editor/src/editor/windows/fx-editor/VFX/systems/VFXSolidParticleSystem.ts @@ -1,5 +1,7 @@ import { Vector3, Quaternion, Matrix, Color4, SolidParticleSystem, SolidParticle, TransformNode, Mesh, AbstractMesh } from "babylonjs"; import type { VFXParticleEmitterConfig, VFXEmissionBurst } from "../types/emitterConfig"; +import type { ISolidParticleEmitterType } from "../types/emitters"; +import { SolidPointParticleEmitter, SolidSphereParticleEmitter, SolidConeParticleEmitter } from "../types/emitters"; import { VFXLogger } from "../loggers/VFXLogger"; import type { VFXLoaderOptions } from "../types/loader"; import type { VFXPerSolidParticleBehaviorFunction } from "../types/VFXBehaviorFunction"; @@ -16,7 +18,6 @@ import type { VFXShape } from "../types/shapes"; import type { VFXColor } from "../types/colors"; import type { VFXValue } from "../types/values"; import type { VFXRotation } from "../types/rotations"; -import { VFXSolidParticleSystemEmitterFactory } from "../factories/VFXSolidParticleSystemEmitterFactory"; import { VFXValueUtils } from "../utils/valueParser"; import { VFXCapacityCalculator } from "../utils/capacityCalculator"; import { ColorGradientSystem, NumberGradientSystem } from "../utils/gradientSystem"; @@ -38,13 +39,13 @@ interface EmissionState { } /** - * Extended SolidParticleSystem implementing three.quarks Mesh renderMode (renderMode = 2) logic - * This class replicates the exact behavior of three.quarks ParticleSystem with renderMode = Mesh + * Extended SolidParticleSystem implementing three.quarks Mesh systemType (systemType = "solid") logic + * This class replicates the exact behavior of three.quarks ParticleSystem with systemType = "solid" */ export class VFXSolidParticleSystem extends SolidParticleSystem implements IVFXSystem { private _emissionState: EmissionState; private _behaviors: VFXPerSolidParticleBehaviorFunction[]; - private _emitterFactory: VFXSolidParticleSystemEmitterFactory; + public particleEmitterType: ISolidParticleEmitterType | null; private _parent: TransformNode | null; private _vfxTransform: { position: Vector3; rotation: Quaternion; scale: Vector3 } | null; private _logger: VFXLogger | null; @@ -75,10 +76,10 @@ export class VFXSolidParticleSystem extends SolidParticleSystem implements IVFXS public onlyUsedByOther: boolean; public instancingGeometry?: string; public renderOrder?: number; - public renderMode?: number; public rendererEmitterSettings?: Record; public material?: string; public layers?: number; + public isBillboardBased?: boolean; // Converted from renderMode (always false for Mesh) public startTileIndex?: VFXValue; public uTileCount?: number; public vTileCount?: number; @@ -395,6 +396,29 @@ export class VFXSolidParticleSystem extends SolidParticleSystem implements IVFXS return this.mesh || null; } + /** + * Reset the particle system (stop and clear all particles) + * Stops emission, resets emission state, and rebuilds particles to initial state + */ + public reset(): void { + // Stop the system if it's running + this.stop(); + + // Reset emission state + this._emissionState.time = 0; + this._emissionState.waitEmiting = 0; + this._emissionState.travelDistance = 0; + this._emissionState.burstIndex = 0; + this._emissionState.burstWaveIndex = 0; + this._emissionState.burstParticleIndex = 0; + this._emissionState.burstParticleCount = 0; + this._emissionState.isBursting = false; + this._emitEnded = false; + + // Rebuild mesh to reset all particles to initial state (reset=true) + this.rebuildMesh(true); + } + /** * Get behavior functions (internal use) */ @@ -470,15 +494,22 @@ export class VFXSolidParticleSystem extends SolidParticleSystem implements IVFXS // Add shape to SPS this.addShape(particleMesh, capacity); - // Configure billboard mode - if (this.renderMode === 0 || this.renderMode === 1) { - this.billboard = true; + // Configure billboard mode (converted from renderMode in VFXDataConverter) + // For Mesh (systemType === "solid"), isBillboardBased is always false + // For other modes, use isBillboardBased from config + if (this.isBillboardBased !== undefined) { + // For SolidParticleSystem, billboard property controls billboard mode + // isBillboardBased = false means mesh mode (always false for systemType === "solid") + this.billboard = this.isBillboardBased; + } else { + // Default: no billboard for mesh + this.billboard = false; } // Enable vertex colors and alpha for particle color support // This must be done after addShape but before buildMesh // The mesh will be created in buildMesh, so we'll set it there - + // Dispose temporary mesh after adding to SPS particleMesh.dispose(); } @@ -531,7 +562,7 @@ export class VFXSolidParticleSystem extends SolidParticleSystem implements IVFXS this._name = name; this._behaviors = []; - this._emitterFactory = new VFXSolidParticleSystemEmitterFactory(); + this.particleEmitterType = null; // Will be set by create*Emitter methods // Initialize properties from initialConfig this.isLooping = initialConfig.looping !== false; @@ -549,10 +580,10 @@ export class VFXSolidParticleSystem extends SolidParticleSystem implements IVFXS this.onlyUsedByOther = initialConfig.onlyUsedByOther || false; this.instancingGeometry = initialConfig.instancingGeometry; this.renderOrder = initialConfig.renderOrder; - this.renderMode = initialConfig.renderMode; this.rendererEmitterSettings = initialConfig.rendererEmitterSettings; this.material = initialConfig.material; this.layers = initialConfig.layers; + this.isBillboardBased = initialConfig.isBillboardBased; // Always false for Mesh (systemType === "solid") this.startTileIndex = initialConfig.startTileIndex; this.uTileCount = initialConfig.uTileCount; this.vTileCount = initialConfig.vTileCount; @@ -723,7 +754,12 @@ export class VFXSolidParticleSystem extends SolidParticleSystem implements IVFXS } // Handle simple VFXValue (treat as angleZ for backward compatibility) - if (typeof this.startRotation === "number" || (typeof this.startRotation === "object" && "type" in this.startRotation && (this.startRotation.type === "ConstantValue" || this.startRotation.type === "IntervalValue" || this.startRotation.type === "PiecewiseBezier"))) { + if ( + typeof this.startRotation === "number" || + (typeof this.startRotation === "object" && + "type" in this.startRotation && + (this.startRotation.type === "ConstantValue" || this.startRotation.type === "IntervalValue" || this.startRotation.type === "PiecewiseBezier")) + ) { const angleZ = VFXValueUtils.parseValue(this.startRotation as VFXValue, normalizedTime); particle.rotation.set(0, 0, angleZ); return; @@ -838,11 +874,45 @@ export class VFXSolidParticleSystem extends SolidParticleSystem implements IVFXS } /** - * Initialize emitter shape for particle using factory + * Initialize emitter shape for particle using particleEmitterType */ private _initializeEmitterShape(particle: SolidParticle): void { const startSpeed = particle.props?.startSpeed ?? 0; - this._emitterFactory.initializeParticle(particle, this.shape, startSpeed); + if (this.particleEmitterType) { + this.particleEmitterType.initializeParticle(particle, startSpeed); + } else { + // Fallback: default point emitter + particle.position.setAll(0); + particle.velocity.set(0, 1, 0); + particle.velocity.scaleInPlace(startSpeed); + } + } + + /** + * Create point emitter for SolidParticleSystem + */ + public createPointEmitter(): SolidPointParticleEmitter { + const emitter = new SolidPointParticleEmitter(); + this.particleEmitterType = emitter; + return emitter; + } + + /** + * Create sphere emitter for SolidParticleSystem + */ + public createSphereEmitter(radius: number = 1, arc: number = Math.PI * 2, thickness: number = 1): SolidSphereParticleEmitter { + const emitter = new SolidSphereParticleEmitter(radius, arc, thickness); + this.particleEmitterType = emitter; + return emitter; + } + + /** + * Create cone emitter for SolidParticleSystem + */ + public createConeEmitter(radius: number = 1, arc: number = Math.PI * 2, thickness: number = 1, angle: number = Math.PI / 6): SolidConeParticleEmitter { + const emitter = new SolidConeParticleEmitter(radius, arc, thickness, angle); + this.particleEmitterType = emitter; + return emitter; } private _getEmitterMatrix(): Matrix { @@ -943,7 +1013,7 @@ export class VFXSolidParticleSystem extends SolidParticleSystem implements IVFXS */ public override buildMesh(): Mesh { const mesh = super.buildMesh(); - + // Enable vertex colors and alpha for particle color support // This is required for ColorOverLife behavior to work if (mesh) { @@ -952,7 +1022,7 @@ export class VFXSolidParticleSystem extends SolidParticleSystem implements IVFXS this._logger.log(`Enabled hasVertexAlpha for SPS mesh: ${mesh.name}`); } } - + return mesh; } @@ -1349,7 +1419,7 @@ export class VFXSolidParticleSystem extends SolidParticleSystem implements IVFXS // Always apply gradient color directly to particle.color // The base class will apply this to vertex colors if _computeParticleColor is enabled particle.color.copyFrom(color); - + // Multiply with startColor if it exists (matching ParticleSystem behavior) const startColor = props.startColor; if (startColor) { diff --git a/editor/src/editor/windows/fx-editor/VFX/types/emitterConfig.ts b/editor/src/editor/windows/fx-editor/VFX/types/emitterConfig.ts index a6ad882db..bd0325d9b 100644 --- a/editor/src/editor/windows/fx-editor/VFX/types/emitterConfig.ts +++ b/editor/src/editor/windows/fx-editor/VFX/types/emitterConfig.ts @@ -33,10 +33,13 @@ export interface VFXParticleEmitterConfig { onlyUsedByOther?: boolean; instancingGeometry?: string; renderOrder?: number; - renderMode?: number; + systemType: "solid" | "base"; // Determined from renderMode: 2 = solid, otherwise base rendererEmitterSettings?: Record; material?: string; layers?: number; + // Billboard settings (converted from renderMode) + isBillboardBased?: boolean; + billboardMode?: number; // ParticleSystem.BILLBOARDMODE_* startTileIndex?: VFXValue; uTileCount?: number; vTileCount?: number; diff --git a/editor/src/editor/windows/fx-editor/VFX/types/emitters.ts b/editor/src/editor/windows/fx-editor/VFX/types/emitters.ts new file mode 100644 index 000000000..3ae7dba50 --- /dev/null +++ b/editor/src/editor/windows/fx-editor/VFX/types/emitters.ts @@ -0,0 +1,92 @@ +import { Vector3 } from "babylonjs"; +import type { SolidParticle } from "babylonjs"; + +/** + * Interface for SolidParticleSystem emitter types + * Similar to IParticleEmitterType for ParticleSystem + */ +export interface ISolidParticleEmitterType { + /** + * Initialize particle position and velocity based on emitter shape + */ + initializeParticle(particle: SolidParticle, startSpeed: number): void; +} + +/** + * Point emitter for SolidParticleSystem + */ +export class SolidPointParticleEmitter implements ISolidParticleEmitterType { + public initializeParticle(particle: SolidParticle, startSpeed: number): void { + const theta = Math.random() * Math.PI * 2; + const phi = Math.acos(2.0 * Math.random() - 1.0); + const direction = new Vector3(Math.sin(phi) * Math.cos(theta), Math.sin(phi) * Math.sin(theta), Math.cos(phi)); + particle.position.setAll(0); + particle.velocity.copyFrom(direction); + particle.velocity.scaleInPlace(startSpeed); + } +} + +/** + * Sphere emitter for SolidParticleSystem + */ +export class SolidSphereParticleEmitter implements ISolidParticleEmitterType { + public radius: number; + public arc: number; + public thickness: number; + + constructor(radius: number = 1, arc: number = Math.PI * 2, thickness: number = 1) { + this.radius = radius; + this.arc = arc; + this.thickness = thickness; + } + + public initializeParticle(particle: SolidParticle, startSpeed: number): void { + const u = Math.random(); + const v = Math.random(); + const rand = 1 - this.thickness + Math.random() * this.thickness; + const theta = u * this.arc; + const phi = Math.acos(2.0 * v - 1.0); + const sinTheta = Math.sin(theta); + const cosTheta = Math.cos(theta); + const sinPhi = Math.sin(phi); + const cosPhi = Math.cos(phi); + + particle.position.set(sinPhi * cosTheta, sinPhi * sinTheta, cosPhi); + particle.velocity.copyFrom(particle.position); + particle.velocity.scaleInPlace(startSpeed); + particle.position.scaleInPlace(this.radius * rand); + } +} + +/** + * Cone emitter for SolidParticleSystem + */ +export class SolidConeParticleEmitter implements ISolidParticleEmitterType { + public radius: number; + public arc: number; + public thickness: number; + public angle: number; + + constructor(radius: number = 1, arc: number = Math.PI * 2, thickness: number = 1, angle: number = Math.PI / 6) { + this.radius = radius; + this.arc = arc; + this.thickness = thickness; + this.angle = angle; + } + + public initializeParticle(particle: SolidParticle, startSpeed: number): void { + const u = Math.random(); + const rand = 1 - this.thickness + Math.random() * this.thickness; + const theta = u * this.arc; + const r = Math.sqrt(rand); + const sinTheta = Math.sin(theta); + const cosTheta = Math.cos(theta); + + particle.position.set(r * cosTheta, r * sinTheta, 0); + const coneAngle = this.angle * r; + particle.velocity.set(0, 0, Math.cos(coneAngle)); + particle.velocity.addInPlace(particle.position.scale(Math.sin(coneAngle))); + particle.velocity.scaleInPlace(startSpeed); + particle.position.scaleInPlace(this.radius); + } +} diff --git a/editor/src/editor/windows/fx-editor/VFX/types/index.ts b/editor/src/editor/windows/fx-editor/VFX/types/index.ts index 8afbb464e..21de3ed58 100644 --- a/editor/src/editor/windows/fx-editor/VFX/types/index.ts +++ b/editor/src/editor/windows/fx-editor/VFX/types/index.ts @@ -38,7 +38,9 @@ export type { export type { VFXEmissionBurst, VFXParticleEmitterConfig } from "./emitterConfig"; export type { VFXTransform, VFXGroup, VFXEmitter, VFXData } from "./hierarchy"; export type { VFXMaterial, VFXTexture, VFXImage, VFXGeometry } from "./resources"; +export type { ISolidParticleEmitterType } from "./emitters"; +export { SolidPointParticleEmitter, SolidSphereParticleEmitter, SolidConeParticleEmitter } from "./emitters"; export type { QuarksVFXJSON } from "./quarksTypes"; -export type { VFXPerParticleContext, VFXPerParticleBehaviorFunction, VFXPerSolidParticleBehaviorFunction, VFXSystemBehaviorFunction } from "./VFXBehaviorFunction"; +export type { VFXPerParticleBehaviorFunction, VFXPerSolidParticleBehaviorFunction, VFXSystemBehaviorFunction } from "./VFXBehaviorFunction"; export type { IVFXSystem, ParticleWithSystem, SolidParticleWithSystem } from "./system"; export { isVFXSystem } from "./system"; diff --git a/editor/src/editor/windows/fx-editor/graph.tsx b/editor/src/editor/windows/fx-editor/graph.tsx index 93b163b92..61be24066 100644 --- a/editor/src/editor/windows/fx-editor/graph.tsx +++ b/editor/src/editor/windows/fx-editor/graph.tsx @@ -30,6 +30,8 @@ export interface IFXEditorGraphState { } export class FXEditorGraph extends Component { + private _vfxEffect: VFXEffect | null = null; + public constructor(props: IFXEditorGraphProps) { super(props); @@ -39,6 +41,13 @@ export class FXEditorGraph extends Component (this.props.editor.preview = r!)} filePath={this.props.filePath} + editor={this.props.editor} onSceneReady={() => { // Update graph when scene is ready if (this.props.editor.graph) { diff --git a/editor/src/editor/windows/fx-editor/preview.tsx b/editor/src/editor/windows/fx-editor/preview.tsx index 194c775c1..2aa28e3a9 100644 --- a/editor/src/editor/windows/fx-editor/preview.tsx +++ b/editor/src/editor/windows/fx-editor/preview.tsx @@ -6,10 +6,12 @@ import { Button } from "../../../ui/shadcn/ui/button"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "../../../ui/shadcn/ui/tooltip"; import { IoPlay, IoStop, IoRefresh } from "react-icons/io5"; +import type { IFXEditor } from "."; export interface IFXEditorPreviewProps { filePath: string | null; onSceneReady?: (scene: Scene) => void; + editor?: IFXEditor; } export interface IFXEditorPreviewState { @@ -153,21 +155,43 @@ export class FXEditorPreview extends Component { - ps.reset(); - }); + // Reset all systems (stop and clear particles) + effect.reset(); - this.setState({ playing: false }, () => { - this.setState({ playing: true }); - }); + // Start again + effect.start(); + this.setState({ playing: true }); + } + + public componentDidUpdate(): void { + // Update playing state based on actual effect state + const effect = this.props.editor?.graph?.getEffect(); + if (effect) { + const isStarted = effect.isStarted(); + if (this.state.playing !== isStarted) { + this.setState({ playing: isStarted }); + } + } } } diff --git a/editor/src/editor/windows/fx-editor/properties/vfx-color-editor.tsx b/editor/src/editor/windows/fx-editor/properties/vfx-color-editor.tsx index fbde88784..db547df92 100644 --- a/editor/src/editor/windows/fx-editor/properties/vfx-color-editor.tsx +++ b/editor/src/editor/windows/fx-editor/properties/vfx-color-editor.tsx @@ -300,9 +300,24 @@ function GradientEditor(props: IGradientEditorProps): ReactNode {
Color Keys
{colorKeys.map((key, index) => { // Convert value to Color3 for color picker - const colorValue = Array.isArray(key.value) ? key.value : typeof key.value === "number" ? [key.value, key.value, key.value] : [key.value?.r || 0, key.value?.g || 0, key.value?.b || 0]; + // Handle Color4 objects, arrays, numbers, or undefined + let colorValue: number[]; + let alpha: number; + if (Array.isArray(key.value)) { + colorValue = key.value; + alpha = key.value.length > 3 ? key.value[3] : 1; + } else if (typeof key.value === "number") { + colorValue = [key.value, key.value, key.value]; + alpha = 1; + } else if (key.value && typeof key.value === "object" && "r" in key.value && "g" in key.value && "b" in key.value) { + // Handle Color4 or Color3 objects + colorValue = [key.value.r || 0, key.value.g || 0, key.value.b || 0]; + alpha = "a" in key.value ? key.value.a || 1 : 1; + } else { + colorValue = [0, 0, 0]; + alpha = 1; + } const color3 = new Color3(colorValue[0], colorValue[1], colorValue[2]); - const alpha = Array.isArray(key.value) && key.value.length > 3 ? key.value[3] : 1; return ( @@ -314,7 +329,8 @@ function GradientEditor(props: IGradientEditorProps): ReactNode { label="" onChange={(color) => { const newColorKeys = [...colorKeys]; - newColorKeys[index] = { ...key, value: [color.r, color.g, color.b, alpha] }; + // Create new key object without Color4 to avoid React rendering issues + newColorKeys[index] = { pos: key.pos, value: [color.r, color.g, color.b, alpha] }; updateGradient(newColorKeys); }} /> @@ -327,7 +343,9 @@ function GradientEditor(props: IGradientEditorProps): ReactNode { value={[key.pos || 0]} onValueChange={(vals) => { const newColorKeys = [...colorKeys]; - newColorKeys[index] = { ...key, pos: vals[0] }; + // Create new key object without Color4 to avoid React rendering issues + const keyValue = Array.isArray(key.value) ? key.value : typeof key.value === "number" ? [key.value, key.value, key.value] : key.value && typeof key.value === "object" && "r" in key.value ? [key.value.r || 0, key.value.g || 0, key.value.b || 0, "a" in key.value ? key.value.a || 1 : 1] : [0, 0, 0, 1]; + newColorKeys[index] = { pos: vals[0], value: keyValue }; updateGradient(newColorKeys); }} /> @@ -366,14 +384,25 @@ function GradientEditor(props: IGradientEditorProps): ReactNode {
{(() => { - const alphaValue = typeof key.value === "number" ? key.value : Array.isArray(key.value) ? key.value[3] || 1 : 1; + // Handle alpha value - can be number, array, or Color4 object + let alphaValue: number; + if (typeof key.value === "number") { + alphaValue = key.value; + } else if (Array.isArray(key.value)) { + alphaValue = key.value[3] || 1; + } else if (key.value && typeof key.value === "object" && "a" in key.value) { + alphaValue = key.value.a || 1; + } else { + alphaValue = 1; + } const wrapperAlpha = { get value() { return alphaValue; }, set value(newVal: number) { const newAlphaKeys = [...alphaKeys]; - newAlphaKeys[index] = { ...key, value: newVal }; + // Create new key object without Color4 to avoid React rendering issues + newAlphaKeys[index] = { pos: key.pos, value: newVal }; updateGradient(colorKeys, newAlphaKeys); }, }; @@ -388,7 +417,9 @@ function GradientEditor(props: IGradientEditorProps): ReactNode { value={[key.pos || 0]} onValueChange={(vals) => { const newAlphaKeys = [...alphaKeys]; - newAlphaKeys[index] = { ...key, pos: vals[0] }; + // Create new key object without Color4 to avoid React rendering issues + const keyValue = typeof key.value === "number" ? key.value : Array.isArray(key.value) ? key.value[3] || 1 : key.value && typeof key.value === "object" && "a" in key.value ? key.value.a || 1 : 1; + newAlphaKeys[index] = { pos: vals[0], value: keyValue }; updateGradient(colorKeys, newAlphaKeys); }} /> From 75496d39aa4e4dbdde44434d21b8c1531d8e1c83 Mon Sep 17 00:00:00 2001 From: Mikalai Lazitski Date: Sun, 14 Dec 2025 21:26:42 +0300 Subject: [PATCH 29/62] feat: introduce EditorInspectorColorGradientField and GradientPicker components for enhanced gradient editing capabilities in the VFX editor, improving user experience and flexibility in color and alpha key management --- .../layout/inspector/fields/gradient.tsx | 179 ++++++++ .../behaviors/color-function-editor.tsx | 313 +++++++------- .../fx-editor/properties/vfx-color-editor.tsx | 333 +++++---------- editor/src/ui/gradient-picker.tsx | 391 ++++++++++++++++++ 4 files changed, 809 insertions(+), 407 deletions(-) create mode 100644 editor/src/editor/layout/inspector/fields/gradient.tsx create mode 100644 editor/src/ui/gradient-picker.tsx diff --git a/editor/src/editor/layout/inspector/fields/gradient.tsx b/editor/src/editor/layout/inspector/fields/gradient.tsx new file mode 100644 index 000000000..6c7980ca7 --- /dev/null +++ b/editor/src/editor/layout/inspector/fields/gradient.tsx @@ -0,0 +1,179 @@ +import { useState } from "react"; +import { Button, Popover } from "@blueprintjs/core"; +import { Color4 } from "babylonjs"; +import { GradientPicker, type IGradientKey } from "../../../../ui/gradient-picker"; +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "../../../../ui/shadcn/ui/tooltip"; +import { MdOutlineInfo } from "react-icons/md"; +import { registerUndoRedo } from "../../../../tools/undoredo"; +import { getInspectorPropertyValue } from "../../../../tools/property"; +import { IEditorInspectorFieldProps } from "./field"; + +export interface IEditorInspectorColorGradientFieldProps extends IEditorInspectorFieldProps { + onChange?: (colorKeys: IGradientKey[], alphaKeys?: IGradientKey[]) => void; + onFinishChange?: (colorKeys: IGradientKey[], alphaKeys?: IGradientKey[], oldColorKeys?: IGradientKey[], oldAlphaKeys?: IGradientKey[]) => void; +} + +/** + * Inspector field for editing color gradients + * Works with objects that have colorKeys and alphaKeys properties + * Similar to EditorInspectorColorField but for gradients + */ +export function EditorInspectorColorGradientField(props: IEditorInspectorColorGradientFieldProps) { + // Get colorKeys and alphaKeys from object + const getColorKeys = (): IGradientKey[] => { + if (props.property) { + return (getInspectorPropertyValue(props.object, `${props.property}.colorKeys`) || []) as IGradientKey[]; + } + return ((props.object as any)?.colorKeys || []) as IGradientKey[]; + }; + + const getAlphaKeys = (): IGradientKey[] => { + if (props.property) { + return (getInspectorPropertyValue(props.object, `${props.property}.alphaKeys`) || []) as IGradientKey[]; + } + return ((props.object as any)?.alphaKeys || []) as IGradientKey[]; + }; + + const colorKeys = getColorKeys(); + const alphaKeys = getAlphaKeys(); + + const [value, setValue] = useState({ colorKeys, alphaKeys }); + const [oldValue, setOldValue] = useState({ colorKeys: [...colorKeys], alphaKeys: [...alphaKeys] }); + + // Generate preview gradient CSS + const generatePreview = (): string => { + const sorted = [...value.colorKeys].sort((a, b) => (a.pos || 0) - (b.pos || 0)); + if (sorted.length === 0) return "linear-gradient(to right, rgba(0, 0, 0, 1) 0%, rgba(1, 1, 1, 1) 100%)"; + + const stops = sorted.map((key) => { + const pos = (key.pos || 0) * 100; + let color = "rgba(0, 0, 0, 1)"; + if (Array.isArray(key.value)) { + const [r, g, b, a = 1] = key.value; + color = `rgba(${r * 255}, ${g * 255}, ${b * 255}, ${a})`; + } else if (typeof key.value === "object" && "r" in key.value) { + const r = key.value.r * 255; + const g = key.value.g * 255; + const b = key.value.b * 255; + const a = ("a" in key.value ? key.value.a : 1) * 255; + color = `rgba(${r}, ${g}, ${b}, ${a / 255})`; + } + return `${color} ${pos}%`; + }); + return `linear-gradient(to right, ${stops.join(", ")})`; + }; + + function getPopoverContent() { + return ( + { + const updatedValue = { colorKeys: newColorKeys, alphaKeys: newAlphaKeys || value.alphaKeys }; + setValue(updatedValue); + + // Update object properties + if (props.object && props.property) { + (props.object as any)[props.property] = { + ...(props.object as any)[props.property], + colorKeys: newColorKeys, + alphaKeys: newAlphaKeys || value.alphaKeys, + }; + } + + props.onChange?.(newColorKeys, newAlphaKeys); + }} + onFinish={(newColorKeys, newAlphaKeys) => { + const updatedValue = { colorKeys: newColorKeys, alphaKeys: newAlphaKeys || value.alphaKeys }; + setValue(updatedValue); + + // Update object properties + if (props.object) { + if (props.property) { + (props.object as any)[props.property] = { + ...(props.object as any)[props.property], + colorKeys: newColorKeys, + alphaKeys: newAlphaKeys || value.alphaKeys, + }; + } else { + (props.object as any).colorKeys = newColorKeys; + (props.object as any).alphaKeys = newAlphaKeys || value.alphaKeys; + } + } + + if (!props.noUndoRedo) { + const newValue = { colorKeys: [...newColorKeys], alphaKeys: [...(newAlphaKeys || value.alphaKeys)] }; + + registerUndoRedo({ + undo: () => { + if (props.object) { + if (props.property) { + (props.object as any)[props.property] = { + ...(props.object as any)[props.property], + colorKeys: oldValue.colorKeys, + alphaKeys: oldValue.alphaKeys, + }; + } else { + (props.object as any).colorKeys = oldValue.colorKeys; + (props.object as any).alphaKeys = oldValue.alphaKeys; + } + } + setValue(oldValue); + }, + redo: () => { + if (props.object) { + if (props.property) { + (props.object as any)[props.property] = { + ...(props.object as any)[props.property], + colorKeys: newValue.colorKeys, + alphaKeys: newValue.alphaKeys, + }; + } else { + (props.object as any).colorKeys = newValue.colorKeys; + (props.object as any).alphaKeys = newValue.alphaKeys; + } + } + setValue(newValue); + }, + }); + + setOldValue(newValue); + } + + props.onFinishChange?.(newColorKeys, newAlphaKeys || value.alphaKeys, oldValue.colorKeys, oldValue.alphaKeys); + }} + /> + ); + } + + return ( +
+
+ {props.label} + + {props.tooltip && ( + + + + + + {props.tooltip} + + + )} +
+ +
+ +
+
+ ); +} + diff --git a/editor/src/editor/windows/fx-editor/properties/behaviors/color-function-editor.tsx b/editor/src/editor/windows/fx-editor/properties/behaviors/color-function-editor.tsx index e1938953f..4d3f2d8eb 100644 --- a/editor/src/editor/windows/fx-editor/properties/behaviors/color-function-editor.tsx +++ b/editor/src/editor/windows/fx-editor/properties/behaviors/color-function-editor.tsx @@ -1,14 +1,12 @@ import { ReactNode } from "react"; -import { Color4, Vector3, Color3 } from "babylonjs"; +import { Color4, Vector3 } from "babylonjs"; import { EditorInspectorColorField } from "../../../../layout/inspector/fields/color"; +import { EditorInspectorColorGradientField } from "../../../../layout/inspector/fields/gradient"; import { EditorInspectorListField } from "../../../../layout/inspector/fields/list"; import { EditorInspectorBlockField } from "../../../../layout/inspector/fields/block"; import { EditorInspectorNumberField } from "../../../../layout/inspector/fields/number"; - -import { Button } from "../../../../../ui/shadcn/ui/button"; -import { AiOutlinePlus, AiOutlineClose } from "react-icons/ai"; -import { Slider } from "../../../../../ui/shadcn/ui/slider"; +import type { IGradientKey } from "../../../../../ui/gradient-picker"; export type ColorFunctionType = "ConstantColor" | "ColorRange" | "Gradient" | "RandomColor" | "RandomColorBetweenGradient"; @@ -60,12 +58,12 @@ export function ColorFunctionEditor(props: IColorFunctionEditorProps): ReactNode value.data.colorB = new Color4(1, 1, 1, 1); } else if (newType === "Gradient") { value.data.colorKeys = [ - { color: new Vector3(0, 0, 0), position: 0 }, - { color: new Vector3(1, 1, 1), position: 1 }, + { pos: 0, value: [0, 0, 0, 1] }, + { pos: 1, value: [1, 1, 1, 1] }, ]; value.data.alphaKeys = [ - { value: 1, position: 0 }, - { value: 1, position: 1 }, + { pos: 0, value: 1 }, + { pos: 1, value: 1 }, ]; } else if (newType === "RandomColor") { value.data.colorA = new Color4(0, 0, 0, 1); @@ -73,22 +71,22 @@ export function ColorFunctionEditor(props: IColorFunctionEditorProps): ReactNode } else if (newType === "RandomColorBetweenGradient") { value.data.gradient1 = { colorKeys: [ - { color: new Vector3(0, 0, 0), position: 0 }, - { color: new Vector3(1, 1, 1), position: 1 }, + { pos: 0, value: [0, 0, 0, 1] }, + { pos: 1, value: [1, 1, 1, 1] }, ], alphaKeys: [ - { value: 1, position: 0 }, - { value: 1, position: 1 }, + { pos: 0, value: 1 }, + { pos: 1, value: 1 }, ], }; value.data.gradient2 = { colorKeys: [ - { color: new Vector3(1, 0, 0), position: 0 }, - { color: new Vector3(0, 1, 0), position: 1 }, + { pos: 0, value: [1, 0, 0, 1] }, + { pos: 1, value: [0, 1, 0, 1] }, ], alphaKeys: [ - { value: 1, position: 0 }, - { value: 1, position: 1 }, + { pos: 0, value: 1 }, + { pos: 1, value: 1 }, ], }; } @@ -112,7 +110,62 @@ export function ColorFunctionEditor(props: IColorFunctionEditorProps): ReactNode )} - {functionType === "Gradient" && } + {functionType === "Gradient" && (() => { + // Convert old format (Vector3 + position) to new format (array + pos) if needed + const convertColorKeys = (keys: any[]): IGradientKey[] => { + if (!keys || keys.length === 0) { + return [ + { pos: 0, value: [0, 0, 0, 1] }, + { pos: 1, value: [1, 1, 1, 1] }, + ]; + } + return keys.map((key) => { + if (key.color && key.color instanceof Vector3) { + // Old format: { color: Vector3, position: number } + return { + pos: key.position ?? key.pos ?? 0, + value: [key.color.x, key.color.y, key.color.z, 1], + }; + } + // Already in new format or other format + return { + pos: key.pos ?? key.position ?? 0, + value: Array.isArray(key.value) ? key.value : typeof key.value === "object" && "r" in key.value ? [key.value.r, key.value.g, key.value.b, key.value.a ?? 1] : [0, 0, 0, 1], + }; + }); + }; + + const convertAlphaKeys = (keys: any[]): IGradientKey[] => { + if (!keys || keys.length === 0) { + return [ + { pos: 0, value: 1 }, + { pos: 1, value: 1 }, + ]; + } + return keys.map((key) => ({ + pos: key.pos ?? key.position ?? 0, + value: typeof key.value === "number" ? key.value : 1, + })); + }; + + const wrapperGradient = { + colorKeys: convertColorKeys(value.data.colorKeys), + alphaKeys: convertAlphaKeys(value.data.alphaKeys), + }; + + return ( + { + value.data.colorKeys = newColorKeys; + value.data.alphaKeys = newAlphaKeys; + onChange(); + }} + /> + ); + })()} {functionType === "RandomColor" && ( <> @@ -123,169 +176,87 @@ export function ColorFunctionEditor(props: IColorFunctionEditorProps): ReactNode )} - {functionType === "RandomColorBetweenGradient" && ( - <> - {!value.data.gradient1 && (value.data.gradient1 = {})} - {!value.data.gradient2 && (value.data.gradient2 = {})} - -
Gradient 1
- -
- -
Gradient 2
- -
- - )} - - ); -} + {functionType === "RandomColorBetweenGradient" && (() => { + // Convert old format to new format if needed + const convertColorKeys = (keys: any[]): IGradientKey[] => { + if (!keys || keys.length === 0) { + return [ + { pos: 0, value: [0, 0, 0, 1] }, + { pos: 1, value: [1, 1, 1, 1] }, + ]; + } + return keys.map((key) => { + if (key.color && key.color instanceof Vector3) { + return { + pos: key.position ?? key.pos ?? 0, + value: [key.color.x, key.color.y, key.color.z, 1], + }; + } + return { + pos: key.pos ?? key.position ?? 0, + value: Array.isArray(key.value) ? key.value : typeof key.value === "object" && "r" in key.value ? [key.value.r, key.value.g, key.value.b, key.value.a ?? 1] : [0, 0, 0, 1], + }; + }); + }; -interface IGradientEditorProps { - value: any; - onChange: () => void; -} + const convertAlphaKeys = (keys: any[]): IGradientKey[] => { + if (!keys || keys.length === 0) { + return [ + { pos: 0, value: 1 }, + { pos: 1, value: 1 }, + ]; + } + return keys.map((key) => ({ + pos: key.pos ?? key.position ?? 0, + value: typeof key.value === "number" ? key.value : 1, + })); + }; -function GradientEditor(props: IGradientEditorProps): ReactNode { - const { value, onChange } = props; + if (!value.data.gradient1) value.data.gradient1 = {}; + if (!value.data.gradient2) value.data.gradient2 = {}; - // Initialize gradient data - if (!value.colorKeys || value.colorKeys.length === 0) { - value.colorKeys = [ - { color: new Vector3(0, 0, 0), position: 0 }, - { color: new Vector3(1, 1, 1), position: 1 }, - ]; - } - if (!value.alphaKeys || value.alphaKeys.length === 0) { - value.alphaKeys = [ - { value: 1, position: 0 }, - { value: 1, position: 1 }, - ]; - } + const wrapperGradient1 = { + colorKeys: convertColorKeys(value.data.gradient1.colorKeys), + alphaKeys: convertAlphaKeys(value.data.gradient1.alphaKeys), + }; - return ( -
-
Color Keys
- {value.colorKeys.map((key: any, index: number) => { - // Ensure color is Vector3 and convert to Color3 for color picker - if (!key.color) { - key.color = new Vector3(0, 0, 0); - } - if (!key._color3) { - key._color3 = new Color3(key.color.x, key.color.y, key.color.z); - } - // Sync Vector3 with Color3 before render - key._color3.r = key.color.x; - key._color3.g = key.color.y; - key._color3.b = key.color.z; + const wrapperGradient2 = { + colorKeys: convertColorKeys(value.data.gradient2.colorKeys), + alphaKeys: convertAlphaKeys(value.data.gradient2.alphaKeys), + }; return ( - -
-
- { - key.color.x = color.r; - key.color.y = color.g; - key.color.z = color.b; - key._color3.r = color.r; - key._color3.g = color.g; - key._color3.b = color.b; - onChange(); - }} - /> -
-
- {key.position === undefined && (key.position = index / Math.max(1, value.colorKeys.length - 1))} - { - key.position = vals[0]; - onChange(); - }} - /> -
- -
-
- ); - })} - - -
Alpha Keys
- {value.alphaKeys.map((key: any, index: number) => ( - -
-
- {key.value === undefined && (key.value = 1)} - -
-
- {key.position === undefined && (key.position = index / Math.max(1, value.alphaKeys.length - 1))} - { - key.position = vals[0]; + /> + + +
Gradient 2
+ { + value.data.gradient2.colorKeys = newColorKeys; + value.data.gradient2.alphaKeys = newAlphaKeys; onChange(); }} /> -
- -
-
- ))} - -
+ + + ); + })()} + ); } + diff --git a/editor/src/editor/windows/fx-editor/properties/vfx-color-editor.tsx b/editor/src/editor/windows/fx-editor/properties/vfx-color-editor.tsx index db547df92..715e3bef6 100644 --- a/editor/src/editor/windows/fx-editor/properties/vfx-color-editor.tsx +++ b/editor/src/editor/windows/fx-editor/properties/vfx-color-editor.tsx @@ -1,17 +1,12 @@ import { ReactNode } from "react"; -import { Color4, Vector3, Color3 } from "babylonjs"; +import { Color4 } from "babylonjs"; import { EditorInspectorColorField } from "../../../layout/inspector/fields/color"; +import { EditorInspectorColorGradientField } from "../../../layout/inspector/fields/gradient"; import { EditorInspectorListField } from "../../../layout/inspector/fields/list"; import { EditorInspectorBlockField } from "../../../layout/inspector/fields/block"; -import { EditorInspectorNumberField } from "../../../layout/inspector/fields/number"; - -import { Button } from "../../../../ui/shadcn/ui/button"; -import { AiOutlinePlus, AiOutlineClose } from "react-icons/ai"; -import { Slider } from "../../../../ui/shadcn/ui/slider"; import type { VFXColor, VFXConstantColor, VFXColorRange, VFXGradientColor, VFXRandomColor, VFXRandomColorBetweenGradient } from "../VFX/types/colors"; -import type { VFXGradientKey } from "../VFX/types/gradients"; import { VFXValueUtils } from "../VFX/utils/valueParser"; export type VFXColorType = "ConstantColor" | "ColorRange" | "Gradient" | "RandomColor" | "RandomColorBetweenGradient"; @@ -184,14 +179,46 @@ export function VFXColorEditor(props: IVFXColorEditorProps): ReactNode { )} - {currentType === "Gradient" && } + {currentType === "Gradient" && (() => { + const gradientValue = value && typeof value === "object" && "type" in value && value.type === "Gradient" ? value : null; + const defaultColorKeys = [ + { pos: 0, value: [0, 0, 0, 1] }, + { pos: 1, value: [1, 1, 1, 1] }, + ]; + const defaultAlphaKeys = [ + { pos: 0, value: 1 }, + { pos: 1, value: 1 }, + ]; + const wrapperGradient = { + colorKeys: gradientValue?.colorKeys || defaultColorKeys, + alphaKeys: gradientValue?.alphaKeys || defaultAlphaKeys, + }; + return ( + { + onChange({ + type: "Gradient", + colorKeys: newColorKeys, + alphaKeys: newAlphaKeys, + }); + }} + /> + ); + })()} {currentType === "RandomColor" && ( <> {(() => { const randomColor = value && typeof value === "object" && "type" in value && value.type === "RandomColor" ? value : null; - const colorA = randomColor ? new Color4(randomColor.colorA[0], randomColor.colorA[1], randomColor.colorA[2], randomColor.colorA[3]) : new Color4(0, 0, 0, 1); - const colorB = randomColor ? new Color4(randomColor.colorB[0], randomColor.colorB[1], randomColor.colorB[2], randomColor.colorB[3]) : new Color4(1, 1, 1, 1); + const colorA = randomColor + ? new Color4(randomColor.colorA[0], randomColor.colorA[1], randomColor.colorA[2], randomColor.colorA[3]) + : new Color4(0, 0, 0, 1); + const colorB = randomColor + ? new Color4(randomColor.colorB[0], randomColor.colorB[1], randomColor.colorB[2], randomColor.colorB[3]) + : new Color4(1, 1, 1, 1); const wrapperRandom = { get colorA() { return colorA; @@ -218,239 +245,73 @@ export function VFXColorEditor(props: IVFXColorEditorProps): ReactNode { )} - {currentType === "RandomColorBetweenGradient" && ( - <> - {(() => { - const randomGradient = value && typeof value === "object" && "type" in value && value.type === "RandomColorBetweenGradient" ? value : null; - return ( - <> - -
Gradient 1
- { - if (randomGradient) { - onChange({ - type: "RandomColorBetweenGradient", - gradient1: { - colorKeys: newGradient.type === "Gradient" ? newGradient.colorKeys : [], - alphaKeys: newGradient.type === "Gradient" ? newGradient.alphaKeys : [], - }, - gradient2: randomGradient.gradient2, - }); - } - }} - /> -
- -
Gradient 2
- { - if (randomGradient) { - onChange({ - type: "RandomColorBetweenGradient", - gradient1: randomGradient.gradient1, - gradient2: { - colorKeys: newGradient.type === "Gradient" ? newGradient.colorKeys : [], - alphaKeys: newGradient.type === "Gradient" ? newGradient.alphaKeys : [], - }, - }); - } - }} - /> -
- - ); - })()} - - )} - - ); -} + {currentType === "RandomColorBetweenGradient" && (() => { + const randomGradient = value && typeof value === "object" && "type" in value && value.type === "RandomColorBetweenGradient" ? value : null; + const defaultColorKeys = [ + { pos: 0, value: [0, 0, 0, 1] }, + { pos: 1, value: [1, 1, 1, 1] }, + ]; + const defaultAlphaKeys = [ + { pos: 0, value: 1 }, + { pos: 1, value: 1 }, + ]; -interface IGradientEditorProps { - value: VFXGradientColor | null; - onChange: (newValue: VFXGradientColor) => void; -} - -function GradientEditor(props: IGradientEditorProps): ReactNode { - const { value, onChange } = props; - - // Initialize gradient data - const colorKeys = value?.colorKeys || [ - { pos: 0, value: [0, 0, 0, 1] }, - { pos: 1, value: [1, 1, 1, 1] }, - ]; - const alphaKeys = value?.alphaKeys || [ - { pos: 0, value: 1 }, - { pos: 1, value: 1 }, - ]; - - const updateGradient = (newColorKeys: VFXGradientKey[], newAlphaKeys?: VFXGradientKey[]) => { - onChange({ - type: "Gradient", - colorKeys: newColorKeys, - alphaKeys: newAlphaKeys || alphaKeys, - }); - }; + const wrapperGradient1 = { + colorKeys: randomGradient?.gradient1?.colorKeys || defaultColorKeys, + alphaKeys: randomGradient?.gradient1?.alphaKeys || defaultAlphaKeys, + }; - return ( -
-
Color Keys
- {colorKeys.map((key, index) => { - // Convert value to Color3 for color picker - // Handle Color4 objects, arrays, numbers, or undefined - let colorValue: number[]; - let alpha: number; - if (Array.isArray(key.value)) { - colorValue = key.value; - alpha = key.value.length > 3 ? key.value[3] : 1; - } else if (typeof key.value === "number") { - colorValue = [key.value, key.value, key.value]; - alpha = 1; - } else if (key.value && typeof key.value === "object" && "r" in key.value && "g" in key.value && "b" in key.value) { - // Handle Color4 or Color3 objects - colorValue = [key.value.r || 0, key.value.g || 0, key.value.b || 0]; - alpha = "a" in key.value ? key.value.a || 1 : 1; - } else { - colorValue = [0, 0, 0]; - alpha = 1; - } - const color3 = new Color3(colorValue[0], colorValue[1], colorValue[2]); + const wrapperGradient2 = { + colorKeys: randomGradient?.gradient2?.colorKeys || defaultColorKeys, + alphaKeys: randomGradient?.gradient2?.alphaKeys || defaultAlphaKeys, + }; return ( - -
-
- { - const newColorKeys = [...colorKeys]; - // Create new key object without Color4 to avoid React rendering issues - newColorKeys[index] = { pos: key.pos, value: [color.r, color.g, color.b, alpha] }; - updateGradient(newColorKeys); - }} - /> -
-
- { - const newColorKeys = [...colorKeys]; - // Create new key object without Color4 to avoid React rendering issues - const keyValue = Array.isArray(key.value) ? key.value : typeof key.value === "number" ? [key.value, key.value, key.value] : key.value && typeof key.value === "object" && "r" in key.value ? [key.value.r || 0, key.value.g || 0, key.value.b || 0, "a" in key.value ? key.value.a || 1 : 1] : [0, 0, 0, 1]; - newColorKeys[index] = { pos: vals[0], value: keyValue }; - updateGradient(newColorKeys); - }} - /> -
- -
-
- ); - })} - - -
Alpha Keys
- {alphaKeys.map((key, index) => ( - -
-
- {(() => { - // Handle alpha value - can be number, array, or Color4 object - let alphaValue: number; - if (typeof key.value === "number") { - alphaValue = key.value; - } else if (Array.isArray(key.value)) { - alphaValue = key.value[3] || 1; - } else if (key.value && typeof key.value === "object" && "a" in key.value) { - alphaValue = key.value.a || 1; - } else { - alphaValue = 1; - } - const wrapperAlpha = { - get value() { - return alphaValue; - }, - set value(newVal: number) { - const newAlphaKeys = [...alphaKeys]; - // Create new key object without Color4 to avoid React rendering issues - newAlphaKeys[index] = { pos: key.pos, value: newVal }; - updateGradient(colorKeys, newAlphaKeys); - }, - }; - return {}} />; - })()} -
-
- { - const newAlphaKeys = [...alphaKeys]; - // Create new key object without Color4 to avoid React rendering issues - const keyValue = typeof key.value === "number" ? key.value : Array.isArray(key.value) ? key.value[3] || 1 : key.value && typeof key.value === "object" && "a" in key.value ? key.value.a || 1 : 1; - newAlphaKeys[index] = { pos: vals[0], value: keyValue }; - updateGradient(colorKeys, newAlphaKeys); + /> + + +
Gradient 2
+ { + if (randomGradient) { + onChange({ + type: "RandomColorBetweenGradient", + gradient1: randomGradient.gradient1, + gradient2: { + colorKeys: newColorKeys, + alphaKeys: newAlphaKeys, + }, + }); + } }} /> -
- -
-
- ))} - -
+ + + ); + })()} + ); } diff --git a/editor/src/ui/gradient-picker.tsx b/editor/src/ui/gradient-picker.tsx new file mode 100644 index 000000000..133f6989e --- /dev/null +++ b/editor/src/ui/gradient-picker.tsx @@ -0,0 +1,391 @@ +import { Component, ReactNode, MouseEvent, useState, useRef, useEffect } from "react"; +import { Color3, Color4 } from "babylonjs"; +import { Color } from "@jniac/color-xplr"; +import { ColorPicker } from "./color-picker"; +import { Button } from "../ui/shadcn/ui/button"; +import { AiOutlineClose } from "react-icons/ai"; + +/** + * Universal gradient key type (not tied to VFX) + */ +export interface IGradientKey { + time?: number; + value: number | [number, number, number, number] | { r: number; g: number; b: number; a?: number }; + pos?: number; +} + +export interface IGradientPickerProps { + colorKeys: IGradientKey[]; + alphaKeys?: IGradientKey[]; + onChange: (colorKeys: IGradientKey[], alphaKeys?: IGradientKey[]) => void; + onFinish?: (colorKeys: IGradientKey[], alphaKeys?: IGradientKey[]) => void; + className?: string; +} + +/** + * Visual gradient picker component + * Allows users to visually edit gradient by clicking on gradient bar, dragging stops, and picking colors + */ +export function GradientPicker(props: IGradientPickerProps): ReactNode { + const { colorKeys, alphaKeys = [], onChange, onFinish, className } = props; + const [selectedKeyIndex, setSelectedKeyIndex] = useState(null); + const [selectedAlphaIndex, setSelectedAlphaIndex] = useState(null); + const gradientRef = useRef(null); + const alphaRef = useRef(null); + const [isDragging, setIsDragging] = useState(false); + const [dragKeyIndex, setDragKeyIndex] = useState(null); + const [isAlphaDragging, setIsAlphaDragging] = useState(false); + const [dragAlphaIndex, setDragAlphaIndex] = useState(null); + + // Sort keys by position + const sortedColorKeys = [...colorKeys].sort((a, b) => (a.pos || 0) - (b.pos || 0)); + const sortedAlphaKeys = [...alphaKeys].sort((a, b) => (a.pos || 0) - (b.pos || 0)); + + // Generate gradient CSS string + const generateGradient = (keys: IGradientKey[]): string => { + const sorted = [...keys].sort((a, b) => (a.pos || 0) - (b.pos || 0)); + const stops = sorted.map((key) => { + const pos = (key.pos || 0) * 100; + let color = "rgba(0, 0, 0, 1)"; + if (Array.isArray(key.value)) { + const [r, g, b, a = 1] = key.value; + color = `rgba(${r * 255}, ${g * 255}, ${b * 255}, ${a})`; + } else if (typeof key.value === "object" && "r" in key.value) { + const r = key.value.r * 255; + const g = key.value.g * 255; + const b = key.value.b * 255; + const a = ("a" in key.value ? key.value.a : 1) * 255; + color = `rgba(${r}, ${g}, ${b}, ${a / 255})`; + } + return `${color} ${pos}%`; + }); + return `linear-gradient(to right, ${stops.join(", ")})`; + }; + + // Get color value from key + const getColorFromKey = (key: IGradientKey): Color4 => { + if (Array.isArray(key.value)) { + const [r, g, b, a = 1] = key.value; + return new Color4(r, g, b, a); + } else if (typeof key.value === "object" && "r" in key.value) { + return new Color4(key.value.r, key.value.g, key.value.b, "a" in key.value ? key.value.a || 1 : 1); + } + return new Color4(0, 0, 0, 1); + }; + + // Handle click on gradient bar to add/select key + const handleGradientClick = (e: MouseEvent, isAlpha: boolean = false) => { + const rect = isAlpha ? alphaRef.current?.getBoundingClientRect() : gradientRef.current?.getBoundingClientRect(); + if (!rect) return; + + const x = e.clientX - rect.left; + const pos = Math.max(0, Math.min(1, x / rect.width)); + + if (isAlpha) { + // Check if clicked near existing alpha key + const nearKeyIndex = sortedAlphaKeys.findIndex((key) => Math.abs((key.pos || 0) - pos) < 0.05); + if (nearKeyIndex >= 0) { + setSelectedAlphaIndex(nearKeyIndex); + return; + } + + // Add new alpha key + const newAlphaKeys = [...alphaKeys, { pos, value: 1 }]; + const sorted = newAlphaKeys.sort((a, b) => (a.pos || 0) - (b.pos || 0)); + const newIndex = sorted.findIndex((key) => key.pos === pos); + setSelectedAlphaIndex(newIndex); + onChange(colorKeys, sorted); + } else { + // Check if clicked near existing color key + const nearKeyIndex = sortedColorKeys.findIndex((key) => Math.abs((key.pos || 0) - pos) < 0.05); + if (nearKeyIndex >= 0) { + setSelectedKeyIndex(nearKeyIndex); + return; + } + + // Interpolate color at position + const color = interpolateColorAtPosition(sortedColorKeys, pos); + const newColorKeys = [...colorKeys, { pos, value: [color.r, color.g, color.b, color.a] }]; + const sorted = newColorKeys.sort((a, b) => (a.pos || 0) - (b.pos || 0)); + const newIndex = sorted.findIndex((key) => key.pos === pos); + setSelectedKeyIndex(newIndex); + onChange(sorted, alphaKeys); + } + }; + + // Interpolate color at position + const interpolateColorAtPosition = (keys: IGradientKey[], pos: number): Color4 => { + if (keys.length === 0) return new Color4(1, 1, 1, 1); + if (keys.length === 1) return getColorFromKey(keys[0]); + + for (let i = 0; i < keys.length - 1; i++) { + const key1 = keys[i]; + const key2 = keys[i + 1]; + const pos1 = key1.pos || 0; + const pos2 = key2.pos || 0; + + if (pos >= pos1 && pos <= pos2) { + const t = (pos - pos1) / (pos2 - pos1); + const color1 = getColorFromKey(key1); + const color2 = getColorFromKey(key2); + return new Color4( + color1.r + (color2.r - color1.r) * t, + color1.g + (color2.g - color1.g) * t, + color1.b + (color2.b - color1.b) * t, + color1.a + (color2.a - color1.a) * t + ); + } + } + + // Outside range, return nearest + if (pos <= (keys[0].pos || 0)) return getColorFromKey(keys[0]); + return getColorFromKey(keys[keys.length - 1]); + }; + + // Handle mouse down on key stop + const handleKeyMouseDown = (e: MouseEvent, index: number, isAlpha: boolean) => { + e.stopPropagation(); + if (isAlpha) { + setIsAlphaDragging(true); + setDragAlphaIndex(index); + setSelectedAlphaIndex(index); + } else { + setIsDragging(true); + setDragKeyIndex(index); + setSelectedKeyIndex(index); + } + }; + + // Handle mouse move for dragging + useEffect(() => { + const handleMouseMove = (e: MouseEvent) => { + if (isDragging && dragKeyIndex !== null && gradientRef.current) { + const rect = gradientRef.current.getBoundingClientRect(); + const x = e.clientX - rect.left; + const pos = Math.max(0, Math.min(1, x / rect.width)); + + const newColorKeys = [...colorKeys]; + const key = sortedColorKeys[dragKeyIndex]; + const originalIndex = colorKeys.findIndex((k) => k === key); + if (originalIndex >= 0) { + newColorKeys[originalIndex] = { ...key, pos }; + onChange(newColorKeys, alphaKeys); + } + } + + if (isAlphaDragging && dragAlphaIndex !== null && alphaRef.current) { + const rect = alphaRef.current.getBoundingClientRect(); + const x = e.clientX - rect.left; + const pos = Math.max(0, Math.min(1, x / rect.width)); + + const newAlphaKeys = [...alphaKeys]; + const key = sortedAlphaKeys[dragAlphaIndex]; + const originalIndex = alphaKeys.findIndex((k) => k === key); + if (originalIndex >= 0) { + newAlphaKeys[originalIndex] = { ...key, pos }; + onChange(colorKeys, newAlphaKeys); + } + } + }; + + const handleMouseUp = () => { + if (isDragging || isAlphaDragging) { + setIsDragging(false); + setIsAlphaDragging(false); + setDragKeyIndex(null); + setDragAlphaIndex(null); + if (onFinish) { + onFinish(colorKeys, alphaKeys); + } + } + }; + + if (isDragging || isAlphaDragging) { + window.addEventListener("mousemove", handleMouseMove as any); + window.addEventListener("mouseup", handleMouseUp); + return () => { + window.removeEventListener("mousemove", handleMouseMove as any); + window.removeEventListener("mouseup", handleMouseUp); + }; + } + }, [isDragging, isAlphaDragging, dragKeyIndex, dragAlphaIndex, colorKeys, alphaKeys, onChange, onFinish, sortedColorKeys, sortedAlphaKeys]); + + // Handle color change for selected key + const handleColorChange = (color: Color3 | Color4) => { + if (selectedKeyIndex === null) return; + + const key = sortedColorKeys[selectedKeyIndex]; + const originalIndex = colorKeys.findIndex((k) => k === key); + if (originalIndex >= 0) { + const newColorKeys = [...colorKeys]; + newColorKeys[originalIndex] = { + ...key, + value: [color.r, color.g, color.b, color instanceof Color4 ? color.a : 1], + }; + onChange(newColorKeys, alphaKeys); + } + }; + + // Handle alpha change for selected alpha key + const handleAlphaChange = (value: number) => { + if (selectedAlphaIndex === null) return; + + const key = sortedAlphaKeys[selectedAlphaIndex]; + const originalIndex = alphaKeys.findIndex((k) => k === key); + if (originalIndex >= 0) { + const newAlphaKeys = [...alphaKeys]; + newAlphaKeys[originalIndex] = { ...key, value }; + onChange(colorKeys, newAlphaKeys); + } + }; + + // Handle delete key + const handleDeleteKey = (index: number, isAlpha: boolean) => { + if (isAlpha) { + if (alphaKeys.length <= 2) return; // Keep at least 2 keys + const newAlphaKeys = alphaKeys.filter((_, i) => i !== index); + setSelectedAlphaIndex(null); + onChange(colorKeys, newAlphaKeys); + } else { + if (colorKeys.length <= 2) return; // Keep at least 2 keys + const newColorKeys = colorKeys.filter((_, i) => i !== index); + setSelectedKeyIndex(null); + onChange(newColorKeys, alphaKeys); + } + }; + + return ( +
+ {/* Color Gradient Bar */} +
+
Color Gradient
+
handleGradientClick(e, false)} + > + {sortedColorKeys.map((key, index) => { + const pos = (key.pos || 0) * 100; + const color = getColorFromKey(key); + const isSelected = selectedKeyIndex === index; + return ( +
handleKeyMouseDown(e, index, false)} + > +
+
+ ); + })} +
+ + {/* Color Picker for Selected Key */} + {selectedKeyIndex !== null && ( +
+
+ handleColorChange(new Color4(color.r, color.g, color.b, color.a))} + onFinish={(color) => { + handleColorChange(new Color4(color.r, color.g, color.b, color.a)); + if (onFinish) { + onFinish(colorKeys, alphaKeys); + } + }} + /> +
+ +
+ )} +
+ + {/* Alpha Gradient Bar */} +
+
Alpha Gradient
+
handleGradientClick(e, true)} + > + {sortedAlphaKeys.map((key, index) => { + const pos = (key.pos || 0) * 100; + const alphaValue = typeof key.value === "number" ? key.value : Array.isArray(key.value) ? key.value[3] || 1 : 1; + const isSelected = selectedAlphaIndex === index; + return ( +
handleKeyMouseDown(e, index, true)} + > +
+
+ ); + })} +
+ + {/* Alpha Slider for Selected Key */} + {selectedAlphaIndex !== null && ( +
+
+ handleAlphaChange(parseFloat(e.target.value))} + className="w-full" + /> +
+ +
+ )} +
+
+ ); +} From fdf08cae003f522c909f58ec6c2126af07a077fa Mon Sep 17 00:00:00 2001 From: Mikalai Lazitski Date: Mon, 15 Dec 2025 13:19:13 +0300 Subject: [PATCH 30/62] refactor: enhance VFX editor properties by adding emission burst management, improving emitter shape configuration, and refining particle renderer settings for better usability and flexibility --- .../windows/fx-editor/properties/emission.tsx | 161 ++++++-- .../fx-editor/properties/emitter-shape.tsx | 317 +++++++++++++-- .../properties/particle-renderer.tsx | 373 ++++++++++++++++-- 3 files changed, 763 insertions(+), 88 deletions(-) diff --git a/editor/src/editor/windows/fx-editor/properties/emission.tsx b/editor/src/editor/windows/fx-editor/properties/emission.tsx index 8eb1401fd..a7b06c4a9 100644 --- a/editor/src/editor/windows/fx-editor/properties/emission.tsx +++ b/editor/src/editor/windows/fx-editor/properties/emission.tsx @@ -3,9 +3,13 @@ import { ReactNode } from "react"; import { EditorInspectorNumberField } from "../../../layout/inspector/fields/number"; import { EditorInspectorSwitchField } from "../../../layout/inspector/fields/switch"; import { EditorInspectorBlockField } from "../../../layout/inspector/fields/block"; +import { EditorInspectorSectionField } from "../../../layout/inspector/fields/section"; +import { EditorInspectorStringField } from "../../../layout/inspector/fields/string"; import type { VFXEffectNode } from "../VFX"; import { VFXParticleSystem, VFXSolidParticleSystem } from "../VFX"; +import { VFXValueEditor } from "./vfx-value-editor"; +import type { VFXEmissionBurst, VFXValue } from "../VFX/types"; export interface IFXEditorEmissionPropertiesProps { nodeData: VFXEffectNode; @@ -21,13 +25,47 @@ export function FXEditorEmissionProperties(props: IFXEditorEmissionPropertiesPro const system = nodeData.system; - // For VFXParticleSystem, show emission properties - if (system instanceof VFXParticleSystem) { - return ( - <> - - - + return ( + <> + {/* Looping / Duration / Prewarm / OnlyUsedByOther */} + + + + + + {/* Emit Over Time */} + + { + (system as any).emissionOverTime = val; + onChange(); + }} + /> + + + {/* Emit Over Distance */} + + { + (system as any).emissionOverDistance = val; + onChange(); + }} + /> + + + {/* Emit Power (min/max) - только для base (есть min/maxEmitPower) */} + {system instanceof VFXParticleSystem && (
Emit Power
@@ -35,24 +73,97 @@ export function FXEditorEmissionProperties(props: IFXEditorEmissionPropertiesPro
- {/* TODO: Add prewarm, onlyUsedByOtherSystem, emitOverDistance properties */} - {/* TODO: Add bursts support */} - - ); - } + )} - // For VFXSolidParticleSystem, show emission properties - if (system instanceof VFXSolidParticleSystem) { - return ( - <> - - - - {/* TODO: Add prewarm, onlyUsedByOtherSystem, emitOverDistance properties */} - {/* TODO: Add bursts support */} - - ); - } + {/* Bursts */} + {renderBursts(system as any, onChange)} + + ); +} + +function renderBursts(system: any, onChange: () => void): ReactNode { + const bursts: (VFXEmissionBurst & { cycle?: number; interval?: number; probability?: number })[] = Array.isArray(system.emissionBursts) + ? system.emissionBursts + : []; + + const addBurst = () => { + bursts.push({ + time: 0, + count: 1, + cycle: 1, + interval: 0, + probability: 1, + }); + system.emissionBursts = bursts; + onChange(); + }; - return null; + const removeBurst = (index: number) => { + bursts.splice(index, 1); + system.emissionBursts = bursts; + onChange(); + }; + + return ( + +
+ {bursts.map((burst, idx) => ( +
+
+
Burst #{idx + 1}
+ +
+
+ { + burst.time = val; + onChange(); + }} + /> + { + burst.count = val; + onChange(); + }} + /> + + + +
+
+ ))} + +
+
+ ); } diff --git a/editor/src/editor/windows/fx-editor/properties/emitter-shape.tsx b/editor/src/editor/windows/fx-editor/properties/emitter-shape.tsx index e495cb57d..354f8d120 100644 --- a/editor/src/editor/windows/fx-editor/properties/emitter-shape.tsx +++ b/editor/src/editor/windows/fx-editor/properties/emitter-shape.tsx @@ -1,9 +1,15 @@ import { Component, ReactNode } from "react"; +import { Vector3 } from "babylonjs"; import { EditorInspectorVectorField } from "../../../layout/inspector/fields/vector"; +import { EditorInspectorNumberField } from "../../../layout/inspector/fields/number"; +import { EditorInspectorSwitchField } from "../../../layout/inspector/fields/switch"; +import { EditorInspectorListField } from "../../../layout/inspector/fields/list"; +import { EditorInspectorBlockField } from "../../../layout/inspector/fields/block"; import type { VFXEffectNode } from "../VFX"; import { VFXParticleSystem, VFXSolidParticleSystem } from "../VFX"; +import type { SolidSphereParticleEmitter, SolidConeParticleEmitter } from "../VFX/types/emitters"; export interface IFXEditorEmitterShapePropertiesProps { nodeData: VFXEffectNode; @@ -12,7 +18,7 @@ export interface IFXEditorEmitterShapePropertiesProps { export class FXEditorEmitterShapeProperties extends Component { public render(): ReactNode { - const { nodeData } = this.props; + const { nodeData, onChange } = this.props; if (nodeData.type !== "particle" || !nodeData.system) { return null; @@ -20,36 +26,297 @@ export class FXEditorEmitterShapeProperties extends Component -
Emitter shape: {system.shape?.type || "Default"}
- {/* TODO: Add shape-specific property editors based on system.shape.type */} - - ); + return this._renderSolidParticleSystemEmitter(system, onChange); } - // For VFXParticleSystem, emitter is a separate object + // For VFXParticleSystem if (system instanceof VFXParticleSystem) { - const emitter = (system as any).emitter; - - if (!emitter) { - return
No emitter found. Emitter shape properties are set during system creation.
; - } - - // Show basic emitter properties - return ( - <> -
Emitter: {emitter.name || emitter.constructor.name}
- {emitter.position && } - {emitter.rotationQuaternion && } - {emitter.scaling && } - {/* TODO: Add shape-specific properties based on emitter type (BoxEmitter, ConeEmitter, etc.) */} - - ); + return this._renderParticleSystemEmitter(system, onChange); } return null; } + + private _renderSolidParticleSystemEmitter(system: VFXSolidParticleSystem, onChange: () => void): ReactNode { + const emitter = system.particleEmitterType; + const emitterType = emitter ? emitter.constructor.name : "Point"; + + // Map emitter class names to display names + const emitterTypeMap: Record = { + SolidPointParticleEmitter: "Point", + SolidSphereParticleEmitter: "Sphere", + SolidConeParticleEmitter: "Cone", + }; + + const currentType = emitterTypeMap[emitterType] || "Point"; + + // Available emitter types for SolidParticleSystem + const emitterTypes = [ + { text: "Point", value: "point" }, + { text: "Sphere", value: "sphere" }, + { text: "Cone", value: "cone" }, + ]; + + return ( + <> + ({ text: t.text, value: t.value }))} + onChange={(value) => { + // Save current properties + const currentRadius = emitter instanceof SolidSphereParticleEmitter || emitter instanceof SolidConeParticleEmitter ? emitter.radius : 1; + const currentArc = emitter instanceof SolidSphereParticleEmitter || emitter instanceof SolidConeParticleEmitter ? emitter.arc : Math.PI * 2; + const currentThickness = emitter instanceof SolidSphereParticleEmitter || emitter instanceof SolidConeParticleEmitter ? emitter.thickness : 1; + const currentAngle = emitter instanceof SolidConeParticleEmitter ? emitter.angle : Math.PI / 6; + + switch (value) { + case "point": + system.createPointEmitter(); + break; + case "sphere": + system.createSphereEmitter(currentRadius, currentArc, currentThickness); + break; + case "cone": + system.createConeEmitter(currentRadius, currentArc, currentThickness, currentAngle); + break; + } + onChange(); + }} + /> + + {emitter instanceof SolidSphereParticleEmitter && ( + <> + + + + + )} + + {emitter instanceof SolidConeParticleEmitter && ( + <> + + + + + + )} + + ); + } + + private _renderParticleSystemEmitter(system: VFXParticleSystem, onChange: () => void): ReactNode { + const emitter = system.particleEmitterType; + if (!emitter) { + return
No emitter found.
; + } + + const emitterType = emitter.getClassName(); + const emitterTypeMap: Record = { + PointParticleEmitter: "point", + BoxParticleEmitter: "box", + SphereParticleEmitter: "sphere", + SphereDirectedParticleEmitter: "sphere", + ConeParticleEmitter: "cone", + ConeDirectedParticleEmitter: "cone", + HemisphericParticleEmitter: "hemisphere", + CylinderParticleEmitter: "cylinder", + CylinderDirectedParticleEmitter: "cylinder", + }; + + const currentType = emitterTypeMap[emitterType] || "point"; + + // Available emitter types for ParticleSystem + const emitterTypes = [ + { text: "Point", value: "point" }, + { text: "Box", value: "box" }, + { text: "Sphere", value: "sphere" }, + { text: "Cone", value: "cone" }, + { text: "Hemisphere", value: "hemisphere" }, + { text: "Cylinder", value: "cylinder" }, + ]; + + return ( + <> + ({ text: t.text, value: t.value }))} + onChange={(value) => { + // Save current properties that might be common + const currentRadius = "radius" in emitter ? (emitter as any).radius : 1; + const currentAngle = "angle" in emitter ? (emitter as any).angle : Math.PI / 6; + const currentHeight = "height" in emitter ? (emitter as any).height : 1; + const currentDirection1 = "direction1" in emitter ? (emitter as any).direction1?.clone() : Vector3.Zero(); + const currentDirection2 = "direction2" in emitter ? (emitter as any).direction2?.clone() : Vector3.Zero(); + const currentMinEmitBox = "minEmitBox" in emitter ? (emitter as any).minEmitBox?.clone() : new Vector3(-0.5, -0.5, -0.5); + const currentMaxEmitBox = "maxEmitBox" in emitter ? (emitter as any).maxEmitBox?.clone() : new Vector3(0.5, 0.5, 0.5); + + switch (value) { + case "point": + system.createPointEmitter(currentDirection1, currentDirection2); + break; + case "box": + system.createBoxEmitter(currentDirection1, currentDirection2, currentMinEmitBox, currentMaxEmitBox); + break; + case "sphere": + system.createSphereEmitter(currentRadius); + break; + case "cone": + system.createConeEmitter(currentRadius, currentAngle); + break; + case "hemisphere": + system.createHemisphericEmitter(currentRadius); + break; + case "cylinder": + system.createCylinderEmitter(currentRadius, currentHeight); + break; + } + + onChange(); + }} + /> + + {emitterType === "BoxParticleEmitter" && ( + <> + +
Direction
+ + +
+ +
Emit Box
+ + +
+ + )} + + {(emitterType === "ConeParticleEmitter" || emitterType === "ConeDirectedParticleEmitter") && ( + <> + + + + + + + {emitterType === "ConeDirectedParticleEmitter" && ( + +
Direction
+ + +
+ )} + + )} + + {(emitterType === "CylinderParticleEmitter" || emitterType === "CylinderDirectedParticleEmitter") && ( + <> + + + + + + {emitterType === "CylinderDirectedParticleEmitter" && ( + +
Direction
+ + +
+ )} + + )} + + {(emitterType === "SphereParticleEmitter" || emitterType === "SphereDirectedParticleEmitter") && ( + <> + + + + + {emitterType === "SphereDirectedParticleEmitter" && ( + +
Direction
+ + +
+ )} + + )} + + {emitterType === "PointParticleEmitter" && ( + +
Direction
+ + +
+ )} + + {emitterType === "HemisphericParticleEmitter" && ( + <> + + + + + )} + + ); + } } diff --git a/editor/src/editor/windows/fx-editor/properties/particle-renderer.tsx b/editor/src/editor/windows/fx-editor/properties/particle-renderer.tsx index 43d0e7505..426ed6509 100644 --- a/editor/src/editor/windows/fx-editor/properties/particle-renderer.tsx +++ b/editor/src/editor/windows/fx-editor/properties/particle-renderer.tsx @@ -5,11 +5,15 @@ import { EditorInspectorNumberField } from "../../../layout/inspector/fields/num import { EditorInspectorSwitchField } from "../../../layout/inspector/fields/switch"; import { EditorInspectorListField } from "../../../layout/inspector/fields/list"; import { EditorInspectorTextureField } from "../../../layout/inspector/fields/texture"; +import { EditorInspectorColorField } from "../../../layout/inspector/fields/color"; +import { EditorInspectorStringField } from "../../../layout/inspector/fields/string"; -import { ParticleSystem, SolidParticleSystem } from "babylonjs"; +import { ParticleSystem, Constants, Material } from "babylonjs"; import type { VFXEffectNode } from "../VFX"; import { VFXParticleSystem, VFXSolidParticleSystem } from "../VFX"; import { IFXEditor } from ".."; +import { VFXValueUtils } from "../VFX/utils/valueParser"; +import { VFXValueEditor } from "./vfx-value-editor"; export interface IFXEditorParticleRendererPropertiesProps { nodeData: VFXEffectNode; @@ -39,9 +43,14 @@ export class FXEditorParticleRendererProperties extends Component + {/* System Mode */} +
System Mode: {systemType === "solid" ? "Mesh (Solid)" : "Billboard (Base)"}
+ + {/* Billboard Mode - только для base */} {isVFXParticleSystem && ( <> this.props.onChange()} /> - this.props.onChange()} /> + this.props.onChange()} + /> )} + + {/* World Space */} + {isVFXParticleSystem && (() => { + // Для VFXParticleSystem, worldSpace = !isLocal + const proxy = { + get worldSpace() { + return !system.isLocal; + }, + set worldSpace(value: boolean) { + system.isLocal = !value; + }, + }; + return this.props.onChange()} />; + })()} {isVFXSolidParticleSystem && ( - <> -
Render Mode: Mesh
- {/* For VFXSolidParticleSystem, material properties are on mesh.material */} - + this.props.onChange()} + /> )} + + {/* Material - для обеих систем */} + {this._getMaterialField()} + + {/* Blend Mode - только для base */} + {isVFXParticleSystem && ( + this.props.onChange()} + /> + )} + + {/* Material Properties - только для solid */} + {isVFXSolidParticleSystem && this._getMaterialProperties()} + + {/* Texture */} {this._getTextureField()} + + {/* Render Order */} + {this._getRenderOrderField()} + + {/* UV Tile */} + {this._getUVTileSection()} + + {/* Start Tile Index */} + {this._getStartTileIndexField()} + + {/* Soft Particles */} {isVFXParticleSystem && ( - <> - this.props.onChange()} - /> - {this._getUVTileSection()} - - + this.props.onChange()} /> + )} + {isVFXSolidParticleSystem && ( + this.props.onChange()} /> )} - {isVFXSolidParticleSystem && this._getRenderModeSpecificProperties("Mesh")} + + {/* Geometry - только для solid */} + {isVFXSolidParticleSystem && this._getGeometryField()} ); } - private _getUVTileSection(): ReactNode { + private _getMaterialField(): ReactNode { + const { nodeData } = this.props; + + if (nodeData.type !== "particle" || !nodeData.system) { + return null; + } + + const system = nodeData.system; + + // Для VFXSolidParticleSystem, material ID хранится в system.material + if (system instanceof VFXSolidParticleSystem) { + return ( + this.props.onChange()} + /> + ); + } + + // Для VFXParticleSystem, material может быть в config или на texture + // Пока просто показываем, что material управляется через texture + return null; + } + + private _getMaterialProperties(): ReactNode { const { nodeData } = this.props; - if (nodeData.type !== "particle" || !nodeData.system || !(nodeData.system instanceof VFXParticleSystem)) { + if (nodeData.type !== "particle" || !nodeData.system) { return null; } - const system = nodeData.system as VFXParticleSystem; + const system = nodeData.system; + let material: Material | null = null; + + // Получаем material в зависимости от типа системы + if (system instanceof VFXSolidParticleSystem && system.mesh && system.mesh.material) { + material = system.mesh.material; + } else if (system instanceof VFXParticleSystem) { + // Для VFXParticleSystem material управляется через blendMode и texture + // Material properties не доступны напрямую + return null; + } + + if (!material) { + return null; + } + + const pbrMaterial = material as any; return ( - - - - {/* TODO: Add startTileIndex and blendTiles if available */} + + {/* Transparent */} + {pbrMaterial.transparencyMode !== undefined && (() => { + // Proxy для transparent property + const transparentProxy = { + get transparent() { + return pbrMaterial.transparencyMode !== Constants.ALPHA_DISABLE; + }, + set transparent(value: boolean) { + pbrMaterial.transparencyMode = value ? Constants.ALPHA_COMBINE : Constants.ALPHA_DISABLE; + }, + }; + return this.props.onChange()} />; + })()} + + {/* Opacity */} + {pbrMaterial.alpha !== undefined && ( + this.props.onChange()} + /> + )} + + {/* Side */} + {pbrMaterial.sideOrientation !== undefined && ( + this.props.onChange()} + /> + )} + + {/* Blending */} + {pbrMaterial.alphaMode !== undefined && ( + this.props.onChange()} + /> + )} + + {/* Color */} + {pbrMaterial.albedoColor !== undefined && ( + this.props.onChange()} + /> + )} ); } @@ -136,16 +304,137 @@ export class FXEditorParticleRendererProperties extends Component this.props.onChange()} + /> + ); + } + + // Для VFXSolidParticleSystem, renderOrder хранится в system.renderOrder и применяется к mesh.renderingGroupId + if (system instanceof VFXSolidParticleSystem) { + // Создаем proxy объект для доступа к renderOrder через mesh.renderingGroupId + const proxy = { + get renderingGroupId() { + return system.mesh?.renderingGroupId ?? system.renderOrder ?? 0; + }, + set renderingGroupId(value: number) { + if (system.mesh) { + system.mesh.renderingGroupId = value; + } + system.renderOrder = value; + }, + }; + + return ( + this.props.onChange()} + /> + ); + } + + return null; + } + + private _getUVTileSection(): ReactNode { + const { nodeData } = this.props; + + if (nodeData.type !== "particle" || !nodeData.system) { + return null; + } + + const system = nodeData.system; + + // Для VFXParticleSystem, используем spriteCellWidth и spriteCellHeight + if (system instanceof VFXParticleSystem) { + return ( + + this.props.onChange()} /> + this.props.onChange()} /> + {/* TODO: Add blendTiles if available for VFXParticleSystem */} + + ); + } + + // Для VFXSolidParticleSystem, используем uTileCount и vTileCount + if (system instanceof VFXSolidParticleSystem) { + return ( + + this.props.onChange()} /> + this.props.onChange()} /> + {system.blendTiles !== undefined && ( + this.props.onChange()} /> + )} + + ); + } + + return null; + } + + private _getStartTileIndexField(): ReactNode { + const { nodeData } = this.props; + + if (nodeData.type !== "particle" || !nodeData.system) { + return null; + } + + const system = nodeData.system; + + // Для VFXParticleSystem, используем startSpriteCellID + if (system instanceof VFXParticleSystem) { + return ( + this.props.onChange()} + /> + ); + } + + // Для VFXSolidParticleSystem, используем startTileIndex (VFXValue) + if (system instanceof VFXSolidParticleSystem && system.startTileIndex !== undefined) { + const getValue = () => system.startTileIndex!; + const setValue = (value: any) => { + system.startTileIndex = value; + this.props.onChange(); + }; + + return ( +
+ +
+ ); } - // TODO: Add properties for other render modes (StretchedBillboard, Trail, etc.) return null; } - private _getMeshField(): ReactNode { + private _getGeometryField(): ReactNode { const { nodeData } = this.props; if (nodeData.type !== "particle" || !nodeData.system || !(nodeData.system instanceof VFXSolidParticleSystem)) { @@ -157,8 +446,16 @@ export class FXEditorParticleRendererProperties extends Component -
Mesh
-
{mesh ?
{mesh.name}
:
No mesh
}
+
Geometry
+
+ {system.instancingGeometry ? ( +
{system.instancingGeometry}
+ ) : mesh ? ( +
{mesh.name}
+ ) : ( +
No geometry
+ )} +
); } From 305f388c260b0f9f2748c736c025924922f1c6fd Mon Sep 17 00:00:00 2001 From: Mikalai Lazitski Date: Mon, 15 Dec 2025 15:19:45 +0300 Subject: [PATCH 31/62] feat: implement prewarm functionality in VFX systems, enhancing performance by allowing systems to initialize with pre-warmed states, and synchronize playing state in FX editor for improved user experience --- .../editor/windows/fx-editor/VFX/VFXEffect.ts | 22 +++++++ .../VFX/factories/VFXSystemFactory.ts | 17 ----- .../VFX/systems/VFXParticleSystem.ts | 2 + .../VFX/systems/VFXSolidParticleSystem.ts | 62 ++++++++++++++++++- editor/src/editor/windows/fx-editor/graph.tsx | 11 ++++ .../src/editor/windows/fx-editor/preview.tsx | 34 +++++++--- 6 files changed, 123 insertions(+), 25 deletions(-) diff --git a/editor/src/editor/windows/fx-editor/VFX/VFXEffect.ts b/editor/src/editor/windows/fx-editor/VFX/VFXEffect.ts index 78a034303..6b48131d8 100644 --- a/editor/src/editor/windows/fx-editor/VFX/VFXEffect.ts +++ b/editor/src/editor/windows/fx-editor/VFX/VFXEffect.ts @@ -379,6 +379,28 @@ export class VFXEffect implements IDisposable { } } + /** + * Apply prewarm to systems that have it enabled + * Should be called after hierarchy is built and all systems are created + * Uses Babylon.js built-in prewarm properties for ParticleSystem + */ + public applyPrewarm(): void { + for (const system of this._systems) { + if (system instanceof VFXParticleSystem && system.prewarm) { + // For ParticleSystem, use Babylon.js built-in prewarm + const duration = system.targetStopDuration || 5; + const cycles = Math.ceil(duration * 60); // Simulate 60 FPS for duration + (system as any).preWarmCycles = cycles; + (system as any).preWarmStepOffset = 1; // Use normal time step + } else if (system instanceof VFXSolidParticleSystem && system.prewarm) { + // For SolidParticleSystem, we need to manually simulate prewarm + // Start the system and let it run for duration + // Note: SPS doesn't have built-in prewarm, so we'll start it normally + // The prewarm effect will be visible when system starts + } + } + } + /** * Check if any system is started */ diff --git a/editor/src/editor/windows/fx-editor/VFX/factories/VFXSystemFactory.ts b/editor/src/editor/windows/fx-editor/VFX/factories/VFXSystemFactory.ts index 78f9ffc70..a2896edb1 100644 --- a/editor/src/editor/windows/fx-editor/VFX/factories/VFXSystemFactory.ts +++ b/editor/src/editor/windows/fx-editor/VFX/factories/VFXSystemFactory.ts @@ -191,14 +191,6 @@ export class VFXSystemFactory { // Continue - system is created, just transform failed } - // Handle prewarm - try { - this._handlePrewarm(particleSystem, vfxEmitter.config.prewarm); - } catch (error) { - this._logger.warn(`${indent}Failed to handle prewarm for system ${vfxEmitter.name}: ${error instanceof Error ? error.message : String(error)}`); - // Continue - prewarm is optional - } - this._logger.log(`${indent}Created particle system: ${vfxEmitter.name}`); return particleSystem; } catch (error) { @@ -298,15 +290,6 @@ export class VFXSystemFactory { return cumulativeScale; } - /** - * Handle prewarm configuration for particle system - */ - private _handlePrewarm(particleSystem: VFXParticleSystem | VFXSolidParticleSystem, prewarm: boolean | undefined): void { - if (prewarm && particleSystem) { - particleSystem.start(); - } - } - // Type guards private _isGroup(vfxObj: VFXGroup | VFXEmitter): vfxObj is VFXGroup { return "children" in vfxObj; diff --git a/editor/src/editor/windows/fx-editor/VFX/systems/VFXParticleSystem.ts b/editor/src/editor/windows/fx-editor/VFX/systems/VFXParticleSystem.ts index 0b4b3a726..67bc88b9f 100644 --- a/editor/src/editor/windows/fx-editor/VFX/systems/VFXParticleSystem.ts +++ b/editor/src/editor/windows/fx-editor/VFX/systems/VFXParticleSystem.ts @@ -44,6 +44,7 @@ export class VFXParticleSystem extends ParticleSystem implements IVFXSystem { public startSize: number; public startSpeed: number; public startColor: Color4; + public prewarm: boolean; private _behaviors: VFXPerParticleBehaviorFunction[]; public readonly behaviorConfigs: VFXBehavior[]; @@ -62,6 +63,7 @@ export class VFXParticleSystem extends ParticleSystem implements IVFXSystem { super(name, capacity, scene); this._behaviors = []; + this.prewarm = config.prewarm || false; // Create proxy array that updates functions when modified this.behaviorConfigs = this._createBehaviorConfigsProxy(config.behaviors || []); diff --git a/editor/src/editor/windows/fx-editor/VFX/systems/VFXSolidParticleSystem.ts b/editor/src/editor/windows/fx-editor/VFX/systems/VFXSolidParticleSystem.ts index 9703e802e..857b3462d 100644 --- a/editor/src/editor/windows/fx-editor/VFX/systems/VFXSolidParticleSystem.ts +++ b/editor/src/editor/windows/fx-editor/VFX/systems/VFXSolidParticleSystem.ts @@ -396,6 +396,53 @@ export class VFXSolidParticleSystem extends SolidParticleSystem implements IVFXS return this.mesh || null; } + /** + * Start the particle system + * Overrides base class to ensure proper initialization + */ + public override start(delay = 0): void { + // Call base class start + super.start(delay); + + // Reset emission state when starting + if (delay === 0) { + this._emissionState.time = 0; + this._emissionState.waitEmiting = 0; + this._emissionState.travelDistance = 0; + this._emissionState.burstIndex = 0; + this._emissionState.burstWaveIndex = 0; + this._emissionState.burstParticleIndex = 0; + this._emissionState.burstParticleCount = 0; + this._emissionState.isBursting = false; + this._emitEnded = false; + + // Ensure particles are visible when starting (they will be updated by setParticles) + // Note: New particles will be spawned and visible automatically + } + } + + /** + * Stop the particle system + * Overrides base class to hide all particles when stopped + */ + public override stop(): void { + // Hide all particles before stopping + const particles = this.particles; + const nbParticles = this.nbParticles; + for (let i = 0; i < nbParticles; i++) { + const particle = particles[i]; + if (particle.alive) { + particle.isVisible = false; + } + } + + // Update particles to apply visibility changes + this.setParticles(); + + // Call base class stop + super.stop(); + } + /** * Reset the particle system (stop and clear all particles) * Stops emission, resets emission state, and rebuilds particles to initial state @@ -1334,7 +1381,20 @@ export class VFXSolidParticleSystem extends SolidParticleSystem implements IVFXS public override beforeUpdateParticles(start?: number, stop?: number, update?: boolean): void { super.beforeUpdateParticles(start, stop, update); - if (!this._started || this._stopped) { + // If system is stopped, hide all particles and return early + if (this._stopped) { + const particles = this.particles; + const nbParticles = this.nbParticles; + for (let i = 0; i < nbParticles; i++) { + const particle = particles[i]; + if (particle.alive) { + particle.isVisible = false; + } + } + return; + } + + if (!this._started) { return; } diff --git a/editor/src/editor/windows/fx-editor/graph.tsx b/editor/src/editor/windows/fx-editor/graph.tsx index 61be24066..c7bc9ddf9 100644 --- a/editor/src/editor/windows/fx-editor/graph.tsx +++ b/editor/src/editor/windows/fx-editor/graph.tsx @@ -100,8 +100,19 @@ export class FXEditorGraph extends Component { + if (this.props.editor?.preview) { + (this.props.editor.preview as any).forceUpdate?.(); + } + }, 100); } catch (error) { console.error("Failed to load FX file:", error); } diff --git a/editor/src/editor/windows/fx-editor/preview.tsx b/editor/src/editor/windows/fx-editor/preview.tsx index 2aa28e3a9..3ea3c13af 100644 --- a/editor/src/editor/windows/fx-editor/preview.tsx +++ b/editor/src/editor/windows/fx-editor/preview.tsx @@ -69,6 +69,21 @@ export class FXEditorPreview extends Component Date: Mon, 15 Dec 2025 16:11:57 +0300 Subject: [PATCH 32/62] feat: enhance FX editor functionality by implementing effect management features, including creating, exporting, and handling particle systems and groups, along with improved node selection and playback controls for a better user experience --- .../editor/windows/fx-editor/VFX/VFXEffect.ts | 287 +++++++++- editor/src/editor/windows/fx-editor/graph.tsx | 506 ++++++++++++++---- .../src/editor/windows/fx-editor/layout.tsx | 1 + .../src/editor/windows/fx-editor/preview.tsx | 211 ++++++-- 4 files changed, 863 insertions(+), 142 deletions(-) diff --git a/editor/src/editor/windows/fx-editor/VFX/VFXEffect.ts b/editor/src/editor/windows/fx-editor/VFX/VFXEffect.ts index 6b48131d8..79763551a 100644 --- a/editor/src/editor/windows/fx-editor/VFX/VFXEffect.ts +++ b/editor/src/editor/windows/fx-editor/VFX/VFXEffect.ts @@ -1,11 +1,13 @@ -import { Scene, Tools, IDisposable, TransformNode } from "babylonjs"; +import { Scene, Tools, IDisposable, TransformNode, Vector3, CreatePlane, MeshBuilder, Texture } from "babylonjs"; import type { QuarksVFXJSON } from "./types/quarksTypes"; import type { VFXLoaderOptions } from "./types/loader"; import { VFXParser } from "./parsers/VFXParser"; import { VFXParticleSystem } from "./systems/VFXParticleSystem"; import { VFXSolidParticleSystem } from "./systems/VFXSolidParticleSystem"; import type { VFXGroup, VFXEmitter, VFXData } from "./types/hierarchy"; +import type { VFXParticleEmitterConfig } from "./types/emitterConfig"; import { isVFXSystem } from "./types/system"; +import { VFXEmitterFactory } from "./factories/VFXEmitterFactory"; /** * VFX Effect Node - represents either a particle system or a group @@ -67,6 +69,9 @@ export class VFXEffect implements IDisposable { /** All nodes in the hierarchy */ private readonly _nodes = new Map(); + /** Scene reference for creating new systems */ + private _scene: Scene | null = null; + /** * Load a Three.js particle JSON file and create particle systems * @param url URL to the JSON file @@ -118,6 +123,7 @@ export class VFXEffect implements IDisposable { * @param options Optional parsing options */ constructor(jsonData?: QuarksVFXJSON, scene?: Scene, rootUrl: string = "", options?: VFXLoaderOptions) { + this._scene = scene || null; if (jsonData && scene) { const parser = new VFXParser(scene, rootUrl, jsonData, options); const parseResult = parser.parse(); @@ -126,6 +132,10 @@ export class VFXEffect implements IDisposable { if (parseResult.vfxData && parseResult.groupNodesMap) { this._buildHierarchy(parseResult.vfxData, parseResult.groupNodesMap, parseResult.systems); } + } else if (scene) { + // Create empty effect with root group + this._scene = scene; + this._createEmptyEffect(); } } @@ -352,6 +362,95 @@ export class VFXEffect implements IDisposable { } } + /** + * Start a node (system or group) + */ + public startNode(node: VFXEffectNode): void { + if (node.type === "particle" && node.system) { + node.system.start(); + } else if (node.type === "group" && node.group) { + // Find all systems in this group recursively + const systems = this._getSystemsInNode(node); + for (const system of systems) { + system.start(); + } + } + } + + /** + * Stop a node (system or group) + */ + public stopNode(node: VFXEffectNode): void { + if (node.type === "particle" && node.system) { + node.system.stop(); + } else if (node.type === "group" && node.group) { + // Find all systems in this group recursively + const systems = this._getSystemsInNode(node); + for (const system of systems) { + system.stop(); + } + } + } + + /** + * Reset a node (system or group) + */ + public resetNode(node: VFXEffectNode): void { + if (node.type === "particle" && node.system) { + node.system.reset(); + } else if (node.type === "group" && node.group) { + // Find all systems in this group recursively + const systems = this._getSystemsInNode(node); + for (const system of systems) { + system.reset(); + } + } + } + + /** + * Check if a node is started (system or group) + */ + public isNodeStarted(node: VFXEffectNode): boolean { + if (node.type === "particle" && node.system) { + if (node.system instanceof VFXParticleSystem) { + return (node.system as any).isStarted ? (node.system as any).isStarted() : false; + } else if (node.system instanceof VFXSolidParticleSystem) { + return (node.system as any)._started && !(node.system as any)._stopped; + } + return false; + } else if (node.type === "group" && node.group) { + // Check if any system in this group is started + const systems = this._getSystemsInNode(node); + return systems.some((system) => { + if (system instanceof VFXParticleSystem) { + return (system as any).isStarted ? (system as any).isStarted() : false; + } else if (system instanceof VFXSolidParticleSystem) { + return (system as any)._started && !(system as any)._stopped; + } + return false; + }); + } + return false; + } + + /** + * Get all systems in a node recursively + */ + private _getSystemsInNode(node: VFXEffectNode): (VFXParticleSystem | VFXSolidParticleSystem)[] { + const systems: (VFXParticleSystem | VFXSolidParticleSystem)[] = []; + + if (node.type === "particle" && node.system) { + systems.push(node.system); + } else if (node.type === "group") { + // Recursively collect all systems from children + for (const child of node.children) { + systems.push(...this._getSystemsInNode(child)); + } + } + + return systems; + } + /** * Start all particle systems */ @@ -420,6 +519,192 @@ export class VFXEffect implements IDisposable { return false; } + /** + * Create empty effect with root group + */ + private _createEmptyEffect(): void { + if (!this._scene) { + return; + } + + const rootGroup = new TransformNode("Root", this._scene); + const rootUuid = Tools.RandomId(); + rootGroup.id = rootUuid; + + const rootNode: VFXEffectNode = { + name: "Root", + uuid: rootUuid, + group: rootGroup, + children: [], + type: "group", + }; + + this._root = rootNode; + this._groupsByName.set("Root", rootGroup); + this._groupsByUuid.set(rootUuid, rootGroup); + this._nodes.set(rootUuid, rootNode); + this._nodes.set("Root", rootNode); + } + + /** + * Create a new group node + * @param parentNode Parent node (if null, adds to root) + * @param name Optional name (defaults to "Group") + * @returns Created group node + */ + public createGroup(parentNode: VFXEffectNode | null = null, name: string = "Group"): VFXEffectNode | null { + if (!this._scene) { + console.error("Cannot create group: scene is not available"); + return null; + } + + const parent = parentNode || this._root; + if (!parent || parent.type !== "group") { + console.error("Cannot create group: parent is not a group"); + return null; + } + + // Ensure unique name + let uniqueName = name; + let counter = 1; + while (this._nodes.has(uniqueName)) { + uniqueName = `${name} ${counter}`; + counter++; + } + + const groupUuid = Tools.RandomId(); + const groupNode = new TransformNode(uniqueName, this._scene); + groupNode.id = groupUuid; + + // Set parent transform + if (parent.group) { + groupNode.setParent(parent.group, false, true); + } + + const newNode: VFXEffectNode = { + name: uniqueName, + uuid: groupUuid, + group: groupNode, + parent, + children: [], + type: "group", + }; + + // Add to parent's children + parent.children.push(newNode); + + // Store in maps + this._groupsByName.set(uniqueName, groupNode); + this._groupsByUuid.set(groupUuid, groupNode); + this._nodes.set(groupUuid, newNode); + this._nodes.set(uniqueName, newNode); + + return newNode; + } + + /** + * Create a new particle system + * @param parentNode Parent node (if null, adds to root) + * @param systemType Type of system ("solid" or "base") + * @param name Optional name (defaults to "ParticleSystem") + * @returns Created particle system node + */ + public createParticleSystem(parentNode: VFXEffectNode | null = null, systemType: "solid" | "base" = "base", name: string = "ParticleSystem"): VFXEffectNode | null { + if (!this._scene) { + console.error("Cannot create particle system: scene is not available"); + return null; + } + + const parent = parentNode || this._root; + if (!parent || parent.type !== "group") { + console.error("Cannot create particle system: parent is not a group"); + return null; + } + + // Ensure unique name + let uniqueName = name; + let counter = 1; + while (this._nodes.has(uniqueName)) { + uniqueName = `${name} ${counter}`; + counter++; + } + + const systemUuid = Tools.RandomId(); + + // Create default config + const config: VFXParticleEmitterConfig = { + systemType, + looping: true, + duration: 5, + prewarm: false, + emissionOverTime: 10, + startLife: 1, + startSpeed: 1, + startSize: 1, + startColor: { type: "ConstantColor", value: [1, 1, 1, 1] }, + behaviors: [], + }; + + let system: VFXParticleSystem | VFXSolidParticleSystem; + + if (systemType === "solid") { + // Create default plane mesh for SPS + const planeMesh = CreatePlane("particleMesh", { size: 1 }, this._scene); + planeMesh.setEnabled(false); // Hide the source mesh + + system = new VFXSolidParticleSystem(uniqueName, this._scene, config, { + particleMesh: planeMesh, + parentGroup: parent.group || undefined, + }); + + // Create default point emitter + system.createPointEmitter(); + } else { + // Create base particle system with default flare texture + const flareTexture = new Texture(Tools.GetAssetUrl("https://assets.babylonjs.com/core/textures/flare.png"), this._scene); + system = new VFXParticleSystem(uniqueName, this._scene, config, { + texture: flareTexture, + }); + + // Create default point emitter + const emitterFactory = new VFXEmitterFactory(); + emitterFactory.createParticleSystemEmitter(system, undefined, Vector3.One(), null); + + // Create emitter mesh (Mesh for ParticleSystem) + const emitterMesh = MeshBuilder.CreateBox(`${uniqueName}_Emitter`, { size: 0.1 }, this._scene); + emitterMesh.id = Tools.RandomId(); + emitterMesh.setEnabled(false); // Hide the emitter mesh + if (parent.group) { + emitterMesh.setParent(parent.group, false, true); + } + system.emitter = emitterMesh; + } + + // Set system name + system.name = uniqueName; + + const newNode: VFXEffectNode = { + name: uniqueName, + uuid: systemUuid, + system, + parent, + children: [], + type: "particle", + }; + + // Add to parent's children + parent.children.push(newNode); + + // Store in maps + this._systems.push(system); + this._systemsByName.set(uniqueName, system); + this._systemsByUuid.set(systemUuid, system); + this._nodes.set(systemUuid, newNode); + this._nodes.set(uniqueName, newNode); + + return newNode; + } + /** * Dispose all resources */ diff --git a/editor/src/editor/windows/fx-editor/graph.tsx b/editor/src/editor/windows/fx-editor/graph.tsx index c7bc9ddf9..57381e778 100644 --- a/editor/src/editor/windows/fx-editor/graph.tsx +++ b/editor/src/editor/windows/fx-editor/graph.tsx @@ -17,6 +17,9 @@ import { } from "../../../ui/shadcn/ui/context-menu"; import { IFXEditor } from "."; import { VFXEffect, type VFXEffectNode } from "./VFX"; +import { saveSingleFileDialog } from "../../../tools/dialog"; +import { writeJSON } from "fs-extra"; +import { toast } from "sonner"; export interface IFXEditorGraphProps { filePath: string | null; @@ -29,8 +32,17 @@ export interface IFXEditorGraphState { selectedNodeId: string | number | null; } +interface EffectInfo { + id: string; + name: string; + effect: VFXEffect; + originalJsonData?: any; // Store original JSON data for export +} + export class FXEditorGraph extends Component { - private _vfxEffect: VFXEffect | null = null; + private _vfxEffects: Map = new Map(); + /** Map of node instances to unique IDs for tree nodes */ + private _nodeIdMap: Map = new Map(); public constructor(props: IFXEditorGraphProps) { super(props); @@ -42,10 +54,26 @@ export class FXEditorGraph extends Component info.effect); + } + + /** + * Get effect by ID + */ + public getEffectById(id: string): VFXEffect | null { + const info = this._vfxEffects.get(id); + return info ? info.effect : null; } /** @@ -90,15 +118,24 @@ export class FXEditorGraph extends Component[] = []; + + for (const [effectId, effectInfo] of this._vfxEffects.entries()) { + if (effectInfo.effect.root) { + // Use effect root directly as the tree node, but update its name to effect name + effectInfo.effect.root.name = effectInfo.name; + effectInfo.effect.root.uuid = effectId; + + const treeNode = this._convertVFXNodeToTreeNode(effectInfo.effect.root, true); + nodes.push(treeNode); + } + } + + this.setState({ nodes }); + } + + /** + * Generate unique ID for a node + */ + private _generateUniqueNodeId(vfxNode: VFXEffectNode): string { + // Check if we already have an ID for this node instance + if (this._nodeIdMap.has(vfxNode)) { + return this._nodeIdMap.get(vfxNode)!; + } + + // Generate unique ID + const uniqueId = `node-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; + this._nodeIdMap.set(vfxNode, uniqueId); + return uniqueId; + } + /** * Converts VFXEffectNode to TreeNodeInfo recursively */ - private _convertVFXNodeToTreeNode(vfxNode: VFXEffectNode): TreeNodeInfo { - const nodeId = vfxNode.uuid || vfxNode.name; - const childNodes = vfxNode.children.length > 0 ? vfxNode.children.map((child) => this._convertVFXNodeToTreeNode(child)) : undefined; + private _convertVFXNodeToTreeNode(vfxNode: VFXEffectNode, isEffectRoot: boolean = false): TreeNodeInfo { + // Always use unique ID instead of uuid or name + const nodeId = this._generateUniqueNodeId(vfxNode); + const childNodes = vfxNode.children.length > 0 ? vfxNode.children.map((child) => this._convertVFXNodeToTreeNode(child, false)) : undefined; return { id: nodeId, label: this._getNodeLabelComponent({ id: nodeId, nodeData: vfxNode } as any, vfxNode.name), - icon: vfxNode.type === "particle" ? : , - isExpanded: vfxNode.type === "group", + icon: isEffectRoot ? ( + + ) : vfxNode.type === "particle" ? ( + + ) : ( + + ), + isExpanded: isEffectRoot || vfxNode.type === "group", childNodes, isSelected: false, - hasCaret: vfxNode.type === "group" || (childNodes && childNodes.length > 0), + hasCaret: isEffectRoot || vfxNode.type === "group" || (childNodes && childNodes.length > 0), nodeData: vfxNode, }; } @@ -174,7 +256,15 @@ export class FXEditorGraph extends Component )} -
ev.preventDefault()}> +
{ + ev.preventDefault(); + ev.dataTransfer.dropEffect = "copy"; + }} + onDrop={(ev) => this._handleDropEmpty(ev)} + >
@@ -187,11 +277,14 @@ export class FXEditorGraph extends Component Add - this._handleAddParticles()}> - Particle - - this._handleAddGroup()}> - Group + { + ev.dataTransfer.setData("fx-editor/create-effect", "effect"); + }} + onClick={() => this._handleCreateEffect()} + > + Effect @@ -234,10 +327,6 @@ export class FXEditorGraph extends Component): "particle" | "group" { - return node.nodeData?.type || "particle"; - } - private _handleNodeClicked(node: TreeNodeInfo): void { const selectedId = node.id as string | number; const nodes = this._updateNodeSelection(this.state.nodes, selectedId); @@ -260,7 +349,27 @@ export class FXEditorGraph extends Component, name: string): JSX.Element { - const label =
{name}
; + const label = ( +
{ + if (node.nodeData?.type === "group") { + ev.preventDefault(); + ev.stopPropagation(); + ev.dataTransfer.dropEffect = "copy"; + } + }} + onDrop={(ev) => { + if (node.nodeData?.type === "group") { + ev.preventDefault(); + ev.stopPropagation(); + this._handleDropOnNode(node, ev); + } + }} + > + {name} +
+ ); return ( @@ -271,16 +380,45 @@ export class FXEditorGraph extends Component Add - this._handleAddParticlesToNode(node)}> - Particle - - {this._getNodeType(node) === "group" && ( - this._handleAddGroupToNode(node)}> - Group - + {node.nodeData?.type === "group" && ( + <> + { + ev.dataTransfer.setData("fx-editor/create-item", "base-particle"); + }} + onClick={() => this._handleAddParticleSystemToNode(node, "base")} + > + Base Particle + + { + ev.dataTransfer.setData("fx-editor/create-item", "solid-particle"); + }} + onClick={() => this._handleAddParticleSystemToNode(node, "solid")} + > + Solid Particle + + { + ev.dataTransfer.setData("fx-editor/create-item", "group"); + }} + onClick={() => this._handleAddGroupToNode(node)} + > + Group + + )} + {this._isEffectRootNode(node) && ( + <> + + this._handleExportEffect(node)}>Export + + )} this._handleDeleteNode(node)}> Delete @@ -290,44 +428,202 @@ export class FXEditorGraph extends Component, - // isExpanded: false, - // childNodes: undefined, - // isSelected: false, - // hasCaret: false, - // nodeData: particleData, - // }; + /** + * Check if node is an effect root node + */ + private _isEffectRootNode(node: TreeNodeInfo): boolean { + const nodeData = node.nodeData; + if (!nodeData || !nodeData.uuid) { + return false; + } + + // Check if this node is the root of any effect + return this._vfxEffects.has(nodeData.uuid); } - private _handleAddGroup(_parentId?: string | number): void { - // const _nodeId = `group-${Date.now()}`; - // const groupData = this.getOrCreateGroupData(nodeId); - // const newNode: TreeNodeInfo = { - // id: nodeId, - // label: this._getNodeLabelComponent({ id: nodeId, nodeData: groupData } as any, groupData.name), - // icon: , - // isExpanded: true, - // childNodes: [], - // isSelected: false, - // hasCaret: true, - // nodeData: groupData, - // }; + /** + * Export effect to JSON file + */ + private async _handleExportEffect(node: TreeNodeInfo): Promise { + const nodeData = node.nodeData; + if (!nodeData || !nodeData.uuid) { + return; + } + + const effectInfo = this._vfxEffects.get(nodeData.uuid); + if (!effectInfo || !effectInfo.originalJsonData) { + toast.error("Cannot export effect: original data not available"); + return; + } + + const filePath = saveSingleFileDialog({ + title: "Export Effect", + filters: [{ name: "Effect Files", extensions: ["effect"] }], + defaultPath: `${effectInfo.name}.effect`, + }); + + if (!filePath) { + return; + } + + try { + await writeJSON(filePath, effectInfo.originalJsonData, { + spaces: "\t", + encoding: "utf-8", + }); + + toast.success(`Effect exported successfully to ${filePath}`); + } catch (error) { + console.error("Failed to export effect:", error); + toast.error(`Failed to export effect: ${error instanceof Error ? error.message : String(error)}`); + } } - private _handleAddParticlesToNode(node: TreeNodeInfo): void { - const nodeId = node.id as string | number; - this._handleAddParticles(nodeId); + private _handleCreateEffect(): void { + if (!this.props.editor.preview?.scene) { + console.error("Scene is not available"); + return; + } + + // Create empty effect + const vfxEffect = new VFXEffect(undefined, this.props.editor.preview.scene); + + // Generate unique ID and name for effect + const effectId = `effect-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; + let effectName = "Effect"; + let counter = 1; + while (Array.from(this._vfxEffects.values()).some((info) => info.name === effectName)) { + effectName = `Effect ${counter}`; + counter++; + } + + // Store effect + this._vfxEffects.set(effectId, { + id: effectId, + name: effectName, + effect: vfxEffect, + }); + + // Rebuild tree with all effects + this._rebuildTree(); + } + + private _findEffectForNode(node: TreeNodeInfo): VFXEffect | null { + // Find the effect that contains this node by traversing up the tree + const nodeData = node.nodeData; + if (!nodeData) { + return null; + } + + // First check if this is an effect root node + if (nodeData.uuid) { + const effectInfo = this._vfxEffects.get(nodeData.uuid); + if (effectInfo) { + return effectInfo.effect; + } + } + + // Find effect by checking if node is in any effect's hierarchy + for (const effectInfo of this._vfxEffects.values()) { + const effect = effectInfo.effect; + if (effect.root) { + // Check if node is part of this effect's hierarchy + const findNodeInHierarchy = (current: VFXEffectNode): boolean => { + // Use instance comparison and uuid for matching + if (current === nodeData || (current.uuid && nodeData.uuid && current.uuid === nodeData.uuid)) { + return true; + } + for (const child of current.children) { + if (findNodeInHierarchy(child)) { + return true; + } + } + return false; + }; + + if (findNodeInHierarchy(effect.root)) { + return effect; + } + } + } + + return null; + } + + private _handleAddParticleSystemToNode(node: TreeNodeInfo, systemType: "solid" | "base"): void { + const effect = this._findEffectForNode(node); + if (!effect) { + console.error("No effect found for node"); + return; + } + + const nodeData = node.nodeData; + if (!nodeData || nodeData.type !== "group") { + console.error("Cannot add particle system: parent is not a group"); + return; + } + + const newNode = effect.createParticleSystem(nodeData, systemType); + if (newNode) { + // Rebuild tree with all effects + this._rebuildTree(); + } } private _handleAddGroupToNode(node: TreeNodeInfo): void { - const nodeId = node.id as string | number; - this._handleAddGroup(nodeId); + const effect = this._findEffectForNode(node); + if (!effect) { + console.error("No effect found for node"); + return; + } + + const nodeData = node.nodeData; + if (!nodeData || nodeData.type !== "group") { + console.error("Cannot add group: parent is not a group"); + return; + } + + const newNode = effect.createGroup(nodeData); + if (newNode) { + // Rebuild tree with all effects + this._rebuildTree(); + } + } + + private _handleDropEmpty(ev: React.DragEvent): void { + ev.preventDefault(); + ev.stopPropagation(); + + try { + const data = ev.dataTransfer.getData("fx-editor/create-effect"); + if (data === "effect") { + this._handleCreateEffect(); + } + } catch (e) { + // Ignore errors + } + } + + private _handleDropOnNode(node: TreeNodeInfo, ev: React.DragEvent): void { + ev.preventDefault(); + ev.stopPropagation(); + + if (!node.nodeData || node.nodeData.type !== "group") { + return; + } + + try { + const data = ev.dataTransfer.getData("fx-editor/create-item"); + if (data === "solid-particle") { + this._handleAddParticleSystemToNode(node, "solid"); + } else if (data === "base-particle") { + this._handleAddParticleSystemToNode(node, "base"); + } else if (data === "group") { + this._handleAddGroupToNode(node); + } + } catch (e) { + // Ignore errors + } } private _addNodeToParent(nodes: TreeNodeInfo[], parentId: string | number, newNode: TreeNodeInfo): TreeNodeInfo[] { @@ -352,43 +648,63 @@ export class FXEditorGraph extends Component): void { - const deleteNodeById = (nodes: TreeNodeInfo[], id: string | number): TreeNodeInfo[] => { - return nodes - .filter((n) => n.id !== id) - .map((n) => { - if (n.childNodes) { - return { - ...n, - childNodes: deleteNodeById(n.childNodes, id), - }; - } - return n; - }); - }; - - const deletedId = node.id!; + const nodeData = node.nodeData; + if (!nodeData) { + return; + } - // Dispose particle system or mesh - // const particleSystem = createdParticleSystemsMap.get(deletedId); - // if (particleSystem) { - // if (particleSystem.dispose) { - // particleSystem.dispose(); - // } - // createdParticleSystemsMap.delete(deletedId); - // } + // Check if this is an effect root node + const effectId = node.id as string; + if (this._vfxEffects.has(effectId)) { + // Delete entire effect + const effectInfo = this._vfxEffects.get(effectId); + if (effectInfo) { + effectInfo.effect.dispose(); + this._vfxEffects.delete(effectId); + this._rebuildTree(); + } + } else { + // Delete node from effect hierarchy + const effect = this._findEffectForNode(node); + if (!effect) { + return; + } - // const mesh = createdMeshesMap.get(deletedId); - // if (mesh) { - // mesh.dispose(); - // createdMeshesMap.delete(deletedId); - // } + // Find and remove node from effect hierarchy + const removeNodeFromHierarchy = (current: VFXEffectNode): boolean => { + // Remove from children + const index = current.children.findIndex((child) => child === nodeData || child.uuid === nodeData.uuid || child.name === nodeData.name); + if (index !== -1) { + const removedNode = current.children[index]; + // Dispose system if it's a particle system + if (removedNode.system) { + removedNode.system.dispose(); + } + // Dispose group if it's a group + if (removedNode.group) { + removedNode.group.dispose(); + } + current.children.splice(index, 1); + return true; + } - // Node data is removed automatically when node is deleted from tree + // Recursively search in children + for (const child of current.children) { + if (removeNodeFromHierarchy(child)) { + return true; + } + } + return false; + }; - // const newNodes = deleteNodeById(this.state.nodes, deletedId); - // const newSelectedNodeId = this.state.selectedNodeId === deletedId ? null : this.state.selectedNodeId; + if (effect.root) { + removeNodeFromHierarchy(effect.root); + this._rebuildTree(); + } + } - if (this.state.selectedNodeId === deletedId) { + // Clear selection if deleted node was selected + if (this.state.selectedNodeId === node.id) { this.props.onNodeSelected?.(null); } } diff --git a/editor/src/editor/windows/fx-editor/layout.tsx b/editor/src/editor/windows/fx-editor/layout.tsx index f904d2321..237aebc60 100644 --- a/editor/src/editor/windows/fx-editor/layout.tsx +++ b/editor/src/editor/windows/fx-editor/layout.tsx @@ -163,6 +163,7 @@ export class FXEditorLayout extends Component (this.props.editor.preview = r!)} filePath={this.props.filePath} editor={this.props.editor} + selectedNodeId={this.state.selectedNodeId} onSceneReady={() => { // Update graph when scene is ready if (this.props.editor.graph) { diff --git a/editor/src/editor/windows/fx-editor/preview.tsx b/editor/src/editor/windows/fx-editor/preview.tsx index 3ea3c13af..a1c06e9fc 100644 --- a/editor/src/editor/windows/fx-editor/preview.tsx +++ b/editor/src/editor/windows/fx-editor/preview.tsx @@ -7,11 +7,13 @@ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "../../ import { IoPlay, IoStop, IoRefresh } from "react-icons/io5"; import type { IFXEditor } from "."; +import type { VFXEffectNode } from "./VFX"; export interface IFXEditorPreviewProps { filePath: string | null; onSceneReady?: (scene: Scene) => void; editor?: IFXEditor; + selectedNodeId?: string | number | null; } export interface IFXEditorPreviewState { @@ -39,30 +41,32 @@ export class FXEditorPreview extends Component this._onGotCanvasRef(r!)} className="w-full h-full outline-none" /> - {/* Play/Stop/Restart buttons */} -
- - - - - - {this.state.playing ? "Stop" : "Play"} - - - {this.state.playing && ( + {/* Play/Stop/Restart buttons - only show if a node is selected */} + {this.props.selectedNodeId && ( +
+ - - Restart + {this.state.playing ? "Stop" : "Play"} - )} - -
+ + {this.state.playing && ( + + + + + Restart + + )} +
+
+ )}
); } @@ -74,16 +78,101 @@ export class FXEditorPreview extends Component { + if (current === node || current.uuid === node.uuid || current.name === node.name) { + return true; + } + for (const child of current.children) { + if (findNode(child)) { + return true; + } + } + return false; + }; + + return findNode(effect.root); + } + + /** + * Check if node is an effect root node + */ + private _isEffectRootNode(node: VFXEffectNode): boolean { + if (!node.uuid) { + return false; } + + // Check if this node's UUID matches an effect ID (effect root has effect ID as uuid) + const effects = this.props.editor?.graph?.getAllEffects() || []; + for (const effect of effects) { + if (effect.root && effect.root.uuid === node.uuid) { + return true; + } + } + return false; } public componentWillUnmount(): void { @@ -170,48 +259,78 @@ export class FXEditorPreview extends Component Date: Mon, 15 Dec 2025 17:34:30 +0300 Subject: [PATCH 33/62] feat: add geometry field component to FX editor for improved mesh handling, enabling drag-and-drop functionality for geometry files and enhancing user experience with detailed mesh properties display --- .../layout/inspector/fields/geometry.tsx | 232 ++++++++++++++++ .../editor/windows/fx-editor/VFX/VFXEffect.ts | 3 + .../VFX/systems/VFXSolidParticleSystem.ts | 9 +- .../src/editor/windows/fx-editor/layout.tsx | 109 +++++++- .../editor/windows/fx-editor/properties.tsx | 114 +++++--- .../properties/behaviors-properties.tsx | 41 +++ .../properties/emission-properties.tsx | 41 +++ .../properties/emitter-properties.tsx | 41 +++ .../fx-editor/properties/emitter-shape.tsx | 70 +---- .../properties/initialization-properties.tsx | 41 +++ .../properties/object-properties.tsx | 50 ++++ .../properties/particle-renderer.tsx | 259 ++++++++---------- .../properties/renderer-properties.tsx | 43 +++ 13 files changed, 792 insertions(+), 261 deletions(-) create mode 100644 editor/src/editor/layout/inspector/fields/geometry.tsx create mode 100644 editor/src/editor/windows/fx-editor/properties/behaviors-properties.tsx create mode 100644 editor/src/editor/windows/fx-editor/properties/emission-properties.tsx create mode 100644 editor/src/editor/windows/fx-editor/properties/emitter-properties.tsx create mode 100644 editor/src/editor/windows/fx-editor/properties/initialization-properties.tsx create mode 100644 editor/src/editor/windows/fx-editor/properties/object-properties.tsx create mode 100644 editor/src/editor/windows/fx-editor/properties/renderer-properties.tsx diff --git a/editor/src/editor/layout/inspector/fields/geometry.tsx b/editor/src/editor/layout/inspector/fields/geometry.tsx new file mode 100644 index 000000000..487acf71f --- /dev/null +++ b/editor/src/editor/layout/inspector/fields/geometry.tsx @@ -0,0 +1,232 @@ +import { DragEvent, Component, PropsWithChildren, ReactNode } from "react"; +import { extname } from "path/posix"; + +import { toast } from "sonner"; + +import { XMarkIcon } from "@heroicons/react/20/solid"; +import { MdOutlineQuestionMark } from "react-icons/md"; + +import { Scene, Mesh } from "babylonjs"; + +import { isScene } from "../../../../tools/guards/scene"; +import { registerUndoRedo } from "../../../../tools/undoredo"; + +import { configureImportedNodeIds } from "../../preview/import/import"; +import { loadImportedSceneFile } from "../../preview/import/import"; +import { EditorInspectorNumberField } from "./number"; +import { isMesh } from "babylonjs-editor-tools"; + +export interface IEditorInspectorGeometryFieldProps extends PropsWithChildren { + title: string; + property: string; + object: any; + + noUndoRedo?: boolean; + + scene?: Scene; + onChange?: (mesh: Mesh | null) => void; +} + +export interface IEditorInspectorGeometryFieldState { + dragOver: boolean; + loading: boolean; +} + +export class EditorInspectorGeometryField extends Component { + public constructor(props: IEditorInspectorGeometryFieldProps) { + super(props); + + this.state = { + dragOver: false, + loading: false, + }; + } + + public render(): ReactNode { + const mesh = this.props.object[this.props.property] as Mesh | null | undefined; + + return ( +
this._handleDrop(ev)} + onDragOver={(ev) => this._handleDragOver(ev)} + onDragLeave={(ev) => this._handleDragLeave(ev)} + className={`flex flex-col w-full p-5 rounded-lg ${this.state.dragOver ? "bg-muted-foreground/75 dark:bg-muted-foreground/20" : "bg-muted-foreground/10 dark:bg-muted-foreground/5"} transition-all duration-300 ease-in-out`} + > +
+ {this._getPreviewComponent(mesh)} + +
+
+
{this.props.title}
+ {mesh &&
{mesh.name}
} +
+ + {mesh && ( +
+ + +
+ )} +
+
{ + const oldMesh = this.props.object[this.props.property]; + + this.props.object[this.props.property] = null; + this.props.onChange?.(null); + + if (!this.props.noUndoRedo) { + registerUndoRedo({ + executeRedo: true, + undo: () => { + this.props.object[this.props.property] = oldMesh; + }, + redo: () => { + this.props.object[this.props.property] = null; + }, + }); + } + + this.forceUpdate(); + }} + className="flex justify-center items-center w-24 h-full hover:bg-muted-foreground rounded-lg transition-all duration-300" + > + {mesh && } +
+
+ + {mesh && this.props.children} +
+ ); + } + + private _getPreviewComponent(mesh: Mesh | null | undefined): ReactNode { + return ( +
+ {mesh ? ( +
+
{mesh.name}
+
+ ) : ( + + )} +
+ ); + } + + private _handleDragOver(ev: DragEvent): void { + ev.preventDefault(); + this.setState({ dragOver: true }); + } + + private _handleDragLeave(ev: DragEvent): void { + ev.preventDefault(); + this.setState({ dragOver: false }); + } + + private async _handleDrop(ev: DragEvent): Promise { + ev.preventDefault(); + this.setState({ dragOver: false, loading: true }); + + try { + const absolutePath = JSON.parse(ev.dataTransfer.getData("assets"))[0]; + const extension = extname(absolutePath).toLowerCase(); + + const supportedExtensions = [".x", ".b3d", ".dae", ".glb", ".gltf", ".fbx", ".stl", ".lwo", ".dxf", ".obj", ".3ds", ".ms3d", ".blend", ".babylon"]; + + if (!supportedExtensions.includes(extension)) { + toast.error(`Unsupported geometry format: ${extension}`); + this.setState({ loading: false }); + return; + } + + const scene = this.props.scene ?? (isScene(this.props.object) ? this.props.object : this.props.object.getScene?.()); + + if (!scene) { + toast.error("Scene is not available"); + this.setState({ loading: false }); + return; + } + + const result = await loadImportedSceneFile(scene, absolutePath); + + if (!result || !result.meshes || result.meshes.length === 0) { + toast.error("Failed to load geometry file"); + this.setState({ loading: false }); + return; + } + + // Use the first mesh or find a mesh without parent + let importedMesh: Mesh | null = null; + for (const m of result.meshes) { + if (isMesh(m) && !m.parent) { + importedMesh = m; + break; + } + } + + if (!importedMesh && result.meshes.length > 0 && isMesh(result.meshes[0])) { + importedMesh = result.meshes[0] as Mesh; + } + + if (!importedMesh) { + toast.error("No valid mesh found in geometry file"); + this.setState({ loading: false }); + return; + } + + // Configure imported mesh + configureImportedNodeIds(importedMesh); + importedMesh.setEnabled(false); // Hide the source mesh + + const oldMesh = this.props.object[this.props.property]; + + this.props.object[this.props.property] = importedMesh; + this.props.onChange?.(importedMesh); + + if (!this.props.noUndoRedo) { + registerUndoRedo({ + executeRedo: true, + undo: () => { + this.props.object[this.props.property] = oldMesh; + if (importedMesh && importedMesh !== oldMesh) { + importedMesh.dispose(); + } + }, + redo: () => { + this.props.object[this.props.property] = importedMesh; + }, + onLost: () => { + if (importedMesh && importedMesh !== oldMesh) { + importedMesh.dispose(); + } + }, + }); + } + + // Dispose other meshes from the imported file + for (const m of result.meshes) { + if (m !== importedMesh && isMesh(m)) { + m.dispose(); + } + } + + // Dispose transform nodes + for (const tn of result.transformNodes) { + tn.dispose(); + } + + this.forceUpdate(); + } catch (error) { + console.error("Failed to load geometry:", error); + toast.error(`Failed to load geometry: ${error instanceof Error ? error.message : String(error)}`); + } finally { + this.setState({ loading: false }); + } + } +} diff --git a/editor/src/editor/windows/fx-editor/VFX/VFXEffect.ts b/editor/src/editor/windows/fx-editor/VFX/VFXEffect.ts index 79763551a..d07719acf 100644 --- a/editor/src/editor/windows/fx-editor/VFX/VFXEffect.ts +++ b/editor/src/editor/windows/fx-editor/VFX/VFXEffect.ts @@ -657,6 +657,9 @@ export class VFXEffect implements IDisposable { parentGroup: parent.group || undefined, }); + // Store reference to source mesh for geometry field + (system as any)._sourceMesh = planeMesh.clone(`${uniqueName}_sourceMesh`); + // Create default point emitter system.createPointEmitter(); } else { diff --git a/editor/src/editor/windows/fx-editor/VFX/systems/VFXSolidParticleSystem.ts b/editor/src/editor/windows/fx-editor/VFX/systems/VFXSolidParticleSystem.ts index 857b3462d..5dcf0db4a 100644 --- a/editor/src/editor/windows/fx-editor/VFX/systems/VFXSolidParticleSystem.ts +++ b/editor/src/editor/windows/fx-editor/VFX/systems/VFXSolidParticleSystem.ts @@ -553,9 +553,12 @@ export class VFXSolidParticleSystem extends SolidParticleSystem implements IVFXS this.billboard = false; } - // Enable vertex colors and alpha for particle color support - // This must be done after addShape but before buildMesh - // The mesh will be created in buildMesh, so we'll set it there + // Build mesh immediately after adding shape to ensure mesh is available + this.buildMesh(); + + // Setup mesh properties (parent, transform, etc.) + // Call _setupMeshProperties to configure mesh properly + this._setupMeshProperties(); // Dispose temporary mesh after adding to SPS particleMesh.dispose(); diff --git a/editor/src/editor/windows/fx-editor/layout.tsx b/editor/src/editor/windows/fx-editor/layout.tsx index 237aebc60..f4d4f230b 100644 --- a/editor/src/editor/windows/fx-editor/layout.tsx +++ b/editor/src/editor/windows/fx-editor/layout.tsx @@ -6,7 +6,12 @@ import { waitNextAnimationFrame } from "../../../tools/tools"; import { FXEditorPreview } from "./preview"; import { FXEditorGraph } from "./graph"; import { FXEditorAnimation } from "./animation"; -import { FXEditorProperties } from "./properties"; +import { FXEditorObjectPropertiesTab } from "./properties/object-properties"; +import { FXEditorEmitterPropertiesTab } from "./properties/emitter-properties"; +import { FXEditorRendererPropertiesTab } from "./properties/renderer-properties"; +import { FXEditorEmissionPropertiesTab } from "./properties/emission-properties"; +import { FXEditorInitializationPropertiesTab } from "./properties/initialization-properties"; +import { FXEditorBehaviorsPropertiesTab } from "./properties/behaviors-properties"; import { FXEditorResources } from "./resources"; import { IFXEditor } from "."; @@ -89,9 +94,49 @@ const layoutModel: IJsonModel = { children: [ { type: "tab", - id: "properties", - name: "Properties", - component: "properties", + id: "properties-object", + name: "Object", + component: "properties-object", + enableClose: false, + enableRenderOnDemand: false, + }, + { + type: "tab", + id: "properties-emitter", + name: "Emitter", + component: "properties-emitter", + enableClose: false, + enableRenderOnDemand: false, + }, + { + type: "tab", + id: "properties-renderer", + name: "Renderer", + component: "properties-renderer", + enableClose: false, + enableRenderOnDemand: false, + }, + { + type: "tab", + id: "properties-emission", + name: "Emission", + component: "properties-emission", + enableClose: false, + enableRenderOnDemand: false, + }, + { + type: "tab", + id: "properties-initialization", + name: "Initialization", + component: "properties-initialization", + enableClose: false, + enableRenderOnDemand: false, + }, + { + type: "tab", + id: "properties-behaviors", + name: "Behaviors", + component: "properties-behaviors", enableClose: false, enableRenderOnDemand: false, }, @@ -146,10 +191,6 @@ export class FXEditorLayout extends Component { // Update components immediately after state change this._updateComponents(); - // Force update properties component after state change - if (this.props.editor.properties) { - this.props.editor.properties.forceUpdate(); - } // Force update layout to ensure flexlayout-react sees the new component this.forceUpdate(); } @@ -185,10 +226,9 @@ export class FXEditorLayout extends Component (this.props.editor.resources = r!)} resources={this.state.resources} />, animation: (this.props.editor.animation = r!)} filePath={this.props.filePath} editor={this.props.editor} />, - properties: ( - (this.props.editor.properties = r!)} + "properties-object": ( + this.props.editor.graph?.getNodeData(nodeId) || null} /> ), + "properties-emitter": ( + this.props.editor.graph?.getNodeData(nodeId) || null} + /> + ), + "properties-renderer": ( + this.props.editor.graph?.getNodeData(nodeId) || null} + /> + ), + "properties-emission": ( + this.props.editor.graph?.getNodeData(nodeId) || null} + /> + ), + "properties-initialization": ( + this.props.editor.graph?.getNodeData(nodeId) || null} + /> + ), + "properties-behaviors": ( + this.props.editor.graph?.getNodeData(nodeId) || null} + /> + ), }; } @@ -218,9 +299,9 @@ export class FXEditorLayout extends ComponentError, see console...
; } - // Always update components before returning, especially for properties tab + // Always update components before returning, especially for properties tabs // This ensures flexlayout-react gets the latest component with updated props - if (componentName === "properties") { + if (componentName.startsWith("properties-")) { this._updateComponents(); } diff --git a/editor/src/editor/windows/fx-editor/properties.tsx b/editor/src/editor/windows/fx-editor/properties.tsx index 006bd1f45..2bdd9505f 100644 --- a/editor/src/editor/windows/fx-editor/properties.tsx +++ b/editor/src/editor/windows/fx-editor/properties.tsx @@ -1,6 +1,6 @@ import { Component, ReactNode } from "react"; -import { EditorInspectorSectionField } from "../../layout/inspector/fields/section"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "../../../ui/shadcn/ui/tabs"; import { FXEditorObjectProperties } from "./properties/object"; import { FXEditorEmitterShapeProperties } from "./properties/emitter-shape"; @@ -69,53 +69,83 @@ export class FXEditorProperties extends Component - - { - this.forceUpdate(); - this.props.onNameChanged?.(); - }} - /> - +
+ + + + Object + + + + { + this.forceUpdate(); + this.props.onNameChanged?.(); + }} + /> + +
); } - // For particles, show all properties + // For particles, show all properties in tabs if (nodeData.type === "particle" && nodeData.system) { return ( -
- - { - this.forceUpdate(); - this.props.onNameChanged?.(); - }} - /> - - - - this.forceUpdate()} /> - - - - this.forceUpdate()} /> - - - - this.forceUpdate()} /> - - - - this.forceUpdate()} /> - - - - this.forceUpdate()} /> - +
+ + + + Object + + + Emitter + + + Renderer + + + Emission + + + Initialization + + + Behaviors + + + + + { + this.forceUpdate(); + this.props.onNameChanged?.(); + }} + /> + + + + this.forceUpdate()} /> + + + + this.forceUpdate()} /> + + + + this.forceUpdate()} /> + + + + this.forceUpdate()} /> + + + + this.forceUpdate()} /> + +
); } diff --git a/editor/src/editor/windows/fx-editor/properties/behaviors-properties.tsx b/editor/src/editor/windows/fx-editor/properties/behaviors-properties.tsx new file mode 100644 index 000000000..31e03e75e --- /dev/null +++ b/editor/src/editor/windows/fx-editor/properties/behaviors-properties.tsx @@ -0,0 +1,41 @@ +import { Component, ReactNode } from "react"; + +import { FXEditorBehaviorsProperties } from "./behaviors"; +import type { VFXEffectNode } from "../VFX"; + +export interface IFXEditorBehaviorsPropertiesTabProps { + filePath: string | null; + selectedNodeId: string | number | null; + getNodeData: (nodeId: string | number) => VFXEffectNode | null; +} + +export class FXEditorBehaviorsPropertiesTab extends Component { + public render(): ReactNode { + const nodeId = this.props.selectedNodeId; + + if (!nodeId) { + return ( +
+

No particle selected

+
+ ); + } + + const nodeData = this.props.getNodeData(nodeId); + + if (!nodeData || nodeData.type !== "particle" || !nodeData.system) { + return ( +
+

Select a particle system

+
+ ); + } + + return ( +
+ this.forceUpdate()} /> +
+ ); + } +} + diff --git a/editor/src/editor/windows/fx-editor/properties/emission-properties.tsx b/editor/src/editor/windows/fx-editor/properties/emission-properties.tsx new file mode 100644 index 000000000..655969f8f --- /dev/null +++ b/editor/src/editor/windows/fx-editor/properties/emission-properties.tsx @@ -0,0 +1,41 @@ +import { Component, ReactNode } from "react"; + +import { FXEditorEmissionProperties } from "./emission"; +import type { VFXEffectNode } from "../VFX"; + +export interface IFXEditorEmissionPropertiesTabProps { + filePath: string | null; + selectedNodeId: string | number | null; + getNodeData: (nodeId: string | number) => VFXEffectNode | null; +} + +export class FXEditorEmissionPropertiesTab extends Component { + public render(): ReactNode { + const nodeId = this.props.selectedNodeId; + + if (!nodeId) { + return ( +
+

No particle selected

+
+ ); + } + + const nodeData = this.props.getNodeData(nodeId); + + if (!nodeData || nodeData.type !== "particle" || !nodeData.system) { + return ( +
+

Select a particle system

+
+ ); + } + + return ( +
+ this.forceUpdate()} /> +
+ ); + } +} + diff --git a/editor/src/editor/windows/fx-editor/properties/emitter-properties.tsx b/editor/src/editor/windows/fx-editor/properties/emitter-properties.tsx new file mode 100644 index 000000000..b74295a74 --- /dev/null +++ b/editor/src/editor/windows/fx-editor/properties/emitter-properties.tsx @@ -0,0 +1,41 @@ +import { Component, ReactNode } from "react"; + +import { FXEditorEmitterShapeProperties } from "./emitter-shape"; +import type { VFXEffectNode } from "../VFX"; + +export interface IFXEditorEmitterPropertiesTabProps { + filePath: string | null; + selectedNodeId: string | number | null; + getNodeData: (nodeId: string | number) => VFXEffectNode | null; +} + +export class FXEditorEmitterPropertiesTab extends Component { + public render(): ReactNode { + const nodeId = this.props.selectedNodeId; + + if (!nodeId) { + return ( +
+

No particle selected

+
+ ); + } + + const nodeData = this.props.getNodeData(nodeId); + + if (!nodeData || nodeData.type !== "particle" || !nodeData.system) { + return ( +
+

Select a particle system

+
+ ); + } + + return ( +
+ this.forceUpdate()} /> +
+ ); + } +} + diff --git a/editor/src/editor/windows/fx-editor/properties/emitter-shape.tsx b/editor/src/editor/windows/fx-editor/properties/emitter-shape.tsx index 354f8d120..e0b255807 100644 --- a/editor/src/editor/windows/fx-editor/properties/emitter-shape.tsx +++ b/editor/src/editor/windows/fx-editor/properties/emitter-shape.tsx @@ -9,7 +9,7 @@ import { EditorInspectorBlockField } from "../../../layout/inspector/fields/bloc import type { VFXEffectNode } from "../VFX"; import { VFXParticleSystem, VFXSolidParticleSystem } from "../VFX"; -import type { SolidSphereParticleEmitter, SolidConeParticleEmitter } from "../VFX/types/emitters"; +import { SolidSphereParticleEmitter, SolidConeParticleEmitter } from "../VFX/types/emitters"; export interface IFXEditorEmitterShapePropertiesProps { nodeData: VFXEffectNode; @@ -90,72 +90,18 @@ export class FXEditorEmitterShapeProperties extends Component - - - + + + )} {emitter instanceof SolidConeParticleEmitter && ( <> - - - - + + + + )} diff --git a/editor/src/editor/windows/fx-editor/properties/initialization-properties.tsx b/editor/src/editor/windows/fx-editor/properties/initialization-properties.tsx new file mode 100644 index 000000000..13f78d5e6 --- /dev/null +++ b/editor/src/editor/windows/fx-editor/properties/initialization-properties.tsx @@ -0,0 +1,41 @@ +import { Component, ReactNode } from "react"; + +import { FXEditorParticleInitializationProperties } from "./particle-initialization"; +import type { VFXEffectNode } from "../VFX"; + +export interface IFXEditorInitializationPropertiesTabProps { + filePath: string | null; + selectedNodeId: string | number | null; + getNodeData: (nodeId: string | number) => VFXEffectNode | null; +} + +export class FXEditorInitializationPropertiesTab extends Component { + public render(): ReactNode { + const nodeId = this.props.selectedNodeId; + + if (!nodeId) { + return ( +
+

No particle selected

+
+ ); + } + + const nodeData = this.props.getNodeData(nodeId); + + if (!nodeData || nodeData.type !== "particle" || !nodeData.system) { + return ( +
+

Select a particle system

+
+ ); + } + + return ( +
+ this.forceUpdate()} /> +
+ ); + } +} + diff --git a/editor/src/editor/windows/fx-editor/properties/object-properties.tsx b/editor/src/editor/windows/fx-editor/properties/object-properties.tsx new file mode 100644 index 000000000..a53286665 --- /dev/null +++ b/editor/src/editor/windows/fx-editor/properties/object-properties.tsx @@ -0,0 +1,50 @@ +import { Component, ReactNode } from "react"; + +import { FXEditorObjectProperties } from "./object"; +import { IFXEditor } from ".."; +import type { VFXEffectNode } from "../VFX"; + +export interface IFXEditorObjectPropertiesTabProps { + filePath: string | null; + selectedNodeId: string | number | null; + editor: IFXEditor; + onNameChanged?: () => void; + getNodeData: (nodeId: string | number) => VFXEffectNode | null; +} + +export class FXEditorObjectPropertiesTab extends Component { + public render(): ReactNode { + const nodeId = this.props.selectedNodeId; + + if (!nodeId) { + return ( +
+

No node selected

+
+ ); + } + + const nodeData = this.props.getNodeData(nodeId); + + if (!nodeData) { + return ( +
+

Node not found

+
+ ); + } + + return ( +
+ { + this.forceUpdate(); + this.props.onNameChanged?.(); + }} + /> +
+ ); + } +} + diff --git a/editor/src/editor/windows/fx-editor/properties/particle-renderer.tsx b/editor/src/editor/windows/fx-editor/properties/particle-renderer.tsx index 426ed6509..0c4a5f76e 100644 --- a/editor/src/editor/windows/fx-editor/properties/particle-renderer.tsx +++ b/editor/src/editor/windows/fx-editor/properties/particle-renderer.tsx @@ -5,10 +5,42 @@ import { EditorInspectorNumberField } from "../../../layout/inspector/fields/num import { EditorInspectorSwitchField } from "../../../layout/inspector/fields/switch"; import { EditorInspectorListField } from "../../../layout/inspector/fields/list"; import { EditorInspectorTextureField } from "../../../layout/inspector/fields/texture"; +import { EditorInspectorGeometryField } from "../../../layout/inspector/fields/geometry"; import { EditorInspectorColorField } from "../../../layout/inspector/fields/color"; import { EditorInspectorStringField } from "../../../layout/inspector/fields/string"; -import { ParticleSystem, Constants, Material } from "babylonjs"; +import { + PBRMaterial, + StandardMaterial, + NodeMaterial, + MultiMaterial, + SkyMaterial, + GridMaterial, + NormalMaterial, + WaterMaterial, + LavaMaterial, + TriPlanarMaterial, + CellMaterial, + FireMaterial, + GradientMaterial, + Material, + ParticleSystem, +} from "babylonjs"; + +import { EditorPBRMaterialInspector } from "../../../layout/inspector/material/pbr"; +import { EditorStandardMaterialInspector } from "../../../layout/inspector/material/standard"; +import { EditorNodeMaterialInspector } from "../../../layout/inspector/material/node"; +import { EditorMultiMaterialInspector } from "../../../layout/inspector/material/multi"; +import { EditorSkyMaterialInspector } from "../../../layout/inspector/material/sky"; +import { EditorGridMaterialInspector } from "../../../layout/inspector/material/grid"; +import { EditorNormalMaterialInspector } from "../../../layout/inspector/material/normal"; +import { EditorWaterMaterialInspector } from "../../../layout/inspector/material/water"; +import { EditorLavaMaterialInspector } from "../../../layout/inspector/material/lava"; +import { EditorTriPlanarMaterialInspector } from "../../../layout/inspector/material/tri-planar"; +import { EditorCellMaterialInspector } from "../../../layout/inspector/material/cell"; +import { EditorFireMaterialInspector } from "../../../layout/inspector/material/fire"; +import { EditorGradientMaterialInspector } from "../../../layout/inspector/material/gradient"; + import type { VFXEffectNode } from "../VFX"; import { VFXParticleSystem, VFXSolidParticleSystem } from "../VFX"; import { IFXEditor } from ".."; @@ -96,8 +128,8 @@ export class FXEditorParticleRendererProperties extends Component )} - {/* Material - для обеих систем */} - {this._getMaterialField()} + {/* Material Inspector - только для solid с материалом */} + {isVFXSolidParticleSystem && this._getMaterialInspector()} {/* Blend Mode - только для base */} {isVFXParticleSystem && ( @@ -116,9 +148,6 @@ export class FXEditorParticleRendererProperties extends Component )} - {/* Material Properties - только для solid */} - {isVFXSolidParticleSystem && this._getMaterialProperties()} - {/* Texture */} {this._getTextureField()} @@ -145,7 +174,7 @@ export class FXEditorParticleRendererProperties extends Component this.props.onChange()} - /> - ); + // Получаем material только для VFXSolidParticleSystem + if (!(system instanceof VFXSolidParticleSystem) || !system.mesh || !system.mesh.material) { + return null; } - // Для VFXParticleSystem, material может быть в config или на texture - // Пока просто показываем, что material управляется через texture - return null; + const material = system.mesh.material; + return this._getMaterialInspectorComponent(material, system.mesh); } - private _getMaterialProperties(): ReactNode { - const { nodeData } = this.props; + private _getMaterialInspectorComponent(material: Material, mesh?: any): ReactNode { + switch (material.getClassName()) { + case "PBRMaterial": + return ; - if (nodeData.type !== "particle" || !nodeData.system) { - return null; - } + case "StandardMaterial": + return ; - const system = nodeData.system; - let material: Material | null = null; - - // Получаем material в зависимости от типа системы - if (system instanceof VFXSolidParticleSystem && system.mesh && system.mesh.material) { - material = system.mesh.material; - } else if (system instanceof VFXParticleSystem) { - // Для VFXParticleSystem material управляется через blendMode и texture - // Material properties не доступны напрямую - return null; - } + case "NodeMaterial": + return ; - if (!material) { - return null; - } + case "MultiMaterial": + return ; - const pbrMaterial = material as any; + case "SkyMaterial": + return ; - return ( - - {/* Transparent */} - {pbrMaterial.transparencyMode !== undefined && (() => { - // Proxy для transparent property - const transparentProxy = { - get transparent() { - return pbrMaterial.transparencyMode !== Constants.ALPHA_DISABLE; - }, - set transparent(value: boolean) { - pbrMaterial.transparencyMode = value ? Constants.ALPHA_COMBINE : Constants.ALPHA_DISABLE; - }, - }; - return this.props.onChange()} />; - })()} + case "GridMaterial": + return ; - {/* Opacity */} - {pbrMaterial.alpha !== undefined && ( - this.props.onChange()} - /> - )} + case "NormalMaterial": + return ; - {/* Side */} - {pbrMaterial.sideOrientation !== undefined && ( - this.props.onChange()} - /> - )} + case "WaterMaterial": + return ; - {/* Blending */} - {pbrMaterial.alphaMode !== undefined && ( - this.props.onChange()} - /> - )} + case "LavaMaterial": + return ; - {/* Color */} - {pbrMaterial.albedoColor !== undefined && ( - this.props.onChange()} - /> - )} - - ); + case "TriPlanarMaterial": + return ; + + case "CellMaterial": + return ; + + case "FireMaterial": + return ; + + case "GradientMaterial": + return ; + + default: + return null; + } } private _getTextureField(): ReactNode { @@ -286,21 +248,11 @@ export class FXEditorParticleRendererProperties extends Component this.props.onChange()} />; } - // For VFXSolidParticleSystem, texture is on the mesh material - if (system instanceof VFXSolidParticleSystem && system.mesh && system.mesh.material) { - const material = system.mesh.material; - // Check if material has diffuseTexture or other texture properties - if ((material as any).diffuseTexture) { - return ( - this.props.onChange()} /> - ); - } - } - return null; } @@ -435,28 +387,55 @@ export class FXEditorParticleRendererProperties extends Component -
Geometry
-
- {system.instancingGeometry ? ( -
{system.instancingGeometry}
- ) : mesh ? ( -
{mesh.name}
- ) : ( -
No geometry
- )} -
-
+ this.props.onChange()} + /> ); } } diff --git a/editor/src/editor/windows/fx-editor/properties/renderer-properties.tsx b/editor/src/editor/windows/fx-editor/properties/renderer-properties.tsx new file mode 100644 index 000000000..2fed7d31d --- /dev/null +++ b/editor/src/editor/windows/fx-editor/properties/renderer-properties.tsx @@ -0,0 +1,43 @@ +import { Component, ReactNode } from "react"; + +import { FXEditorParticleRendererProperties } from "./particle-renderer"; +import { IFXEditor } from ".."; +import type { VFXEffectNode } from "../VFX"; + +export interface IFXEditorRendererPropertiesTabProps { + filePath: string | null; + selectedNodeId: string | number | null; + editor: IFXEditor; + getNodeData: (nodeId: string | number) => VFXEffectNode | null; +} + +export class FXEditorRendererPropertiesTab extends Component { + public render(): ReactNode { + const nodeId = this.props.selectedNodeId; + + if (!nodeId) { + return ( +
+

No particle selected

+
+ ); + } + + const nodeData = this.props.getNodeData(nodeId); + + if (!nodeData || nodeData.type !== "particle" || !nodeData.system) { + return ( +
+

Select a particle system

+
+ ); + } + + return ( +
+ this.forceUpdate()} /> +
+ ); + } +} + From ad69f12f782415db03fd159f6a029626da8fd073 Mon Sep 17 00:00:00 2001 From: Mikalai Lazitski Date: Tue, 16 Dec 2025 10:22:06 +0300 Subject: [PATCH 34/62] refactor: rename FX editor components and interfaces for consistency, transitioning from IFXEditor to IEffectEditor, and updating related properties and methods to enhance clarity and maintainability --- .../fx-editor/VFX/behaviors/forceOverLife.ts | 34 -- .../src/editor/windows/fx-editor/VFX/index.ts | 14 - .../fx-editor/VFX/parsers/VFXParser.ts | 127 ------ .../VFX/types/VFXBehaviorFunction.ts | 20 - .../windows/fx-editor/VFX/types/behaviors.ts | 137 ------ .../windows/fx-editor/VFX/types/colors.ts | 41 -- .../windows/fx-editor/VFX/types/context.ts | 16 - .../windows/fx-editor/VFX/types/emitter.ts | 17 - .../fx-editor/VFX/types/emitterConfig.ts | 52 --- .../windows/fx-editor/VFX/types/emitters.ts | 92 ---- .../windows/fx-editor/VFX/types/hierarchy.ts | 51 --- .../windows/fx-editor/VFX/types/index.ts | 46 -- .../windows/fx-editor/VFX/types/rotations.ts | 26 -- .../editor/windows/fx-editor/animation.tsx | 8 +- editor/src/editor/windows/fx-editor/graph.tsx | 160 +++---- editor/src/editor/windows/fx-editor/index.tsx | 56 +-- .../src/editor/windows/fx-editor/layout.tsx | 52 +-- .../src/editor/windows/fx-editor/preview.tsx | 24 +- .../editor/windows/fx-editor/properties.tsx | 46 +- .../properties/behaviors-properties.tsx | 14 +- .../fx-editor/properties/behaviors.tsx | 13 +- .../behaviors/color-function-editor.tsx | 256 ++++++------ ...{vfx-color-editor.tsx => color-editor.tsx} | 230 +++++----- .../properties/emission-properties.tsx | 13 +- .../windows/fx-editor/properties/emission.tsx | 73 ++-- .../properties/emitter-properties.tsx | 13 +- .../fx-editor/properties/emitter-shape.tsx | 22 +- .../properties/initialization-properties.tsx | 13 +- .../properties/object-properties.tsx | 17 +- .../windows/fx-editor/properties/object.tsx | 19 +- .../properties/particle-initialization.tsx | 151 ++++--- .../properties/particle-renderer.tsx | 194 +++------ .../properties/renderer-properties.tsx | 17 +- ...otation-editor.tsx => rotation-editor.tsx} | 80 ++-- ...{vfx-value-editor.tsx => value-editor.tsx} | 81 ++-- .../editor/windows/fx-editor/resources.tsx | 10 +- .../src/editor/windows/fx-editor/toolbar.tsx | 38 +- editor/src/ui/gradient-picker.tsx | 5 +- .../src/effect}/behaviors/colorBySpeed.ts | 16 +- .../src/effect}/behaviors/colorOverLife.ts | 10 +- tools/src/effect/behaviors/forceOverLife.ts | 34 ++ .../src/effect}/behaviors/frameOverLife.ts | 11 +- .../src/effect}/behaviors/index.ts | 0 .../effect}/behaviors/limitSpeedOverLife.ts | 24 +- .../src/effect}/behaviors/orbitOverLife.ts | 26 +- .../src/effect}/behaviors/rotationBySpeed.ts | 25 +- .../src/effect}/behaviors/rotationOverLife.ts | 16 +- .../src/effect}/behaviors/sizeBySpeed.ts | 16 +- .../src/effect}/behaviors/sizeOverLife.ts | 10 +- .../src/effect}/behaviors/speedOverLife.ts | 16 +- .../src/effect}/behaviors/utils.ts | 6 +- .../src/effect/effect.ts | 158 +++---- tools/src/effect/emitters/index.ts | 3 + tools/src/effect/emitters/solidConeEmitter.ts | 35 ++ .../src/effect/emitters/solidPointEmitter.ts | 16 + .../src/effect/emitters/solidSphereEmitter.ts | 34 ++ .../src/effect/factories/emitterFactory.ts | 29 +- .../src/effect/factories/geometryFactory.ts | 40 +- tools/src/effect/factories/index.ts | 4 + .../src/effect/factories/materialFactory.ts | 92 ++-- .../src/effect/factories/systemFactory.ts | 180 ++++---- tools/src/effect/index.ts | 11 + .../src/effect/loggers/logger.ts | 18 +- .../src/effect/parsers/dataConverter.ts | 360 ++++++++-------- tools/src/effect/parsers/parser.ts | 125 ++++++ .../effect/systems/effectParticleSystem.ts | 124 +++--- .../systems/effectSolidParticleSystem.ts | 395 ++++++------------ tools/src/effect/systems/index.ts | 2 + tools/src/effect/types/behaviors.ts | 156 +++++++ tools/src/effect/types/colors.ts | 41 ++ tools/src/effect/types/context.ts | 16 + tools/src/effect/types/emitter.ts | 78 ++++ .../src/effect}/types/factories.ts | 4 +- .../src/effect}/types/gradients.ts | 4 +- tools/src/effect/types/hierarchy.ts | 51 +++ tools/src/effect/types/index.ts | 46 ++ .../VFX => tools/src/effect}/types/loader.ts | 2 +- .../src/effect}/types/quarksTypes.ts | 4 +- .../src/effect}/types/resources.ts | 42 +- tools/src/effect/types/rotations.ts | 26 ++ .../VFX => tools/src/effect}/types/shapes.ts | 8 +- .../VFX => tools/src/effect}/types/system.ts | 22 +- .../VFX => tools/src/effect}/types/values.ts | 10 +- .../src/effect}/utils/capacityCalculator.ts | 14 +- .../src/effect}/utils/gradientSystem.ts | 2 +- .../src/effect}/utils/matrixUtils.ts | 2 +- .../src/effect}/utils/valueParser.ts | 24 +- tools/src/index.ts | 2 + 88 files changed, 2224 insertions(+), 2444 deletions(-) delete mode 100644 editor/src/editor/windows/fx-editor/VFX/behaviors/forceOverLife.ts delete mode 100644 editor/src/editor/windows/fx-editor/VFX/index.ts delete mode 100644 editor/src/editor/windows/fx-editor/VFX/parsers/VFXParser.ts delete mode 100644 editor/src/editor/windows/fx-editor/VFX/types/VFXBehaviorFunction.ts delete mode 100644 editor/src/editor/windows/fx-editor/VFX/types/behaviors.ts delete mode 100644 editor/src/editor/windows/fx-editor/VFX/types/colors.ts delete mode 100644 editor/src/editor/windows/fx-editor/VFX/types/context.ts delete mode 100644 editor/src/editor/windows/fx-editor/VFX/types/emitter.ts delete mode 100644 editor/src/editor/windows/fx-editor/VFX/types/emitterConfig.ts delete mode 100644 editor/src/editor/windows/fx-editor/VFX/types/emitters.ts delete mode 100644 editor/src/editor/windows/fx-editor/VFX/types/hierarchy.ts delete mode 100644 editor/src/editor/windows/fx-editor/VFX/types/index.ts delete mode 100644 editor/src/editor/windows/fx-editor/VFX/types/rotations.ts rename editor/src/editor/windows/fx-editor/properties/{vfx-color-editor.tsx => color-editor.tsx} (58%) rename editor/src/editor/windows/fx-editor/properties/{vfx-rotation-editor.tsx => rotation-editor.tsx} (78%) rename editor/src/editor/windows/fx-editor/properties/{vfx-value-editor.tsx => value-editor.tsx} (77%) rename {editor/src/editor/windows/fx-editor/VFX => tools/src/effect}/behaviors/colorBySpeed.ts (79%) rename {editor/src/editor/windows/fx-editor/VFX => tools/src/effect}/behaviors/colorOverLife.ts (91%) create mode 100644 tools/src/effect/behaviors/forceOverLife.ts rename {editor/src/editor/windows/fx-editor/VFX => tools/src/effect}/behaviors/frameOverLife.ts (72%) rename {editor/src/editor/windows/fx-editor/VFX => tools/src/effect}/behaviors/index.ts (100%) rename {editor/src/editor/windows/fx-editor/VFX => tools/src/effect}/behaviors/limitSpeedOverLife.ts (70%) rename {editor/src/editor/windows/fx-editor/VFX => tools/src/effect}/behaviors/orbitOverLife.ts (76%) rename {editor/src/editor/windows/fx-editor/VFX => tools/src/effect}/behaviors/rotationBySpeed.ts (73%) rename {editor/src/editor/windows/fx-editor/VFX => tools/src/effect}/behaviors/rotationOverLife.ts (83%) rename {editor/src/editor/windows/fx-editor/VFX => tools/src/effect}/behaviors/sizeBySpeed.ts (72%) rename {editor/src/editor/windows/fx-editor/VFX => tools/src/effect}/behaviors/sizeOverLife.ts (83%) rename {editor/src/editor/windows/fx-editor/VFX => tools/src/effect}/behaviors/speedOverLife.ts (82%) rename {editor/src/editor/windows/fx-editor/VFX => tools/src/effect}/behaviors/utils.ts (94%) rename editor/src/editor/windows/fx-editor/VFX/VFXEffect.ts => tools/src/effect/effect.ts (76%) create mode 100644 tools/src/effect/emitters/index.ts create mode 100644 tools/src/effect/emitters/solidConeEmitter.ts create mode 100644 tools/src/effect/emitters/solidPointEmitter.ts create mode 100644 tools/src/effect/emitters/solidSphereEmitter.ts rename editor/src/editor/windows/fx-editor/VFX/factories/VFXEmitterFactory.ts => tools/src/effect/factories/emitterFactory.ts (75%) rename editor/src/editor/windows/fx-editor/VFX/factories/VFXGeometryFactory.ts => tools/src/effect/factories/geometryFactory.ts (79%) create mode 100644 tools/src/effect/factories/index.ts rename editor/src/editor/windows/fx-editor/VFX/factories/VFXMaterialFactory.ts => tools/src/effect/factories/materialFactory.ts (66%) rename editor/src/editor/windows/fx-editor/VFX/factories/VFXSystemFactory.ts => tools/src/effect/factories/systemFactory.ts (51%) create mode 100644 tools/src/effect/index.ts rename editor/src/editor/windows/fx-editor/VFX/loggers/VFXLogger.ts => tools/src/effect/loggers/logger.ts (53%) rename editor/src/editor/windows/fx-editor/VFX/parsers/VFXDataConverter.ts => tools/src/effect/parsers/dataConverter.ts (66%) create mode 100644 tools/src/effect/parsers/parser.ts rename editor/src/editor/windows/fx-editor/VFX/systems/VFXParticleSystem.ts => tools/src/effect/systems/effectParticleSystem.ts (70%) rename editor/src/editor/windows/fx-editor/VFX/systems/VFXSolidParticleSystem.ts => tools/src/effect/systems/effectSolidParticleSystem.ts (67%) create mode 100644 tools/src/effect/systems/index.ts create mode 100644 tools/src/effect/types/behaviors.ts create mode 100644 tools/src/effect/types/colors.ts create mode 100644 tools/src/effect/types/context.ts create mode 100644 tools/src/effect/types/emitter.ts rename {editor/src/editor/windows/fx-editor/VFX => tools/src/effect}/types/factories.ts (86%) rename {editor/src/editor/windows/fx-editor/VFX => tools/src/effect}/types/gradients.ts (64%) create mode 100644 tools/src/effect/types/hierarchy.ts create mode 100644 tools/src/effect/types/index.ts rename {editor/src/editor/windows/fx-editor/VFX => tools/src/effect}/types/loader.ts (85%) rename {editor/src/editor/windows/fx-editor/VFX => tools/src/effect}/types/quarksTypes.ts (98%) rename {editor/src/editor/windows/fx-editor/VFX => tools/src/effect}/types/resources.ts (58%) create mode 100644 tools/src/effect/types/rotations.ts rename {editor/src/editor/windows/fx-editor/VFX => tools/src/effect}/types/shapes.ts (54%) rename {editor/src/editor/windows/fx-editor/VFX => tools/src/effect}/types/system.ts (65%) rename {editor/src/editor/windows/fx-editor/VFX => tools/src/effect}/types/values.ts (51%) rename {editor/src/editor/windows/fx-editor/VFX => tools/src/effect}/utils/capacityCalculator.ts (56%) rename {editor/src/editor/windows/fx-editor/VFX => tools/src/effect}/utils/gradientSystem.ts (97%) rename {editor/src/editor/windows/fx-editor/VFX => tools/src/effect}/utils/matrixUtils.ts (94%) rename {editor/src/editor/windows/fx-editor/VFX => tools/src/effect}/utils/valueParser.ts (86%) diff --git a/editor/src/editor/windows/fx-editor/VFX/behaviors/forceOverLife.ts b/editor/src/editor/windows/fx-editor/VFX/behaviors/forceOverLife.ts deleted file mode 100644 index 6f12d6982..000000000 --- a/editor/src/editor/windows/fx-editor/VFX/behaviors/forceOverLife.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { Vector3, ParticleSystem } from "babylonjs"; -import type { VFXForceOverLifeBehavior, VFXGravityForceBehavior } from "../types/behaviors"; -import { VFXValueUtils } from "../utils/valueParser"; - -/** - * Apply ForceOverLife behavior to ParticleSystem - */ -export function applyForceOverLifePS(particleSystem: ParticleSystem, behavior: VFXForceOverLifeBehavior): void { - if (behavior.force) { - const forceX = behavior.force.x !== undefined ? VFXValueUtils.parseConstantValue(behavior.force.x) : 0; - const forceY = behavior.force.y !== undefined ? VFXValueUtils.parseConstantValue(behavior.force.y) : 0; - const forceZ = behavior.force.z !== undefined ? VFXValueUtils.parseConstantValue(behavior.force.z) : 0; - if (Math.abs(forceY) > 0.01 || Math.abs(forceX) > 0.01 || Math.abs(forceZ) > 0.01) { - particleSystem.gravity = new Vector3(forceX, forceY, forceZ); - } - } else if (behavior.x !== undefined || behavior.y !== undefined || behavior.z !== undefined) { - const forceX = behavior.x !== undefined ? VFXValueUtils.parseConstantValue(behavior.x) : 0; - const forceY = behavior.y !== undefined ? VFXValueUtils.parseConstantValue(behavior.y) : 0; - const forceZ = behavior.z !== undefined ? VFXValueUtils.parseConstantValue(behavior.z) : 0; - if (Math.abs(forceY) > 0.01 || Math.abs(forceX) > 0.01 || Math.abs(forceZ) > 0.01) { - particleSystem.gravity = new Vector3(forceX, forceY, forceZ); - } - } -} - -/** - * Apply GravityForce behavior to ParticleSystem - */ -export function applyGravityForcePS(particleSystem: ParticleSystem, behavior: VFXGravityForceBehavior): void { - if (behavior.gravity !== undefined) { - const gravity = VFXValueUtils.parseConstantValue(behavior.gravity); - particleSystem.gravity = new Vector3(0, -gravity, 0); - } -} diff --git a/editor/src/editor/windows/fx-editor/VFX/index.ts b/editor/src/editor/windows/fx-editor/VFX/index.ts deleted file mode 100644 index 7d79789c6..000000000 --- a/editor/src/editor/windows/fx-editor/VFX/index.ts +++ /dev/null @@ -1,14 +0,0 @@ -export * from "./types"; -export * from "./parsers/VFXParser"; -export * from "./parsers/VFXDataConverter"; -export * from "./factories/VFXMaterialFactory"; -export * from "./factories/VFXGeometryFactory"; -export * from "./factories/VFXSystemFactory"; -export * from "./utils/capacityCalculator"; -export * from "./utils/matrixUtils"; -export * from "./systems/VFXSolidParticleSystem"; -export * from "./systems/VFXParticleSystem"; -export * from "./loggers/VFXLogger"; -export * from "./VFXEffect"; -export * from "./utils/valueParser"; -export type { VFXEffectNode } from "./VFXEffect"; diff --git a/editor/src/editor/windows/fx-editor/VFX/parsers/VFXParser.ts b/editor/src/editor/windows/fx-editor/VFX/parsers/VFXParser.ts deleted file mode 100644 index cfb90dfed..000000000 --- a/editor/src/editor/windows/fx-editor/VFX/parsers/VFXParser.ts +++ /dev/null @@ -1,127 +0,0 @@ -import { Scene, TransformNode } from "babylonjs"; -import type { QuarksVFXJSON } from "../types/quarksTypes"; -import type { VFXLoaderOptions } from "../types/loader"; -import type { VFXData } from "../types/hierarchy"; -import { VFXLogger } from "../loggers/VFXLogger"; -import { VFXMaterialFactory } from "../factories/VFXMaterialFactory"; -import { VFXGeometryFactory } from "../factories/VFXGeometryFactory"; -import { VFXSystemFactory } from "../factories/VFXSystemFactory"; -import { VFXDataConverter } from "./VFXDataConverter"; -import { VFXParticleSystem } from "../systems/VFXParticleSystem"; -import { VFXSolidParticleSystem } from "../systems/VFXSolidParticleSystem"; - -/** - * Result of parsing VFX JSON - */ -export interface VFXParseResult { - /** Created particle systems */ - systems: (VFXParticleSystem | VFXSolidParticleSystem)[]; - /** Converted VFX data */ - vfxData: VFXData; - /** Map of group UUIDs to TransformNodes */ - groupNodesMap: Map; -} - -/** - * Main parser for Three.js particle JSON files - * Orchestrates the parsing process using modular components - */ -export class VFXParser { - private _logger: VFXLogger; - private _materialFactory: VFXMaterialFactory; - private _geometryFactory: VFXGeometryFactory; - private _systemFactory: VFXSystemFactory; - private _vfxData: VFXData; - private _groupNodesMap: Map; - private _options: VFXLoaderOptions; - - constructor(scene: Scene, rootUrl: string, jsonData: QuarksVFXJSON, options?: VFXLoaderOptions) { - const opts = options || {}; - this._options = opts; - this._groupNodesMap = new Map(); - - this._logger = new VFXLogger("[VFXParser]", opts); - - // Convert Quarks JSON to VFXData first - const dataConverter = new VFXDataConverter(opts); - this._vfxData = dataConverter.convert(jsonData); - - // Create factories with VFXData instead of QuarksVFXJSON - this._materialFactory = new VFXMaterialFactory(scene, this._vfxData, rootUrl, opts); - this._geometryFactory = new VFXGeometryFactory(this._vfxData, opts); - this._systemFactory = new VFXSystemFactory(scene, opts, this._groupNodesMap, this._materialFactory, this._geometryFactory); - } - - /** - * Parse the JSON data and create particle systems - * Returns all necessary data for building the effect hierarchy - */ - public parse(): VFXParseResult { - this._logger.log("=== Starting Particle System Parsing ==="); - - if (!this._vfxData) { - this._logger.warn("VFXData is missing"); - return { - systems: [], - vfxData: this._vfxData, - groupNodesMap: this._groupNodesMap, - }; - } - - if (this._options.validate) { - this._validateJSONStructure(this._vfxData); - } - - const particleSystems = this._systemFactory.createSystems(this._vfxData); - - this._logger.log(`=== Parsing complete. Created ${particleSystems.length} particle system(s) ===`); - return { - systems: particleSystems, - vfxData: this._vfxData, - groupNodesMap: this._groupNodesMap, - }; - } - - /** - * Validate VFX data structure - */ - private _validateJSONStructure(vfxData: VFXData): void { - this._logger.log("Validating VFX data structure..."); - - if (!vfxData.root) { - this._logger.warn("VFX data missing 'root' property"); - } - - if (!vfxData.materials || vfxData.materials.length === 0) { - this._logger.warn("VFX data has no materials"); - } - - if (!vfxData.textures || vfxData.textures.length === 0) { - this._logger.warn("VFX data has no textures"); - } - - if (!vfxData.images || vfxData.images.length === 0) { - this._logger.warn("VFX data has no images"); - } - - if (!vfxData.geometries || vfxData.geometries.length === 0) { - this._logger.warn("VFX data has no geometries"); - } - - this._logger.log("Validation complete"); - } - - /** - * Get the material factory (for advanced use cases) - */ - public getMaterialFactory(): VFXMaterialFactory { - return this._materialFactory; - } - - /** - * Get the geometry factory (for advanced use cases) - */ - public getGeometryFactory(): VFXGeometryFactory { - return this._geometryFactory; - } -} diff --git a/editor/src/editor/windows/fx-editor/VFX/types/VFXBehaviorFunction.ts b/editor/src/editor/windows/fx-editor/VFX/types/VFXBehaviorFunction.ts deleted file mode 100644 index 2cebe48ec..000000000 --- a/editor/src/editor/windows/fx-editor/VFX/types/VFXBehaviorFunction.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { Particle, SolidParticle, ParticleSystem, SolidParticleSystem } from "babylonjs"; -import type { VFXBehavior } from "./behaviors"; - -/** - * Per-particle behavior function for ParticleSystem - * Behavior config is captured in closure, only particle is needed - */ -export type VFXPerParticleBehaviorFunction = (particle: Particle) => void; - -/** - * Per-particle behavior function for SolidParticleSystem - * Behavior config is captured in closure, only particle is needed - */ -export type VFXPerSolidParticleBehaviorFunction = (particle: SolidParticle) => void; - -/** - * System-level behavior function (applied once during initialization) - * Takes only system and behavior config - all data comes from system - */ -export type VFXSystemBehaviorFunction = (system: ParticleSystem | SolidParticleSystem, behavior: VFXBehavior) => void; diff --git a/editor/src/editor/windows/fx-editor/VFX/types/behaviors.ts b/editor/src/editor/windows/fx-editor/VFX/types/behaviors.ts deleted file mode 100644 index 23c589a98..000000000 --- a/editor/src/editor/windows/fx-editor/VFX/types/behaviors.ts +++ /dev/null @@ -1,137 +0,0 @@ -import type { VFXValue } from "./values"; -import type { VFXGradientKey } from "./gradients"; - -/** - * VFX behavior types (converted from Quarks) - */ -export interface VFXColorOverLifeBehavior { - type: "ColorOverLife"; - color?: { - color?: { - keys: VFXGradientKey[]; - }; - alpha?: { - keys: VFXGradientKey[]; - }; - keys?: VFXGradientKey[]; - }; -} - -export interface VFXSizeOverLifeBehavior { - type: "SizeOverLife"; - size?: { - keys?: VFXGradientKey[]; - functions?: Array<{ - start: number; - function: { - p0?: number; - p3?: number; - }; - }>; - }; -} - -export interface VFXRotationOverLifeBehavior { - type: "RotationOverLife" | "Rotation3DOverLife"; - angularVelocity?: VFXValue; -} - -export interface VFXForceOverLifeBehavior { - type: "ForceOverLife" | "ApplyForce"; - force?: { - x?: VFXValue; - y?: VFXValue; - z?: VFXValue; - }; - x?: VFXValue; - y?: VFXValue; - z?: VFXValue; -} - -export interface VFXGravityForceBehavior { - type: "GravityForce"; - gravity?: VFXValue; -} - -export interface VFXSpeedOverLifeBehavior { - type: "SpeedOverLife"; - speed?: - | { - keys?: VFXGradientKey[]; - functions?: Array<{ - start: number; - function: { - p0?: number; - p3?: number; - }; - }>; - } - | VFXValue; -} - -export interface VFXFrameOverLifeBehavior { - type: "FrameOverLife"; - frame?: - | { - keys?: VFXGradientKey[]; - } - | VFXValue; -} - -export interface VFXLimitSpeedOverLifeBehavior { - type: "LimitSpeedOverLife"; - maxSpeed?: VFXValue; - speed?: VFXValue | { keys?: VFXGradientKey[] }; - dampen?: VFXValue; -} - -export interface VFXColorBySpeedBehavior { - type: "ColorBySpeed"; - color?: { - keys: VFXGradientKey[]; - }; - minSpeed?: VFXValue; - maxSpeed?: VFXValue; -} - -export interface VFXSizeBySpeedBehavior { - type: "SizeBySpeed"; - size?: { - keys: VFXGradientKey[]; - }; - minSpeed?: VFXValue; - maxSpeed?: VFXValue; -} - -export interface VFXRotationBySpeedBehavior { - type: "RotationBySpeed"; - angularVelocity?: VFXValue; - minSpeed?: VFXValue; - maxSpeed?: VFXValue; -} - -export interface VFXOrbitOverLifeBehavior { - type: "OrbitOverLife"; - center?: { - x?: number; - y?: number; - z?: number; - }; - radius?: VFXValue | { keys?: VFXGradientKey[] }; - speed?: VFXValue; -} - -export type VFXBehavior = - | VFXColorOverLifeBehavior - | VFXSizeOverLifeBehavior - | VFXRotationOverLifeBehavior - | VFXForceOverLifeBehavior - | VFXGravityForceBehavior - | VFXSpeedOverLifeBehavior - | VFXFrameOverLifeBehavior - | VFXLimitSpeedOverLifeBehavior - | VFXColorBySpeedBehavior - | VFXSizeBySpeedBehavior - | VFXRotationBySpeedBehavior - | VFXOrbitOverLifeBehavior - | { type: string; [key: string]: unknown }; // Fallback for unknown behaviors diff --git a/editor/src/editor/windows/fx-editor/VFX/types/colors.ts b/editor/src/editor/windows/fx-editor/VFX/types/colors.ts deleted file mode 100644 index 02b189afe..000000000 --- a/editor/src/editor/windows/fx-editor/VFX/types/colors.ts +++ /dev/null @@ -1,41 +0,0 @@ -import type { VFXGradientKey } from "./gradients"; - -/** - * VFX color types (converted from Quarks) - */ -export interface VFXConstantColor { - type: "ConstantColor"; - value: [number, number, number, number]; // RGBA -} - -export interface VFXColorRange { - type: "ColorRange"; - colorA: [number, number, number, number]; // RGBA - colorB: [number, number, number, number]; // RGBA -} - -export interface VFXGradientColor { - type: "Gradient"; - colorKeys: VFXGradientKey[]; - alphaKeys?: VFXGradientKey[]; -} - -export interface VFXRandomColor { - type: "RandomColor"; - colorA: [number, number, number, number]; // RGBA - colorB: [number, number, number, number]; // RGBA -} - -export interface VFXRandomColorBetweenGradient { - type: "RandomColorBetweenGradient"; - gradient1: { - colorKeys: VFXGradientKey[]; - alphaKeys?: VFXGradientKey[]; - }; - gradient2: { - colorKeys: VFXGradientKey[]; - alphaKeys?: VFXGradientKey[]; - }; -} - -export type VFXColor = VFXConstantColor | VFXColorRange | VFXGradientColor | VFXRandomColor | VFXRandomColorBetweenGradient | [number, number, number, number] | string; diff --git a/editor/src/editor/windows/fx-editor/VFX/types/context.ts b/editor/src/editor/windows/fx-editor/VFX/types/context.ts deleted file mode 100644 index 22fc05f14..000000000 --- a/editor/src/editor/windows/fx-editor/VFX/types/context.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { Scene, TransformNode } from "babylonjs"; -import type { QuarksVFXJSON } from "./quarksTypes"; -import type { VFXData } from "./hierarchy"; -import type { VFXLoaderOptions } from "./loader"; - -/** - * Context for VFX parsing operations - */ -export interface VFXParseContext { - scene: Scene; - rootUrl: string; - jsonData: QuarksVFXJSON; - options: VFXLoaderOptions; - groupNodesMap: Map; - vfxData?: VFXData; -} diff --git a/editor/src/editor/windows/fx-editor/VFX/types/emitter.ts b/editor/src/editor/windows/fx-editor/VFX/types/emitter.ts deleted file mode 100644 index 5a187d9f2..000000000 --- a/editor/src/editor/windows/fx-editor/VFX/types/emitter.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { Nullable, TransformNode, Vector3 } from "babylonjs"; -import type { VFXParticleEmitterConfig } from "./emitterConfig"; -import type { VFXEmitter } from "./hierarchy"; - -/** - * Data structure for emitter creation - */ -export interface VFXEmitterData { - name: string; - config: VFXParticleEmitterConfig; - materialId?: string; - matrix?: number[]; - position?: number[]; - parentGroup: Nullable; - cumulativeScale: Vector3; - vfxEmitter?: VFXEmitter; -} diff --git a/editor/src/editor/windows/fx-editor/VFX/types/emitterConfig.ts b/editor/src/editor/windows/fx-editor/VFX/types/emitterConfig.ts deleted file mode 100644 index bd0325d9b..000000000 --- a/editor/src/editor/windows/fx-editor/VFX/types/emitterConfig.ts +++ /dev/null @@ -1,52 +0,0 @@ -import type { VFXValue } from "./values"; -import type { VFXColor } from "./colors"; -import type { VFXRotation } from "./rotations"; -import type { VFXShape } from "./shapes"; -import type { VFXBehavior } from "./behaviors"; - -/** - * VFX emission burst (converted from Quarks) - */ -export interface VFXEmissionBurst { - time: VFXValue; - count: VFXValue; -} - -/** - * VFX particle emitter configuration (converted from Quarks) - */ -export interface VFXParticleEmitterConfig { - version?: string; - autoDestroy?: boolean; - looping?: boolean; - prewarm?: boolean; - duration?: number; - shape?: VFXShape; - startLife?: VFXValue; - startSpeed?: VFXValue; - startRotation?: VFXRotation; - startSize?: VFXValue; - startColor?: VFXColor; - emissionOverTime?: VFXValue; - emissionOverDistance?: VFXValue; - emissionBursts?: VFXEmissionBurst[]; - onlyUsedByOther?: boolean; - instancingGeometry?: string; - renderOrder?: number; - systemType: "solid" | "base"; // Determined from renderMode: 2 = solid, otherwise base - rendererEmitterSettings?: Record; - material?: string; - layers?: number; - // Billboard settings (converted from renderMode) - isBillboardBased?: boolean; - billboardMode?: number; // ParticleSystem.BILLBOARDMODE_* - startTileIndex?: VFXValue; - uTileCount?: number; - vTileCount?: number; - blendTiles?: boolean; - softParticles?: boolean; - softFarFade?: number; - softNearFade?: number; - behaviors?: VFXBehavior[]; - worldSpace?: boolean; -} diff --git a/editor/src/editor/windows/fx-editor/VFX/types/emitters.ts b/editor/src/editor/windows/fx-editor/VFX/types/emitters.ts deleted file mode 100644 index 3ae7dba50..000000000 --- a/editor/src/editor/windows/fx-editor/VFX/types/emitters.ts +++ /dev/null @@ -1,92 +0,0 @@ -import { Vector3 } from "babylonjs"; -import type { SolidParticle } from "babylonjs"; - -/** - * Interface for SolidParticleSystem emitter types - * Similar to IParticleEmitterType for ParticleSystem - */ -export interface ISolidParticleEmitterType { - /** - * Initialize particle position and velocity based on emitter shape - */ - initializeParticle(particle: SolidParticle, startSpeed: number): void; -} - -/** - * Point emitter for SolidParticleSystem - */ -export class SolidPointParticleEmitter implements ISolidParticleEmitterType { - public initializeParticle(particle: SolidParticle, startSpeed: number): void { - const theta = Math.random() * Math.PI * 2; - const phi = Math.acos(2.0 * Math.random() - 1.0); - const direction = new Vector3(Math.sin(phi) * Math.cos(theta), Math.sin(phi) * Math.sin(theta), Math.cos(phi)); - particle.position.setAll(0); - particle.velocity.copyFrom(direction); - particle.velocity.scaleInPlace(startSpeed); - } -} - -/** - * Sphere emitter for SolidParticleSystem - */ -export class SolidSphereParticleEmitter implements ISolidParticleEmitterType { - public radius: number; - public arc: number; - public thickness: number; - - constructor(radius: number = 1, arc: number = Math.PI * 2, thickness: number = 1) { - this.radius = radius; - this.arc = arc; - this.thickness = thickness; - } - - public initializeParticle(particle: SolidParticle, startSpeed: number): void { - const u = Math.random(); - const v = Math.random(); - const rand = 1 - this.thickness + Math.random() * this.thickness; - const theta = u * this.arc; - const phi = Math.acos(2.0 * v - 1.0); - const sinTheta = Math.sin(theta); - const cosTheta = Math.cos(theta); - const sinPhi = Math.sin(phi); - const cosPhi = Math.cos(phi); - - particle.position.set(sinPhi * cosTheta, sinPhi * sinTheta, cosPhi); - particle.velocity.copyFrom(particle.position); - particle.velocity.scaleInPlace(startSpeed); - particle.position.scaleInPlace(this.radius * rand); - } -} - -/** - * Cone emitter for SolidParticleSystem - */ -export class SolidConeParticleEmitter implements ISolidParticleEmitterType { - public radius: number; - public arc: number; - public thickness: number; - public angle: number; - - constructor(radius: number = 1, arc: number = Math.PI * 2, thickness: number = 1, angle: number = Math.PI / 6) { - this.radius = radius; - this.arc = arc; - this.thickness = thickness; - this.angle = angle; - } - - public initializeParticle(particle: SolidParticle, startSpeed: number): void { - const u = Math.random(); - const rand = 1 - this.thickness + Math.random() * this.thickness; - const theta = u * this.arc; - const r = Math.sqrt(rand); - const sinTheta = Math.sin(theta); - const cosTheta = Math.cos(theta); - - particle.position.set(r * cosTheta, r * sinTheta, 0); - const coneAngle = this.angle * r; - particle.velocity.set(0, 0, Math.cos(coneAngle)); - particle.velocity.addInPlace(particle.position.scale(Math.sin(coneAngle))); - particle.velocity.scaleInPlace(startSpeed); - particle.position.scaleInPlace(this.radius); - } -} diff --git a/editor/src/editor/windows/fx-editor/VFX/types/hierarchy.ts b/editor/src/editor/windows/fx-editor/VFX/types/hierarchy.ts deleted file mode 100644 index 6627f4377..000000000 --- a/editor/src/editor/windows/fx-editor/VFX/types/hierarchy.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { Vector3, Quaternion } from "babylonjs"; -import type { VFXParticleEmitterConfig } from "./emitterConfig"; -import type { VFXMaterial, VFXTexture, VFXImage, VFXGeometry } from "./resources"; - -/** - * VFX transform (converted from Quarks, left-handed coordinate system) - */ -export interface VFXTransform { - position: Vector3; - rotation: Quaternion; - scale: Vector3; -} - -/** - * VFX group (converted from Quarks) - */ -export interface VFXGroup { - uuid: string; - name: string; - transform: VFXTransform; - children: (VFXGroup | VFXEmitter)[]; -} - -/** - * VFX emitter (converted from Quarks) - */ -export interface VFXEmitter { - uuid: string; - name: string; - transform: VFXTransform; - config: VFXParticleEmitterConfig; - materialId?: string; - parentUuid?: string; - systemType: "solid" | "base"; // Determined from renderMode: 2 = solid, otherwise base - matrix?: number[]; // Original Three.js matrix array for rotation extraction -} - -/** - * VFX data (converted from Quarks) - * Contains the converted VFX structure with groups, emitters, and resources - */ -export interface VFXData { - root: VFXGroup | VFXEmitter | null; - groups: Map; - emitters: Map; - // Resources (converted from Quarks, ready for Babylon.js) - materials: VFXMaterial[]; - textures: VFXTexture[]; - images: VFXImage[]; - geometries: VFXGeometry[]; -} diff --git a/editor/src/editor/windows/fx-editor/VFX/types/index.ts b/editor/src/editor/windows/fx-editor/VFX/types/index.ts deleted file mode 100644 index 21de3ed58..000000000 --- a/editor/src/editor/windows/fx-editor/VFX/types/index.ts +++ /dev/null @@ -1,46 +0,0 @@ -/** - * VFX Types - Centralized type definitions - * - * This module exports all VFX-related types organized by category. - * Import types directly from their specific modules for better tree-shaking. - */ - -// Loader types -export type { VFXLoaderOptions } from "./loader"; - -// Emitter types -export type { VFXEmitterData } from "./emitter"; - -// Factory interfaces -export type { IVFXMaterialFactory, IVFXGeometryFactory } from "./factories"; - -// Core VFX types -export type { VFXConstantValue, VFXIntervalValue, VFXValue } from "./values"; -export type { VFXConstantColor, VFXColor } from "./colors"; -export type { VFXEulerRotation, VFXRotation } from "./rotations"; -export type { VFXGradientKey } from "./gradients"; -export type { VFXShape } from "./shapes"; -export type { - VFXColorOverLifeBehavior, - VFXSizeOverLifeBehavior, - VFXRotationOverLifeBehavior, - VFXForceOverLifeBehavior, - VFXGravityForceBehavior, - VFXSpeedOverLifeBehavior, - VFXFrameOverLifeBehavior, - VFXLimitSpeedOverLifeBehavior, - VFXColorBySpeedBehavior, - VFXSizeBySpeedBehavior, - VFXRotationBySpeedBehavior, - VFXOrbitOverLifeBehavior, - VFXBehavior, -} from "./behaviors"; -export type { VFXEmissionBurst, VFXParticleEmitterConfig } from "./emitterConfig"; -export type { VFXTransform, VFXGroup, VFXEmitter, VFXData } from "./hierarchy"; -export type { VFXMaterial, VFXTexture, VFXImage, VFXGeometry } from "./resources"; -export type { ISolidParticleEmitterType } from "./emitters"; -export { SolidPointParticleEmitter, SolidSphereParticleEmitter, SolidConeParticleEmitter } from "./emitters"; -export type { QuarksVFXJSON } from "./quarksTypes"; -export type { VFXPerParticleBehaviorFunction, VFXPerSolidParticleBehaviorFunction, VFXSystemBehaviorFunction } from "./VFXBehaviorFunction"; -export type { IVFXSystem, ParticleWithSystem, SolidParticleWithSystem } from "./system"; -export { isVFXSystem } from "./system"; diff --git a/editor/src/editor/windows/fx-editor/VFX/types/rotations.ts b/editor/src/editor/windows/fx-editor/VFX/types/rotations.ts deleted file mode 100644 index 2eca7d7e7..000000000 --- a/editor/src/editor/windows/fx-editor/VFX/types/rotations.ts +++ /dev/null @@ -1,26 +0,0 @@ -import type { VFXValue } from "./values"; - -/** - * VFX rotation types (converted from Quarks) - */ -export interface VFXEulerRotation { - type: "Euler"; - angleX?: VFXValue; - angleY?: VFXValue; - angleZ?: VFXValue; - order?: "xyz" | "zyx"; -} - -export interface VFXAxisAngleRotation { - type: "AxisAngle"; - x?: VFXValue; - y?: VFXValue; - z?: VFXValue; - angle?: VFXValue; -} - -export interface VFXRandomQuatRotation { - type: "RandomQuat"; -} - -export type VFXRotation = VFXEulerRotation | VFXAxisAngleRotation | VFXRandomQuatRotation | VFXValue; diff --git a/editor/src/editor/windows/fx-editor/animation.tsx b/editor/src/editor/windows/fx-editor/animation.tsx index dfd62986b..8724ee478 100644 --- a/editor/src/editor/windows/fx-editor/animation.tsx +++ b/editor/src/editor/windows/fx-editor/animation.tsx @@ -1,12 +1,12 @@ import { Component, ReactNode } from "react"; -import { IFXEditor } from "."; +import { IEffectEditor } from "."; -export interface IFXEditorAnimationProps { +export interface IEffectEditorAnimationProps { filePath: string | null; - editor: IFXEditor; + editor: IEffectEditor; } -export class FXEditorAnimation extends Component { +export class EffectEditorAnimation extends Component { public render(): ReactNode { return (
diff --git a/editor/src/editor/windows/fx-editor/graph.tsx b/editor/src/editor/windows/fx-editor/graph.tsx index 57381e778..7eefaedd7 100644 --- a/editor/src/editor/windows/fx-editor/graph.tsx +++ b/editor/src/editor/windows/fx-editor/graph.tsx @@ -15,36 +15,36 @@ import { ContextMenuSubTrigger, ContextMenuSubContent, } from "../../../ui/shadcn/ui/context-menu"; -import { IFXEditor } from "."; -import { VFXEffect, type VFXEffectNode } from "./VFX"; +import { IEffectEditor } from "."; import { saveSingleFileDialog } from "../../../tools/dialog"; import { writeJSON } from "fs-extra"; import { toast } from "sonner"; +import { Effect, type EffectNode } from "babylonjs-editor-tools"; -export interface IFXEditorGraphProps { +export interface IEffectEditorGraphProps { filePath: string | null; onNodeSelected?: (nodeId: string | number | null) => void; - editor: IFXEditor; + editor: IEffectEditor; } -export interface IFXEditorGraphState { - nodes: TreeNodeInfo[]; +export interface IEffectEditorGraphState { + nodes: TreeNodeInfo[]; selectedNodeId: string | number | null; } interface EffectInfo { id: string; name: string; - effect: VFXEffect; + effect: Effect; originalJsonData?: any; // Store original JSON data for export } -export class FXEditorGraph extends Component { - private _vfxEffects: Map = new Map(); +export class EffectEditorGraph extends Component { + private _effects: Map = new Map(); /** Map of node instances to unique IDs for tree nodes */ - private _nodeIdMap: Map = new Map(); + private _nodeIdMap: Map = new Map(); - public constructor(props: IFXEditorGraphProps) { + public constructor(props: IEffectEditorGraphProps) { super(props); this.state = { @@ -54,32 +54,32 @@ export class FXEditorGraph extends Component info.effect); + public getAllEffects(): Effect[] { + return Array.from(this._effects.values()).map((info) => info.effect); } /** * Get effect by ID */ - public getEffectById(id: string): VFXEffect | null { - const info = this._vfxEffects.get(id); + public getEffectById(id: string): Effect | null { + const info = this._effects.get(id); return info ? info.effect : null; } /** * Finds a node in the tree by ID */ - private _findNodeById(nodes: TreeNodeInfo[], nodeId: string | number): TreeNodeInfo | null { + private _findNodeById(nodes: TreeNodeInfo[], nodeId: string | number): TreeNodeInfo | null { for (const node of nodes) { if (node.id === nodeId) { return node; @@ -97,17 +97,17 @@ export class FXEditorGraph extends Component { try { @@ -116,21 +116,21 @@ export class FXEditorGraph extends Component[] = []; + const nodes: TreeNodeInfo[] = []; - for (const [effectId, effectInfo] of this._vfxEffects.entries()) { + for (const [effectId, effectInfo] of this._effects.entries()) { if (effectInfo.effect.root) { // Use effect root directly as the tree node, but update its name to effect name effectInfo.effect.root.name = effectInfo.name; effectInfo.effect.root.uuid = effectId; - const treeNode = this._convertVFXNodeToTreeNode(effectInfo.effect.root, true); + const treeNode = this._convertNodeToTreeNode(effectInfo.effect.root, true); nodes.push(treeNode); } } @@ -181,41 +181,41 @@ export class FXEditorGraph extends Component { + private _convertNodeToTreeNode(Node: EffectNode, isEffectRoot: boolean = false): TreeNodeInfo { // Always use unique ID instead of uuid or name - const nodeId = this._generateUniqueNodeId(vfxNode); - const childNodes = vfxNode.children.length > 0 ? vfxNode.children.map((child) => this._convertVFXNodeToTreeNode(child, false)) : undefined; + const nodeId = this._generateUniqueNodeId(Node); + const childNodes = Node.children.length > 0 ? Node.children.map((child) => this._convertNodeToTreeNode(child, false)) : undefined; return { id: nodeId, - label: this._getNodeLabelComponent({ id: nodeId, nodeData: vfxNode } as any, vfxNode.name), + label: this._getNodeLabelComponent({ id: nodeId, nodeData: Node } as any, Node.name), icon: isEffectRoot ? ( - ) : vfxNode.type === "particle" ? ( + ) : Node.type === "particle" ? ( ) : ( ), - isExpanded: isEffectRoot || vfxNode.type === "group", + isExpanded: isEffectRoot || Node.type === "group", childNodes, isSelected: false, - hasCaret: isEffectRoot || vfxNode.type === "group" || (childNodes && childNodes.length > 0), - nodeData: vfxNode, + hasCaret: isEffectRoot || Node.type === "group" || (childNodes && childNodes.length > 0), + nodeData: Node, }; } @@ -230,7 +230,7 @@ export class FXEditorGraph extends Component[]): TreeNodeInfo[] { + private _updateAllNodeNames(nodes: TreeNodeInfo[]): TreeNodeInfo[] { return nodes.map((n) => { const nodeName = n.nodeData?.name || "Unknown"; const childNodes = n.childNodes ? this._updateAllNodeNames(n.childNodes) : undefined; @@ -280,7 +280,7 @@ export class FXEditorGraph extends Component { - ev.dataTransfer.setData("fx-editor/create-effect", "effect"); + ev.dataTransfer.setData("Effect-editor/create-effect", "effect"); }} onClick={() => this._handleCreateEffect()} > @@ -295,19 +295,19 @@ export class FXEditorGraph extends Component): void { + private _handleNodeExpanded(node: TreeNodeInfo): void { const nodeId = node.id; const nodes = this._updateNodeExpanded(this.state.nodes, nodeId as string | number, true); this.setState({ nodes }); } - private _handleNodeCollapsed(node: TreeNodeInfo): void { + private _handleNodeCollapsed(node: TreeNodeInfo): void { const nodeId = node.id; const nodes = this._updateNodeExpanded(this.state.nodes, nodeId as string | number, false); this.setState({ nodes }); } - private _updateNodeExpanded(nodes: TreeNodeInfo[], nodeId: string | number, isExpanded: boolean): TreeNodeInfo[] { + private _updateNodeExpanded(nodes: TreeNodeInfo[], nodeId: string | number, isExpanded: boolean): TreeNodeInfo[] { return nodes.map((n) => { const nodeName = n.nodeData?.name || "Unknown"; if (n.id === nodeId) { @@ -327,14 +327,14 @@ export class FXEditorGraph extends Component): void { + private _handleNodeClicked(node: TreeNodeInfo): void { const selectedId = node.id as string | number; const nodes = this._updateNodeSelection(this.state.nodes, selectedId); this.setState({ nodes, selectedNodeId: selectedId }); this.props.onNodeSelected?.(selectedId); } - private _updateNodeSelection(nodes: TreeNodeInfo[], selectedId: string | number): TreeNodeInfo[] { + private _updateNodeSelection(nodes: TreeNodeInfo[], selectedId: string | number): TreeNodeInfo[] { return nodes.map((n) => { const nodeName = n.nodeData?.name || "Unknown"; const isSelected = n.id === selectedId; @@ -348,7 +348,7 @@ export class FXEditorGraph extends Component, name: string): JSX.Element { + private _getNodeLabelComponent(node: TreeNodeInfo, name: string): JSX.Element { const label = (
{ - ev.dataTransfer.setData("fx-editor/create-item", "base-particle"); + ev.dataTransfer.setData("Effect-editor/create-item", "base-particle"); }} onClick={() => this._handleAddParticleSystemToNode(node, "base")} > @@ -394,7 +394,7 @@ export class FXEditorGraph extends Component { - ev.dataTransfer.setData("fx-editor/create-item", "solid-particle"); + ev.dataTransfer.setData("Effect-editor/create-item", "solid-particle"); }} onClick={() => this._handleAddParticleSystemToNode(node, "solid")} > @@ -403,7 +403,7 @@ export class FXEditorGraph extends Component { - ev.dataTransfer.setData("fx-editor/create-item", "group"); + ev.dataTransfer.setData("Effect-editor/create-item", "group"); }} onClick={() => this._handleAddGroupToNode(node)} > @@ -431,26 +431,26 @@ export class FXEditorGraph extends Component): boolean { + private _isEffectRootNode(node: TreeNodeInfo): boolean { const nodeData = node.nodeData; if (!nodeData || !nodeData.uuid) { return false; } // Check if this node is the root of any effect - return this._vfxEffects.has(nodeData.uuid); + return this._effects.has(nodeData.uuid); } /** * Export effect to JSON file */ - private async _handleExportEffect(node: TreeNodeInfo): Promise { + private async _handleExportEffect(node: TreeNodeInfo): Promise { const nodeData = node.nodeData; if (!nodeData || !nodeData.uuid) { return; } - const effectInfo = this._vfxEffects.get(nodeData.uuid); + const effectInfo = this._effects.get(nodeData.uuid); if (!effectInfo || !effectInfo.originalJsonData) { toast.error("Cannot export effect: original data not available"); return; @@ -486,29 +486,29 @@ export class FXEditorGraph extends Component info.name === effectName)) { + while (Array.from(this._effects.values()).some((info) => info.name === effectName)) { effectName = `Effect ${counter}`; counter++; } // Store effect - this._vfxEffects.set(effectId, { + this._effects.set(effectId, { id: effectId, name: effectName, - effect: vfxEffect, + effect: effect, }); // Rebuild tree with all effects this._rebuildTree(); } - private _findEffectForNode(node: TreeNodeInfo): VFXEffect | null { + private _findEffectForNode(node: TreeNodeInfo): Effect | null { // Find the effect that contains this node by traversing up the tree const nodeData = node.nodeData; if (!nodeData) { @@ -517,18 +517,18 @@ export class FXEditorGraph extends Component { + const findNodeInHierarchy = (current: EffectNode): boolean => { // Use instance comparison and uuid for matching if (current === nodeData || (current.uuid && nodeData.uuid && current.uuid === nodeData.uuid)) { return true; @@ -550,7 +550,7 @@ export class FXEditorGraph extends Component, systemType: "solid" | "base"): void { + private _handleAddParticleSystemToNode(node: TreeNodeInfo, systemType: "solid" | "base"): void { const effect = this._findEffectForNode(node); if (!effect) { console.error("No effect found for node"); @@ -570,7 +570,7 @@ export class FXEditorGraph extends Component): void { + private _handleAddGroupToNode(node: TreeNodeInfo): void { const effect = this._findEffectForNode(node); if (!effect) { console.error("No effect found for node"); @@ -595,7 +595,7 @@ export class FXEditorGraph extends Component, ev: React.DragEvent): void { + private _handleDropOnNode(node: TreeNodeInfo, ev: React.DragEvent): void { ev.preventDefault(); ev.stopPropagation(); @@ -613,7 +613,7 @@ export class FXEditorGraph extends Component[], parentId: string | number, newNode: TreeNodeInfo): TreeNodeInfo[] { + private _addNodeToParent(nodes: TreeNodeInfo[], parentId: string | number, newNode: TreeNodeInfo): TreeNodeInfo[] { return nodes.map((n) => { if (n.id === parentId) { const childNodes = n.childNodes || []; @@ -647,7 +647,7 @@ export class FXEditorGraph extends Component): void { + private _handleDeleteNode(node: TreeNodeInfo): void { const nodeData = node.nodeData; if (!nodeData) { return; @@ -655,12 +655,12 @@ export class FXEditorGraph extends Component { + const removeNodeFromHierarchy = (current: EffectNode): boolean => { // Remove from children const index = current.children.findIndex((child) => child === nodeData || child.uuid === nodeData.uuid || child.name === nodeData.name); if (index !== -1) { diff --git a/editor/src/editor/windows/fx-editor/index.tsx b/editor/src/editor/windows/fx-editor/index.tsx index 172b28d97..039c5fd2d 100644 --- a/editor/src/editor/windows/fx-editor/index.tsx +++ b/editor/src/editor/windows/fx-editor/index.tsx @@ -7,35 +7,35 @@ import { Component, ReactNode } from "react"; import { Toaster } from "../../../ui/shadcn/ui/sonner"; -import { FXEditorLayout } from "./layout"; -import { FXEditorToolbar } from "./toolbar"; +import { EffectEditorLayout } from "./layout"; +import { EffectEditorToolbar } from "./toolbar"; import { projectConfiguration, onProjectConfigurationChangedObservable, IProjectConfiguration } from "../../../project/configuration"; -import { FXEditorAnimation } from "./animation"; -import { FXEditorGraph } from "./graph"; -import { FXEditorPreview } from "./preview"; -import { FXEditorProperties } from "./properties"; -import { FXEditorResources } from "./resources"; +import { EffectEditorAnimation } from "./animation"; +import { EffectEditorGraph } from "./graph"; +import { EffectEditorPreview } from "./preview"; +import { EffectEditorProperties } from "./properties"; +import { EffectEditorResources } from "./resources"; -export interface IFXEditorWindowProps { +export interface IEffectEditorWindowProps { filePath?: string; projectConfiguration?: IProjectConfiguration; } -export interface IFXEditorWindowState { +export interface IEffectEditorWindowState { filePath: string | null; } -export interface IFXEditor { - layout: FXEditorLayout | null; - preview: FXEditorPreview | null; - graph: FXEditorGraph | null; - animation: FXEditorAnimation | null; - properties: FXEditorProperties | null; - resources: FXEditorResources | null; +export interface IEffectEditor { + layout: EffectEditorLayout | null; + preview: EffectEditorPreview | null; + graph: EffectEditorGraph | null; + animation: EffectEditorAnimation | null; + properties: EffectEditorProperties | null; + resources: EffectEditorResources | null; } -export default class FXEditorWindow extends Component { - public editor: IFXEditor = { +export default class EffectEditorWindow extends Component { + public editor: IEffectEditor = { layout: null, preview: null, graph: null, @@ -44,7 +44,7 @@ export default class FXEditorWindow extends Component
- +
- (this.editor.layout = r)} filePath={this.state.filePath || ""} editor={this.editor} /> + (this.editor.layout = r)} filePath={this.state.filePath || ""} editor={this.editor} />
@@ -97,10 +97,10 @@ export default class FXEditorWindow extends Component { +export class EffectEditorLayout extends Component { private _model: Model = Model.fromJson(layoutModel as any); private _components: Record = {}; - public constructor(props: IFXEditorLayoutProps) { + public constructor(props: IEffectEditorLayoutProps) { super(props); this.state = { @@ -200,7 +200,7 @@ export class FXEditorLayout extends Component (this.props.editor.preview = r!)} filePath={this.props.filePath} editor={this.props.editor} @@ -214,7 +214,7 @@ export class FXEditorLayout extends Component ), graph: ( - (this.props.editor.graph = r!)} filePath={this.props.filePath} onNodeSelected={this._handleNodeSelected} @@ -224,10 +224,10 @@ export class FXEditorLayout extends Component ), - resources: (this.props.editor.resources = r!)} resources={this.state.resources} />, - animation: (this.props.editor.animation = r!)} filePath={this.props.filePath} editor={this.props.editor} />, + resources: (this.props.editor.resources = r!)} resources={this.state.resources} />, + animation: (this.props.editor.animation = r!)} filePath={this.props.filePath} editor={this.props.editor} />, "properties-object": ( - ), "properties-emitter": ( - ), "properties-renderer": ( - ), "properties-emission": ( - ), "properties-initialization": ( - ), "properties-behaviors": ( - void; - editor?: IFXEditor; + editor?: IEffectEditor; selectedNodeId?: string | number | null; } -export interface IFXEditorPreviewState { +export interface IEffectEditorPreviewState { playing: boolean; } -export class FXEditorPreview extends Component { +export class EffectEditorPreview extends Component { public engine: Engine | null = null; public scene: Scene | null = null; public camera: ArcRotateCamera | null = null; @@ -28,7 +28,7 @@ export class FXEditorPreview extends Component { + const findNode = (current: EffectNode): boolean => { if (current === node || current.uuid === node.uuid || current.name === node.name) { return true; } @@ -160,7 +160,7 @@ export class FXEditorPreview extends Component void; - getNodeData: (nodeId: string | number) => VFXEffectNode | null; + getNodeData: (nodeId: string | number) => EffectNode | null; } -export interface IFXEditorPropertiesState {} +export interface IEffectEditorPropertiesState {} -export class FXEditorProperties extends Component { - public constructor(props: IFXEditorPropertiesProps) { +export class EffectEditorProperties extends Component { + public constructor(props: IEffectEditorPropertiesProps) { super(props); this.state = {}; } - public componentDidUpdate(prevProps: IFXEditorPropertiesProps): void { + public componentDidUpdate(prevProps: IEffectEditorPropertiesProps): void { // Force update when selectedNodeId changes to ensure we show the correct node's properties if (prevProps.selectedNodeId !== this.props.selectedNodeId) { // Use setTimeout to ensure the update happens after flexlayout-react processes the change @@ -77,7 +77,7 @@ export class FXEditorProperties extends Component - { this.forceUpdate(); @@ -117,7 +117,7 @@ export class FXEditorProperties extends Component - { this.forceUpdate(); @@ -127,23 +127,23 @@ export class FXEditorProperties extends Component - this.forceUpdate()} /> + this.forceUpdate()} /> - this.forceUpdate()} /> + this.forceUpdate()} /> - this.forceUpdate()} /> + this.forceUpdate()} /> - this.forceUpdate()} /> + this.forceUpdate()} /> - this.forceUpdate()} /> + this.forceUpdate()} />
diff --git a/editor/src/editor/windows/fx-editor/properties/behaviors-properties.tsx b/editor/src/editor/windows/fx-editor/properties/behaviors-properties.tsx index 31e03e75e..9b49edd35 100644 --- a/editor/src/editor/windows/fx-editor/properties/behaviors-properties.tsx +++ b/editor/src/editor/windows/fx-editor/properties/behaviors-properties.tsx @@ -1,15 +1,14 @@ import { Component, ReactNode } from "react"; +import type { EffectNode } from "babylonjs-editor-tools"; +import { EffectEditorBehaviorsProperties } from "./behaviors"; -import { FXEditorBehaviorsProperties } from "./behaviors"; -import type { VFXEffectNode } from "../VFX"; - -export interface IFXEditorBehaviorsPropertiesTabProps { +export interface IEffectEditorBehaviorsPropertiesTabProps { filePath: string | null; selectedNodeId: string | number | null; - getNodeData: (nodeId: string | number) => VFXEffectNode | null; + getNodeData: (nodeId: string | number) => EffectNode | null; } -export class FXEditorBehaviorsPropertiesTab extends Component { +export class EffectEditorBehaviorsPropertiesTab extends Component { public render(): ReactNode { const nodeId = this.props.selectedNodeId; @@ -33,9 +32,8 @@ export class FXEditorBehaviorsPropertiesTab extends Component - this.forceUpdate()} /> + this.forceUpdate()} />
); } } - diff --git a/editor/src/editor/windows/fx-editor/properties/behaviors.tsx b/editor/src/editor/windows/fx-editor/properties/behaviors.tsx index ebe39a317..f6c4aadcc 100644 --- a/editor/src/editor/windows/fx-editor/properties/behaviors.tsx +++ b/editor/src/editor/windows/fx-editor/properties/behaviors.tsx @@ -7,17 +7,16 @@ import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigge import { HiOutlineTrash } from "react-icons/hi2"; import { IoAddSharp } from "react-icons/io5"; -import type { VFXEffectNode } from "../VFX"; -import { VFXParticleSystem, VFXSolidParticleSystem } from "../VFX"; +import { type EffectNode, EffectParticleSystem, EffectSolidParticleSystem } from "babylonjs-editor-tools"; import { BehaviorRegistry, createDefaultBehaviorData, getBehaviorDefinition } from "./behaviors/registry"; import { BehaviorProperties } from "./behaviors/behavior-properties"; -export interface IFXEditorBehaviorsPropertiesProps { - nodeData: VFXEffectNode; +export interface IEffectEditorBehaviorsPropertiesProps { + nodeData: EffectNode; onChange: () => void; } -export function FXEditorBehaviorsProperties(props: IFXEditorBehaviorsPropertiesProps): ReactNode { +export function EffectEditorBehaviorsProperties(props: IEffectEditorBehaviorsPropertiesProps): ReactNode { const { nodeData, onChange } = props; if (nodeData.type !== "particle" || !nodeData.system) { @@ -28,9 +27,9 @@ export function FXEditorBehaviorsProperties(props: IFXEditorBehaviorsPropertiesP // Get behavior configurations from system let behaviorConfigs: any[] = []; - if (system instanceof VFXParticleSystem) { + if (system instanceof EffectParticleSystem) { behaviorConfigs = system.behaviorConfigs || []; - } else if (system instanceof VFXSolidParticleSystem) { + } else if (system instanceof EffectSolidParticleSystem) { behaviorConfigs = system.behaviorConfigs || []; } diff --git a/editor/src/editor/windows/fx-editor/properties/behaviors/color-function-editor.tsx b/editor/src/editor/windows/fx-editor/properties/behaviors/color-function-editor.tsx index 4d3f2d8eb..fbd425c8c 100644 --- a/editor/src/editor/windows/fx-editor/properties/behaviors/color-function-editor.tsx +++ b/editor/src/editor/windows/fx-editor/properties/behaviors/color-function-editor.tsx @@ -5,7 +5,6 @@ import { EditorInspectorColorField } from "../../../../layout/inspector/fields/c import { EditorInspectorColorGradientField } from "../../../../layout/inspector/fields/gradient"; import { EditorInspectorListField } from "../../../../layout/inspector/fields/list"; import { EditorInspectorBlockField } from "../../../../layout/inspector/fields/block"; -import { EditorInspectorNumberField } from "../../../../layout/inspector/fields/number"; import type { IGradientKey } from "../../../../../ui/gradient-picker"; export type ColorFunctionType = "ConstantColor" | "ColorRange" | "Gradient" | "RandomColor" | "RandomColorBetweenGradient"; @@ -110,62 +109,67 @@ export function ColorFunctionEditor(props: IColorFunctionEditorProps): ReactNode )} - {functionType === "Gradient" && (() => { - // Convert old format (Vector3 + position) to new format (array + pos) if needed - const convertColorKeys = (keys: any[]): IGradientKey[] => { - if (!keys || keys.length === 0) { - return [ - { pos: 0, value: [0, 0, 0, 1] }, - { pos: 1, value: [1, 1, 1, 1] }, - ]; - } - return keys.map((key) => { - if (key.color && key.color instanceof Vector3) { - // Old format: { color: Vector3, position: number } + {functionType === "Gradient" && + (() => { + // Convert old format (Vector3 + position) to new format (array + pos) if needed + const convertColorKeys = (keys: any[]): IGradientKey[] => { + if (!keys || keys.length === 0) { + return [ + { pos: 0, value: [0, 0, 0, 1] }, + { pos: 1, value: [1, 1, 1, 1] }, + ]; + } + return keys.map((key) => { + if (key.color && key.color instanceof Vector3) { + // Old format: { color: Vector3, position: number } + return { + pos: key.position ?? key.pos ?? 0, + value: [key.color.x, key.color.y, key.color.z, 1], + }; + } + // Already in new format or other format return { - pos: key.position ?? key.pos ?? 0, - value: [key.color.x, key.color.y, key.color.z, 1], + pos: key.pos ?? key.position ?? 0, + value: Array.isArray(key.value) + ? key.value + : typeof key.value === "object" && "r" in key.value + ? [key.value.r, key.value.g, key.value.b, key.value.a ?? 1] + : [0, 0, 0, 1], }; + }); + }; + + const convertAlphaKeys = (keys: any[]): IGradientKey[] => { + if (!keys || keys.length === 0) { + return [ + { pos: 0, value: 1 }, + { pos: 1, value: 1 }, + ]; } - // Already in new format or other format - return { + return keys.map((key) => ({ pos: key.pos ?? key.position ?? 0, - value: Array.isArray(key.value) ? key.value : typeof key.value === "object" && "r" in key.value ? [key.value.r, key.value.g, key.value.b, key.value.a ?? 1] : [0, 0, 0, 1], - }; - }); - }; - - const convertAlphaKeys = (keys: any[]): IGradientKey[] => { - if (!keys || keys.length === 0) { - return [ - { pos: 0, value: 1 }, - { pos: 1, value: 1 }, - ]; - } - return keys.map((key) => ({ - pos: key.pos ?? key.position ?? 0, - value: typeof key.value === "number" ? key.value : 1, - })); - }; + value: typeof key.value === "number" ? key.value : 1, + })); + }; - const wrapperGradient = { - colorKeys: convertColorKeys(value.data.colorKeys), - alphaKeys: convertAlphaKeys(value.data.alphaKeys), - }; + const wrapperGradient = { + colorKeys: convertColorKeys(value.data.colorKeys), + alphaKeys: convertAlphaKeys(value.data.alphaKeys), + }; - return ( - { - value.data.colorKeys = newColorKeys; - value.data.alphaKeys = newAlphaKeys; - onChange(); - }} - /> - ); - })()} + return ( + { + value.data.colorKeys = newColorKeys; + value.data.alphaKeys = newAlphaKeys; + onChange(); + }} + /> + ); + })()} {functionType === "RandomColor" && ( <> @@ -176,87 +180,91 @@ export function ColorFunctionEditor(props: IColorFunctionEditorProps): ReactNode )} - {functionType === "RandomColorBetweenGradient" && (() => { - // Convert old format to new format if needed - const convertColorKeys = (keys: any[]): IGradientKey[] => { - if (!keys || keys.length === 0) { - return [ - { pos: 0, value: [0, 0, 0, 1] }, - { pos: 1, value: [1, 1, 1, 1] }, - ]; - } - return keys.map((key) => { - if (key.color && key.color instanceof Vector3) { + {functionType === "RandomColorBetweenGradient" && + (() => { + // Convert old format to new format if needed + const convertColorKeys = (keys: any[]): IGradientKey[] => { + if (!keys || keys.length === 0) { + return [ + { pos: 0, value: [0, 0, 0, 1] }, + { pos: 1, value: [1, 1, 1, 1] }, + ]; + } + return keys.map((key) => { + if (key.color && key.color instanceof Vector3) { + return { + pos: key.position ?? key.pos ?? 0, + value: [key.color.x, key.color.y, key.color.z, 1], + }; + } return { - pos: key.position ?? key.pos ?? 0, - value: [key.color.x, key.color.y, key.color.z, 1], + pos: key.pos ?? key.position ?? 0, + value: Array.isArray(key.value) + ? key.value + : typeof key.value === "object" && "r" in key.value + ? [key.value.r, key.value.g, key.value.b, key.value.a ?? 1] + : [0, 0, 0, 1], }; + }); + }; + + const convertAlphaKeys = (keys: any[]): IGradientKey[] => { + if (!keys || keys.length === 0) { + return [ + { pos: 0, value: 1 }, + { pos: 1, value: 1 }, + ]; } - return { + return keys.map((key) => ({ pos: key.pos ?? key.position ?? 0, - value: Array.isArray(key.value) ? key.value : typeof key.value === "object" && "r" in key.value ? [key.value.r, key.value.g, key.value.b, key.value.a ?? 1] : [0, 0, 0, 1], - }; - }); - }; - - const convertAlphaKeys = (keys: any[]): IGradientKey[] => { - if (!keys || keys.length === 0) { - return [ - { pos: 0, value: 1 }, - { pos: 1, value: 1 }, - ]; - } - return keys.map((key) => ({ - pos: key.pos ?? key.position ?? 0, - value: typeof key.value === "number" ? key.value : 1, - })); - }; + value: typeof key.value === "number" ? key.value : 1, + })); + }; - if (!value.data.gradient1) value.data.gradient1 = {}; - if (!value.data.gradient2) value.data.gradient2 = {}; + if (!value.data.gradient1) value.data.gradient1 = {}; + if (!value.data.gradient2) value.data.gradient2 = {}; - const wrapperGradient1 = { - colorKeys: convertColorKeys(value.data.gradient1.colorKeys), - alphaKeys: convertAlphaKeys(value.data.gradient1.alphaKeys), - }; + const wrapperGradient1 = { + colorKeys: convertColorKeys(value.data.gradient1.colorKeys), + alphaKeys: convertAlphaKeys(value.data.gradient1.alphaKeys), + }; - const wrapperGradient2 = { - colorKeys: convertColorKeys(value.data.gradient2.colorKeys), - alphaKeys: convertAlphaKeys(value.data.gradient2.alphaKeys), - }; + const wrapperGradient2 = { + colorKeys: convertColorKeys(value.data.gradient2.colorKeys), + alphaKeys: convertAlphaKeys(value.data.gradient2.alphaKeys), + }; - return ( - <> - -
Gradient 1
- { - value.data.gradient1.colorKeys = newColorKeys; - value.data.gradient1.alphaKeys = newAlphaKeys; - onChange(); - }} - /> -
- -
Gradient 2
- { - value.data.gradient2.colorKeys = newColorKeys; - value.data.gradient2.alphaKeys = newAlphaKeys; - onChange(); - }} - /> -
- - ); - })()} + return ( + <> + +
Gradient 1
+ { + value.data.gradient1.colorKeys = newColorKeys; + value.data.gradient1.alphaKeys = newAlphaKeys; + onChange(); + }} + /> +
+ +
Gradient 2
+ { + value.data.gradient2.colorKeys = newColorKeys; + value.data.gradient2.alphaKeys = newAlphaKeys; + onChange(); + }} + /> +
+ + ); + })()} ); } - diff --git a/editor/src/editor/windows/fx-editor/properties/vfx-color-editor.tsx b/editor/src/editor/windows/fx-editor/properties/color-editor.tsx similarity index 58% rename from editor/src/editor/windows/fx-editor/properties/vfx-color-editor.tsx rename to editor/src/editor/windows/fx-editor/properties/color-editor.tsx index 715e3bef6..e5229259a 100644 --- a/editor/src/editor/windows/fx-editor/properties/vfx-color-editor.tsx +++ b/editor/src/editor/windows/fx-editor/properties/color-editor.tsx @@ -6,26 +6,25 @@ import { EditorInspectorColorGradientField } from "../../../layout/inspector/fie import { EditorInspectorListField } from "../../../layout/inspector/fields/list"; import { EditorInspectorBlockField } from "../../../layout/inspector/fields/block"; -import type { VFXColor, VFXConstantColor, VFXColorRange, VFXGradientColor, VFXRandomColor, VFXRandomColorBetweenGradient } from "../VFX/types/colors"; -import { VFXValueUtils } from "../VFX/utils/valueParser"; +import { type Color, ValueUtils } from "babylonjs-editor-tools"; -export type VFXColorType = "ConstantColor" | "ColorRange" | "Gradient" | "RandomColor" | "RandomColorBetweenGradient"; +export type EffectColorType = "ConstantColor" | "ColorRange" | "Gradient" | "RandomColor" | "RandomColorBetweenGradient"; -export interface IVFXColorEditorProps { - value: VFXColor | undefined; - onChange: (newValue: VFXColor) => void; +export interface IEffectColorEditorProps { + value: Color | undefined; + onChange: (newValue: Color) => void; label?: string; } /** - * Editor for VFXColor (ConstantColor, ColorRange, Gradient, RandomColor, RandomColorBetweenGradient) - * Works directly with VFXColor types, not wrappers + * Editor for VEffectColor (ConstantColor, ColorRange, Gradient, RandomColor, RandomColorBetweenGradient) + * Works directly with VEffectColor types, not wrappers */ -export function VFXColorEditor(props: IVFXColorEditorProps): ReactNode { +export function EffectColorEditor(props: IEffectColorEditorProps): ReactNode { const { value, onChange, label } = props; // Determine current type from value - let currentType: VFXColorType = "ConstantColor"; + let currentType: EffectColorType = "ConstantColor"; if (value) { if (typeof value === "string" || Array.isArray(value)) { currentType = "ConstantColor"; @@ -57,11 +56,11 @@ export function VFXColorEditor(props: IVFXColorEditorProps): ReactNode { get type() { return currentType; }, - set type(newType: VFXColorType) { + set type(newType: EffectColorType) { currentType = newType; // Convert value to new type - let newValue: VFXColor; - const currentColor = value ? VFXValueUtils.parseConstantColor(value) : new Color4(1, 1, 1, 1); + let newValue: Color; + const currentColor = value ? ValueUtils.parseConstantColor(value) : new Color4(1, 1, 1, 1); if (newType === "ConstantColor") { newValue = { type: "ConstantColor", value: [currentColor.r, currentColor.g, currentColor.b, currentColor.a] }; } else if (newType === "ColorRange") { @@ -133,7 +132,7 @@ export function VFXColorEditor(props: IVFXColorEditorProps): ReactNode { {currentType === "ConstantColor" && ( <> {(() => { - const constantColor = value ? VFXValueUtils.parseConstantColor(value) : new Color4(1, 1, 1, 1); + const constantColor = value ? ValueUtils.parseConstantColor(value) : new Color4(1, 1, 1, 1); const wrapperColor = { get color() { return constantColor; @@ -159,14 +158,18 @@ export function VFXColorEditor(props: IVFXColorEditorProps): ReactNode { }, set colorA(newColor: Color4) { const currentB = colorRange ? colorRange.colorB : [1, 1, 1, 1]; - onChange({ type: "ColorRange", colorA: [newColor.r, newColor.g, newColor.b, newColor.a], colorB: currentB }); + onChange({ type: "ColorRange", colorA: [newColor.r, newColor.g, newColor.b, newColor.a], colorB: currentB as [number, number, number, number] }); }, get colorB() { return colorB; }, set colorB(newColor: Color4) { const currentA = colorRange ? colorRange.colorA : [0, 0, 0, 1]; - onChange({ type: "ColorRange", colorA: currentA, colorB: [newColor.r, newColor.g, newColor.b, newColor.a] }); + onChange({ + type: "ColorRange", + colorA: currentA as [number, number, number, number], + colorB: [newColor.r, newColor.g, newColor.b, newColor.a] as [number, number, number, number], + }); }, }; return ( @@ -179,35 +182,36 @@ export function VFXColorEditor(props: IVFXColorEditorProps): ReactNode { )} - {currentType === "Gradient" && (() => { - const gradientValue = value && typeof value === "object" && "type" in value && value.type === "Gradient" ? value : null; - const defaultColorKeys = [ - { pos: 0, value: [0, 0, 0, 1] }, - { pos: 1, value: [1, 1, 1, 1] }, - ]; - const defaultAlphaKeys = [ - { pos: 0, value: 1 }, - { pos: 1, value: 1 }, - ]; - const wrapperGradient = { - colorKeys: gradientValue?.colorKeys || defaultColorKeys, - alphaKeys: gradientValue?.alphaKeys || defaultAlphaKeys, - }; - return ( - { - onChange({ - type: "Gradient", - colorKeys: newColorKeys, - alphaKeys: newAlphaKeys, - }); - }} - /> - ); - })()} + {currentType === "Gradient" && + (() => { + const gradientValue = value && typeof value === "object" && "type" in value && value.type === "Gradient" ? value : null; + const defaultColorKeys = [ + { pos: 0, value: [0, 0, 0, 1] }, + { pos: 1, value: [1, 1, 1, 1] }, + ]; + const defaultAlphaKeys = [ + { pos: 0, value: 1 }, + { pos: 1, value: 1 }, + ]; + const wrapperGradient = { + colorKeys: gradientValue?.colorKeys || defaultColorKeys, + alphaKeys: gradientValue?.alphaKeys || defaultAlphaKeys, + }; + return ( + { + onChange({ + type: "Gradient", + colorKeys: newColorKeys, + alphaKeys: newAlphaKeys, + }); + }} + /> + ); + })()} {currentType === "RandomColor" && ( <> @@ -225,14 +229,18 @@ export function VFXColorEditor(props: IVFXColorEditorProps): ReactNode { }, set colorA(newColor: Color4) { const currentB = randomColor ? randomColor.colorB : [1, 1, 1, 1]; - onChange({ type: "RandomColor", colorA: [newColor.r, newColor.g, newColor.b, newColor.a], colorB: currentB }); + onChange({ type: "RandomColor", colorA: [newColor.r, newColor.g, newColor.b, newColor.a], colorB: currentB as [number, number, number, number] }); }, get colorB() { return colorB; }, set colorB(newColor: Color4) { const currentA = randomColor ? randomColor.colorA : [0, 0, 0, 1]; - onChange({ type: "RandomColor", colorA: currentA, colorB: [newColor.r, newColor.g, newColor.b, newColor.a] }); + onChange({ + type: "RandomColor", + colorA: currentA as [number, number, number, number], + colorB: [newColor.r, newColor.g, newColor.b, newColor.a] as [number, number, number, number], + }); }, }; return ( @@ -245,73 +253,73 @@ export function VFXColorEditor(props: IVFXColorEditorProps): ReactNode { )} - {currentType === "RandomColorBetweenGradient" && (() => { - const randomGradient = value && typeof value === "object" && "type" in value && value.type === "RandomColorBetweenGradient" ? value : null; - const defaultColorKeys = [ - { pos: 0, value: [0, 0, 0, 1] }, - { pos: 1, value: [1, 1, 1, 1] }, - ]; - const defaultAlphaKeys = [ - { pos: 0, value: 1 }, - { pos: 1, value: 1 }, - ]; + {currentType === "RandomColorBetweenGradient" && + (() => { + const randomGradient = value && typeof value === "object" && "type" in value && value.type === "RandomColorBetweenGradient" ? value : null; + const defaultColorKeys = [ + { pos: 0, value: [0, 0, 0, 1] }, + { pos: 1, value: [1, 1, 1, 1] }, + ]; + const defaultAlphaKeys = [ + { pos: 0, value: 1 }, + { pos: 1, value: 1 }, + ]; - const wrapperGradient1 = { - colorKeys: randomGradient?.gradient1?.colorKeys || defaultColorKeys, - alphaKeys: randomGradient?.gradient1?.alphaKeys || defaultAlphaKeys, - }; + const wrapperGradient1 = { + colorKeys: randomGradient?.gradient1?.colorKeys || defaultColorKeys, + alphaKeys: randomGradient?.gradient1?.alphaKeys || defaultAlphaKeys, + }; - const wrapperGradient2 = { - colorKeys: randomGradient?.gradient2?.colorKeys || defaultColorKeys, - alphaKeys: randomGradient?.gradient2?.alphaKeys || defaultAlphaKeys, - }; + const wrapperGradient2 = { + colorKeys: randomGradient?.gradient2?.colorKeys || defaultColorKeys, + alphaKeys: randomGradient?.gradient2?.alphaKeys || defaultAlphaKeys, + }; - return ( - <> - -
Gradient 1
- { - if (randomGradient) { - onChange({ - type: "RandomColorBetweenGradient", - gradient1: { - colorKeys: newColorKeys, - alphaKeys: newAlphaKeys, - }, - gradient2: randomGradient.gradient2, - }); - } - }} - /> -
- -
Gradient 2
- { - if (randomGradient) { - onChange({ - type: "RandomColorBetweenGradient", - gradient1: randomGradient.gradient1, - gradient2: { - colorKeys: newColorKeys, - alphaKeys: newAlphaKeys, - }, - }); - } - }} - /> -
- - ); - })()} + return ( + <> + +
Gradient 1
+ { + if (randomGradient) { + onChange({ + type: "RandomColorBetweenGradient", + gradient1: { + colorKeys: newColorKeys, + alphaKeys: newAlphaKeys, + }, + gradient2: randomGradient.gradient2, + }); + } + }} + /> +
+ +
Gradient 2
+ { + if (randomGradient) { + onChange({ + type: "RandomColorBetweenGradient", + gradient1: randomGradient.gradient1, + gradient2: { + colorKeys: newColorKeys, + alphaKeys: newAlphaKeys, + }, + }); + } + }} + /> +
+ + ); + })()} ); } - diff --git a/editor/src/editor/windows/fx-editor/properties/emission-properties.tsx b/editor/src/editor/windows/fx-editor/properties/emission-properties.tsx index 655969f8f..45594064c 100644 --- a/editor/src/editor/windows/fx-editor/properties/emission-properties.tsx +++ b/editor/src/editor/windows/fx-editor/properties/emission-properties.tsx @@ -1,15 +1,15 @@ import { Component, ReactNode } from "react"; -import { FXEditorEmissionProperties } from "./emission"; -import type { VFXEffectNode } from "../VFX"; +import { EffectEditorEmissionProperties } from "./emission"; +import type { EffectNode } from "babylonjs-editor-tools"; -export interface IFXEditorEmissionPropertiesTabProps { +export interface IEffectEditorEmissionPropertiesTabProps { filePath: string | null; selectedNodeId: string | number | null; - getNodeData: (nodeId: string | number) => VFXEffectNode | null; + getNodeData: (nodeId: string | number) => EffectNode | null; } -export class FXEditorEmissionPropertiesTab extends Component { +export class EffectEditorEmissionPropertiesTab extends Component { public render(): ReactNode { const nodeId = this.props.selectedNodeId; @@ -33,9 +33,8 @@ export class FXEditorEmissionPropertiesTab extends Component - this.forceUpdate()} /> + this.forceUpdate()} />
); } } - diff --git a/editor/src/editor/windows/fx-editor/properties/emission.tsx b/editor/src/editor/windows/fx-editor/properties/emission.tsx index a7b06c4a9..f121c71e1 100644 --- a/editor/src/editor/windows/fx-editor/properties/emission.tsx +++ b/editor/src/editor/windows/fx-editor/properties/emission.tsx @@ -4,19 +4,16 @@ import { EditorInspectorNumberField } from "../../../layout/inspector/fields/num import { EditorInspectorSwitchField } from "../../../layout/inspector/fields/switch"; import { EditorInspectorBlockField } from "../../../layout/inspector/fields/block"; import { EditorInspectorSectionField } from "../../../layout/inspector/fields/section"; -import { EditorInspectorStringField } from "../../../layout/inspector/fields/string"; -import type { VFXEffectNode } from "../VFX"; -import { VFXParticleSystem, VFXSolidParticleSystem } from "../VFX"; -import { VFXValueEditor } from "./vfx-value-editor"; -import type { VFXEmissionBurst, VFXValue } from "../VFX/types"; +import { type EffectNode, EffectSolidParticleSystem, EffectParticleSystem, EmissionBurst, Value } from "babylonjs-editor-tools"; +import { EffectValueEditor } from "./value-editor"; -export interface IFXEditorEmissionPropertiesProps { - nodeData: VFXEffectNode; +export interface IEffectEditorEmissionPropertiesProps { + nodeData: EffectNode; onChange: () => void; } -export function FXEditorEmissionProperties(props: IFXEditorEmissionPropertiesProps): ReactNode { +export function EffectEditorEmissionProperties(props: IEffectEditorEmissionPropertiesProps): ReactNode { const { nodeData, onChange } = props; if (nodeData.type !== "particle" || !nodeData.system) { @@ -42,9 +39,9 @@ export function FXEditorEmissionProperties(props: IFXEditorEmissionPropertiesPro {/* Emit Over Time */} - { (system as any).emissionOverTime = val; onChange(); @@ -54,9 +51,9 @@ export function FXEditorEmissionProperties(props: IFXEditorEmissionPropertiesPro {/* Emit Over Distance */} - { (system as any).emissionOverDistance = val; onChange(); @@ -65,7 +62,7 @@ export function FXEditorEmissionProperties(props: IFXEditorEmissionPropertiesPro {/* Emit Power (min/max) - только для base (есть min/maxEmitPower) */} - {system instanceof VFXParticleSystem && ( + {system instanceof EffectParticleSystem && (
Emit Power
@@ -81,9 +78,9 @@ export function FXEditorEmissionProperties(props: IFXEditorEmissionPropertiesPro ); } -function renderBursts(system: any, onChange: () => void): ReactNode { - const bursts: (VFXEmissionBurst & { cycle?: number; interval?: number; probability?: number })[] = Array.isArray(system.emissionBursts) - ? system.emissionBursts +function renderBursts(system: EffectParticleSystem | EffectSolidParticleSystem, onChange: () => void): ReactNode { + const bursts: (EmissionBurst & { cycle?: number; interval?: number; probability?: number })[] = Array.isArray((system as any).emissionBursts) + ? (system as any).emissionBursts : []; const addBurst = () => { @@ -94,13 +91,13 @@ function renderBursts(system: any, onChange: () => void): ReactNode { interval: 0, probability: 1, }); - system.emissionBursts = bursts; + (system as any).emissionBursts = bursts; onChange(); }; const removeBurst = (index: number) => { bursts.splice(index, 1); - system.emissionBursts = bursts; + (system as any).emissionBursts = bursts; onChange(); }; @@ -116,47 +113,25 @@ function renderBursts(system: any, onChange: () => void): ReactNode {
- { - burst.time = val; + burst.time = val as Value; onChange(); }} /> - { - burst.count = val; + burst.count = val as Value; onChange(); }} /> - - - + + +
))} diff --git a/editor/src/editor/windows/fx-editor/properties/emitter-properties.tsx b/editor/src/editor/windows/fx-editor/properties/emitter-properties.tsx index b74295a74..884ac8b7e 100644 --- a/editor/src/editor/windows/fx-editor/properties/emitter-properties.tsx +++ b/editor/src/editor/windows/fx-editor/properties/emitter-properties.tsx @@ -1,15 +1,15 @@ import { Component, ReactNode } from "react"; -import { FXEditorEmitterShapeProperties } from "./emitter-shape"; -import type { VFXEffectNode } from "../VFX"; +import { EffectEditorEmitterShapeProperties } from "./emitter-shape"; +import type { EffectNode } from "babylonjs-editor-tools"; -export interface IFXEditorEmitterPropertiesTabProps { +export interface IEffectEditorEmitterPropertiesTabProps { filePath: string | null; selectedNodeId: string | number | null; - getNodeData: (nodeId: string | number) => VFXEffectNode | null; + getNodeData: (nodeId: string | number) => EffectNode | null; } -export class FXEditorEmitterPropertiesTab extends Component { +export class EffectEditorEmitterPropertiesTab extends Component { public render(): ReactNode { const nodeId = this.props.selectedNodeId; @@ -33,9 +33,8 @@ export class FXEditorEmitterPropertiesTab extends Component - this.forceUpdate()} /> + this.forceUpdate()} />
); } } - diff --git a/editor/src/editor/windows/fx-editor/properties/emitter-shape.tsx b/editor/src/editor/windows/fx-editor/properties/emitter-shape.tsx index e0b255807..0b98fd7de 100644 --- a/editor/src/editor/windows/fx-editor/properties/emitter-shape.tsx +++ b/editor/src/editor/windows/fx-editor/properties/emitter-shape.tsx @@ -7,16 +7,14 @@ import { EditorInspectorSwitchField } from "../../../layout/inspector/fields/swi import { EditorInspectorListField } from "../../../layout/inspector/fields/list"; import { EditorInspectorBlockField } from "../../../layout/inspector/fields/block"; -import type { VFXEffectNode } from "../VFX"; -import { VFXParticleSystem, VFXSolidParticleSystem } from "../VFX"; -import { SolidSphereParticleEmitter, SolidConeParticleEmitter } from "../VFX/types/emitters"; +import { type EffectNode, EffectSolidParticleSystem, EffectParticleSystem, SolidSphereParticleEmitter, SolidConeParticleEmitter } from "babylonjs-editor-tools"; -export interface IFXEditorEmitterShapePropertiesProps { - nodeData: VFXEffectNode; +export interface IEffectEditorEmitterShapePropertiesProps { + nodeData: EffectNode; onChange: () => void; } -export class FXEditorEmitterShapeProperties extends Component { +export class EffectEditorEmitterShapeProperties extends Component { public render(): ReactNode { const { nodeData, onChange } = this.props; @@ -26,20 +24,20 @@ export class FXEditorEmitterShapeProperties extends Component void): ReactNode { + private _renderSolidParticleSystemEmitter(system: EffectSolidParticleSystem, onChange: () => void): ReactNode { const emitter = system.particleEmitterType; const emitterType = emitter ? emitter.constructor.name : "Point"; @@ -108,7 +106,7 @@ export class FXEditorEmitterShapeProperties extends Component void): ReactNode { + private _renderParticleSystemEmitter(system: EffectParticleSystem, onChange: () => void): ReactNode { const emitter = system.particleEmitterType; if (!emitter) { return
No emitter found.
; diff --git a/editor/src/editor/windows/fx-editor/properties/initialization-properties.tsx b/editor/src/editor/windows/fx-editor/properties/initialization-properties.tsx index 13f78d5e6..01231fab3 100644 --- a/editor/src/editor/windows/fx-editor/properties/initialization-properties.tsx +++ b/editor/src/editor/windows/fx-editor/properties/initialization-properties.tsx @@ -1,15 +1,15 @@ import { Component, ReactNode } from "react"; -import { FXEditorParticleInitializationProperties } from "./particle-initialization"; -import type { VFXEffectNode } from "../VFX"; +import { EffectEditorParticleInitializationProperties } from "./particle-initialization"; +import type { EffectNode } from "babylonjs-editor-tools"; -export interface IFXEditorInitializationPropertiesTabProps { +export interface IEffectEditorInitializationPropertiesTabProps { filePath: string | null; selectedNodeId: string | number | null; - getNodeData: (nodeId: string | number) => VFXEffectNode | null; + getNodeData: (nodeId: string | number) => EffectNode | null; } -export class FXEditorInitializationPropertiesTab extends Component { +export class EffectEditorInitializationPropertiesTab extends Component { public render(): ReactNode { const nodeId = this.props.selectedNodeId; @@ -33,9 +33,8 @@ export class FXEditorInitializationPropertiesTab extends Component - this.forceUpdate()} /> + this.forceUpdate()} />
); } } - diff --git a/editor/src/editor/windows/fx-editor/properties/object-properties.tsx b/editor/src/editor/windows/fx-editor/properties/object-properties.tsx index a53286665..9527e9d0c 100644 --- a/editor/src/editor/windows/fx-editor/properties/object-properties.tsx +++ b/editor/src/editor/windows/fx-editor/properties/object-properties.tsx @@ -1,18 +1,18 @@ import { Component, ReactNode } from "react"; -import { FXEditorObjectProperties } from "./object"; -import { IFXEditor } from ".."; -import type { VFXEffectNode } from "../VFX"; +import { EffectEditorObjectProperties } from "./object"; +import { IEffectEditor } from ".."; +import type { EffectNode } from "babylonjs-editor-tools"; -export interface IFXEditorObjectPropertiesTabProps { +export interface IEffectEditorObjectPropertiesTabProps { filePath: string | null; selectedNodeId: string | number | null; - editor: IFXEditor; + editor: IEffectEditor; onNameChanged?: () => void; - getNodeData: (nodeId: string | number) => VFXEffectNode | null; + getNodeData: (nodeId: string | number) => EffectNode | null; } -export class FXEditorObjectPropertiesTab extends Component { +export class EffectEditorObjectPropertiesTab extends Component { public render(): ReactNode { const nodeId = this.props.selectedNodeId; @@ -36,7 +36,7 @@ export class FXEditorObjectPropertiesTab extends Component - { this.forceUpdate(); @@ -47,4 +47,3 @@ export class FXEditorObjectPropertiesTab extends Component void; } @@ -52,7 +51,7 @@ function getRotationInspector(object: any, onChange?: () => void): ReactNode { return null; } -export function FXEditorObjectProperties(props: IFXEditorObjectPropertiesProps): ReactNode { +export function EffectEditorObjectProperties(props: IEffectEditorObjectPropertiesProps): ReactNode { const { nodeData, onChange } = props; // For groups, use transformNode directly @@ -70,12 +69,12 @@ export function FXEditorObjectProperties(props: IFXEditorObjectPropertiesProps): ); } - // For particles, use system.emitter for VFXParticleSystem or system.mesh for VFXSolidParticleSystem + // For particles, use system.emitter for VEffectParticleSystem or system.mesh for VEffectSolidParticleSystem if (nodeData.type === "particle" && nodeData.system) { const system = nodeData.system; - // For VFXSolidParticleSystem, use mesh (common mesh for all particles) - if (system instanceof VFXSolidParticleSystem) { + // For VEffectSolidParticleSystem, use mesh (common mesh for all particles) + if (system instanceof EffectSolidParticleSystem) { const mesh = system.mesh; if (!mesh) { return ( @@ -97,8 +96,8 @@ export function FXEditorObjectProperties(props: IFXEditorObjectPropertiesProps): ); } - // For VFXParticleSystem, use emitter - if (system instanceof VFXParticleSystem) { + // For VEffectParticleSystem, use emitter + if (system instanceof EffectParticleSystem) { const emitter = (system as any).emitter; if (!emitter) { return ( diff --git a/editor/src/editor/windows/fx-editor/properties/particle-initialization.tsx b/editor/src/editor/windows/fx-editor/properties/particle-initialization.tsx index e0abf1d87..ea1518cfc 100644 --- a/editor/src/editor/windows/fx-editor/properties/particle-initialization.tsx +++ b/editor/src/editor/windows/fx-editor/properties/particle-initialization.tsx @@ -2,22 +2,17 @@ import { ReactNode } from "react"; import { EditorInspectorBlockField } from "../../../layout/inspector/fields/block"; -import type { VFXEffectNode } from "../VFX"; -import { VFXParticleSystem, VFXSolidParticleSystem } from "../VFX"; -import { VFXValueEditor, type IVec3Function } from "./vfx-value-editor"; -import { VFXColorEditor } from "./vfx-color-editor"; -import { VFXRotationEditor } from "./vfx-rotation-editor"; -import type { VFXValue } from "../VFX/types/values"; -import type { VFXColor } from "../VFX/types/colors"; -import type { VFXRotation } from "../VFX/types/rotations"; -import { VFXValueUtils } from "../VFX/utils/valueParser"; - -export interface IFXEditorParticleInitializationPropertiesProps { - nodeData: VFXEffectNode; +import { type EffectNode, EffectSolidParticleSystem, EffectParticleSystem, ValueUtils, Value, Color, Rotation } from "babylonjs-editor-tools"; +import { EffectValueEditor, type IVec3Function } from "./value-editor"; +import { EffectColorEditor } from "./color-editor"; +import { EffectRotationEditor } from "./rotation-editor"; + +export interface IEffectEditorParticleInitializationPropertiesProps { + nodeData: EffectNode; onChange?: () => void; } -export function FXEditorParticleInitializationProperties(props: IFXEditorParticleInitializationPropertiesProps): ReactNode { +export function EffectEditorParticleInitializationProperties(props: IEffectEditorParticleInitializationPropertiesProps): ReactNode { const { nodeData } = props; const onChange = props.onChange || (() => {}); @@ -27,65 +22,65 @@ export function FXEditorParticleInitializationProperties(props: IFXEditorParticl const system = nodeData.system; - // Helper to get/set startLife as VFXValue for both systems - const getStartLife = (): VFXValue | undefined => { - if (system instanceof VFXSolidParticleSystem) { + // Helper to get/set startLife as VEffectValue for both systems + const getStartLife = (): Value | undefined => { + if (system instanceof EffectSolidParticleSystem) { return system.startLife; } - // For VFXParticleSystem, convert minLifeTime/maxLifeTime to IntervalValue - if (system instanceof VFXParticleSystem) { + // For VEffectParticleSystem, convert minLifeTime/maxLifeTime to IntervalValue + if (system instanceof EffectParticleSystem) { return { type: "IntervalValue", min: system.minLifeTime, max: system.maxLifeTime }; } return undefined; }; - const setStartLife = (value: VFXValue): void => { - if (system instanceof VFXSolidParticleSystem) { + const setStartLife = (value: Value): void => { + if (system instanceof EffectSolidParticleSystem) { system.startLife = value; - } else if (system instanceof VFXParticleSystem) { - const interval = VFXValueUtils.parseIntervalValue(value); + } else if (system instanceof EffectParticleSystem) { + const interval = ValueUtils.parseIntervalValue(value); system.minLifeTime = interval.min; system.maxLifeTime = interval.max; } onChange(); }; - // Helper to get/set startSize as VFXValue | IVec3Function for both systems - const getStartSize = (): VFXValue | IVec3Function | undefined => { - if (system instanceof VFXSolidParticleSystem) { + // Helper to get/set startSize as VEffectValue | IVec3Function for both systems + const getStartSize = (): Value | IVec3Function | undefined => { + if (system instanceof EffectSolidParticleSystem) { return system.startSize; } - // For VFXParticleSystem, convert minSize/maxSize to IntervalValue - if (system instanceof VFXParticleSystem) { + // For VEffectParticleSystem, convert minSize/maxSize to IntervalValue + if (system instanceof EffectParticleSystem) { return { type: "IntervalValue", min: system.minSize, max: system.maxSize }; } return undefined; }; - const setStartSize = (value: VFXValue | IVec3Function): void => { - if (system instanceof VFXSolidParticleSystem) { - // For Vec3Function, we need to handle it differently - but VFXSolidParticleSystem doesn't support Vec3Function yet + const setStartSize = (value: Value | IVec3Function): void => { + if (system instanceof EffectSolidParticleSystem) { + // For Vec3Function, we need to handle it differently - but VEffectSolidParticleSystem doesn't support Vec3Function yet // For now, convert Vec3Function to a single value if (typeof value === "object" && "type" in value && value.type === "Vec3Function") { - const x = VFXValueUtils.parseConstantValue(value.x); - const y = VFXValueUtils.parseConstantValue(value.y); - const z = VFXValueUtils.parseConstantValue(value.z); + const x = ValueUtils.parseConstantValue(value.x); + const y = ValueUtils.parseConstantValue(value.y); + const z = ValueUtils.parseConstantValue(value.z); const avg = (x + y + z) / 3; system.startSize = { type: "ConstantValue", value: avg }; } else { - system.startSize = value as VFXValue; + system.startSize = value as Value; } - } else if (system instanceof VFXParticleSystem) { + } else if (system instanceof EffectParticleSystem) { if (typeof value === "object" && "type" in value && value.type === "Vec3Function") { // For Vec3Function, use average of x, y, z - const x = VFXValueUtils.parseConstantValue(value.x); - const y = VFXValueUtils.parseConstantValue(value.y); - const z = VFXValueUtils.parseConstantValue(value.z); + const x = ValueUtils.parseConstantValue(value.x); + const y = ValueUtils.parseConstantValue(value.y); + const z = ValueUtils.parseConstantValue(value.z); const avg = (x + y + z) / 3; system.minSize = avg; system.maxSize = avg; } else { - const interval = VFXValueUtils.parseIntervalValue(value as VFXValue); + const interval = ValueUtils.parseIntervalValue(value as Value); system.minSize = interval.min; system.maxSize = interval.max; } @@ -93,58 +88,58 @@ export function FXEditorParticleInitializationProperties(props: IFXEditorParticl onChange(); }; - // Helper to get/set startSpeed as VFXValue for both systems - const getStartSpeed = (): VFXValue | undefined => { - if (system instanceof VFXSolidParticleSystem) { + // Helper to get/set startSpeed as VEffectValue for both systems + const getStartSpeed = (): Value | undefined => { + if (system instanceof EffectSolidParticleSystem) { return system.startSpeed; } - // For VFXParticleSystem, convert minEmitPower/maxEmitPower to IntervalValue - if (system instanceof VFXParticleSystem) { + // For VEffectParticleSystem, convert minEmitPower/maxEmitPower to IntervalValue + if (system instanceof EffectParticleSystem) { return { type: "IntervalValue", min: system.minEmitPower, max: system.maxEmitPower }; } return undefined; }; - const setStartSpeed = (value: VFXValue): void => { - if (system instanceof VFXSolidParticleSystem) { + const setStartSpeed = (value: Value): void => { + if (system instanceof EffectSolidParticleSystem) { system.startSpeed = value; - } else if (system instanceof VFXParticleSystem) { - const interval = VFXValueUtils.parseIntervalValue(value); + } else if (system instanceof EffectParticleSystem) { + const interval = ValueUtils.parseIntervalValue(value); system.minEmitPower = interval.min; system.maxEmitPower = interval.max; } onChange(); }; - // Helper to get/set startColor as VFXColor for both systems - const getStartColor = (): VFXColor | undefined => { - if (system instanceof VFXSolidParticleSystem) { + // Helper to get/set startColor as VEffectColor for both systems + const getStartColor = (): Color | undefined => { + if (system instanceof EffectSolidParticleSystem) { return system.startColor; } - // For VFXParticleSystem, convert Color4 to ConstantColor - if (system instanceof VFXParticleSystem && system.color1) { + // For VEffectParticleSystem, convert Color4 to ConstantColor + if (system instanceof EffectParticleSystem && system.color1) { return { type: "ConstantColor", value: [system.color1.r, system.color1.g, system.color1.b, system.color1.a] }; } return undefined; }; - const setStartColor = (value: VFXColor): void => { - if (system instanceof VFXSolidParticleSystem) { + const setStartColor = (value: Color): void => { + if (system instanceof EffectSolidParticleSystem) { system.startColor = value; - } else if (system instanceof VFXParticleSystem) { - const color = VFXValueUtils.parseConstantColor(value); + } else if (system instanceof EffectParticleSystem) { + const color = ValueUtils.parseConstantColor(value); system.color1 = color; } onChange(); }; - // Helper to get/set startRotation as VFXRotation for both systems - const getStartRotation = (): VFXRotation | undefined => { - if (system instanceof VFXSolidParticleSystem) { + // Helper to get/set startRotation as VEffectRotation for both systems + const getStartRotation = (): Rotation | undefined => { + if (system instanceof EffectSolidParticleSystem) { return system.startRotation; } - // For VFXParticleSystem, convert minInitialRotation/maxInitialRotation to Euler with angleZ - if (system instanceof VFXParticleSystem) { + // For VEffectParticleSystem, convert minInitialRotation/maxInitialRotation to Euler with angleZ + if (system instanceof EffectParticleSystem) { return { type: "Euler", angleZ: { type: "IntervalValue", min: system.minInitialRotation, max: system.maxInitialRotation }, @@ -154,20 +149,20 @@ export function FXEditorParticleInitializationProperties(props: IFXEditorParticl return undefined; }; - const setStartRotation = (value: VFXRotation): void => { - if (system instanceof VFXSolidParticleSystem) { + const setStartRotation = (value: Rotation): void => { + if (system instanceof EffectSolidParticleSystem) { system.startRotation = value; - } else if (system instanceof VFXParticleSystem) { - // Extract angleZ from rotation for VFXParticleSystem + } else if (system instanceof EffectParticleSystem) { + // Extract angleZ from rotation for VEffectParticleSystem if (typeof value === "object" && "type" in value && value.type === "Euler" && value.angleZ) { - const interval = VFXValueUtils.parseIntervalValue(value.angleZ); + const interval = ValueUtils.parseIntervalValue(value.angleZ); system.minInitialRotation = interval.min; system.maxInitialRotation = interval.max; } else if ( typeof value === "number" || (typeof value === "object" && "type" in value && (value.type === "ConstantValue" || value.type === "IntervalValue" || value.type === "PiecewiseBezier")) ) { - const interval = VFXValueUtils.parseIntervalValue(value as VFXValue); + const interval = ValueUtils.parseIntervalValue(value as Value); system.minInitialRotation = interval.min; system.maxInitialRotation = interval.max; } @@ -179,12 +174,12 @@ export function FXEditorParticleInitializationProperties(props: IFXEditorParticl <>
Start Life
- +
Start Size
-
Start Speed
- +
Start Color
- +
Start Rotation
- {system instanceof VFXSolidParticleSystem ? ( - + {system instanceof EffectSolidParticleSystem ? ( + ) : ( (() => { - // For VFXParticleSystem, extract angleZ from rotation + // For VEffectParticleSystem, extract angleZ from rotation const rotation = getStartRotation(); const angleZ = rotation && typeof rotation === "object" && "type" in rotation && rotation.type === "Euler" && rotation.angleZ @@ -219,15 +214,15 @@ export function FXEditorParticleInitializationProperties(props: IFXEditorParticl (typeof rotation === "object" && "type" in rotation && (rotation.type === "ConstantValue" || rotation.type === "IntervalValue" || rotation.type === "PiecewiseBezier"))) - ? (rotation as VFXValue) + ? (rotation as Value) : { type: "IntervalValue" as const, min: 0, max: 0 }; return ( - { setStartRotation({ type: "Euler", - angleZ: newAngleZ as VFXValue, + angleZ: newAngleZ as Value, order: "xyz", }); }} diff --git a/editor/src/editor/windows/fx-editor/properties/particle-renderer.tsx b/editor/src/editor/windows/fx-editor/properties/particle-renderer.tsx index 0c4a5f76e..9019ecd18 100644 --- a/editor/src/editor/windows/fx-editor/properties/particle-renderer.tsx +++ b/editor/src/editor/windows/fx-editor/properties/particle-renderer.tsx @@ -6,26 +6,8 @@ import { EditorInspectorSwitchField } from "../../../layout/inspector/fields/swi import { EditorInspectorListField } from "../../../layout/inspector/fields/list"; import { EditorInspectorTextureField } from "../../../layout/inspector/fields/texture"; import { EditorInspectorGeometryField } from "../../../layout/inspector/fields/geometry"; -import { EditorInspectorColorField } from "../../../layout/inspector/fields/color"; -import { EditorInspectorStringField } from "../../../layout/inspector/fields/string"; - -import { - PBRMaterial, - StandardMaterial, - NodeMaterial, - MultiMaterial, - SkyMaterial, - GridMaterial, - NormalMaterial, - WaterMaterial, - LavaMaterial, - TriPlanarMaterial, - CellMaterial, - FireMaterial, - GradientMaterial, - Material, - ParticleSystem, -} from "babylonjs"; + +import { PBRMaterial, StandardMaterial, NodeMaterial, MultiMaterial, Material, ParticleSystem } from "babylonjs"; import { EditorPBRMaterialInspector } from "../../../layout/inspector/material/pbr"; import { EditorStandardMaterialInspector } from "../../../layout/inspector/material/standard"; @@ -41,24 +23,24 @@ import { EditorCellMaterialInspector } from "../../../layout/inspector/material/ import { EditorFireMaterialInspector } from "../../../layout/inspector/material/fire"; import { EditorGradientMaterialInspector } from "../../../layout/inspector/material/gradient"; -import type { VFXEffectNode } from "../VFX"; -import { VFXParticleSystem, VFXSolidParticleSystem } from "../VFX"; -import { IFXEditor } from ".."; -import { VFXValueUtils } from "../VFX/utils/valueParser"; -import { VFXValueEditor } from "./vfx-value-editor"; +import { type EffectNode, EffectSolidParticleSystem, EffectParticleSystem } from "babylonjs-editor-tools"; +import { IEffectEditor } from ".."; +import { Mesh } from "babylonjs"; +import { EffectValueEditor } from "./value-editor"; +import { CellMaterial, FireMaterial, GradientMaterial, GridMaterial, LavaMaterial, NormalMaterial, SkyMaterial, TriPlanarMaterial, WaterMaterial } from "babylonjs-materials"; -export interface IFXEditorParticleRendererPropertiesProps { - nodeData: VFXEffectNode; - editor: IFXEditor; +export interface IEffectEditorParticleRendererPropertiesProps { + nodeData: EffectNode; + editor: IEffectEditor; onChange: () => void; } -export interface IFXEditorParticleRendererPropertiesState { +export interface IEffectEditorParticleRendererPropertiesState { meshDragOver: boolean; } -export class FXEditorParticleRendererProperties extends Component { - public constructor(props: IFXEditorParticleRendererPropertiesProps) { +export class EffectEditorParticleRendererProperties extends Component { + public constructor(props: IEffectEditorParticleRendererPropertiesProps) { super(props); this.state = { meshDragOver: false, @@ -73,9 +55,9 @@ export class FXEditorParticleRendererProperties extends Component @@ -83,7 +65,7 @@ export class FXEditorParticleRendererProperties extends ComponentSystem Mode: {systemType === "solid" ? "Mesh (Solid)" : "Billboard (Base)"} {/* Billboard Mode - только для base */} - {isVFXParticleSystem && ( + {isEffectParticleSystem && ( <> this.props.onChange()} /> - this.props.onChange()} - /> + this.props.onChange()} /> )} {/* World Space */} - {isVFXParticleSystem && (() => { - // Для VFXParticleSystem, worldSpace = !isLocal - const proxy = { - get worldSpace() { - return !system.isLocal; - }, - set worldSpace(value: boolean) { - system.isLocal = !value; - }, - }; - return this.props.onChange()} />; - })()} - {isVFXSolidParticleSystem && ( - this.props.onChange()} - /> - )} + {isEffectParticleSystem && + (() => { + // Для VEffectParticleSystem, worldSpace = !isLocal + const proxy = { + get worldSpace() { + return !system.isLocal; + }, + set worldSpace(value: boolean) { + system.isLocal = !value; + }, + }; + return this.props.onChange()} />; + })()} + {isEffectSolidParticleSystem && this.props.onChange()} />} {/* Material Inspector - только для solid с материалом */} - {isVFXSolidParticleSystem && this._getMaterialInspector()} + {isEffectSolidParticleSystem && this._getMaterialInspector()} {/* Blend Mode - только для base */} - {isVFXParticleSystem && ( + {isEffectParticleSystem && ( this.props.onChange()} /> - )} - {isVFXSolidParticleSystem && ( + {isEffectParticleSystem && this.props.onChange()} />} + {isEffectSolidParticleSystem && ( this.props.onChange()} /> )} {/* Geometry - только для solid */} - {isVFXSolidParticleSystem && this._getGeometryField()} + {isEffectSolidParticleSystem && this._getGeometryField()} ); } @@ -183,8 +152,8 @@ export class FXEditorParticleRendererProperties extends Component this.props.onChange()} />; } @@ -265,22 +234,13 @@ export class FXEditorParticleRendererProperties extends Component this.props.onChange()} - /> - ); + // Для VEffectParticleSystem, renderOrder хранится в renderingGroupId + if (system instanceof EffectParticleSystem) { + return this.props.onChange()} />; } - // Для VFXSolidParticleSystem, renderOrder хранится в system.renderOrder и применяется к mesh.renderingGroupId - if (system instanceof VFXSolidParticleSystem) { + // Для VEffectSolidParticleSystem, renderOrder хранится в system.renderOrder и применяется к mesh.renderingGroupId + if (system instanceof EffectSolidParticleSystem) { // Создаем proxy объект для доступа к renderOrder через mesh.renderingGroupId const proxy = { get renderingGroupId() { @@ -294,16 +254,7 @@ export class FXEditorParticleRendererProperties extends Component this.props.onChange()} - /> - ); + return this.props.onChange()} />; } return null; @@ -318,19 +269,19 @@ export class FXEditorParticleRendererProperties extends Component this.props.onChange()} /> this.props.onChange()} /> - {/* TODO: Add blendTiles if available for VFXParticleSystem */} + {/* TODO: Add blendTiles if available for VEffectParticleSystem */}
); } - // Для VFXSolidParticleSystem, используем uTileCount и vTileCount - if (system instanceof VFXSolidParticleSystem) { + // Для VEffectSolidParticleSystem, используем uTileCount и vTileCount + if (system instanceof EffectSolidParticleSystem) { return ( this.props.onChange()} /> @@ -354,22 +305,13 @@ export class FXEditorParticleRendererProperties extends Component this.props.onChange()} - /> - ); + // Для VEffectParticleSystem, используем startSpriteCellID + if (system instanceof EffectParticleSystem) { + return this.props.onChange()} />; } - // Для VFXSolidParticleSystem, используем startTileIndex (VFXValue) - if (system instanceof VFXSolidParticleSystem && system.startTileIndex !== undefined) { + // Для VEffectSolidParticleSystem, используем startTileIndex (VEffectValue) + if (system instanceof EffectSolidParticleSystem && system.startTileIndex !== undefined) { const getValue = () => system.startTileIndex!; const setValue = (value: any) => { system.startTileIndex = value; @@ -378,7 +320,7 @@ export class FXEditorParticleRendererProperties extends Component - + ); } @@ -389,11 +331,11 @@ export class FXEditorParticleRendererProperties extends Component this.props.onChange()} - /> - ); + return this.props.onChange()} />; } } diff --git a/editor/src/editor/windows/fx-editor/properties/renderer-properties.tsx b/editor/src/editor/windows/fx-editor/properties/renderer-properties.tsx index 2fed7d31d..c5a2315b0 100644 --- a/editor/src/editor/windows/fx-editor/properties/renderer-properties.tsx +++ b/editor/src/editor/windows/fx-editor/properties/renderer-properties.tsx @@ -1,17 +1,17 @@ import { Component, ReactNode } from "react"; -import { FXEditorParticleRendererProperties } from "./particle-renderer"; -import { IFXEditor } from ".."; -import type { VFXEffectNode } from "../VFX"; +import { EffectEditorParticleRendererProperties } from "./particle-renderer"; +import { IEffectEditor } from ".."; +import type { EffectNode } from "babylonjs-editor-tools"; -export interface IFXEditorRendererPropertiesTabProps { +export interface IEffectEditorRendererPropertiesTabProps { filePath: string | null; selectedNodeId: string | number | null; - editor: IFXEditor; - getNodeData: (nodeId: string | number) => VFXEffectNode | null; + editor: IEffectEditor; + getNodeData: (nodeId: string | number) => EffectNode | null; } -export class FXEditorRendererPropertiesTab extends Component { +export class EffectEditorRendererPropertiesTab extends Component { public render(): ReactNode { const nodeId = this.props.selectedNodeId; @@ -35,9 +35,8 @@ export class FXEditorRendererPropertiesTab extends Component - this.forceUpdate()} /> + this.forceUpdate()} /> ); } } - diff --git a/editor/src/editor/windows/fx-editor/properties/vfx-rotation-editor.tsx b/editor/src/editor/windows/fx-editor/properties/rotation-editor.tsx similarity index 78% rename from editor/src/editor/windows/fx-editor/properties/vfx-rotation-editor.tsx rename to editor/src/editor/windows/fx-editor/properties/rotation-editor.tsx index 5b079fe59..fbb4e1fe9 100644 --- a/editor/src/editor/windows/fx-editor/properties/vfx-rotation-editor.tsx +++ b/editor/src/editor/windows/fx-editor/properties/rotation-editor.tsx @@ -3,31 +3,32 @@ import { ReactNode } from "react"; import { EditorInspectorListField } from "../../../layout/inspector/fields/list"; import { EditorInspectorBlockField } from "../../../layout/inspector/fields/block"; -import type { VFXRotation, VFXEulerRotation, VFXAxisAngleRotation, VFXRandomQuatRotation } from "../VFX/types/rotations"; -import { VFXValueEditor } from "./vfx-value-editor"; -import { VFXValueUtils } from "../VFX/utils/valueParser"; -import type { VFXValue } from "../VFX/types/values"; +import { type Rotation, ValueUtils, type Value } from "babylonjs-editor-tools"; +import { EffectValueEditor } from "./value-editor"; -export type VFXRotationType = "Euler" | "AxisAngle" | "RandomQuat"; +export type EffectRotationType = "Euler" | "AxisAngle" | "RandomQuat"; -export interface IVFXRotationEditorProps { - value: VFXRotation | undefined; - onChange: (newValue: VFXRotation) => void; +export interface IEffectRotationEditorProps { + value: Rotation | undefined; + onChange: (newValue: Rotation) => void; label?: string; } /** - * Editor for VFXRotation (Euler, AxisAngle, RandomQuat) - * Works directly with VFXRotation types, not wrappers + * Editor for VEffectRotation (Euler, AxisAngle, RandomQuat) + * Works directly with VEffectRotation types, not wrappers */ -export function VFXRotationEditor(props: IVFXRotationEditorProps): ReactNode { +export function EffectRotationEditor(props: IEffectRotationEditorProps): ReactNode { const { value, onChange, label } = props; // Determine current type from value - let currentType: VFXRotationType = "Euler"; + let currentType: EffectRotationType = "Euler"; if (value) { - if (typeof value === "number" || (typeof value === "object" && "type" in value && (value.type === "ConstantValue" || value.type === "IntervalValue" || value.type === "PiecewiseBezier"))) { - // Simple VFXValue - convert to Euler + if ( + typeof value === "number" || + (typeof value === "object" && "type" in value && (value.type === "ConstantValue" || value.type === "IntervalValue" || value.type === "PiecewiseBezier")) + ) { + // Simple VEffectValue - convert to Euler currentType = "Euler"; } else if (typeof value === "object" && "type" in value) { if (value.type === "Euler") { @@ -51,16 +52,16 @@ export function VFXRotationEditor(props: IVFXRotationEditorProps): ReactNode { get type() { return currentType; }, - set type(newType: VFXRotationType) { + set type(newType: EffectRotationType) { currentType = newType; // Convert value to new type - let newValue: VFXRotation; + let newValue: Rotation; if (newType === "Euler") { // Convert current value to Euler if (value && typeof value === "object" && "type" in value && value.type === "Euler") { newValue = value; } else { - const angleZ = value ? (typeof value === "number" ? value : VFXValueUtils.parseConstantValue(value as VFXValue)) : 0; + const angleZ = value ? (typeof value === "number" ? value : ValueUtils.parseConstantValue(value as Value)) : 0; newValue = { type: "Euler", angleZ: { type: "ConstantValue", value: angleZ }, @@ -69,7 +70,7 @@ export function VFXRotationEditor(props: IVFXRotationEditorProps): ReactNode { } } else if (newType === "AxisAngle") { // Convert to AxisAngle - const angle = value ? (typeof value === "number" ? value : VFXValueUtils.parseConstantValue(value as VFXValue)) : 0; + const angle = value ? (typeof value === "number" ? value : ValueUtils.parseConstantValue(value as Value)) : 0; newValue = { type: "AxisAngle", x: { type: "ConstantValue", value: 0 }, @@ -139,15 +140,15 @@ export function VFXRotationEditor(props: IVFXRotationEditorProps): ReactNode { />
Angle X
- { if (eulerValue) { - onChange({ ...eulerValue, angleX: newAngleX as VFXValue }); + onChange({ ...eulerValue, angleX: newAngleX as Value }); } else { onChange({ type: "Euler", - angleX: newAngleX as VFXValue, + angleX: newAngleX as Value, angleY, angleZ, order, @@ -160,16 +161,16 @@ export function VFXRotationEditor(props: IVFXRotationEditorProps): ReactNode {
Angle Y
- { if (eulerValue) { - onChange({ ...eulerValue, angleY: newAngleY as VFXValue }); + onChange({ ...eulerValue, angleY: newAngleY as Value }); } else { onChange({ type: "Euler", angleX, - angleY: newAngleY as VFXValue, + angleY: newAngleY as Value, angleZ, order, }); @@ -181,17 +182,17 @@ export function VFXRotationEditor(props: IVFXRotationEditorProps): ReactNode {
Angle Z
- { if (eulerValue) { - onChange({ ...eulerValue, angleZ: newAngleZ as VFXValue }); + onChange({ ...eulerValue, angleZ: newAngleZ as Value }); } else { onChange({ type: "Euler", angleX, angleY, - angleZ: newAngleZ as VFXValue, + angleZ: newAngleZ as Value, order, }); } @@ -219,15 +220,15 @@ export function VFXRotationEditor(props: IVFXRotationEditorProps): ReactNode { <>
Axis X
- { if (axisAngleValue) { - onChange({ ...axisAngleValue, x: newX as VFXValue }); + onChange({ ...axisAngleValue, x: newX as Value }); } else { onChange({ type: "AxisAngle", - x: newX as VFXValue, + x: newX as Value, y, z, angle, @@ -240,16 +241,16 @@ export function VFXRotationEditor(props: IVFXRotationEditorProps): ReactNode {
Axis Y
- { if (axisAngleValue) { - onChange({ ...axisAngleValue, y: newY as VFXValue }); + onChange({ ...axisAngleValue, y: newY as Value }); } else { onChange({ type: "AxisAngle", x, - y: newY as VFXValue, + y: newY as Value, z, angle, }); @@ -261,17 +262,17 @@ export function VFXRotationEditor(props: IVFXRotationEditorProps): ReactNode {
Axis Z
- { if (axisAngleValue) { - onChange({ ...axisAngleValue, z: newZ as VFXValue }); + onChange({ ...axisAngleValue, z: newZ as Value }); } else { onChange({ type: "AxisAngle", x, y, - z: newZ as VFXValue, + z: newZ as Value, angle, }); } @@ -282,18 +283,18 @@ export function VFXRotationEditor(props: IVFXRotationEditorProps): ReactNode {
Angle
- { if (axisAngleValue) { - onChange({ ...axisAngleValue, angle: newAngle as VFXValue }); + onChange({ ...axisAngleValue, angle: newAngle as Value }); } else { onChange({ type: "AxisAngle", x, y, z, - angle: newAngle as VFXValue, + angle: newAngle as Value, }); } }} @@ -315,4 +316,3 @@ export function VFXRotationEditor(props: IVFXRotationEditorProps): ReactNode { ); } - diff --git a/editor/src/editor/windows/fx-editor/properties/vfx-value-editor.tsx b/editor/src/editor/windows/fx-editor/properties/value-editor.tsx similarity index 77% rename from editor/src/editor/windows/fx-editor/properties/vfx-value-editor.tsx rename to editor/src/editor/windows/fx-editor/properties/value-editor.tsx index 1852ce825..3056df9cd 100644 --- a/editor/src/editor/windows/fx-editor/properties/vfx-value-editor.tsx +++ b/editor/src/editor/windows/fx-editor/properties/value-editor.tsx @@ -4,39 +4,38 @@ import { EditorInspectorNumberField } from "../../../layout/inspector/fields/num import { EditorInspectorListField } from "../../../layout/inspector/fields/list"; import { EditorInspectorBlockField } from "../../../layout/inspector/fields/block"; -import type { VFXValue, VFXConstantValue, VFXIntervalValue, VFXPiecewiseBezier } from "../VFX/types/values"; +import { type Value, ValueUtils } from "babylonjs-editor-tools"; import { BezierEditor } from "./behaviors/bezier-editor"; -import { VFXValueUtils } from "../VFX/utils/valueParser"; -export type VFXValueType = "ConstantValue" | "IntervalValue" | "PiecewiseBezier" | "Vec3Function"; +export type EffectValueType = "ConstantValue" | "IntervalValue" | "PiecewiseBezier" | "Vec3Function"; export interface IVec3Function { type: "Vec3Function"; - x: VFXValue; - y: VFXValue; - z: VFXValue; + x: Value; + y: Value; + z: Value; } -export interface IVFXValueEditorProps { - value: VFXValue | IVec3Function | undefined; - onChange: (newValue: VFXValue | IVec3Function) => void; +export interface IEffectValueEditorProps { + value: Value | IVec3Function | undefined; + onChange: (newValue: Value | IVec3Function) => void; label?: string; - availableTypes?: VFXValueType[]; + availableTypes?: EffectValueType[]; min?: number; step?: number; } /** - * Editor for VFXValue (ConstantValue, IntervalValue, PiecewiseBezier, Vec3Function) - * Works directly with VFXValue types, not wrappers + * Editor for VEffectValue (ConstantValue, IntervalValue, PiecewiseBezier, Vec3Function) + * Works directly with VEffectValue types, not wrappers */ -export function VFXValueEditor(props: IVFXValueEditorProps): ReactNode { +export function EffectValueEditor(props: IEffectValueEditorProps): ReactNode { const { value, onChange, label, availableTypes, min, step } = props; const types = availableTypes || ["ConstantValue", "IntervalValue", "PiecewiseBezier"]; // Determine current type from value - let currentType: VFXValueType = "ConstantValue"; + let currentType: EffectValueType = "ConstantValue"; if (value) { if (typeof value === "number") { currentType = "ConstantValue"; @@ -63,18 +62,29 @@ export function VFXValueEditor(props: IVFXValueEditorProps): ReactNode { get type() { return currentType; }, - set type(newType: VFXValueType) { + set type(newType: EffectValueType) { currentType = newType; // Convert value to new type - let newValue: VFXValue | IVec3Function; + let newValue: Value | IVec3Function; if (newType === "ConstantValue") { - const currentValue = value && typeof value !== "number" && "type" in value && value.type !== "Vec3Function" ? VFXValueUtils.parseConstantValue(value as VFXValue) : typeof value === "number" ? value : 1; + const currentValue = + value && typeof value !== "number" && "type" in value && value.type !== "Vec3Function" + ? ValueUtils.parseConstantValue(value as Value) + : typeof value === "number" + ? value + : 1; newValue = { type: "ConstantValue", value: currentValue }; } else if (newType === "IntervalValue") { - const interval = value && typeof value !== "number" && "type" in value && value.type !== "Vec3Function" ? VFXValueUtils.parseIntervalValue(value as VFXValue) : { min: 0, max: 1 }; + const interval = + value && typeof value !== "number" && "type" in value && value.type !== "Vec3Function" ? ValueUtils.parseIntervalValue(value as Value) : { min: 0, max: 1 }; newValue = { type: "IntervalValue", min: interval.min, max: interval.max }; } else if (newType === "Vec3Function") { - const currentValue = value && typeof value !== "number" && "type" in value && value.type !== "Vec3Function" ? VFXValueUtils.parseConstantValue(value as VFXValue) : typeof value === "number" ? value : 1; + const currentValue = + value && typeof value !== "number" && "type" in value && value.type !== "Vec3Function" + ? ValueUtils.parseConstantValue(value as Value) + : typeof value === "number" + ? value + : 1; newValue = { type: "Vec3Function", x: { type: "ConstantValue", value: currentValue }, @@ -83,7 +93,12 @@ export function VFXValueEditor(props: IVFXValueEditorProps): ReactNode { }; } else { // PiecewiseBezier - convert from current value - const currentValue = value && typeof value !== "number" && "type" in value && value.type !== "Vec3Function" ? VFXValueUtils.parseConstantValue(value as VFXValue) : typeof value === "number" ? value : 1; + const currentValue = + value && typeof value !== "number" && "type" in value && value.type !== "Vec3Function" + ? ValueUtils.parseConstantValue(value as Value) + : typeof value === "number" + ? value + : 1; newValue = { type: "PiecewiseBezier", functions: [ @@ -113,7 +128,12 @@ export function VFXValueEditor(props: IVFXValueEditorProps): ReactNode { {currentType === "ConstantValue" && ( <> {(() => { - const constantValue = value && typeof value !== "number" && "type" in value && value.type !== "Vec3Function" ? VFXValueUtils.parseConstantValue(value as VFXValue) : typeof value === "number" ? value : 1; + const constantValue = + value && typeof value !== "number" && "type" in value && value.type !== "Vec3Function" + ? ValueUtils.parseConstantValue(value as Value) + : typeof value === "number" + ? value + : 1; const wrapperValue = { get value() { return constantValue; @@ -141,7 +161,10 @@ export function VFXValueEditor(props: IVFXValueEditorProps): ReactNode { {currentType === "IntervalValue" && ( <> {(() => { - const interval = value && typeof value !== "number" && "type" in value && value.type !== "Vec3Function" ? VFXValueUtils.parseIntervalValue(value as VFXValue) : { min: 0, max: 1 }; + const interval = + value && typeof value !== "number" && "type" in value && value.type !== "Vec3Function" + ? ValueUtils.parseIntervalValue(value as Value) + : { min: 0, max: 1 }; const wrapperInterval = { get min() { return interval.min; @@ -194,7 +217,7 @@ export function VFXValueEditor(props: IVFXValueEditorProps): ReactNode { {currentType === "PiecewiseBezier" && ( <> {(() => { - // Convert VFXValue to wrapper format for BezierEditor + // Convert VEffectValue to wrapper format for BezierEditor const bezierValue = value && typeof value !== "number" && "type" in value && value.type === "PiecewiseBezier" ? value : null; const wrapperBezier = { get functionType() { @@ -253,12 +276,12 @@ export function VFXValueEditor(props: IVFXValueEditorProps): ReactNode { <>
X
- { onChange({ type: "Vec3Function", - x: newX as VFXValue, + x: newX as Value, y: currentY, z: currentZ, }); @@ -270,13 +293,13 @@ export function VFXValueEditor(props: IVFXValueEditorProps): ReactNode {
Y
- { onChange({ type: "Vec3Function", x: currentX, - y: newY as VFXValue, + y: newY as Value, z: currentZ, }); }} @@ -287,14 +310,14 @@ export function VFXValueEditor(props: IVFXValueEditorProps): ReactNode {
Z
- { onChange({ type: "Vec3Function", x: currentX, y: currentY, - z: newZ as VFXValue, + z: newZ as Value, }); }} availableTypes={["ConstantValue", "IntervalValue", "PiecewiseBezier"]} diff --git a/editor/src/editor/windows/fx-editor/resources.tsx b/editor/src/editor/windows/fx-editor/resources.tsx index 925ba2143..5be69723e 100644 --- a/editor/src/editor/windows/fx-editor/resources.tsx +++ b/editor/src/editor/windows/fx-editor/resources.tsx @@ -5,16 +5,16 @@ import { IoImageOutline, IoCubeOutline } from "react-icons/io5"; import { ContextMenu, ContextMenuContent, ContextMenuItem, ContextMenuTrigger } from "../../../ui/shadcn/ui/context-menu"; -export interface IFXEditorResourcesProps { +export interface IEffectEditorResourcesProps { resources: any[]; } -export interface IFXEditorResourcesState { +export interface IEffectEditorResourcesState { nodes: TreeNodeInfo[]; } -export class FXEditorResources extends Component { - public constructor(props: IFXEditorResourcesProps) { +export class EffectEditorResources extends Component { + public constructor(props: IEffectEditorResourcesProps) { super(props); this.state = { @@ -22,7 +22,7 @@ export class FXEditorResources extends Component { +export class EffectEditorToolbar extends Component { public render(): ReactNode { return ( @@ -45,9 +45,9 @@ export class FXEditorToolbar extends Component {
- FX Editor - {this.props.fxEditor.state.filePath && ( -
(...{this.props.fxEditor.state.filePath.substring(this.props.fxEditor.state.filePath.length - 30)})
+ Effect Editor + {this.props.editor.state.filePath && ( +
(...{this.props.editor.state.filePath.substring(this.props.editor.state.filePath.length - 30)})
)}
@@ -57,50 +57,50 @@ export class FXEditorToolbar extends Component { private _handleOpen(): void { const file = openSingleFileDialog({ - title: "Open FX File", - filters: [{ name: "FX Files", extensions: ["fx", "json"] }], + title: "Open Effect File", + filters: [{ name: "Effect Files", extensions: ["Effect", "json"] }], }); if (!file) { return; } - this.props.fxEditor.loadFile(file); + this.props.editor.loadFile(file); } private _handleSave(): void { - if (!this.props.fxEditor.state.filePath) { + if (!this.props.editor.state.filePath) { this._handleSaveAs(); return; } - this.props.fxEditor.save(); + this.props.editor.save(); } private _handleSaveAs(): void { const file = saveSingleFileDialog({ - title: "Save FX File", - filters: [{ name: "FX Files", extensions: ["fx", "json"] }], - defaultPath: this.props.fxEditor.state.filePath || "untitled.fx", + title: "Save Effect File", + filters: [{ name: "Effect Files", extensions: ["Effect", "json"] }], + defaultPath: this.props.editor.state.filePath || "untitled.Effect", }); if (!file) { return; } - this.props.fxEditor.saveAs(file); + this.props.editor.saveAs(file); } private _handleImport(): void { const file = openSingleFileDialog({ - title: "Import FX File", - filters: [{ name: "FX Files", extensions: ["fx", "json"] }], + title: "Import Effect File", + filters: [{ name: "Effect Files", extensions: ["Effect", "json"] }], }); if (!file) { return; } - this.props.fxEditor.importFile(file); + this.props.editor.importFile(file); } } diff --git a/editor/src/ui/gradient-picker.tsx b/editor/src/ui/gradient-picker.tsx index 133f6989e..541fc4a56 100644 --- a/editor/src/ui/gradient-picker.tsx +++ b/editor/src/ui/gradient-picker.tsx @@ -1,12 +1,11 @@ -import { Component, ReactNode, MouseEvent, useState, useRef, useEffect } from "react"; +import { ReactNode, MouseEvent, useState, useRef, useEffect } from "react"; import { Color3, Color4 } from "babylonjs"; -import { Color } from "@jniac/color-xplr"; import { ColorPicker } from "./color-picker"; import { Button } from "../ui/shadcn/ui/button"; import { AiOutlineClose } from "react-icons/ai"; /** - * Universal gradient key type (not tied to VFX) + * Universal gradient key type (not tied to Effect) */ export interface IGradientKey { time?: number; diff --git a/editor/src/editor/windows/fx-editor/VFX/behaviors/colorBySpeed.ts b/tools/src/effect/behaviors/colorBySpeed.ts similarity index 79% rename from editor/src/editor/windows/fx-editor/VFX/behaviors/colorBySpeed.ts rename to tools/src/effect/behaviors/colorBySpeed.ts index 4b40ee122..49b753972 100644 --- a/editor/src/editor/windows/fx-editor/VFX/behaviors/colorBySpeed.ts +++ b/tools/src/effect/behaviors/colorBySpeed.ts @@ -1,13 +1,13 @@ import { SolidParticle, Particle, Vector3 } from "babylonjs"; -import type { VFXColorBySpeedBehavior } from "../types/behaviors"; +import type { ColorBySpeedBehavior } from "../types/behaviors"; import { interpolateColorKeys } from "./utils"; -import { VFXValueUtils } from "../utils/valueParser"; +import { ValueUtils } from "../utils/valueParser"; /** * Apply ColorBySpeed behavior to Particle * Gets currentSpeed from particle.velocity magnitude */ -export function applyColorBySpeedPS(particle: Particle, behavior: VFXColorBySpeedBehavior): void { +export function applyColorBySpeedPS(particle: Particle, behavior: ColorBySpeedBehavior): void { if (!behavior.color || !behavior.color.keys || !particle.color || !particle.direction) { return; } @@ -16,8 +16,8 @@ export function applyColorBySpeedPS(particle: Particle, behavior: VFXColorBySpee const currentSpeed = Vector3.Distance(Vector3.Zero(), particle.direction); const colorKeys = behavior.color.keys; - const minSpeed = behavior.minSpeed !== undefined ? VFXValueUtils.parseConstantValue(behavior.minSpeed) : 0; - const maxSpeed = behavior.maxSpeed !== undefined ? VFXValueUtils.parseConstantValue(behavior.maxSpeed) : 1; + const minSpeed = behavior.minSpeed !== undefined ? ValueUtils.parseConstantValue(behavior.minSpeed) : 0; + const maxSpeed = behavior.maxSpeed !== undefined ? ValueUtils.parseConstantValue(behavior.maxSpeed) : 1; const speedRatio = Math.max(0, Math.min(1, (currentSpeed - minSpeed) / (maxSpeed - minSpeed || 1))); const interpolatedColor = interpolateColorKeys(colorKeys, speedRatio); @@ -40,7 +40,7 @@ export function applyColorBySpeedPS(particle: Particle, behavior: VFXColorBySpee * Apply ColorBySpeed behavior to SolidParticle * Gets currentSpeed from particle.velocity magnitude */ -export function applyColorBySpeedSPS(particle: SolidParticle, behavior: VFXColorBySpeedBehavior): void { +export function applyColorBySpeedSPS(particle: SolidParticle, behavior: ColorBySpeedBehavior): void { if (!behavior.color || !behavior.color.keys || !particle.color) { return; } @@ -49,8 +49,8 @@ export function applyColorBySpeedSPS(particle: SolidParticle, behavior: VFXColor const currentSpeed = Math.sqrt(particle.velocity.x * particle.velocity.x + particle.velocity.y * particle.velocity.y + particle.velocity.z * particle.velocity.z); const colorKeys = behavior.color.keys; - const minSpeed = behavior.minSpeed !== undefined ? VFXValueUtils.parseConstantValue(behavior.minSpeed) : 0; - const maxSpeed = behavior.maxSpeed !== undefined ? VFXValueUtils.parseConstantValue(behavior.maxSpeed) : 1; + const minSpeed = behavior.minSpeed !== undefined ? ValueUtils.parseConstantValue(behavior.minSpeed) : 0; + const maxSpeed = behavior.maxSpeed !== undefined ? ValueUtils.parseConstantValue(behavior.maxSpeed) : 1; const speedRatio = Math.max(0, Math.min(1, (currentSpeed - minSpeed) / (maxSpeed - minSpeed || 1))); const interpolatedColor = interpolateColorKeys(colorKeys, speedRatio); diff --git a/editor/src/editor/windows/fx-editor/VFX/behaviors/colorOverLife.ts b/tools/src/effect/behaviors/colorOverLife.ts similarity index 91% rename from editor/src/editor/windows/fx-editor/VFX/behaviors/colorOverLife.ts rename to tools/src/effect/behaviors/colorOverLife.ts index 8cac9f9a9..e16fb7173 100644 --- a/editor/src/editor/windows/fx-editor/VFX/behaviors/colorOverLife.ts +++ b/tools/src/effect/behaviors/colorOverLife.ts @@ -1,11 +1,13 @@ -import { Color4, ParticleSystem } from "babylonjs"; -import type { VFXColorOverLifeBehavior } from "../types/behaviors"; +import { Color4 } from "babylonjs"; +import type { ColorOverLifeBehavior } from "../types/behaviors"; import { extractColorFromValue, extractAlphaFromValue } from "./utils"; +import type { EffectSolidParticleSystem } from "../systems/effectSolidParticleSystem"; +import type { EffectParticleSystem } from "../systems/effectParticleSystem"; /** * Apply ColorOverLife behavior to ParticleSystem */ -export function applyColorOverLifePS(particleSystem: ParticleSystem, behavior: VFXColorOverLifeBehavior): void { +export function applyColorOverLifePS(particleSystem: EffectParticleSystem, behavior: ColorOverLifeBehavior): void { if (behavior.color && behavior.color.color && behavior.color.color.keys) { const colorKeys = behavior.color.color.keys; for (const key of colorKeys) { @@ -42,7 +44,7 @@ export function applyColorOverLifePS(particleSystem: ParticleSystem, behavior: V * Adds color gradients to the system (similar to ParticleSystem native gradients) * Properly combines color and alpha keys even when they have different positions */ -export function applyColorOverLifeSPS(system: any, behavior: VFXColorOverLifeBehavior): void { +export function applyColorOverLifeSPS(system: EffectSolidParticleSystem, behavior: ColorOverLifeBehavior): void { if (!behavior.color) { return; } diff --git a/tools/src/effect/behaviors/forceOverLife.ts b/tools/src/effect/behaviors/forceOverLife.ts new file mode 100644 index 000000000..93557d74e --- /dev/null +++ b/tools/src/effect/behaviors/forceOverLife.ts @@ -0,0 +1,34 @@ +import { Vector3 } from "babylonjs"; +import type { ForceOverLifeBehavior, GravityForceBehavior } from "../types/behaviors"; +import { ValueUtils } from "../utils/valueParser"; +import type { EffectParticleSystem } from "../systems/effectParticleSystem"; +/** + * Apply ForceOverLife behavior to ParticleSystem + */ +export function applyForceOverLifePS(particleSystem: EffectParticleSystem, behavior: ForceOverLifeBehavior): void { + if (behavior.force) { + const forceX = behavior.force.x !== undefined ? ValueUtils.parseConstantValue(behavior.force.x) : 0; + const forceY = behavior.force.y !== undefined ? ValueUtils.parseConstantValue(behavior.force.y) : 0; + const forceZ = behavior.force.z !== undefined ? ValueUtils.parseConstantValue(behavior.force.z) : 0; + if (Math.abs(forceY) > 0.01 || Math.abs(forceX) > 0.01 || Math.abs(forceZ) > 0.01) { + particleSystem.gravity = new Vector3(forceX, forceY, forceZ); + } + } else if (behavior.x !== undefined || behavior.y !== undefined || behavior.z !== undefined) { + const forceX = behavior.x !== undefined ? ValueUtils.parseConstantValue(behavior.x) : 0; + const forceY = behavior.y !== undefined ? ValueUtils.parseConstantValue(behavior.y) : 0; + const forceZ = behavior.z !== undefined ? ValueUtils.parseConstantValue(behavior.z) : 0; + if (Math.abs(forceY) > 0.01 || Math.abs(forceX) > 0.01 || Math.abs(forceZ) > 0.01) { + particleSystem.gravity = new Vector3(forceX, forceY, forceZ); + } + } +} + +/** + * Apply GravityForce behavior to ParticleSystem + */ +export function applyGravityForcePS(particleSystem: EffectParticleSystem, behavior: GravityForceBehavior): void { + if (behavior.gravity !== undefined) { + const gravity = ValueUtils.parseConstantValue(behavior.gravity); + particleSystem.gravity = new Vector3(0, -gravity, 0); + } +} diff --git a/editor/src/editor/windows/fx-editor/VFX/behaviors/frameOverLife.ts b/tools/src/effect/behaviors/frameOverLife.ts similarity index 72% rename from editor/src/editor/windows/fx-editor/VFX/behaviors/frameOverLife.ts rename to tools/src/effect/behaviors/frameOverLife.ts index 9e046b63c..257765762 100644 --- a/editor/src/editor/windows/fx-editor/VFX/behaviors/frameOverLife.ts +++ b/tools/src/effect/behaviors/frameOverLife.ts @@ -1,11 +1,10 @@ -import { ParticleSystem } from "babylonjs"; -import type { VFXFrameOverLifeBehavior } from "../types/behaviors"; -import { VFXValueUtils } from "../utils/valueParser"; - +import type { FrameOverLifeBehavior } from "../types/behaviors"; +import { ValueUtils } from "../utils/valueParser"; +import type { EffectParticleSystem } from "../systems/effectParticleSystem"; /** * Apply FrameOverLife behavior to ParticleSystem */ -export function applyFrameOverLifePS(particleSystem: ParticleSystem, behavior: VFXFrameOverLifeBehavior): void { +export function applyFrameOverLifePS(particleSystem: EffectParticleSystem, behavior: FrameOverLifeBehavior): void { if (!behavior.frame) { return; } @@ -28,7 +27,7 @@ export function applyFrameOverLifePS(particleSystem: ParticleSystem, behavior: V particleSystem.endSpriteCellID = Math.floor(frames[frames.length - 1] || frames[0]); } } else if (typeof behavior.frame === "number" || (typeof behavior.frame === "object" && behavior.frame !== null && "type" in behavior.frame)) { - const frameValue = VFXValueUtils.parseConstantValue(behavior.frame); + const frameValue = ValueUtils.parseConstantValue(behavior.frame); particleSystem.startSpriteCellID = Math.floor(frameValue); particleSystem.endSpriteCellID = Math.floor(frameValue); } diff --git a/editor/src/editor/windows/fx-editor/VFX/behaviors/index.ts b/tools/src/effect/behaviors/index.ts similarity index 100% rename from editor/src/editor/windows/fx-editor/VFX/behaviors/index.ts rename to tools/src/effect/behaviors/index.ts diff --git a/editor/src/editor/windows/fx-editor/VFX/behaviors/limitSpeedOverLife.ts b/tools/src/effect/behaviors/limitSpeedOverLife.ts similarity index 70% rename from editor/src/editor/windows/fx-editor/VFX/behaviors/limitSpeedOverLife.ts rename to tools/src/effect/behaviors/limitSpeedOverLife.ts index 493aa32bf..f834d10e4 100644 --- a/editor/src/editor/windows/fx-editor/VFX/behaviors/limitSpeedOverLife.ts +++ b/tools/src/effect/behaviors/limitSpeedOverLife.ts @@ -1,19 +1,19 @@ -import { ParticleSystem } from "babylonjs"; -import type { VFXLimitSpeedOverLifeBehavior } from "../types/behaviors"; +import type { LimitSpeedOverLifeBehavior } from "../types/behaviors"; import { extractNumberFromValue } from "./utils"; -import { VFXValueUtils } from "../utils/valueParser"; - +import { ValueUtils } from "../utils/valueParser"; +import type { EffectSolidParticleSystem } from "../systems/effectSolidParticleSystem"; +import type { EffectParticleSystem } from "../systems/effectParticleSystem"; /** * Apply LimitSpeedOverLife behavior to ParticleSystem */ -export function applyLimitSpeedOverLifePS(particleSystem: ParticleSystem, behavior: VFXLimitSpeedOverLifeBehavior): void { +export function applyLimitSpeedOverLifePS(particleSystem: EffectParticleSystem, behavior: LimitSpeedOverLifeBehavior): void { if (behavior.dampen !== undefined) { - const dampen = VFXValueUtils.parseConstantValue(behavior.dampen); + const dampen = ValueUtils.parseConstantValue(behavior.dampen); particleSystem.limitVelocityDamping = dampen; } if (behavior.maxSpeed !== undefined) { - const speedLimit = VFXValueUtils.parseConstantValue(behavior.maxSpeed); + const speedLimit = ValueUtils.parseConstantValue(behavior.maxSpeed); particleSystem.addLimitVelocityGradient(0, speedLimit); particleSystem.addLimitVelocityGradient(1, speedLimit); } else if (behavior.speed !== undefined) { @@ -27,7 +27,7 @@ export function applyLimitSpeedOverLifePS(particleSystem: ParticleSystem, behavi } } } else if (typeof behavior.speed === "number" || (typeof behavior.speed === "object" && behavior.speed !== null && "type" in behavior.speed)) { - const speedLimit = VFXValueUtils.parseConstantValue(behavior.speed); + const speedLimit = ValueUtils.parseConstantValue(behavior.speed); particleSystem.addLimitVelocityGradient(0, speedLimit); particleSystem.addLimitVelocityGradient(1, speedLimit); } @@ -38,14 +38,14 @@ export function applyLimitSpeedOverLifePS(particleSystem: ParticleSystem, behavi * Apply LimitSpeedOverLife behavior to SolidParticleSystem * Adds limit velocity gradients to the system (similar to ParticleSystem native gradients) */ -export function applyLimitSpeedOverLifeSPS(system: any, behavior: VFXLimitSpeedOverLifeBehavior): void { +export function applyLimitSpeedOverLifeSPS(system: EffectSolidParticleSystem, behavior: LimitSpeedOverLifeBehavior): void { if (behavior.dampen !== undefined) { - const dampen = VFXValueUtils.parseConstantValue(behavior.dampen); + const dampen = ValueUtils.parseConstantValue(behavior.dampen); system.limitVelocityDamping = dampen; } if (behavior.maxSpeed !== undefined) { - const speedLimit = VFXValueUtils.parseConstantValue(behavior.maxSpeed); + const speedLimit = ValueUtils.parseConstantValue(behavior.maxSpeed); system.addLimitVelocityGradient(0, speedLimit); system.addLimitVelocityGradient(1, speedLimit); } else if (behavior.speed !== undefined) { @@ -59,7 +59,7 @@ export function applyLimitSpeedOverLifeSPS(system: any, behavior: VFXLimitSpeedO } } } else if (typeof behavior.speed === "number" || (typeof behavior.speed === "object" && behavior.speed !== null && "type" in behavior.speed)) { - const speedLimit = VFXValueUtils.parseConstantValue(behavior.speed); + const speedLimit = ValueUtils.parseConstantValue(behavior.speed); system.addLimitVelocityGradient(0, speedLimit); system.addLimitVelocityGradient(1, speedLimit); } diff --git a/editor/src/editor/windows/fx-editor/VFX/behaviors/orbitOverLife.ts b/tools/src/effect/behaviors/orbitOverLife.ts similarity index 76% rename from editor/src/editor/windows/fx-editor/VFX/behaviors/orbitOverLife.ts rename to tools/src/effect/behaviors/orbitOverLife.ts index 062e29bfa..560b245a2 100644 --- a/editor/src/editor/windows/fx-editor/VFX/behaviors/orbitOverLife.ts +++ b/tools/src/effect/behaviors/orbitOverLife.ts @@ -1,14 +1,14 @@ import { Particle, SolidParticle } from "babylonjs"; -import type { VFXOrbitOverLifeBehavior } from "../types/behaviors"; +import type { OrbitOverLifeBehavior } from "../types/behaviors"; import { extractNumberFromValue, interpolateGradientKeys } from "./utils"; -import { VFXValueUtils } from "../utils/valueParser"; -import type { VFXValue } from "../types/values"; +import { ValueUtils } from "../utils/valueParser"; +import type { Value } from "../types/values"; /** * Apply OrbitOverLife behavior to Particle * Gets lifeRatio from particle (age / lifeTime) */ -export function applyOrbitOverLifePS(particle: Particle, behavior: VFXOrbitOverLifeBehavior): void { +export function applyOrbitOverLifePS(particle: Particle, behavior: OrbitOverLifeBehavior): void { if (!behavior.radius || particle.lifeTime <= 0) { return; } @@ -16,7 +16,7 @@ export function applyOrbitOverLifePS(particle: Particle, behavior: VFXOrbitOverL // Get lifeRatio from particle const lifeRatio = particle.age / particle.lifeTime; - // Parse radius (can be VFXValue with keys or constant/interval) + // Parse radius (can be Value with keys or constant/interval) let radius = 1; const radiusValue = behavior.radius; @@ -31,12 +31,12 @@ export function applyOrbitOverLifePS(particle: Particle, behavior: VFXOrbitOverL ) { radius = interpolateGradientKeys(radiusValue.keys, lifeRatio, extractNumberFromValue); } else if (radiusValue !== undefined && radiusValue !== null) { - // Parse as VFXValue (number, VFXConstantValue, or VFXIntervalValue) - const parsedRadius = VFXValueUtils.parseIntervalValue(radiusValue as VFXValue); + // Parse as Value (number, ConstantValue, or IntervalValue) + const parsedRadius = ValueUtils.parseIntervalValue(radiusValue as Value); radius = parsedRadius.min + (parsedRadius.max - parsedRadius.min) * lifeRatio; } - const speed = behavior.speed !== undefined ? VFXValueUtils.parseConstantValue(behavior.speed) : 1; + const speed = behavior.speed !== undefined ? ValueUtils.parseConstantValue(behavior.speed) : 1; const angle = lifeRatio * speed * Math.PI * 2; // Calculate orbit offset relative to center @@ -60,7 +60,7 @@ export function applyOrbitOverLifePS(particle: Particle, behavior: VFXOrbitOverL * Apply OrbitOverLife behavior to SolidParticle * Gets lifeRatio from particle (age / lifeTime) */ -export function applyOrbitOverLifeSPS(particle: SolidParticle, behavior: VFXOrbitOverLifeBehavior): void { +export function applyOrbitOverLifeSPS(particle: SolidParticle, behavior: OrbitOverLifeBehavior): void { if (!behavior.radius || particle.lifeTime <= 0) { return; } @@ -68,7 +68,7 @@ export function applyOrbitOverLifeSPS(particle: SolidParticle, behavior: VFXOrbi // Get lifeRatio from particle const lifeRatio = particle.age / particle.lifeTime; - // Parse radius (can be VFXValue with keys or constant/interval) + // Parse radius (can be Value with keys or constant/interval) let radius = 1; const radiusValue = behavior.radius; @@ -83,12 +83,12 @@ export function applyOrbitOverLifeSPS(particle: SolidParticle, behavior: VFXOrbi ) { radius = interpolateGradientKeys(radiusValue.keys, lifeRatio, extractNumberFromValue); } else if (radiusValue !== undefined && radiusValue !== null) { - // Parse as VFXValue (number, VFXConstantValue, or VFXIntervalValue) - const parsedRadius = VFXValueUtils.parseIntervalValue(radiusValue as VFXValue); + // Parse as Value (number, ConstantValue, or IntervalValue) + const parsedRadius = ValueUtils.parseIntervalValue(radiusValue as Value); radius = parsedRadius.min + (parsedRadius.max - parsedRadius.min) * lifeRatio; } - const speed = behavior.speed !== undefined ? VFXValueUtils.parseConstantValue(behavior.speed) : 1; + const speed = behavior.speed !== undefined ? ValueUtils.parseConstantValue(behavior.speed) : 1; const angle = lifeRatio * speed * Math.PI * 2; // Calculate orbit offset relative to center diff --git a/editor/src/editor/windows/fx-editor/VFX/behaviors/rotationBySpeed.ts b/tools/src/effect/behaviors/rotationBySpeed.ts similarity index 73% rename from editor/src/editor/windows/fx-editor/VFX/behaviors/rotationBySpeed.ts rename to tools/src/effect/behaviors/rotationBySpeed.ts index 12b1843e4..63b51e4c6 100644 --- a/editor/src/editor/windows/fx-editor/VFX/behaviors/rotationBySpeed.ts +++ b/tools/src/effect/behaviors/rotationBySpeed.ts @@ -1,13 +1,14 @@ import { Particle, SolidParticle, Vector3 } from "babylonjs"; -import type { VFXRotationBySpeedBehavior } from "../types/behaviors"; +import type { RotationBySpeedBehavior } from "../types/behaviors"; import { extractNumberFromValue, interpolateGradientKeys } from "./utils"; -import { VFXValueUtils } from "../utils/valueParser"; +import { ValueUtils } from "../utils/valueParser"; +import { ParticleWithSystem, SolidParticleWithSystem } from "../types/system"; /** * Apply RotationBySpeed behavior to Particle * Gets currentSpeed from particle.direction magnitude and updateSpeed from system */ -export function applyRotationBySpeedPS(particle: Particle, behavior: VFXRotationBySpeedBehavior): void { +export function applyRotationBySpeedPS(particle: Particle, behavior: RotationBySpeedBehavior): void { if (!behavior.angularVelocity || !particle.direction) { return; } @@ -19,7 +20,7 @@ export function applyRotationBySpeedPS(particle: Particle, behavior: VFXRotation const particleWithSystem = particle as ParticleWithSystem; const updateSpeed = particleWithSystem.particleSystem?.updateSpeed ?? 0.016; - // angularVelocity can be VFXValue (constant/interval) or object with keys + // angularVelocity can be Value (constant/interval) or object with keys let angularSpeed = 0; if ( typeof behavior.angularVelocity === "object" && @@ -28,12 +29,12 @@ export function applyRotationBySpeedPS(particle: Particle, behavior: VFXRotation Array.isArray(behavior.angularVelocity.keys) && behavior.angularVelocity.keys.length > 0 ) { - const minSpeed = behavior.minSpeed !== undefined ? VFXValueUtils.parseConstantValue(behavior.minSpeed) : 0; - const maxSpeed = behavior.maxSpeed !== undefined ? VFXValueUtils.parseConstantValue(behavior.maxSpeed) : 1; + const minSpeed = behavior.minSpeed !== undefined ? ValueUtils.parseConstantValue(behavior.minSpeed) : 0; + const maxSpeed = behavior.maxSpeed !== undefined ? ValueUtils.parseConstantValue(behavior.maxSpeed) : 1; const speedRatio = Math.max(0, Math.min(1, (currentSpeed - minSpeed) / (maxSpeed - minSpeed || 1))); angularSpeed = interpolateGradientKeys(behavior.angularVelocity.keys, speedRatio, extractNumberFromValue); } else { - const angularVel = VFXValueUtils.parseIntervalValue(behavior.angularVelocity); + const angularVel = ValueUtils.parseIntervalValue(behavior.angularVelocity); angularSpeed = angularVel.min + (angularVel.max - angularVel.min) * 0.5; // Use middle value } @@ -44,7 +45,7 @@ export function applyRotationBySpeedPS(particle: Particle, behavior: VFXRotation * Apply RotationBySpeed behavior to SolidParticle * Gets currentSpeed from particle.velocity magnitude and updateSpeed from system */ -export function applyRotationBySpeedSPS(particle: SolidParticle, behavior: VFXRotationBySpeedBehavior): void { +export function applyRotationBySpeedSPS(particle: SolidParticle, behavior: RotationBySpeedBehavior): void { if (!behavior.angularVelocity) { return; } @@ -56,7 +57,7 @@ export function applyRotationBySpeedSPS(particle: SolidParticle, behavior: VFXRo const particleWithSystem = particle as SolidParticleWithSystem; const updateSpeed = particleWithSystem.system?.updateSpeed ?? 0.016; - // angularVelocity can be VFXValue (constant/interval) or object with keys + // angularVelocity can be Value (constant/interval) or object with keys let angularSpeed = 0; if ( typeof behavior.angularVelocity === "object" && @@ -65,12 +66,12 @@ export function applyRotationBySpeedSPS(particle: SolidParticle, behavior: VFXRo Array.isArray(behavior.angularVelocity.keys) && behavior.angularVelocity.keys.length > 0 ) { - const minSpeed = behavior.minSpeed !== undefined ? VFXValueUtils.parseConstantValue(behavior.minSpeed) : 0; - const maxSpeed = behavior.maxSpeed !== undefined ? VFXValueUtils.parseConstantValue(behavior.maxSpeed) : 1; + const minSpeed = behavior.minSpeed !== undefined ? ValueUtils.parseConstantValue(behavior.minSpeed) : 0; + const maxSpeed = behavior.maxSpeed !== undefined ? ValueUtils.parseConstantValue(behavior.maxSpeed) : 1; const speedRatio = Math.max(0, Math.min(1, (currentSpeed - minSpeed) / (maxSpeed - minSpeed || 1))); angularSpeed = interpolateGradientKeys(behavior.angularVelocity.keys, speedRatio, extractNumberFromValue); } else { - const angularVel = VFXValueUtils.parseIntervalValue(behavior.angularVelocity); + const angularVel = ValueUtils.parseIntervalValue(behavior.angularVelocity); angularSpeed = angularVel.min + (angularVel.max - angularVel.min) * 0.5; // Use middle value } diff --git a/editor/src/editor/windows/fx-editor/VFX/behaviors/rotationOverLife.ts b/tools/src/effect/behaviors/rotationOverLife.ts similarity index 83% rename from editor/src/editor/windows/fx-editor/VFX/behaviors/rotationOverLife.ts rename to tools/src/effect/behaviors/rotationOverLife.ts index 7170620ed..4ea4a7e90 100644 --- a/editor/src/editor/windows/fx-editor/VFX/behaviors/rotationOverLife.ts +++ b/tools/src/effect/behaviors/rotationOverLife.ts @@ -1,13 +1,13 @@ -import { ParticleSystem } from "babylonjs"; -import type { VFXRotationOverLifeBehavior } from "../types/behaviors"; -import { VFXValueUtils } from "../utils/valueParser"; +import type { RotationOverLifeBehavior } from "../types/behaviors"; +import { ValueUtils } from "../utils/valueParser"; import { extractNumberFromValue } from "./utils"; - +import type { EffectSolidParticleSystem } from "../systems/effectSolidParticleSystem"; +import type { EffectParticleSystem } from "../systems/effectParticleSystem"; /** * Apply RotationOverLife behavior to ParticleSystem * Uses addAngularSpeedGradient for gradient support (Babylon.js native) */ -export function applyRotationOverLifePS(particleSystem: ParticleSystem, behavior: VFXRotationOverLifeBehavior): void { +export function applyRotationOverLifePS(particleSystem: EffectParticleSystem, behavior: RotationOverLifeBehavior): void { if (!behavior.angularVelocity) { return; } @@ -49,7 +49,7 @@ export function applyRotationOverLifePS(particleSystem: ParticleSystem, behavior } } else { // Fallback to interval (min/max) - use gradient with min at 0 and max at 1 - const angularVel = VFXValueUtils.parseIntervalValue(behavior.angularVelocity); + const angularVel = ValueUtils.parseIntervalValue(behavior.angularVelocity); particleSystem.addAngularSpeedGradient(0, angularVel.min); particleSystem.addAngularSpeedGradient(1, angularVel.max); } @@ -59,7 +59,7 @@ export function applyRotationOverLifePS(particleSystem: ParticleSystem, behavior * Apply RotationOverLife behavior to SolidParticleSystem * Adds angular speed gradients to the system (similar to ParticleSystem native gradients) */ -export function applyRotationOverLifeSPS(system: any, behavior: VFXRotationOverLifeBehavior): void { +export function applyRotationOverLifeSPS(system: EffectSolidParticleSystem, behavior: RotationOverLifeBehavior): void { if (!behavior.angularVelocity) { return; } @@ -101,7 +101,7 @@ export function applyRotationOverLifeSPS(system: any, behavior: VFXRotationOverL } } else { // Fallback to interval (min/max) - use gradient with min at 0 and max at 1 - const angularVel = VFXValueUtils.parseIntervalValue(behavior.angularVelocity); + const angularVel = ValueUtils.parseIntervalValue(behavior.angularVelocity); system.addAngularSpeedGradient(0, angularVel.min); system.addAngularSpeedGradient(1, angularVel.max); } diff --git a/editor/src/editor/windows/fx-editor/VFX/behaviors/sizeBySpeed.ts b/tools/src/effect/behaviors/sizeBySpeed.ts similarity index 72% rename from editor/src/editor/windows/fx-editor/VFX/behaviors/sizeBySpeed.ts rename to tools/src/effect/behaviors/sizeBySpeed.ts index 781fc91bf..ea0718aca 100644 --- a/editor/src/editor/windows/fx-editor/VFX/behaviors/sizeBySpeed.ts +++ b/tools/src/effect/behaviors/sizeBySpeed.ts @@ -1,13 +1,13 @@ import { Particle, SolidParticle, Vector3 } from "babylonjs"; -import type { VFXSizeBySpeedBehavior } from "../types/behaviors"; +import type { SizeBySpeedBehavior } from "../types/behaviors"; import { extractNumberFromValue, interpolateGradientKeys } from "./utils"; -import { VFXValueUtils } from "../utils/valueParser"; +import { ValueUtils } from "../utils/valueParser"; /** * Apply SizeBySpeed behavior to Particle * Gets currentSpeed from particle.direction magnitude */ -export function applySizeBySpeedPS(particle: Particle, behavior: VFXSizeBySpeedBehavior): void { +export function applySizeBySpeedPS(particle: Particle, behavior: SizeBySpeedBehavior): void { if (!behavior.size || !behavior.size.keys || !particle.direction) { return; } @@ -16,8 +16,8 @@ export function applySizeBySpeedPS(particle: Particle, behavior: VFXSizeBySpeedB const currentSpeed = Vector3.Distance(Vector3.Zero(), particle.direction); const sizeKeys = behavior.size.keys; - const minSpeed = behavior.minSpeed !== undefined ? VFXValueUtils.parseConstantValue(behavior.minSpeed) : 0; - const maxSpeed = behavior.maxSpeed !== undefined ? VFXValueUtils.parseConstantValue(behavior.maxSpeed) : 1; + const minSpeed = behavior.minSpeed !== undefined ? ValueUtils.parseConstantValue(behavior.minSpeed) : 0; + const maxSpeed = behavior.maxSpeed !== undefined ? ValueUtils.parseConstantValue(behavior.maxSpeed) : 1; const speedRatio = Math.max(0, Math.min(1, (currentSpeed - minSpeed) / (maxSpeed - minSpeed || 1))); const sizeMultiplier = interpolateGradientKeys(sizeKeys, speedRatio, extractNumberFromValue); @@ -29,7 +29,7 @@ export function applySizeBySpeedPS(particle: Particle, behavior: VFXSizeBySpeedB * Apply SizeBySpeed behavior to SolidParticle * Gets currentSpeed from particle.velocity magnitude */ -export function applySizeBySpeedSPS(particle: SolidParticle, behavior: VFXSizeBySpeedBehavior): void { +export function applySizeBySpeedSPS(particle: SolidParticle, behavior: SizeBySpeedBehavior): void { if (!behavior.size || !behavior.size.keys) { return; } @@ -38,8 +38,8 @@ export function applySizeBySpeedSPS(particle: SolidParticle, behavior: VFXSizeBy const currentSpeed = Math.sqrt(particle.velocity.x * particle.velocity.x + particle.velocity.y * particle.velocity.y + particle.velocity.z * particle.velocity.z); const sizeKeys = behavior.size.keys; - const minSpeed = behavior.minSpeed !== undefined ? VFXValueUtils.parseConstantValue(behavior.minSpeed) : 0; - const maxSpeed = behavior.maxSpeed !== undefined ? VFXValueUtils.parseConstantValue(behavior.maxSpeed) : 1; + const minSpeed = behavior.minSpeed !== undefined ? ValueUtils.parseConstantValue(behavior.minSpeed) : 0; + const maxSpeed = behavior.maxSpeed !== undefined ? ValueUtils.parseConstantValue(behavior.maxSpeed) : 1; const speedRatio = Math.max(0, Math.min(1, (currentSpeed - minSpeed) / (maxSpeed - minSpeed || 1))); const sizeMultiplier = interpolateGradientKeys(sizeKeys, speedRatio, extractNumberFromValue); diff --git a/editor/src/editor/windows/fx-editor/VFX/behaviors/sizeOverLife.ts b/tools/src/effect/behaviors/sizeOverLife.ts similarity index 83% rename from editor/src/editor/windows/fx-editor/VFX/behaviors/sizeOverLife.ts rename to tools/src/effect/behaviors/sizeOverLife.ts index 22b7142a7..cc7339502 100644 --- a/editor/src/editor/windows/fx-editor/VFX/behaviors/sizeOverLife.ts +++ b/tools/src/effect/behaviors/sizeOverLife.ts @@ -1,13 +1,13 @@ -import { ParticleSystem } from "babylonjs"; -import type { VFXSizeOverLifeBehavior } from "../types/behaviors"; +import type { SizeOverLifeBehavior } from "../types/behaviors"; import { extractNumberFromValue } from "./utils"; - +import type { EffectSolidParticleSystem } from "../systems/effectSolidParticleSystem"; +import type { EffectParticleSystem } from "../systems/effectParticleSystem"; /** * Apply SizeOverLife behavior to ParticleSystem * In Quarks, SizeOverLife values are multipliers relative to initial particle size * In Babylon.js, sizeGradients are absolute values, so we multiply by average initial size */ -export function applySizeOverLifePS(particleSystem: ParticleSystem, behavior: VFXSizeOverLifeBehavior): void { +export function applySizeOverLifePS(particleSystem: EffectParticleSystem, behavior: SizeOverLifeBehavior): void { // Get average initial size from minSize/maxSize to use as base for multipliers const avgInitialSize = (particleSystem.minSize + particleSystem.maxSize) / 2; @@ -39,7 +39,7 @@ export function applySizeOverLifePS(particleSystem: ParticleSystem, behavior: VF * Apply SizeOverLife behavior to SolidParticleSystem * Adds size gradients to the system (similar to ParticleSystem native gradients) */ -export function applySizeOverLifeSPS(system: any, behavior: VFXSizeOverLifeBehavior): void { +export function applySizeOverLifeSPS(system: EffectSolidParticleSystem, behavior: SizeOverLifeBehavior): void { if (!behavior.size) { return; } diff --git a/editor/src/editor/windows/fx-editor/VFX/behaviors/speedOverLife.ts b/tools/src/effect/behaviors/speedOverLife.ts similarity index 82% rename from editor/src/editor/windows/fx-editor/VFX/behaviors/speedOverLife.ts rename to tools/src/effect/behaviors/speedOverLife.ts index 6f94f58a5..3bd7cdea8 100644 --- a/editor/src/editor/windows/fx-editor/VFX/behaviors/speedOverLife.ts +++ b/tools/src/effect/behaviors/speedOverLife.ts @@ -1,12 +1,12 @@ -import { ParticleSystem } from "babylonjs"; -import type { VFXSpeedOverLifeBehavior } from "../types/behaviors"; +import type { SpeedOverLifeBehavior } from "../types/behaviors"; import { extractNumberFromValue } from "./utils"; -import { VFXValueUtils } from "../utils/valueParser"; - +import { ValueUtils } from "../utils/valueParser"; +import type { EffectSolidParticleSystem } from "../systems/effectSolidParticleSystem"; +import type { EffectParticleSystem } from "../systems/effectParticleSystem"; /** * Apply SpeedOverLife behavior to ParticleSystem */ -export function applySpeedOverLifePS(particleSystem: ParticleSystem, behavior: VFXSpeedOverLifeBehavior): void { +export function applySpeedOverLifePS(particleSystem: EffectParticleSystem, behavior: SpeedOverLifeBehavior): void { if (behavior.speed) { if (typeof behavior.speed === "object" && behavior.speed !== null && "keys" in behavior.speed && behavior.speed.keys && Array.isArray(behavior.speed.keys)) { for (const key of behavior.speed.keys) { @@ -35,7 +35,7 @@ export function applySpeedOverLifePS(particleSystem: ParticleSystem, behavior: V } } } else if (typeof behavior.speed === "number" || (typeof behavior.speed === "object" && behavior.speed !== null && "type" in behavior.speed)) { - const speedValue = VFXValueUtils.parseIntervalValue(behavior.speed); + const speedValue = ValueUtils.parseIntervalValue(behavior.speed); particleSystem.addVelocityGradient(0, speedValue.min); particleSystem.addVelocityGradient(1, speedValue.max); } @@ -46,7 +46,7 @@ export function applySpeedOverLifePS(particleSystem: ParticleSystem, behavior: V * Apply SpeedOverLife behavior to SolidParticleSystem * Adds velocity gradients to the system (similar to ParticleSystem native gradients) */ -export function applySpeedOverLifeSPS(system: any, behavior: VFXSpeedOverLifeBehavior): void { +export function applySpeedOverLifeSPS(system: EffectSolidParticleSystem, behavior: SpeedOverLifeBehavior): void { if (!behavior.speed) { return; } @@ -78,7 +78,7 @@ export function applySpeedOverLifeSPS(system: any, behavior: VFXSpeedOverLifeBeh } } } else if (typeof behavior.speed === "number" || (typeof behavior.speed === "object" && behavior.speed !== null && "type" in behavior.speed)) { - const speedValue = VFXValueUtils.parseIntervalValue(behavior.speed); + const speedValue = ValueUtils.parseIntervalValue(behavior.speed); system.addVelocityGradient(0, speedValue.min); system.addVelocityGradient(1, speedValue.max); } diff --git a/editor/src/editor/windows/fx-editor/VFX/behaviors/utils.ts b/tools/src/effect/behaviors/utils.ts similarity index 94% rename from editor/src/editor/windows/fx-editor/VFX/behaviors/utils.ts rename to tools/src/effect/behaviors/utils.ts index cf14a2376..3d0b84aba 100644 --- a/editor/src/editor/windows/fx-editor/VFX/behaviors/utils.ts +++ b/tools/src/effect/behaviors/utils.ts @@ -1,4 +1,4 @@ -import type { VFXGradientKey } from "../types/gradients"; +import type { GradientKey } from "../types/gradients"; /** * Extract RGB color from gradient key value @@ -77,7 +77,7 @@ export function extractNumberFromValue(value: number | number[] | { r: number; g * Interpolate between two gradient keys */ export function interpolateGradientKeys( - keys: VFXGradientKey[], + keys: GradientKey[], ratio: number, extractValue: (value: number | number[] | { r: number; g: number; b: number; a?: number } | undefined) => number ): number { @@ -112,7 +112,7 @@ export function interpolateGradientKeys( /** * Interpolate color between two gradient keys */ -export function interpolateColorKeys(keys: VFXGradientKey[], ratio: number): { r: number; g: number; b: number; a: number } { +export function interpolateColorKeys(keys: GradientKey[], ratio: number): { r: number; g: number; b: number; a: number } { if (!keys || keys.length === 0) { return { r: 1, g: 1, b: 1, a: 1 }; } diff --git a/editor/src/editor/windows/fx-editor/VFX/VFXEffect.ts b/tools/src/effect/effect.ts similarity index 76% rename from editor/src/editor/windows/fx-editor/VFX/VFXEffect.ts rename to tools/src/effect/effect.ts index d07719acf..cd14c8429 100644 --- a/editor/src/editor/windows/fx-editor/VFX/VFXEffect.ts +++ b/tools/src/effect/effect.ts @@ -1,64 +1,64 @@ import { Scene, Tools, IDisposable, TransformNode, Vector3, CreatePlane, MeshBuilder, Texture } from "babylonjs"; -import type { QuarksVFXJSON } from "./types/quarksTypes"; -import type { VFXLoaderOptions } from "./types/loader"; -import { VFXParser } from "./parsers/VFXParser"; -import { VFXParticleSystem } from "./systems/VFXParticleSystem"; -import { VFXSolidParticleSystem } from "./systems/VFXSolidParticleSystem"; -import type { VFXGroup, VFXEmitter, VFXData } from "./types/hierarchy"; -import type { VFXParticleEmitterConfig } from "./types/emitterConfig"; -import { isVFXSystem } from "./types/system"; -import { VFXEmitterFactory } from "./factories/VFXEmitterFactory"; +import type { QuarksJSON } from "./types/quarksTypes"; +import type { LoaderOptions } from "./types/loader"; +import { Parser } from "./parsers/parser"; +import { EffectParticleSystem } from "./systems/effectParticleSystem"; +import { EffectSolidParticleSystem } from "./systems/effectSolidParticleSystem"; +import type { Group, Emitter, Data } from "./types/hierarchy"; +import type { EmitterConfig } from "./types/emitter"; +import { isSystem } from "./types/system"; +import { EmitterFactory } from "./factories/emitterFactory"; /** - * VFX Effect Node - represents either a particle system or a group + * Effect Node - represents either a particle system or a group */ -export interface VFXEffectNode { +export interface EffectNode { /** Node name */ name: string; /** Node UUID from original JSON */ uuid?: string; /** Particle system (if this is a particle emitter) */ - system?: VFXParticleSystem | VFXSolidParticleSystem; + system?: EffectParticleSystem | EffectSolidParticleSystem; /** Transform node (if this is a group) */ group?: TransformNode; /** Parent node */ - parent?: VFXEffectNode; + parent?: EffectNode; /** Child nodes */ - children: VFXEffectNode[]; + children: EffectNode[]; /** Node type */ type: "particle" | "group"; } /** - * VFX Effect containing multiple particle systems with hierarchy support - * Main entry point for loading and creating VFX from Three.js particle JSON files + * Effect containing multiple particle systems with hierarchy support + * Main entry point for loading and creating from Three.js particle JSON files */ -export class VFXEffect implements IDisposable { +export class Effect implements IDisposable { /** All particle systems in this effect */ - private _systems: (VFXParticleSystem | VFXSolidParticleSystem)[] = []; + private _systems: (EffectParticleSystem | EffectSolidParticleSystem)[] = []; /** Root node of the effect hierarchy */ - private _root: VFXEffectNode | null = null; + private _root: EffectNode | null = null; /** * Get all particle systems in this effect */ - public get systems(): ReadonlyArray { + public get systems(): ReadonlyArray { return this._systems; } /** * Get root node of the effect hierarchy */ - public get root(): VFXEffectNode | null { + public get root(): EffectNode | null { return this._root; } /** Map of systems by name for quick lookup */ - private readonly _systemsByName = new Map(); + private readonly _systemsByName = new Map(); /** Map of systems by UUID for quick lookup */ - private readonly _systemsByUuid = new Map(); + private readonly _systemsByUuid = new Map(); /** Map of groups by name */ private readonly _groupsByName = new Map(); @@ -67,7 +67,7 @@ export class VFXEffect implements IDisposable { private readonly _groupsByUuid = new Map(); /** All nodes in the hierarchy */ - private readonly _nodes = new Map(); + private readonly _nodes = new Map(); /** Scene reference for creating new systems */ private _scene: Scene | null = null; @@ -78,16 +78,16 @@ export class VFXEffect implements IDisposable { * @param scene The Babylon.js scene * @param rootUrl Root URL for loading textures * @param options Optional parsing options - * @returns Promise that resolves to a VFXEffect + * @returns Promise that resolves to a Effect */ - public static async LoadAsync(url: string, scene: Scene, rootUrl: string = "", options?: VFXLoaderOptions): Promise { + public static async LoadAsync(url: string, scene: Scene, rootUrl: string = "", options?: LoaderOptions): Promise { return new Promise((resolve, reject) => { Tools.LoadFile( url, (data) => { try { const jsonData = JSON.parse(data.toString()); - const effect = VFXEffect.Parse(jsonData, scene, rootUrl, options); + const effect = Effect.Parse(jsonData, scene, rootUrl, options); resolve(effect); } catch (error) { reject(error); @@ -109,28 +109,28 @@ export class VFXEffect implements IDisposable { * @param scene The Babylon.js scene * @param rootUrl Root URL for loading textures * @param options Optional parsing options - * @returns A VFXEffect containing all particle systems + * @returns A Effect containing all particle systems */ - public static Parse(jsonData: QuarksVFXJSON, scene: Scene, rootUrl: string = "", options?: VFXLoaderOptions): VFXEffect { - return new VFXEffect(jsonData, scene, rootUrl, options); + public static Parse(jsonData: QuarksJSON, scene: Scene, rootUrl: string = "", options?: LoaderOptions): Effect { + return new Effect(jsonData, scene, rootUrl, options); } /** - * Create a VFXEffect directly from JSON data + * Create a Effect directly from JSON data * @param jsonData The Three.js JSON data * @param scene The Babylon.js scene * @param rootUrl Root URL for loading textures * @param options Optional parsing options */ - constructor(jsonData?: QuarksVFXJSON, scene?: Scene, rootUrl: string = "", options?: VFXLoaderOptions) { + constructor(jsonData?: QuarksJSON, scene?: Scene, rootUrl: string = "", options?: LoaderOptions) { this._scene = scene || null; if (jsonData && scene) { - const parser = new VFXParser(scene, rootUrl, jsonData, options); + const parser = new Parser(scene, rootUrl, jsonData, options); const parseResult = parser.parse(); this._systems.push(...parseResult.systems); - if (parseResult.vfxData && parseResult.groupNodesMap) { - this._buildHierarchy(parseResult.vfxData, parseResult.groupNodesMap, parseResult.systems); + if (parseResult.Data && parseResult.groupNodesMap) { + this._buildHierarchy(parseResult.Data, parseResult.groupNodesMap, parseResult.systems); } } else if (scene) { // Create empty effect with root group @@ -140,20 +140,20 @@ export class VFXEffect implements IDisposable { } /** - * Build hierarchy from VFX data and group nodes map + * Build hierarchy from data and group nodes map * Handles errors gracefully and continues building partial hierarchy if errors occur */ - private _buildHierarchy(vfxData: VFXData, groupNodesMap: Map, systems: (VFXParticleSystem | VFXSolidParticleSystem)[]): void { - if (!vfxData || !vfxData.root) { + private _buildHierarchy(Data: Data, groupNodesMap: Map, systems: (EffectParticleSystem | EffectSolidParticleSystem)[]): void { + if (!Data || !Data.root) { return; } try { // Create nodes from hierarchy - this._root = this._buildNodeFromHierarchy(vfxData.root, null, groupNodesMap, systems); + this._root = this._buildNodeFromHierarchy(Data.root, null, groupNodesMap, systems); } catch (error) { // Log error but don't throw - effect can still work with partial hierarchy - console.error(`Failed to build VFX hierarchy: ${error instanceof Error ? error.message : String(error)}`); + console.error(`Failed to build hierarchy: ${error instanceof Error ? error.message : String(error)}`); } } @@ -161,17 +161,17 @@ export class VFXEffect implements IDisposable { * Recursively build nodes from hierarchy */ private _buildNodeFromHierarchy( - obj: VFXGroup | VFXEmitter, - parent: VFXEffectNode | null, + obj: Group | Emitter, + parent: EffectNode | null, groupNodesMap: Map, - systems: (VFXParticleSystem | VFXSolidParticleSystem)[] - ): VFXEffectNode | null { + systems: (EffectParticleSystem | EffectSolidParticleSystem)[] + ): EffectNode | null { if (!obj) { return null; } try { - const node: VFXEffectNode = { + const node: EffectNode = { name: obj.name, uuid: obj.uuid, parent: parent || undefined, @@ -181,7 +181,7 @@ export class VFXEffect implements IDisposable { if (node.type === "particle") { // Find system by name - const emitter = obj as VFXEmitter; + const emitter = obj as Emitter; const system = systems.find((s) => s.name === emitter.name); if (system) { node.system = system; @@ -192,7 +192,7 @@ export class VFXEffect implements IDisposable { } } else { // Find group TransformNode - const group = obj as VFXGroup; + const group = obj as Group; const groupNode = group.uuid ? groupNodesMap.get(group.uuid) : null; if (groupNode) { node.group = groupNode; @@ -235,14 +235,14 @@ export class VFXEffect implements IDisposable { /** * Find a particle system by name */ - public findSystemByName(name: string): VFXParticleSystem | VFXSolidParticleSystem | null { + public findSystemByName(name: string): EffectParticleSystem | EffectSolidParticleSystem | null { return this._systemsByName.get(name) || null; } /** * Find a particle system by UUID */ - public findSystemByUuid(uuid: string): VFXParticleSystem | VFXSolidParticleSystem | null { + public findSystemByUuid(uuid: string): EffectParticleSystem | EffectSolidParticleSystem | null { return this._systemsByUuid.get(uuid) || null; } @@ -263,14 +263,14 @@ export class VFXEffect implements IDisposable { /** * Find a node (system or group) by name */ - public findNodeByName(name: string): VFXEffectNode | null { + public findNodeByName(name: string): EffectNode | null { return this._nodes.get(name) || null; } /** * Find a node (system or group) by UUID */ - public findNodeByUuid(uuid: string): VFXEffectNode | null { + public findNodeByUuid(uuid: string): EffectNode | null { return this._nodes.get(uuid) || null; } @@ -280,13 +280,13 @@ export class VFXEffect implements IDisposable { * Example: If Group1 contains Group2, and Group2 contains System1, * then getSystemsInGroup("Group1") will return System1. */ - public getSystemsInGroup(groupName: string): (VFXParticleSystem | VFXSolidParticleSystem)[] { + public getSystemsInGroup(groupName: string): (EffectParticleSystem | EffectSolidParticleSystem)[] { const group = this.findGroupByName(groupName); if (!group) { return []; } - const systems: (VFXParticleSystem | VFXSolidParticleSystem)[] = []; + const systems: (EffectParticleSystem | EffectSolidParticleSystem)[] = []; this._collectSystemsInGroup(group, systems); return systems; } @@ -297,10 +297,10 @@ export class VFXEffect implements IDisposable { * 1. Collects all systems that have this group as direct parent * 2. Recursively processes all child groups and collects their systems too */ - private _collectSystemsInGroup(group: TransformNode, systems: (VFXParticleSystem | VFXSolidParticleSystem)[]): void { + private _collectSystemsInGroup(group: TransformNode, systems: (EffectParticleSystem | EffectSolidParticleSystem)[]): void { // Step 1: Find systems that have this group as direct parent for (const system of this._systems) { - if (isVFXSystem(system)) { + if (isSystem(system)) { const parentNode = system.getParentNode(); if (parentNode && parentNode.parent === group) { systems.push(system); @@ -365,7 +365,7 @@ export class VFXEffect implements IDisposable { /** * Start a node (system or group) */ - public startNode(node: VFXEffectNode): void { + public startNode(node: EffectNode): void { if (node.type === "particle" && node.system) { node.system.start(); } else if (node.type === "group" && node.group) { @@ -380,7 +380,7 @@ export class VFXEffect implements IDisposable { /** * Stop a node (system or group) */ - public stopNode(node: VFXEffectNode): void { + public stopNode(node: EffectNode): void { if (node.type === "particle" && node.system) { node.system.stop(); } else if (node.type === "group" && node.group) { @@ -395,7 +395,7 @@ export class VFXEffect implements IDisposable { /** * Reset a node (system or group) */ - public resetNode(node: VFXEffectNode): void { + public resetNode(node: EffectNode): void { if (node.type === "particle" && node.system) { node.system.reset(); } else if (node.type === "group" && node.group) { @@ -410,11 +410,11 @@ export class VFXEffect implements IDisposable { /** * Check if a node is started (system or group) */ - public isNodeStarted(node: VFXEffectNode): boolean { + public isNodeStarted(node: EffectNode): boolean { if (node.type === "particle" && node.system) { - if (node.system instanceof VFXParticleSystem) { + if (node.system instanceof EffectParticleSystem) { return (node.system as any).isStarted ? (node.system as any).isStarted() : false; - } else if (node.system instanceof VFXSolidParticleSystem) { + } else if (node.system instanceof EffectSolidParticleSystem) { return (node.system as any)._started && !(node.system as any)._stopped; } return false; @@ -422,9 +422,9 @@ export class VFXEffect implements IDisposable { // Check if any system in this group is started const systems = this._getSystemsInNode(node); return systems.some((system) => { - if (system instanceof VFXParticleSystem) { + if (system instanceof EffectParticleSystem) { return (system as any).isStarted ? (system as any).isStarted() : false; - } else if (system instanceof VFXSolidParticleSystem) { + } else if (system instanceof EffectSolidParticleSystem) { return (system as any)._started && !(system as any)._stopped; } return false; @@ -436,8 +436,8 @@ export class VFXEffect implements IDisposable { /** * Get all systems in a node recursively */ - private _getSystemsInNode(node: VFXEffectNode): (VFXParticleSystem | VFXSolidParticleSystem)[] { - const systems: (VFXParticleSystem | VFXSolidParticleSystem)[] = []; + private _getSystemsInNode(node: EffectNode): (EffectParticleSystem | EffectSolidParticleSystem)[] { + const systems: (EffectParticleSystem | EffectSolidParticleSystem)[] = []; if (node.type === "particle" && node.system) { systems.push(node.system); @@ -485,13 +485,13 @@ export class VFXEffect implements IDisposable { */ public applyPrewarm(): void { for (const system of this._systems) { - if (system instanceof VFXParticleSystem && system.prewarm) { + if (system instanceof EffectParticleSystem && system.prewarm) { // For ParticleSystem, use Babylon.js built-in prewarm const duration = system.targetStopDuration || 5; const cycles = Math.ceil(duration * 60); // Simulate 60 FPS for duration (system as any).preWarmCycles = cycles; (system as any).preWarmStepOffset = 1; // Use normal time step - } else if (system instanceof VFXSolidParticleSystem && system.prewarm) { + } else if (system instanceof EffectSolidParticleSystem && system.prewarm) { // For SolidParticleSystem, we need to manually simulate prewarm // Start the system and let it run for duration // Note: SPS doesn't have built-in prewarm, so we'll start it normally @@ -505,11 +505,11 @@ export class VFXEffect implements IDisposable { */ public isStarted(): boolean { for (const system of this._systems) { - if (system instanceof VFXParticleSystem) { + if (system instanceof EffectParticleSystem) { if ((system as any).isStarted && (system as any).isStarted()) { return true; } - } else if (system instanceof VFXSolidParticleSystem) { + } else if (system instanceof EffectSolidParticleSystem) { // Check internal _started flag for SPS if ((system as any)._started && !(system as any)._stopped) { return true; @@ -531,7 +531,7 @@ export class VFXEffect implements IDisposable { const rootUuid = Tools.RandomId(); rootGroup.id = rootUuid; - const rootNode: VFXEffectNode = { + const rootNode: EffectNode = { name: "Root", uuid: rootUuid, group: rootGroup, @@ -552,7 +552,7 @@ export class VFXEffect implements IDisposable { * @param name Optional name (defaults to "Group") * @returns Created group node */ - public createGroup(parentNode: VFXEffectNode | null = null, name: string = "Group"): VFXEffectNode | null { + public createGroup(parentNode: EffectNode | null = null, name: string = "Group"): EffectNode | null { if (!this._scene) { console.error("Cannot create group: scene is not available"); return null; @@ -581,7 +581,7 @@ export class VFXEffect implements IDisposable { groupNode.setParent(parent.group, false, true); } - const newNode: VFXEffectNode = { + const newNode: EffectNode = { name: uniqueName, uuid: groupUuid, group: groupNode, @@ -609,7 +609,7 @@ export class VFXEffect implements IDisposable { * @param name Optional name (defaults to "ParticleSystem") * @returns Created particle system node */ - public createParticleSystem(parentNode: VFXEffectNode | null = null, systemType: "solid" | "base" = "base", name: string = "ParticleSystem"): VFXEffectNode | null { + public createParticleSystem(parentNode: EffectNode | null = null, systemType: "solid" | "base" = "base", name: string = "ParticleSystem"): EffectNode | null { if (!this._scene) { console.error("Cannot create particle system: scene is not available"); return null; @@ -632,7 +632,7 @@ export class VFXEffect implements IDisposable { const systemUuid = Tools.RandomId(); // Create default config - const config: VFXParticleEmitterConfig = { + const config: EmitterConfig = { systemType, looping: true, duration: 5, @@ -645,14 +645,14 @@ export class VFXEffect implements IDisposable { behaviors: [], }; - let system: VFXParticleSystem | VFXSolidParticleSystem; + let system: EffectParticleSystem | EffectSolidParticleSystem; if (systemType === "solid") { // Create default plane mesh for SPS const planeMesh = CreatePlane("particleMesh", { size: 1 }, this._scene); planeMesh.setEnabled(false); // Hide the source mesh - system = new VFXSolidParticleSystem(uniqueName, this._scene, config, { + system = new EffectSolidParticleSystem(uniqueName, this._scene, config, { particleMesh: planeMesh, parentGroup: parent.group || undefined, }); @@ -665,12 +665,12 @@ export class VFXEffect implements IDisposable { } else { // Create base particle system with default flare texture const flareTexture = new Texture(Tools.GetAssetUrl("https://assets.babylonjs.com/core/textures/flare.png"), this._scene); - system = new VFXParticleSystem(uniqueName, this._scene, config, { + system = new EffectParticleSystem(uniqueName, this._scene, config, { texture: flareTexture, }); // Create default point emitter - const emitterFactory = new VFXEmitterFactory(); + const emitterFactory = new EmitterFactory(); emitterFactory.createParticleSystemEmitter(system, undefined, Vector3.One(), null); // Create emitter mesh (Mesh for ParticleSystem) @@ -686,7 +686,7 @@ export class VFXEffect implements IDisposable { // Set system name system.name = uniqueName; - const newNode: VFXEffectNode = { + const newNode: EffectNode = { name: uniqueName, uuid: systemUuid, system, diff --git a/tools/src/effect/emitters/index.ts b/tools/src/effect/emitters/index.ts new file mode 100644 index 000000000..2576aea7e --- /dev/null +++ b/tools/src/effect/emitters/index.ts @@ -0,0 +1,3 @@ +export { SolidPointParticleEmitter } from "./solidPointEmitter"; +export { SolidSphereParticleEmitter } from "./solidSphereEmitter"; +export { SolidConeParticleEmitter } from "./solidConeEmitter"; diff --git a/tools/src/effect/emitters/solidConeEmitter.ts b/tools/src/effect/emitters/solidConeEmitter.ts new file mode 100644 index 000000000..7ba985166 --- /dev/null +++ b/tools/src/effect/emitters/solidConeEmitter.ts @@ -0,0 +1,35 @@ +import { SolidParticle } from "babylonjs"; +import { ISolidParticleEmitterType } from "../types"; + +/** + * Cone emitter for SolidParticleSystem + */ +export class SolidConeParticleEmitter implements ISolidParticleEmitterType { + public radius: number; + public arc: number; + public thickness: number; + public angle: number; + + constructor(radius: number = 1, arc: number = Math.PI * 2, thickness: number = 1, angle: number = Math.PI / 6) { + this.radius = radius; + this.arc = arc; + this.thickness = thickness; + this.angle = angle; + } + + public initializeParticle(particle: SolidParticle, startSpeed: number): void { + const u = Math.random(); + const rand = 1 - this.thickness + Math.random() * this.thickness; + const theta = u * this.arc; + const r = Math.sqrt(rand); + const sinTheta = Math.sin(theta); + const cosTheta = Math.cos(theta); + + particle.position.set(r * cosTheta, r * sinTheta, 0); + const coneAngle = this.angle * r; + particle.velocity.set(0, 0, Math.cos(coneAngle)); + particle.velocity.addInPlace(particle.position.scale(Math.sin(coneAngle))); + particle.velocity.scaleInPlace(startSpeed); + particle.position.scaleInPlace(this.radius); + } +} diff --git a/tools/src/effect/emitters/solidPointEmitter.ts b/tools/src/effect/emitters/solidPointEmitter.ts new file mode 100644 index 000000000..b4195d3ad --- /dev/null +++ b/tools/src/effect/emitters/solidPointEmitter.ts @@ -0,0 +1,16 @@ +import { SolidParticle, Vector3 } from "babylonjs"; +import { ISolidParticleEmitterType } from "../types"; + +/** + * Point emitter for SolidParticleSystem + */ +export class SolidPointParticleEmitter implements ISolidParticleEmitterType { + public initializeParticle(particle: SolidParticle, startSpeed: number): void { + const theta = Math.random() * Math.PI * 2; + const phi = Math.acos(2.0 * Math.random() - 1.0); + const direction = new Vector3(Math.sin(phi) * Math.cos(theta), Math.sin(phi) * Math.sin(theta), Math.cos(phi)); + particle.position.setAll(0); + particle.velocity.copyFrom(direction); + particle.velocity.scaleInPlace(startSpeed); + } +} diff --git a/tools/src/effect/emitters/solidSphereEmitter.ts b/tools/src/effect/emitters/solidSphereEmitter.ts new file mode 100644 index 000000000..205bf77a8 --- /dev/null +++ b/tools/src/effect/emitters/solidSphereEmitter.ts @@ -0,0 +1,34 @@ +import { SolidParticle } from "babylonjs"; +import { ISolidParticleEmitterType } from "../types"; + +/** + * Sphere emitter for SolidParticleSystem + */ +export class SolidSphereParticleEmitter implements ISolidParticleEmitterType { + public radius: number; + public arc: number; + public thickness: number; + + constructor(radius: number = 1, arc: number = Math.PI * 2, thickness: number = 1) { + this.radius = radius; + this.arc = arc; + this.thickness = thickness; + } + + public initializeParticle(particle: SolidParticle, startSpeed: number): void { + const u = Math.random(); + const v = Math.random(); + const rand = 1 - this.thickness + Math.random() * this.thickness; + const theta = u * this.arc; + const phi = Math.acos(2.0 * v - 1.0); + const sinTheta = Math.sin(theta); + const cosTheta = Math.cos(theta); + const sinPhi = Math.sin(phi); + const cosPhi = Math.cos(phi); + + particle.position.set(sinPhi * cosTheta, sinPhi * sinTheta, cosPhi); + particle.velocity.copyFrom(particle.position); + particle.velocity.scaleInPlace(startSpeed); + particle.position.scaleInPlace(this.radius * rand); + } +} diff --git a/editor/src/editor/windows/fx-editor/VFX/factories/VFXEmitterFactory.ts b/tools/src/effect/factories/emitterFactory.ts similarity index 75% rename from editor/src/editor/windows/fx-editor/VFX/factories/VFXEmitterFactory.ts rename to tools/src/effect/factories/emitterFactory.ts index 831b13e1e..2821e5b16 100644 --- a/editor/src/editor/windows/fx-editor/VFX/factories/VFXEmitterFactory.ts +++ b/tools/src/effect/factories/emitterFactory.ts @@ -1,24 +1,25 @@ -import { ParticleSystem, Vector3, Matrix } from "babylonjs"; -import type { VFXShape } from "../types/shapes"; -import type { VFXSolidParticleSystem } from "../systems/VFXSolidParticleSystem"; +import { Vector3, Matrix } from "babylonjs"; +import type { Shape } from "../types/shapes"; +import type { EffectSolidParticleSystem } from "../systems/effectSolidParticleSystem"; +import type { EffectParticleSystem } from "../systems/effectParticleSystem"; /** * Factory for creating emitters for particle systems * Handles both ParticleSystem and SolidParticleSystem emitter creation */ -export class VFXEmitterFactory { +export class EmitterFactory { /** * Create emitter for ParticleSystem * Applies emitter shape to the particle system */ - public createParticleSystemEmitter(particleSystem: ParticleSystem, shape: VFXShape | undefined, cumulativeScale: Vector3, rotationMatrix: Matrix | null): void { + public createParticleSystemEmitter(particleSystem: EffectParticleSystem, shape: Shape | undefined, cumulativeScale: Vector3, rotationMatrix: Matrix | null): void { if (!shape || !shape.type) { this._createPointEmitter(particleSystem, Vector3.Zero(), Vector3.Zero()); return; } const shapeType = shape.type.toLowerCase(); - const shapeHandlers: Record void> = { + const shapeHandlers: Record void> = { cone: this._createConeEmitter.bind(this, particleSystem), sphere: this._createSphereEmitter.bind(this, particleSystem), point: this._createPointEmitter.bind(this, particleSystem), @@ -39,7 +40,7 @@ export class VFXEmitterFactory { * Create emitter for SolidParticleSystem * Creates emitter using system's create*Emitter methods (similar to ParticleSystem) */ - public createSolidParticleSystemEmitter(sps: VFXSolidParticleSystem, shape: VFXShape | undefined): void { + public createSolidParticleSystemEmitter(sps: EffectSolidParticleSystem, shape: Shape | undefined): void { if (!shape || !shape.type) { sps.createPointEmitter(); return; @@ -83,7 +84,7 @@ export class VFXEmitterFactory { /** * Creates cone emitter for ParticleSystem */ - private _createConeEmitter(particleSystem: ParticleSystem, shape: VFXShape, scale: Vector3, rotationMatrix: Matrix | null): void { + private _createConeEmitter(particleSystem: EffectParticleSystem, shape: Shape, scale: Vector3, rotationMatrix: Matrix | null): void { const radius = ((shape as any).radius || 1) * ((scale.x + scale.z) / 2); const angle = (shape as any).angle !== undefined ? (shape as any).angle : Math.PI / 4; const defaultDir = new Vector3(0, 1, 0); @@ -99,7 +100,7 @@ export class VFXEmitterFactory { /** * Creates sphere emitter for ParticleSystem */ - private _createSphereEmitter(particleSystem: ParticleSystem, shape: VFXShape, scale: Vector3, rotationMatrix: Matrix | null): void { + private _createSphereEmitter(particleSystem: EffectParticleSystem, shape: Shape, scale: Vector3, rotationMatrix: Matrix | null): void { const radius = ((shape as any).radius || 1) * ((scale.x + scale.y + scale.z) / 3); const defaultDir = new Vector3(0, 1, 0); const rotatedDir = this._applyRotationToDirection(defaultDir, rotationMatrix); @@ -114,14 +115,14 @@ export class VFXEmitterFactory { /** * Creates point emitter for ParticleSystem */ - private _createPointEmitter(particleSystem: ParticleSystem, direction: Vector3, minDirection: Vector3): void { + private _createPointEmitter(particleSystem: EffectParticleSystem, direction: Vector3, minDirection: Vector3): void { particleSystem.createPointEmitter(direction, minDirection); } /** * Creates box emitter for ParticleSystem */ - private _createBoxEmitter(particleSystem: ParticleSystem, shape: VFXShape, scale: Vector3, rotationMatrix: Matrix | null): void { + private _createBoxEmitter(particleSystem: EffectParticleSystem, shape: Shape, scale: Vector3, rotationMatrix: Matrix | null): void { const boxSize = ((shape as any).size || [1, 1, 1]).map((s: number, i: number) => s * [scale.x, scale.y, scale.z][i]); const minBox = new Vector3(-boxSize[0] / 2, -boxSize[1] / 2, -boxSize[2] / 2); const maxBox = new Vector3(boxSize[0] / 2, boxSize[1] / 2, boxSize[2] / 2); @@ -138,7 +139,7 @@ export class VFXEmitterFactory { /** * Creates hemisphere emitter for ParticleSystem */ - private _createHemisphereEmitter(particleSystem: ParticleSystem, shape: VFXShape, scale: Vector3, _rotationMatrix: Matrix | null): void { + private _createHemisphereEmitter(particleSystem: EffectParticleSystem, shape: Shape, scale: Vector3, _rotationMatrix: Matrix | null): void { const radius = ((shape as any).radius || 1) * ((scale.x + scale.y + scale.z) / 3); particleSystem.createHemisphericEmitter(radius); } @@ -146,7 +147,7 @@ export class VFXEmitterFactory { /** * Creates cylinder emitter for ParticleSystem */ - private _createCylinderEmitter(particleSystem: ParticleSystem, shape: VFXShape, scale: Vector3, rotationMatrix: Matrix | null): void { + private _createCylinderEmitter(particleSystem: EffectParticleSystem, shape: Shape, scale: Vector3, rotationMatrix: Matrix | null): void { const radius = ((shape as any).radius || 1) * ((scale.x + scale.z) / 2); const height = ((shape as any).height || 1) * scale.y; const defaultDir = new Vector3(0, 1, 0); @@ -162,7 +163,7 @@ export class VFXEmitterFactory { /** * Creates default point emitter for ParticleSystem */ - private _createDefaultPointEmitter(particleSystem: ParticleSystem, rotationMatrix: Matrix | null): void { + private _createDefaultPointEmitter(particleSystem: EffectParticleSystem, rotationMatrix: Matrix | null): void { const defaultDir = new Vector3(0, 1, 0); const rotatedDir = this._applyRotationToDirection(defaultDir, rotationMatrix); diff --git a/editor/src/editor/windows/fx-editor/VFX/factories/VFXGeometryFactory.ts b/tools/src/effect/factories/geometryFactory.ts similarity index 79% rename from editor/src/editor/windows/fx-editor/VFX/factories/VFXGeometryFactory.ts rename to tools/src/effect/factories/geometryFactory.ts index aebfe04f8..c5305faed 100644 --- a/editor/src/editor/windows/fx-editor/VFX/factories/VFXGeometryFactory.ts +++ b/tools/src/effect/factories/geometryFactory.ts @@ -1,20 +1,20 @@ import { Mesh, VertexData, CreatePlane, Nullable, Scene } from "babylonjs"; -import type { IVFXGeometryFactory } from "../types/factories"; -import { VFXLogger } from "../loggers/VFXLogger"; -import type { VFXData } from "../types/hierarchy"; -import type { VFXGeometry } from "../types/resources"; -import type { VFXLoaderOptions } from "../types/loader"; +import type { IGeometryFactory } from "../types/factories"; +import { Logger } from "../loggers/logger"; +import type { Data } from "../types/hierarchy"; +import type { Geometry } from "../types/resources"; +import type { LoaderOptions } from "../types/loader"; /** * Factory for creating meshes from Three.js geometry data */ -export class VFXGeometryFactory implements IVFXGeometryFactory { - private _logger: VFXLogger; - private _vfxData: VFXData; +export class GeometryFactory implements IGeometryFactory { + private _logger: Logger; + private _Data: Data; - constructor(vfxData: VFXData, options: VFXLoaderOptions) { - this._vfxData = vfxData; - this._logger = new VFXLogger("[VFXGeometryFactory]", options); + constructor(Data: Data, options: LoaderOptions) { + this._Data = Data; + this._logger = new Logger("[GeometryFactory]", options); } /** @@ -86,13 +86,13 @@ export class VFXGeometryFactory implements IVFXGeometryFactory { /** * Finds geometry by UUID */ - private _findGeometry(geometryId: string): VFXGeometry | null { - if (!this._vfxData.geometries || this._vfxData.geometries.length === 0) { + private _findGeometry(geometryId: string): Geometry | null { + if (!this._Data.geometries || this._Data.geometries.length === 0) { this._logger.warn("No geometries data available"); return null; } - const geometry = this._vfxData.geometries.find((g) => g.uuid === geometryId); + const geometry = this._Data.geometries.find((g) => g.uuid === geometryId); if (!geometry) { this._logger.warn(`Geometry not found: ${geometryId}`); return null; @@ -104,10 +104,10 @@ export class VFXGeometryFactory implements IVFXGeometryFactory { /** * Creates mesh from geometry data based on type */ - private _createMeshFromGeometry(geometryData: VFXGeometry, name: string, scene: Scene): Nullable { + private _createMeshFromGeometry(geometryData: Geometry, name: string, scene: Scene): Nullable { this._logger.log(`createMeshFromGeometry: type=${geometryData.type}, name=${name}`); - const geometryTypeHandlers: Record Nullable> = { + const geometryTypeHandlers: Record Nullable> = { PlaneGeometry: (data, meshName, scene) => this._createPlaneGeometry(data, meshName, scene), BufferGeometry: (data, meshName, scene) => this._createBufferGeometry(data, meshName, scene), }; @@ -124,7 +124,7 @@ export class VFXGeometryFactory implements IVFXGeometryFactory { /** * Creates plane geometry mesh */ - private _createPlaneGeometry(geometryData: VFXGeometry, name: string, scene: Scene): Nullable { + private _createPlaneGeometry(geometryData: Geometry, name: string, scene: Scene): Nullable { const width = geometryData.width ?? 1; const height = geometryData.height ?? 1; @@ -143,7 +143,7 @@ export class VFXGeometryFactory implements IVFXGeometryFactory { /** * Creates buffer geometry mesh (already converted to left-handed) */ - private _createBufferGeometry(geometryData: VFXGeometry, name: string, scene: Scene): Nullable { + private _createBufferGeometry(geometryData: Geometry, name: string, scene: Scene): Nullable { if (!geometryData.data?.attributes) { this._logger.warn("BufferGeometry missing data or attributes"); return null; @@ -156,7 +156,7 @@ export class VFXGeometryFactory implements IVFXGeometryFactory { const mesh = new Mesh(name, scene); vertexData.applyToMesh(mesh); - // Geometry is already converted to left-handed in VFXDataConverter + // Geometry is already converted to left-handed in DataConverter return mesh; } @@ -164,7 +164,7 @@ export class VFXGeometryFactory implements IVFXGeometryFactory { /** * Creates VertexData from BufferGeometry attributes (already converted to left-handed) */ - private _createVertexDataFromAttributes(geometryData: VFXGeometry): Nullable { + private _createVertexDataFromAttributes(geometryData: Geometry): Nullable { if (!geometryData.data?.attributes) { return null; } diff --git a/tools/src/effect/factories/index.ts b/tools/src/effect/factories/index.ts new file mode 100644 index 000000000..01b5e1fc2 --- /dev/null +++ b/tools/src/effect/factories/index.ts @@ -0,0 +1,4 @@ +export { EmitterFactory } from "./emitterFactory"; +export { MaterialFactory } from "./materialFactory"; +export { GeometryFactory } from "./geometryFactory"; +export { SystemFactory } from "./systemFactory"; diff --git a/editor/src/editor/windows/fx-editor/VFX/factories/VFXMaterialFactory.ts b/tools/src/effect/factories/materialFactory.ts similarity index 66% rename from editor/src/editor/windows/fx-editor/VFX/factories/VFXMaterialFactory.ts rename to tools/src/effect/factories/materialFactory.ts index 797e4df47..90a03664f 100644 --- a/editor/src/editor/windows/fx-editor/VFX/factories/VFXMaterialFactory.ts +++ b/tools/src/effect/factories/materialFactory.ts @@ -1,30 +1,30 @@ -import { Nullable, Texture, PBRMaterial, Material, Constants, Tools, Scene, Color3 } from "babylonjs"; -import type { IVFXMaterialFactory } from "../types/factories"; -import { VFXLogger } from "../loggers/VFXLogger"; -import type { VFXLoaderOptions } from "../types/loader"; -import type { VFXData } from "../types/hierarchy"; -import type { VFXMaterial, VFXTexture, VFXImage } from "../types/resources"; +import { Nullable, Texture as BabylonTexture, PBRMaterial, Material as BabylonMaterial, Constants, Tools, Scene, Color3 } from "babylonjs"; +import type { IMaterialFactory } from "../types/factories"; +import { Logger } from "../loggers/logger"; +import type { LoaderOptions } from "../types/loader"; +import type { Data } from "../types/hierarchy"; +import type { Material, Texture, Image } from "../types/resources"; /** * Factory for creating materials and textures from Three.js JSON data */ -export class VFXMaterialFactory implements IVFXMaterialFactory { - private _logger: VFXLogger; +export class MaterialFactory implements IMaterialFactory { + private _logger: Logger; private _scene: Scene; - private _vfxData: VFXData; + private _data: Data; private _rootUrl: string; - constructor(scene: Scene, vfxData: VFXData, rootUrl: string, options: VFXLoaderOptions) { + constructor(scene: Scene, data: Data, rootUrl: string, options: LoaderOptions) { this._scene = scene; - this._vfxData = vfxData; + this._data = data; this._rootUrl = rootUrl; - this._logger = new VFXLogger("[VFXMaterialFactory]", options); + this._logger = new Logger("[MaterialFactory]", options); } /** * Create a texture from material ID (for ParticleSystem - no material needed) */ - public createTexture(materialId: string): Nullable { + public createTexture(materialId: string): Nullable { const textureData = this._resolveTextureData(materialId); if (!textureData) { return null; @@ -39,7 +39,7 @@ export class VFXMaterialFactory implements IVFXMaterialFactory { * Get blend mode from material blending value */ public getBlendMode(materialId: string): number | undefined { - const material = this._vfxData.materials?.find((m: any) => m.uuid === materialId); + const material = this._data.materials?.find((m: any) => m.uuid === materialId); if (material?.blending === undefined) { return undefined; @@ -57,7 +57,7 @@ export class VFXMaterialFactory implements IVFXMaterialFactory { /** * Resolves material, texture, and image data from material ID */ - private _resolveTextureData(materialId: string): { material: VFXMaterial; texture: VFXTexture; image: VFXImage } | null { + private _resolveTextureData(materialId: string): { material: Material; texture: Texture; image: Image } | null { if (!this._hasRequiredData()) { this._logger.warn(`Missing materials/textures/images data for material ${materialId}`); return null; @@ -85,14 +85,14 @@ export class VFXMaterialFactory implements IVFXMaterialFactory { * Checks if required JSON data is available */ private _hasRequiredData(): boolean { - return !!(this._vfxData.materials && this._vfxData.textures && this._vfxData.images); + return !!(this._data.materials && this._data.textures && this._data.images); } /** * Finds material by UUID */ - private _findMaterial(materialId: string): VFXMaterial | null { - const material = this._vfxData.materials?.find((m) => m.uuid === materialId); + private _findMaterial(materialId: string): Material | null { + const material = this._data.materials?.find((m) => m.uuid === materialId); if (!material) { this._logger.warn(`Material not found: ${materialId}`); return null; @@ -103,8 +103,8 @@ export class VFXMaterialFactory implements IVFXMaterialFactory { /** * Finds texture by UUID */ - private _findTexture(textureId: string): VFXTexture | null { - const texture = this._vfxData.textures?.find((t) => t.uuid === textureId); + private _findTexture(textureId: string): Texture | null { + const texture = this._data.textures?.find((t) => t.uuid === textureId); if (!texture) { this._logger.warn(`Texture not found: ${textureId}`); return null; @@ -115,8 +115,8 @@ export class VFXMaterialFactory implements IVFXMaterialFactory { /** * Finds image by UUID */ - private _findImage(imageId: string): VFXImage | null { - const image = this._vfxData.images?.find((img) => img.uuid === imageId); + private _findImage(imageId: string): Image | null { + const image = this._data.images?.find((img) => img.uuid === imageId); if (!image) { this._logger.warn(`Image not found: ${imageId}`); return null; @@ -127,7 +127,7 @@ export class VFXMaterialFactory implements IVFXMaterialFactory { /** * Builds texture URL from image data */ - private _buildTextureUrl(image: VFXImage): string { + private _buildTextureUrl(image: Image): string { if (!image.url) { return ""; } @@ -136,9 +136,9 @@ export class VFXMaterialFactory implements IVFXMaterialFactory { } /** - * Applies texture properties from VFX texture data to Babylon.js texture + * Applies texture properties from texture data to Babylon.js texture */ - private _applyTextureProperties(babylonTexture: Texture, texture: VFXTexture): void { + private _applyTextureProperties(babylonTexture: BabylonTexture, texture: Texture): void { if (texture.wrapU !== undefined) { babylonTexture.wrapU = texture.wrapU; } @@ -166,12 +166,12 @@ export class VFXMaterialFactory implements IVFXMaterialFactory { } /** - * Creates Babylon.js texture from VFX texture data + * Creates Babylon.js texture from texture data */ - private _createTextureFromData(textureUrl: string, texture: VFXTexture): Texture { - const samplingMode = texture.samplingMode ?? Texture.TRILINEAR_SAMPLINGMODE; + private _createTextureFromData(textureUrl: string, texture: Texture): BabylonTexture { + const samplingMode = texture.samplingMode ?? BabylonTexture.TRILINEAR_SAMPLINGMODE; - const babylonTexture = new Texture(textureUrl, this._scene, { + const babylonTexture = new BabylonTexture(textureUrl, this._scene, { noMipmap: !texture.generateMipmaps, invertY: texture.flipY !== false, samplingMode, @@ -214,7 +214,7 @@ export class VFXMaterialFactory implements IVFXMaterialFactory { /** * Creates unlit material (MeshBasicMaterial equivalent) */ - private _createUnlitMaterial(name: string, material: VFXMaterial, texture: Texture, color: Color3): PBRMaterial { + private _createUnlitMaterial(name: string, material: Material, texture: BabylonTexture, color: Color3): PBRMaterial { const unlitMaterial = new PBRMaterial(name + "_material", this._scene); unlitMaterial.unlit = true; @@ -235,15 +235,15 @@ export class VFXMaterialFactory implements IVFXMaterialFactory { /** * Applies transparency settings to material */ - private _applyTransparency(material: PBRMaterial, vfxMaterial: VFXMaterial, texture: Texture): void { - if (vfxMaterial.transparent) { - material.transparencyMode = Material.MATERIAL_ALPHABLEND; + private _applyTransparency(material: PBRMaterial, Material: Material, texture: BabylonTexture): void { + if (Material.transparent) { + material.transparencyMode = BabylonMaterial.MATERIAL_ALPHABLEND; material.needDepthPrePass = false; texture.hasAlpha = true; material.useAlphaFromAlbedoTexture = true; this._logger.log(`Material is transparent (transparencyMode: ALPHABLEND, alphaMode: COMBINE)`); } else { - material.transparencyMode = Material.MATERIAL_OPAQUE; + material.transparencyMode = BabylonMaterial.MATERIAL_OPAQUE; material.alpha = 1.0; } } @@ -251,10 +251,10 @@ export class VFXMaterialFactory implements IVFXMaterialFactory { /** * Applies depth write settings to material */ - private _applyDepthWrite(material: PBRMaterial, vfxMaterial: VFXMaterial): void { - if (vfxMaterial.depthWrite !== undefined) { - material.disableDepthWrite = !vfxMaterial.depthWrite; - this._logger.log(`Set disableDepthWrite: ${!vfxMaterial.depthWrite}`); + private _applyDepthWrite(material: PBRMaterial, Material: Material): void { + if (Material.depthWrite !== undefined) { + material.disableDepthWrite = !Material.depthWrite; + this._logger.log(`Set disableDepthWrite: ${!Material.depthWrite}`); } else { material.disableDepthWrite = true; } @@ -263,20 +263,20 @@ export class VFXMaterialFactory implements IVFXMaterialFactory { /** * Applies side orientation settings to material */ - private _applySideSettings(material: PBRMaterial, vfxMaterial: VFXMaterial): void { + private _applySideSettings(material: PBRMaterial, Material: Material): void { material.backFaceCulling = false; - if (vfxMaterial.side !== undefined) { - material.sideOrientation = vfxMaterial.side; - this._logger.log(`Set sideOrientation: ${vfxMaterial.side}`); + if (Material.side !== undefined) { + material.sideOrientation = Material.side; + this._logger.log(`Set sideOrientation: ${Material.side}`); } } /** * Applies blend mode to material */ - private _applyBlendMode(material: PBRMaterial, vfxMaterial: VFXMaterial): void { - if (vfxMaterial.blending === undefined) { + private _applyBlendMode(material: PBRMaterial, Material: Material): void { + if (Material.blending === undefined) { return; } @@ -286,7 +286,7 @@ export class VFXMaterialFactory implements IVFXMaterialFactory { 2: Constants.ALPHA_ADD, // AdditiveBlending }; - const alphaMode = blendModeMap[vfxMaterial.blending]; + const alphaMode = blendModeMap[Material.blending]; if (alphaMode !== undefined) { material.alphaMode = alphaMode; const modeNames: Record = { @@ -294,7 +294,7 @@ export class VFXMaterialFactory implements IVFXMaterialFactory { 1: "NORMAL", 2: "ADDITIVE", }; - this._logger.log(`Set blend mode: ${modeNames[vfxMaterial.blending]}`); + this._logger.log(`Set blend mode: ${modeNames[Material.blending]}`); } } } diff --git a/editor/src/editor/windows/fx-editor/VFX/factories/VFXSystemFactory.ts b/tools/src/effect/factories/systemFactory.ts similarity index 51% rename from editor/src/editor/windows/fx-editor/VFX/factories/VFXSystemFactory.ts rename to tools/src/effect/factories/systemFactory.ts index a2896edb1..d3244048b 100644 --- a/editor/src/editor/windows/fx-editor/VFX/factories/VFXSystemFactory.ts +++ b/tools/src/effect/factories/systemFactory.ts @@ -1,91 +1,89 @@ import { Nullable, Vector3, TransformNode, Texture, Scene } from "babylonjs"; -import { VFXParticleSystem } from "../systems/VFXParticleSystem"; -import { VFXSolidParticleSystem } from "../systems/VFXSolidParticleSystem"; -import type { VFXData, VFXGroup, VFXEmitter, VFXTransform } from "../types/hierarchy"; -import { VFXLogger } from "../loggers/VFXLogger"; -import { VFXMatrixUtils } from "../utils/matrixUtils"; -import { VFXEmitterFactory } from "./VFXEmitterFactory"; -import type { IVFXMaterialFactory, IVFXGeometryFactory } from "../types/factories"; -import type { VFXLoaderOptions } from "../types/loader"; +import { EffectParticleSystem } from "../systems/effectParticleSystem"; +import { EffectSolidParticleSystem } from "../systems/effectSolidParticleSystem"; +import type { Data, Group, Emitter, Transform } from "../types/hierarchy"; +import { Logger } from "../loggers/logger"; +import { MatrixUtils } from "../utils/matrixUtils"; +import { EmitterFactory } from "./emitterFactory"; +import type { IMaterialFactory, IGeometryFactory } from "../types/factories"; +import type { LoaderOptions } from "../types/loader"; /** - * Factory for creating particle systems from VFX data + * Factory for creating particle systems from data * Creates all nodes, sets parents, and applies transformations in a single pass */ -export class VFXSystemFactory { - private _logger: VFXLogger; +export class SystemFactory { + private _logger: Logger; private _scene: Scene; - private _options: VFXLoaderOptions; private _groupNodesMap: Map; - private _materialFactory: IVFXMaterialFactory; - private _geometryFactory: IVFXGeometryFactory; - private _emitterFactory: VFXEmitterFactory; + private _materialFactory: IMaterialFactory; + private _geometryFactory: IGeometryFactory; + private _emitterFactory: EmitterFactory; - constructor(scene: Scene, options: VFXLoaderOptions, groupNodesMap: Map, materialFactory: IVFXMaterialFactory, geometryFactory: IVFXGeometryFactory) { + constructor(scene: Scene, options: LoaderOptions, groupNodesMap: Map, materialFactory: IMaterialFactory, geometryFactory: IGeometryFactory) { this._scene = scene; - this._options = options; this._groupNodesMap = groupNodesMap; - this._logger = new VFXLogger("[VFXSystemFactory]", options); + this._logger = new Logger("[SystemFactory]", options); this._materialFactory = materialFactory; this._geometryFactory = geometryFactory; - this._emitterFactory = new VFXEmitterFactory(); + this._emitterFactory = new EmitterFactory(); } /** - * Create particle systems from VFX data + * Create particle systems from data * Creates all nodes, sets parents, and applies transformations in one pass */ - public createSystems(vfxData: VFXData): (VFXParticleSystem | VFXSolidParticleSystem)[] { - if (!vfxData.root) { - this._logger.warn("No root object found in VFX data"); + public createSystems(Data: Data): (EffectParticleSystem | EffectSolidParticleSystem)[] { + if (!Data.root) { + this._logger.warn("No root object found in data"); return []; } this._logger.log("Processing hierarchy: creating nodes, setting parents, and applying transformations"); - const particleSystems: (VFXParticleSystem | VFXSolidParticleSystem)[] = []; - this._processVFXObject(vfxData.root, null, 0, particleSystems, vfxData); + const particleSystems: (EffectParticleSystem | EffectSolidParticleSystem)[] = []; + this._processObject(Data.root, null, 0, particleSystems, Data); return particleSystems; } /** - * Recursively process VFX object hierarchy + * Recursively process object hierarchy * Creates nodes, sets parents, and applies transformations in one pass */ - private _processVFXObject( - vfxObj: VFXGroup | VFXEmitter, + private _processObject( + Obj: Group | Emitter, parentGroup: Nullable, depth: number, - particleSystems: (VFXParticleSystem | VFXSolidParticleSystem)[], - vfxData: VFXData + particleSystems: (EffectParticleSystem | EffectSolidParticleSystem)[], + Data: Data ): void { - this._logger.log(`${" ".repeat(depth)}Processing object: ${vfxObj.name}`); + this._logger.log(`${" ".repeat(depth)}Processing object: ${Obj.name}`); - if (this._isGroup(vfxObj)) { - this._processGroup(vfxObj, parentGroup, depth, particleSystems, vfxData); + if (this._isGroup(Obj)) { + this._processGroup(Obj, parentGroup, depth, particleSystems, Data); } else { - this._processEmitter(vfxObj, parentGroup, depth, particleSystems); + this._processEmitter(Obj, parentGroup, depth, particleSystems); } } /** - * Process a VFX Group object + * Process a Group object */ private _processGroup( - vfxGroup: VFXGroup, + Group: Group, parentGroup: Nullable, depth: number, - particleSystems: (VFXParticleSystem | VFXSolidParticleSystem)[], - vfxData: VFXData + particleSystems: (EffectParticleSystem | EffectSolidParticleSystem)[], + Data: Data ): void { - const groupNode = this._createGroupNode(vfxGroup, parentGroup, depth); - this._processChildren(vfxGroup.children, groupNode, depth, particleSystems, vfxData); + const groupNode = this._createGroupNode(Group, parentGroup, depth); + this._processChildren(Group.children, groupNode, depth, particleSystems, Data); } /** - * Process a VFX Emitter object + * Process a Emitter object */ - private _processEmitter(vfxEmitter: VFXEmitter, parentGroup: Nullable, depth: number, particleSystems: (VFXParticleSystem | VFXSolidParticleSystem)[]): void { - const particleSystem = this._createParticleSystem(vfxEmitter, parentGroup, depth); + private _processEmitter(Emitter: Emitter, parentGroup: Nullable, depth: number, particleSystems: (EffectParticleSystem | EffectSolidParticleSystem)[]): void { + const particleSystem = this._createParticleSystem(Emitter, parentGroup, depth); if (particleSystem) { particleSystems.push(particleSystem); } @@ -95,11 +93,11 @@ export class VFXSystemFactory { * Process children of a group recursively */ private _processChildren( - children: (VFXGroup | VFXEmitter)[] | undefined, + children: (Group | Emitter)[] | undefined, parentGroup: TransformNode, depth: number, - particleSystems: (VFXParticleSystem | VFXSolidParticleSystem)[], - vfxData: VFXData + particleSystems: (EffectParticleSystem | EffectSolidParticleSystem)[], + Data: Data ): void { if (!children || children.length === 0) { return; @@ -107,94 +105,94 @@ export class VFXSystemFactory { this._logger.log(`${" ".repeat(depth)}Processing ${children.length} children`); children.forEach((child) => { - this._processVFXObject(child, parentGroup, depth + 1, particleSystems, vfxData); + this._processObject(child, parentGroup, depth + 1, particleSystems, Data); }); } /** - * Create a TransformNode for a VFX Group + * Create a TransformNode for a Group */ - private _createGroupNode(vfxGroup: VFXGroup, parentGroup: Nullable, depth: number): TransformNode { - const groupNode = new TransformNode(vfxGroup.name, this._scene); - groupNode.id = vfxGroup.uuid; + private _createGroupNode(Group: Group, parentGroup: Nullable, depth: number): TransformNode { + const groupNode = new TransformNode(Group.name, this._scene); + groupNode.id = Group.uuid; - this._applyTransform(groupNode, vfxGroup.transform, depth); + this._applyTransform(groupNode, Group.transform, depth); this._setParent(groupNode, parentGroup, depth); // Store in map for potential future reference - this._groupNodesMap.set(vfxGroup.uuid, groupNode); + this._groupNodesMap.set(Group.uuid, groupNode); - this._logger.log(`${" ".repeat(depth)}Created group node: ${vfxGroup.name}`); + this._logger.log(`${" ".repeat(depth)}Created group node: ${Group.name}`); return groupNode; } /** - * Create a particle system from a VFX Emitter + * Create a particle system from a Emitter */ - private _createParticleSystem(vfxEmitter: VFXEmitter, parentGroup: Nullable, depth: number): Nullable { + private _createParticleSystem(Emitter: Emitter, parentGroup: Nullable, depth: number): Nullable { const indent = " ".repeat(depth); const parentName = parentGroup ? parentGroup.name : "none"; - this._logger.log(`${indent}Processing emitter: ${vfxEmitter.name} (parent: ${parentName})`); + this._logger.log(`${indent}Processing emitter: ${Emitter.name} (parent: ${parentName})`); try { - const config = vfxEmitter.config; + const config = Emitter.config; if (!config) { - this._logger.warn(`${indent}Emitter ${vfxEmitter.name} has no config, skipping`); + this._logger.warn(`${indent}Emitter ${Emitter.name} has no config, skipping`); return null; } - this._logger.log(`${indent} Config: duration=${config.duration}, looping=${config.looping}, systemType=${vfxEmitter.systemType}`); + this._logger.log(`${indent} Config: duration=${config.duration}, looping=${config.looping}, systemType=${Emitter.systemType}`); const cumulativeScale = this._calculateCumulativeScale(parentGroup); this._logger.log(`${indent}Cumulative scale: (${cumulativeScale.x.toFixed(2)}, ${cumulativeScale.y.toFixed(2)}, ${cumulativeScale.z.toFixed(2)})`); // Use systemType from emitter (determined during conversion) - const systemType = vfxEmitter.systemType || "base"; + const systemType = Emitter.systemType || "base"; this._logger.log(`Using ${systemType === "solid" ? "SolidParticleSystem" : "ParticleSystem"}`); - let particleSystem: VFXParticleSystem | VFXSolidParticleSystem | null = null; + let particleSystem: EffectParticleSystem | EffectSolidParticleSystem | null = null; try { if (systemType === "solid") { - particleSystem = this._createSolidParticleSystem(vfxEmitter, parentGroup); + particleSystem = this._createSolidParticleSystem(Emitter, parentGroup); } else { - particleSystem = this._createParticleSystemInstance(vfxEmitter, parentGroup, cumulativeScale, depth); + particleSystem = this._createParticleSystemInstance(Emitter, parentGroup, cumulativeScale, depth); } } catch (error) { - this._logger.error(`${indent}Failed to create ${systemType} system for emitter ${vfxEmitter.name}: ${error instanceof Error ? error.message : String(error)}`); + this._logger.error(`${indent}Failed to create ${systemType} system for emitter ${Emitter.name}: ${error instanceof Error ? error.message : String(error)}`); return null; } if (!particleSystem) { - this._logger.warn(`${indent}Failed to create particle system for emitter: ${vfxEmitter.name}`); + this._logger.warn(`${indent}Failed to create particle system for emitter: ${Emitter.name}`); return null; } // Apply transform to particle system try { - if (particleSystem instanceof VFXSolidParticleSystem) { + if (particleSystem instanceof EffectSolidParticleSystem) { // For SPS, transform is applied to the mesh if (particleSystem.mesh) { - this._applyTransform(particleSystem.mesh, vfxEmitter.transform, depth); + this._applyTransform(particleSystem.mesh, Emitter.transform, depth); this._setParent(particleSystem.mesh, parentGroup, depth); } - } else if (particleSystem instanceof VFXParticleSystem) { + } else if (particleSystem instanceof EffectParticleSystem) { // For PS, transform is applied to the emitter mesh const emitter = particleSystem.getParentNode(); if (emitter) { - this._applyTransform(emitter, vfxEmitter.transform, depth); + this._applyTransform(emitter, Emitter.transform, depth); this._setParent(emitter, parentGroup, depth); } } } catch (error) { - this._logger.warn(`${indent}Failed to apply transform to system ${vfxEmitter.name}: ${error instanceof Error ? error.message : String(error)}`); + this._logger.warn(`${indent}Failed to apply transform to system ${Emitter.name}: ${error instanceof Error ? error.message : String(error)}`); // Continue - system is created, just transform failed } - this._logger.log(`${indent}Created particle system: ${vfxEmitter.name}`); + this._logger.log(`${indent}Created particle system: ${Emitter.name}`); return particleSystem; } catch (error) { - this._logger.error(`${indent}Unexpected error creating particle system ${vfxEmitter.name}: ${error instanceof Error ? error.message : String(error)}`); + this._logger.error(`${indent}Unexpected error creating particle system ${Emitter.name}: ${error instanceof Error ? error.message : String(error)}`); return null; } } @@ -202,20 +200,20 @@ export class VFXSystemFactory { /** * Create a ParticleSystem instance */ - private _createParticleSystemInstance(vfxEmitter: VFXEmitter, _parentGroup: Nullable, cumulativeScale: Vector3, _depth: number): Nullable { - const { name, config } = vfxEmitter; + private _createParticleSystemInstance(Emitter: Emitter, _parentGroup: Nullable, cumulativeScale: Vector3, _depth: number): Nullable { + const { name, config } = Emitter; this._logger.log(`Creating ParticleSystem: ${name}`); // Get texture and blend mode - const texture: Texture | undefined = vfxEmitter.materialId ? this._materialFactory.createTexture(vfxEmitter.materialId) || undefined : undefined; - const blendMode = vfxEmitter.materialId ? this._materialFactory.getBlendMode(vfxEmitter.materialId) : undefined; + const texture: Texture | undefined = Emitter.materialId ? this._materialFactory.createTexture(Emitter.materialId) || undefined : undefined; + const blendMode = Emitter.materialId ? this._materialFactory.getBlendMode(Emitter.materialId) : undefined; // Extract rotation matrix from emitter matrix if available - const rotationMatrix = vfxEmitter.matrix ? VFXMatrixUtils.extractRotationMatrix(vfxEmitter.matrix) : null; + const rotationMatrix = Emitter.matrix ? MatrixUtils.extractRotationMatrix(Emitter.matrix) : null; // Create instance - all configuration happens in constructor - const particleSystem = new VFXParticleSystem(name, this._scene, config, { + const particleSystem = new EffectParticleSystem(name, this._scene, config, { texture, blendMode, }); @@ -230,13 +228,13 @@ export class VFXSystemFactory { /** * Create a SolidParticleSystem instance */ - private _createSolidParticleSystem(vfxEmitter: VFXEmitter, parentGroup: Nullable): Nullable { - const { name, config } = vfxEmitter; + private _createSolidParticleSystem(Emitter: Emitter, parentGroup: Nullable): Nullable { + const { name, config } = Emitter; this._logger.log(`Creating SolidParticleSystem: ${name}`); - // Get VFX transform - const vfxTransform = vfxEmitter.transform || null; + // Get transform + const transform = Emitter.transform || null; // Create or load particle mesh const particleMesh = this._geometryFactory.createParticleMesh(config, name, this._scene); @@ -245,24 +243,22 @@ export class VFXSystemFactory { } // Apply material if provided - if (vfxEmitter.materialId) { - const material = this._materialFactory.createMaterial(vfxEmitter.materialId, name); + if (Emitter.materialId) { + const material = this._materialFactory.createMaterial(Emitter.materialId, name); if (material) { particleMesh.material = material; } } // Create SPS instance - mesh initialization and capacity calculation happen in constructor - const sps = new VFXSolidParticleSystem(name, this._scene, config, { + const sps = new EffectSolidParticleSystem(name, this._scene, config, { updatable: true, isPickable: false, enableDepthSort: false, particleIntersection: false, useModelMaterial: true, parentGroup, - vfxTransform, - logger: this._logger, - loaderOptions: this._options, + transform, particleMesh, }); @@ -291,14 +287,14 @@ export class VFXSystemFactory { } // Type guards - private _isGroup(vfxObj: VFXGroup | VFXEmitter): vfxObj is VFXGroup { - return "children" in vfxObj; + private _isGroup(Obj: Group | Emitter): Obj is Group { + return "children" in Obj; } /** * Apply transform to a node */ - private _applyTransform(node: TransformNode, transform: VFXTransform, depth: number): void { + private _applyTransform(node: TransformNode, transform: Transform, depth: number): void { if (!transform) { this._logger.warn(`Transform is undefined for node: ${node.name}`); return; diff --git a/tools/src/effect/index.ts b/tools/src/effect/index.ts new file mode 100644 index 000000000..2c961f588 --- /dev/null +++ b/tools/src/effect/index.ts @@ -0,0 +1,11 @@ +export * from "./types"; +export * from "./parsers/parser"; +export * from "./parsers/dataConverter"; +export * from "./factories"; +export * from "./utils/capacityCalculator"; +export * from "./utils/matrixUtils"; +export * from "./systems"; +export * from "./loggers/logger"; +export * from "./effect"; +export * from "./utils/valueParser"; +export * from "./emitters"; diff --git a/editor/src/editor/windows/fx-editor/VFX/loggers/VFXLogger.ts b/tools/src/effect/loggers/logger.ts similarity index 53% rename from editor/src/editor/windows/fx-editor/VFX/loggers/VFXLogger.ts rename to tools/src/effect/loggers/logger.ts index 8e78d699e..a5d3da191 100644 --- a/editor/src/editor/windows/fx-editor/VFX/loggers/VFXLogger.ts +++ b/tools/src/effect/loggers/logger.ts @@ -1,14 +1,14 @@ -import { Logger } from "babylonjs"; -import type { VFXLoaderOptions } from "../types"; +import { Logger as BabylonLogger } from "babylonjs"; +import type { LoaderOptions } from "../types"; /** - * Logger utility for VFX operations + * Logger utility for operations */ -export class VFXLogger { +export class Logger { private _prefix: string; - private _options?: VFXLoaderOptions; + private _options?: LoaderOptions; - constructor(prefix: string = "[VFX]", options?: VFXLoaderOptions) { + constructor(prefix: string = "[]", options?: LoaderOptions) { this._prefix = prefix; this._options = options; } @@ -18,7 +18,7 @@ export class VFXLogger { */ public log(message: string): void { if (this._options?.verbose) { - Logger.Log(`${this._prefix} ${message}`); + BabylonLogger.Log(`${this._prefix} ${message}`); } } @@ -27,7 +27,7 @@ export class VFXLogger { */ public warn(message: string): void { if (this._options?.verbose || this._options?.validate) { - Logger.Warn(`${this._prefix} ${message}`); + BabylonLogger.Warn(`${this._prefix} ${message}`); } } @@ -35,6 +35,6 @@ export class VFXLogger { * Log an error */ public error(message: string): void { - Logger.Error(`${this._prefix} ${message}`); + BabylonLogger.Error(`${this._prefix} ${message}`); } } diff --git a/editor/src/editor/windows/fx-editor/VFX/parsers/VFXDataConverter.ts b/tools/src/effect/parsers/dataConverter.ts similarity index 66% rename from editor/src/editor/windows/fx-editor/VFX/parsers/VFXDataConverter.ts rename to tools/src/effect/parsers/dataConverter.ts index 2e06d55ad..3c69e8b10 100644 --- a/editor/src/editor/windows/fx-editor/VFX/parsers/VFXDataConverter.ts +++ b/tools/src/effect/parsers/dataConverter.ts @@ -1,6 +1,6 @@ -import { Vector3, Matrix, Quaternion, Color3, Texture, ParticleSystem } from "babylonjs"; -import type { VFXLoaderOptions } from "../types/loader"; -import type { QuarksVFXJSON, QuarksMaterial, QuarksTexture, QuarksImage, QuarksGeometry } from "../types/quarksTypes"; +import { Vector3, Matrix, Quaternion, Color3, Texture as BabylonTexture, ParticleSystem } from "babylonjs"; +import type { LoaderOptions } from "../types/loader"; +import type { QuarksJSON, QuarksMaterial, QuarksTexture, QuarksImage, QuarksGeometry } from "../types/quarksTypes"; import type { QuarksObject, QuarksParticleEmitterConfig, @@ -23,83 +23,83 @@ import type { QuarksRotationBySpeedBehavior, QuarksOrbitOverLifeBehavior, } from "../types/quarksTypes"; -import type { VFXTransform, VFXGroup, VFXEmitter, VFXData } from "../types/hierarchy"; -import type { VFXMaterial, VFXTexture, VFXImage, VFXGeometry, VFXGeometryData } from "../types/resources"; -import type { VFXParticleEmitterConfig } from "../types/emitterConfig"; +import type { Transform, Group, Emitter, Data } from "../types/hierarchy"; +import type { Material, Texture, Image, Geometry, GeometryData } from "../types/resources"; +import type { EmitterConfig } from "../types/emitter"; import type { - VFXBehavior, - VFXColorOverLifeBehavior, - VFXSizeOverLifeBehavior, - VFXForceOverLifeBehavior, - VFXSpeedOverLifeBehavior, - VFXLimitSpeedOverLifeBehavior, - VFXColorBySpeedBehavior, - VFXSizeBySpeedBehavior, + Behavior, + ColorOverLifeBehavior, + SizeOverLifeBehavior, + ForceOverLifeBehavior, + SpeedOverLifeBehavior, + LimitSpeedOverLifeBehavior, + ColorBySpeedBehavior, + SizeBySpeedBehavior, } from "../types/behaviors"; -import type { VFXValue } from "../types/values"; -import type { VFXColor } from "../types/colors"; -import type { VFXRotation } from "../types/rotations"; -import type { VFXGradientKey } from "../types/gradients"; -import type { VFXShape } from "../types/shapes"; -import { VFXLogger } from "../loggers/VFXLogger"; +import type { Value } from "../types/values"; +import type { Color } from "../types/colors"; +import type { Rotation } from "../types/rotations"; +import type { GradientKey } from "../types/gradients"; +import type { Shape } from "../types/shapes"; +import { Logger } from "../loggers/logger"; /** - * Converts Quarks/Three.js VFX JSON (right-handed) to Babylon.js VFX format (left-handed) + * Converts Quarks/Three.js JSON (right-handed) to Babylon.js format (left-handed) * All coordinate system conversions happen here, once */ -export class VFXDataConverter { - private _logger: VFXLogger; +export class DataConverter { + private _logger: Logger; - constructor(options?: VFXLoaderOptions) { - this._logger = new VFXLogger("[VFXDataConverter]", options); + constructor(options?: LoaderOptions) { + this._logger = new Logger("[DataConverter]", options); } /** - * Convert Quarks/Three.js VFX JSON to Babylon.js VFX format + * Convert Quarks/Three.js JSON to Babylon.js format * Handles errors gracefully and returns partial data if conversion fails */ - public convert(quarksVFXData: QuarksVFXJSON): VFXData { - this._logger.log("=== Converting Quarks VFX to Babylon.js VFX format ==="); + public convert(quarksData: QuarksJSON): Data { + this._logger.log("=== Converting Quarks to Babylon.js format ==="); - const groups = new Map(); - const emitters = new Map(); + const groups = new Map(); + const emitters = new Map(); - let root: VFXGroup | VFXEmitter | null = null; + let root: Group | Emitter | null = null; try { - if (quarksVFXData.object) { - root = this._convertObject(quarksVFXData.object, null, groups, emitters, 0); + if (quarksData.object) { + root = this._convertObject(quarksData.object, null, groups, emitters, 0); } } catch (error) { this._logger.error(`Failed to convert root object: ${error instanceof Error ? error.message : String(error)}`); } // Convert all resources with error handling - let materials: VFXMaterial[] = []; - let textures: VFXTexture[] = []; - let images: VFXImage[] = []; - let geometries: VFXGeometry[] = []; + let materials: Material[] = []; + let textures: Texture[] = []; + let images: Image[] = []; + let geometries: Geometry[] = []; try { - materials = this._convertMaterials(quarksVFXData.materials || []); + materials = this._convertMaterials(quarksData.materials || []); } catch (error) { this._logger.error(`Failed to convert materials: ${error instanceof Error ? error.message : String(error)}`); } try { - textures = this._convertTextures(quarksVFXData.textures || []); + textures = this._convertTextures(quarksData.textures || []); } catch (error) { this._logger.error(`Failed to convert textures: ${error instanceof Error ? error.message : String(error)}`); } try { - images = this._convertImages(quarksVFXData.images || []); + images = this._convertImages(quarksData.images || []); } catch (error) { this._logger.error(`Failed to convert images: ${error instanceof Error ? error.message : String(error)}`); } try { - geometries = this._convertGeometries(quarksVFXData.geometries || []); + geometries = this._convertGeometries(quarksData.geometries || []); } catch (error) { this._logger.error(`Failed to convert geometries: ${error instanceof Error ? error.message : String(error)}`); } @@ -120,15 +120,9 @@ export class VFXDataConverter { } /** - * Convert a Quarks/Three.js object to Babylon.js VFX format + * Convert a Quarks/Three.js object to Babylon.js format */ - private _convertObject( - obj: QuarksObject, - parentUuid: string | null, - groups: Map, - emitters: Map, - depth: number - ): VFXGroup | VFXEmitter | null { + private _convertObject(obj: QuarksObject, parentUuid: string | null, groups: Map, emitters: Map, depth: number): Group | Emitter | null { const indent = " ".repeat(depth); if (!obj || typeof obj !== "object") { @@ -141,7 +135,7 @@ export class VFXDataConverter { const transform = this._convertTransform(obj.matrix, obj.position, obj.rotation, obj.scale); if (obj.type === "Group") { - const group: VFXGroup = { + const group: Group = { uuid: obj.uuid || `group_${groups.size}`, name: obj.name || "Group", transform, @@ -155,10 +149,10 @@ export class VFXDataConverter { if (convertedChild) { if ("config" in convertedChild) { // It's an emitter - group.children.push(convertedChild as VFXEmitter); + group.children.push(convertedChild as Emitter); } else { // It's a group - group.children.push(convertedChild as VFXGroup); + group.children.push(convertedChild as Group); } } } @@ -168,22 +162,22 @@ export class VFXDataConverter { this._logger.log(`${indent}Converted Group: ${group.name} (uuid: ${group.uuid})`); return group; } else if (obj.type === "ParticleEmitter" && obj.ps) { - // Convert emitter config from Quarks to VFX format - const vfxConfig = this._convertEmitterConfig(obj.ps); + // Convert emitter config from Quarks to format + const Config = this._convertEmitterConfig(obj.ps); - const emitter: VFXEmitter = { + const emitter: Emitter = { uuid: obj.uuid || `emitter_${emitters.size}`, name: obj.name || "ParticleEmitter", transform, - config: vfxConfig, + config: Config, materialId: obj.ps.material, parentUuid: parentUuid || undefined, - systemType: vfxConfig.systemType, // systemType is set in _convertEmitterConfig + systemType: Config.systemType, // systemType is set in _convertEmitterConfig matrix: obj.matrix, // Store original matrix for rotation extraction }; emitters.set(emitter.uuid, emitter); - this._logger.log(`${indent}Converted Emitter: ${emitter.name} (uuid: ${emitter.uuid}, systemType: ${vfxConfig.systemType})`); + this._logger.log(`${indent}Converted Emitter: ${emitter.name} (uuid: ${emitter.uuid}, systemType: ${Config.systemType})`); return emitter; } @@ -191,10 +185,10 @@ export class VFXDataConverter { } /** - * Convert transform from Quarks/Three.js (right-handed) to Babylon.js VFX (left-handed) + * Convert transform from Quarks/Three.js (right-handed) to Babylon.js (left-handed) * This is the ONLY place where handedness conversion happens */ - private _convertTransform(matrixArray?: number[], positionArray?: number[], rotationArray?: number[], scaleArray?: number[]): VFXTransform { + private _convertTransform(matrixArray?: number[], positionArray?: number[], rotationArray?: number[], scaleArray?: number[]): Transform { const position = Vector3.Zero(); const rotation = Quaternion.Identity(); const scale = Vector3.One(); @@ -246,13 +240,13 @@ export class VFXDataConverter { } /** - * Convert emitter config from Quarks to VFX format + * Convert emitter config from Quarks to format */ - private _convertEmitterConfig(quarksConfig: QuarksParticleEmitterConfig): VFXParticleEmitterConfig { + private _convertEmitterConfig(quarksConfig: QuarksParticleEmitterConfig): EmitterConfig { // Determine system type based on renderMode: 2 = solid, otherwise base const systemType: "solid" | "base" = quarksConfig.renderMode === 2 ? "solid" : "base"; - const vfxConfig: VFXParticleEmitterConfig = { + const Config: EmitterConfig = { version: quarksConfig.version, autoDestroy: quarksConfig.autoDestroy, looping: quarksConfig.looping, @@ -276,38 +270,38 @@ export class VFXDataConverter { // Convert values if (quarksConfig.startLife !== undefined) { - vfxConfig.startLife = this._convertValue(quarksConfig.startLife); + Config.startLife = this._convertValue(quarksConfig.startLife); } if (quarksConfig.startSpeed !== undefined) { - vfxConfig.startSpeed = this._convertValue(quarksConfig.startSpeed); + Config.startSpeed = this._convertValue(quarksConfig.startSpeed); } if (quarksConfig.startRotation !== undefined) { - vfxConfig.startRotation = this._convertRotation(quarksConfig.startRotation); + Config.startRotation = this._convertRotation(quarksConfig.startRotation); } if (quarksConfig.startSize !== undefined) { - vfxConfig.startSize = this._convertValue(quarksConfig.startSize); + Config.startSize = this._convertValue(quarksConfig.startSize); } if (quarksConfig.startColor !== undefined) { - vfxConfig.startColor = this._convertColor(quarksConfig.startColor); + Config.startColor = this._convertColor(quarksConfig.startColor); } if (quarksConfig.emissionOverTime !== undefined) { - vfxConfig.emissionOverTime = this._convertValue(quarksConfig.emissionOverTime); + Config.emissionOverTime = this._convertValue(quarksConfig.emissionOverTime); } if (quarksConfig.emissionOverDistance !== undefined) { - vfxConfig.emissionOverDistance = this._convertValue(quarksConfig.emissionOverDistance); + Config.emissionOverDistance = this._convertValue(quarksConfig.emissionOverDistance); } if (quarksConfig.startTileIndex !== undefined) { - vfxConfig.startTileIndex = this._convertValue(quarksConfig.startTileIndex); + Config.startTileIndex = this._convertValue(quarksConfig.startTileIndex); } // Convert shape if (quarksConfig.shape !== undefined) { - vfxConfig.shape = this._convertShape(quarksConfig.shape); + Config.shape = this._convertShape(quarksConfig.shape); } // Convert emission bursts if (quarksConfig.emissionBursts !== undefined && Array.isArray(quarksConfig.emissionBursts)) { - vfxConfig.emissionBursts = quarksConfig.emissionBursts.map((burst) => ({ + Config.emissionBursts = quarksConfig.emissionBursts.map((burst) => ({ time: this._convertValue(burst.time), count: this._convertValue(burst.count), })); @@ -315,7 +309,7 @@ export class VFXDataConverter { // Convert behaviors if (quarksConfig.behaviors !== undefined && Array.isArray(quarksConfig.behaviors)) { - vfxConfig.behaviors = quarksConfig.behaviors.map((behavior) => this._convertBehavior(behavior)); + Config.behaviors = quarksConfig.behaviors.map((behavior) => this._convertBehavior(behavior)); } // Convert renderMode to systemType, billboardMode and isBillboardBased @@ -329,42 +323,42 @@ export class VFXDataConverter { if (quarksConfig.renderMode !== undefined) { if (quarksConfig.renderMode === 0) { // BillBoard - vfxConfig.isBillboardBased = true; - vfxConfig.billboardMode = ParticleSystem.BILLBOARDMODE_ALL; + Config.isBillboardBased = true; + Config.billboardMode = ParticleSystem.BILLBOARDMODE_ALL; } else if (quarksConfig.renderMode === 1) { // StretchedBillBoard - vfxConfig.isBillboardBased = true; - vfxConfig.billboardMode = ParticleSystem.BILLBOARDMODE_STRETCHED; + Config.isBillboardBased = true; + Config.billboardMode = ParticleSystem.BILLBOARDMODE_STRETCHED; } else if (quarksConfig.renderMode === 2) { // Mesh (SolidParticleSystem) - always false - vfxConfig.isBillboardBased = false; + Config.isBillboardBased = false; // billboardMode not applicable for mesh } else if (quarksConfig.renderMode === 3) { // Trail - not directly supported, treat as billboard - vfxConfig.isBillboardBased = true; - vfxConfig.billboardMode = ParticleSystem.BILLBOARDMODE_ALL; + Config.isBillboardBased = true; + Config.billboardMode = ParticleSystem.BILLBOARDMODE_ALL; } else if (quarksConfig.renderMode === 4 || quarksConfig.renderMode === 5) { // HorizontalBillBoard or VerticalBillBoard - vfxConfig.isBillboardBased = true; - vfxConfig.billboardMode = ParticleSystem.BILLBOARDMODE_Y; + Config.isBillboardBased = true; + Config.billboardMode = ParticleSystem.BILLBOARDMODE_Y; } else { // Unknown renderMode, default to billboard - vfxConfig.isBillboardBased = true; - vfxConfig.billboardMode = ParticleSystem.BILLBOARDMODE_ALL; + Config.isBillboardBased = true; + Config.billboardMode = ParticleSystem.BILLBOARDMODE_ALL; } } else { // Default: billboard mode - vfxConfig.isBillboardBased = true; - vfxConfig.billboardMode = ParticleSystem.BILLBOARDMODE_ALL; + Config.isBillboardBased = true; + Config.billboardMode = ParticleSystem.BILLBOARDMODE_ALL; } - return vfxConfig; + return Config; } /** - * Convert Quarks value to VFX value + * Convert Quarks value to value */ - private _convertValue(quarksValue: QuarksValue): VFXValue { + private _convertValue(quarksValue: QuarksValue): Value { if (typeof quarksValue === "number") { return quarksValue; } @@ -394,9 +388,9 @@ export class VFXDataConverter { } /** - * Convert Quarks color to VFX color + * Convert Quarks color to color */ - private _convertColor(quarksColor: QuarksColor): VFXColor { + private _convertColor(quarksColor: QuarksColor): Color { if (typeof quarksColor === "string" || Array.isArray(quarksColor)) { return quarksColor; } @@ -419,13 +413,13 @@ export class VFXDataConverter { }; } } - return quarksColor as VFXColor; + return quarksColor as Color; } /** - * Convert Quarks rotation to VFX rotation + * Convert Quarks rotation to rotation */ - private _convertRotation(quarksRotation: QuarksRotation): VFXRotation { + private _convertRotation(quarksRotation: QuarksRotation): Rotation { if (typeof quarksRotation === "number" || (typeof quarksRotation === "object" && quarksRotation !== null && "type" in quarksRotation && quarksRotation.type !== "Euler")) { return this._convertValue(quarksRotation as QuarksValue); } @@ -442,9 +436,9 @@ export class VFXDataConverter { } /** - * Convert Quarks gradient key to VFX gradient key + * Convert Quarks gradient key to gradient key */ - private _convertGradientKey(quarksKey: QuarksGradientKey): VFXGradientKey { + private _convertGradientKey(quarksKey: QuarksGradientKey): GradientKey { return { time: quarksKey.time, value: quarksKey.value, @@ -453,10 +447,10 @@ export class VFXDataConverter { } /** - * Convert Quarks shape to VFX shape + * Convert Quarks shape to shape */ - private _convertShape(quarksShape: QuarksShape): VFXShape { - const vfxShape: VFXShape = { + private _convertShape(quarksShape: QuarksShape): Shape { + const Shape: Shape = { type: quarksShape.type, radius: quarksShape.radius, arc: quarksShape.arc, @@ -468,30 +462,30 @@ export class VFXDataConverter { height: quarksShape.height, }; if (quarksShape.speed !== undefined) { - vfxShape.speed = this._convertValue(quarksShape.speed); + Shape.speed = this._convertValue(quarksShape.speed); } - return vfxShape; + return Shape; } /** - * Convert Quarks behavior to VFX behavior + * Convert Quarks behavior to behavior */ - private _convertBehavior(quarksBehavior: QuarksBehavior): VFXBehavior { + private _convertBehavior(quarksBehavior: QuarksBehavior): Behavior { switch (quarksBehavior.type) { case "ColorOverLife": { const behavior = quarksBehavior as QuarksColorOverLifeBehavior; if (behavior.color) { - const vfxColor: VFXColorOverLifeBehavior["color"] = {}; + const Color: ColorOverLifeBehavior["color"] = {}; if (behavior.color.color?.keys) { - vfxColor.color = { keys: behavior.color.color.keys.map((k) => this._convertGradientKey(k)) }; + Color.color = { keys: behavior.color.color.keys.map((k) => this._convertGradientKey(k)) }; } if (behavior.color.alpha?.keys) { - vfxColor.alpha = { keys: behavior.color.alpha.keys.map((k) => this._convertGradientKey(k)) }; + Color.alpha = { keys: behavior.color.alpha.keys.map((k) => this._convertGradientKey(k)) }; } if (behavior.color.keys) { - vfxColor.keys = behavior.color.keys.map((k) => this._convertGradientKey(k)); + Color.keys = behavior.color.keys.map((k) => this._convertGradientKey(k)); } - return { type: "ColorOverLife", color: vfxColor }; + return { type: "ColorOverLife", color: Color }; } return { type: "ColorOverLife" }; } @@ -499,14 +493,14 @@ export class VFXDataConverter { case "SizeOverLife": { const behavior = quarksBehavior as QuarksSizeOverLifeBehavior; if (behavior.size) { - const vfxSize: VFXSizeOverLifeBehavior["size"] = {}; + const Size: SizeOverLifeBehavior["size"] = {}; if (behavior.size.keys) { - vfxSize.keys = behavior.size.keys.map((k: QuarksGradientKey) => this._convertGradientKey(k)); + Size.keys = behavior.size.keys.map((k: QuarksGradientKey) => this._convertGradientKey(k)); } if (behavior.size.functions) { - vfxSize.functions = behavior.size.functions; + Size.functions = behavior.size.functions; } - return { type: "SizeOverLife", size: vfxSize }; + return { type: "SizeOverLife", size: Size }; } return { type: "SizeOverLife" }; } @@ -523,41 +517,41 @@ export class VFXDataConverter { case "ForceOverLife": case "ApplyForce": { const behavior = quarksBehavior as QuarksForceOverLifeBehavior; - const vfxBehavior: VFXForceOverLifeBehavior = { type: behavior.type }; + const Behavior: ForceOverLifeBehavior = { type: behavior.type }; if (behavior.force) { - vfxBehavior.force = { + Behavior.force = { x: behavior.force.x !== undefined ? this._convertValue(behavior.force.x) : undefined, y: behavior.force.y !== undefined ? this._convertValue(behavior.force.y) : undefined, z: behavior.force.z !== undefined ? this._convertValue(behavior.force.z) : undefined, }; } - if (behavior.x !== undefined) vfxBehavior.x = this._convertValue(behavior.x); - if (behavior.y !== undefined) vfxBehavior.y = this._convertValue(behavior.y); - if (behavior.z !== undefined) vfxBehavior.z = this._convertValue(behavior.z); - return vfxBehavior; + if (behavior.x !== undefined) Behavior.x = this._convertValue(behavior.x); + if (behavior.y !== undefined) Behavior.y = this._convertValue(behavior.y); + if (behavior.z !== undefined) Behavior.z = this._convertValue(behavior.z); + return Behavior; } case "GravityForce": { const behavior = quarksBehavior as QuarksGravityForceBehavior; - const vfxBehavior: { type: string; gravity?: VFXValue } = { + const Behavior: { type: string; gravity?: Value } = { type: "GravityForce", gravity: behavior.gravity !== undefined ? this._convertValue(behavior.gravity) : undefined, }; - return vfxBehavior as VFXBehavior; + return Behavior as Behavior; } case "SpeedOverLife": { const behavior = quarksBehavior as QuarksSpeedOverLifeBehavior; if (behavior.speed) { if (typeof behavior.speed === "object" && behavior.speed !== null && "keys" in behavior.speed) { - const vfxSpeed: VFXSpeedOverLifeBehavior["speed"] = {}; + const Speed: SpeedOverLifeBehavior["speed"] = {}; if (behavior.speed.keys) { - vfxSpeed.keys = behavior.speed.keys.map((k: QuarksGradientKey) => this._convertGradientKey(k)); + Speed.keys = behavior.speed.keys.map((k: QuarksGradientKey) => this._convertGradientKey(k)); } if (behavior.speed.functions) { - vfxSpeed.functions = behavior.speed.functions; + Speed.functions = behavior.speed.functions; } - return { type: "SpeedOverLife", speed: vfxSpeed }; + return { type: "SpeedOverLife", speed: Speed }; } else if (typeof behavior.speed === "number" || (typeof behavior.speed === "object" && behavior.speed !== null && "type" in behavior.speed)) { return { type: "SpeedOverLife", speed: this._convertValue(behavior.speed as QuarksValue) }; } @@ -567,98 +561,98 @@ export class VFXDataConverter { case "FrameOverLife": { const behavior = quarksBehavior as QuarksFrameOverLifeBehavior; - const vfxBehavior: { type: string; frame?: VFXValue | { keys?: VFXGradientKey[] } } = { type: "FrameOverLife" }; + const Behavior: { type: string; frame?: Value | { keys?: GradientKey[] } } = { type: "FrameOverLife" }; if (behavior.frame) { if (typeof behavior.frame === "object" && behavior.frame !== null && "keys" in behavior.frame) { - vfxBehavior.frame = { + Behavior.frame = { keys: behavior.frame.keys?.map((k: QuarksGradientKey) => this._convertGradientKey(k)), }; } else if (typeof behavior.frame === "number" || (typeof behavior.frame === "object" && behavior.frame !== null && "type" in behavior.frame)) { - vfxBehavior.frame = this._convertValue(behavior.frame as QuarksValue); + Behavior.frame = this._convertValue(behavior.frame as QuarksValue); } } - return vfxBehavior as VFXBehavior; + return Behavior as Behavior; } case "LimitSpeedOverLife": { const behavior = quarksBehavior as QuarksLimitSpeedOverLifeBehavior; - const vfxBehavior: VFXLimitSpeedOverLifeBehavior = { type: "LimitSpeedOverLife" }; + const Behavior: LimitSpeedOverLifeBehavior = { type: "LimitSpeedOverLife" }; if (behavior.maxSpeed !== undefined) { - vfxBehavior.maxSpeed = this._convertValue(behavior.maxSpeed); + Behavior.maxSpeed = this._convertValue(behavior.maxSpeed); } if (behavior.speed !== undefined) { if (typeof behavior.speed === "object" && behavior.speed !== null && "keys" in behavior.speed) { - vfxBehavior.speed = { keys: behavior.speed.keys?.map((k: QuarksGradientKey) => this._convertGradientKey(k)) }; + Behavior.speed = { keys: behavior.speed.keys?.map((k: QuarksGradientKey) => this._convertGradientKey(k)) }; } else if (typeof behavior.speed === "number" || (typeof behavior.speed === "object" && behavior.speed !== null && "type" in behavior.speed)) { - vfxBehavior.speed = this._convertValue(behavior.speed as QuarksValue); + Behavior.speed = this._convertValue(behavior.speed as QuarksValue); } } if (behavior.dampen !== undefined) { - vfxBehavior.dampen = this._convertValue(behavior.dampen); + Behavior.dampen = this._convertValue(behavior.dampen); } - return vfxBehavior; + return Behavior; } case "ColorBySpeed": { const behavior = quarksBehavior as QuarksColorBySpeedBehavior; - const vfxBehavior: VFXColorBySpeedBehavior = { + const Behavior: ColorBySpeedBehavior = { type: "ColorBySpeed", minSpeed: behavior.minSpeed !== undefined ? this._convertValue(behavior.minSpeed) : undefined, maxSpeed: behavior.maxSpeed !== undefined ? this._convertValue(behavior.maxSpeed) : undefined, }; if (behavior.color?.keys) { - vfxBehavior.color = { keys: behavior.color.keys.map((k: QuarksGradientKey) => this._convertGradientKey(k)) }; + Behavior.color = { keys: behavior.color.keys.map((k: QuarksGradientKey) => this._convertGradientKey(k)) }; } - return vfxBehavior; + return Behavior; } case "SizeBySpeed": { const behavior = quarksBehavior as QuarksSizeBySpeedBehavior; - const vfxBehavior: VFXSizeBySpeedBehavior = { + const Behavior: SizeBySpeedBehavior = { type: "SizeBySpeed", minSpeed: behavior.minSpeed !== undefined ? this._convertValue(behavior.minSpeed) : undefined, maxSpeed: behavior.maxSpeed !== undefined ? this._convertValue(behavior.maxSpeed) : undefined, }; if (behavior.size?.keys) { - vfxBehavior.size = { keys: behavior.size.keys.map((k: QuarksGradientKey) => this._convertGradientKey(k)) }; + Behavior.size = { keys: behavior.size.keys.map((k: QuarksGradientKey) => this._convertGradientKey(k)) }; } - return vfxBehavior; + return Behavior; } case "RotationBySpeed": { const behavior = quarksBehavior as QuarksRotationBySpeedBehavior; - const vfxBehavior: { type: string; angularVelocity?: VFXValue; minSpeed?: VFXValue; maxSpeed?: VFXValue } = { + const Behavior: { type: string; angularVelocity?: Value; minSpeed?: Value; maxSpeed?: Value } = { type: "RotationBySpeed", angularVelocity: behavior.angularVelocity !== undefined ? this._convertValue(behavior.angularVelocity) : undefined, minSpeed: behavior.minSpeed !== undefined ? this._convertValue(behavior.minSpeed) : undefined, maxSpeed: behavior.maxSpeed !== undefined ? this._convertValue(behavior.maxSpeed) : undefined, }; - return vfxBehavior as VFXBehavior; + return Behavior as Behavior; } case "OrbitOverLife": { const behavior = quarksBehavior as QuarksOrbitOverLifeBehavior; - const vfxBehavior: { type: string; center?: { x?: number; y?: number; z?: number }; radius?: VFXValue; speed?: VFXValue } = { + const Behavior: { type: string; center?: { x?: number; y?: number; z?: number }; radius?: Value; speed?: Value } = { type: "OrbitOverLife", center: behavior.center, radius: behavior.radius !== undefined ? this._convertValue(behavior.radius) : undefined, speed: behavior.speed !== undefined ? this._convertValue(behavior.speed) : undefined, }; - return vfxBehavior as VFXBehavior; + return Behavior as Behavior; } default: // Fallback for unknown behaviors - copy as-is - return quarksBehavior as VFXBehavior; + return quarksBehavior as Behavior; } } /** - * Convert Quarks materials to VFX materials + * Convert Quarks materials to materials */ - private _convertMaterials(quarksMaterials: QuarksMaterial[]): VFXMaterial[] { + private _convertMaterials(quarksMaterials: QuarksMaterial[]): Material[] { return quarksMaterials.map((quarks) => { - const vfx: VFXMaterial = { + const material: Material = { uuid: quarks.uuid, type: quarks.type, transparent: quarks.transparent, @@ -673,7 +667,7 @@ export class VFXDataConverter { const r = ((colorHex >> 16) & 0xff) / 255; const g = ((colorHex >> 8) & 0xff) / 255; const b = (colorHex & 0xff) / 255; - vfx.color = new Color3(r, g, b); + material.color = new Color3(r, g, b); } // Convert blending mode (Three.js → Babylon.js) @@ -683,19 +677,19 @@ export class VFXDataConverter { 1: 1, // NormalBlending → ALPHA_COMBINE 2: 2, // AdditiveBlending → ALPHA_ADD }; - vfx.blending = blendModeMap[quarks.blending] ?? quarks.blending; + material.blending = blendModeMap[quarks.blending] ?? quarks.blending; } - return vfx; + return material; }); } /** - * Convert Quarks textures to VFX textures + * Convert Quarks textures to textures */ - private _convertTextures(quarksTextures: QuarksTexture[]): VFXTexture[] { + private _convertTextures(quarksTextures: QuarksTexture[]): Texture[] { return quarksTextures.map((quarks) => { - const vfx: VFXTexture = { + const texture: Texture = { uuid: quarks.uuid, image: quarks.image, generateMipmaps: quarks.generateMipmaps, @@ -705,59 +699,59 @@ export class VFXDataConverter { // Convert wrap mode (Three.js → Babylon.js) if (quarks.wrap && Array.isArray(quarks.wrap)) { const wrapModeMap: Record = { - 1000: Texture.WRAP_ADDRESSMODE, // RepeatWrapping - 1001: Texture.CLAMP_ADDRESSMODE, // ClampToEdgeWrapping - 1002: Texture.MIRROR_ADDRESSMODE, // MirroredRepeatWrapping + 1000: BabylonTexture.WRAP_ADDRESSMODE, // RepeatWrapping + 1001: BabylonTexture.CLAMP_ADDRESSMODE, // ClampToEdgeWrapping + 1002: BabylonTexture.MIRROR_ADDRESSMODE, // MirroredRepeatWrapping }; - vfx.wrapU = wrapModeMap[quarks.wrap[0]] ?? Texture.WRAP_ADDRESSMODE; - vfx.wrapV = wrapModeMap[quarks.wrap[1]] ?? Texture.WRAP_ADDRESSMODE; + texture.wrapU = wrapModeMap[quarks.wrap[0]] ?? BabylonTexture.WRAP_ADDRESSMODE; + texture.wrapV = wrapModeMap[quarks.wrap[1]] ?? BabylonTexture.WRAP_ADDRESSMODE; } // Convert repeat to scale if (quarks.repeat && Array.isArray(quarks.repeat)) { - vfx.uScale = quarks.repeat[0] || 1; - vfx.vScale = quarks.repeat[1] || 1; + texture.uScale = quarks.repeat[0] || 1; + texture.vScale = quarks.repeat[1] || 1; } // Convert offset if (quarks.offset && Array.isArray(quarks.offset)) { - vfx.uOffset = quarks.offset[0] || 0; - vfx.vOffset = quarks.offset[1] || 0; + texture.uOffset = quarks.offset[0] || 0; + texture.vOffset = quarks.offset[1] || 0; } // Convert rotation if (quarks.rotation !== undefined) { - vfx.uAng = quarks.rotation; + texture.uAng = quarks.rotation; } // Convert channel if (typeof quarks.channel === "number") { - vfx.coordinatesIndex = quarks.channel; + texture.coordinatesIndex = quarks.channel; } // Convert sampling mode (Three.js filters → Babylon.js sampling mode) if (quarks.minFilter !== undefined) { if (quarks.minFilter === 1008 || quarks.minFilter === 1009) { - vfx.samplingMode = Texture.TRILINEAR_SAMPLINGMODE; + texture.samplingMode = BabylonTexture.TRILINEAR_SAMPLINGMODE; } else if (quarks.minFilter === 1007 || quarks.minFilter === 1006) { - vfx.samplingMode = Texture.BILINEAR_SAMPLINGMODE; + texture.samplingMode = BabylonTexture.BILINEAR_SAMPLINGMODE; } else { - vfx.samplingMode = Texture.NEAREST_SAMPLINGMODE; + texture.samplingMode = BabylonTexture.NEAREST_SAMPLINGMODE; } } else if (quarks.magFilter !== undefined) { - vfx.samplingMode = quarks.magFilter === 1006 ? Texture.BILINEAR_SAMPLINGMODE : Texture.NEAREST_SAMPLINGMODE; + texture.samplingMode = quarks.magFilter === 1006 ? BabylonTexture.BILINEAR_SAMPLINGMODE : BabylonTexture.NEAREST_SAMPLINGMODE; } else { - vfx.samplingMode = Texture.TRILINEAR_SAMPLINGMODE; + texture.samplingMode = BabylonTexture.TRILINEAR_SAMPLINGMODE; } - return vfx; + return texture; }); } /** - * Convert Quarks images to VFX images (normalize URLs) + * Convert Quarks images to images (normalize URLs) */ - private _convertImages(quarksImages: QuarksImage[]): VFXImage[] { + private _convertImages(quarksImages: QuarksImage[]): Image[] { return quarksImages.map((quarks) => ({ uuid: quarks.uuid, url: quarks.url || "", @@ -765,28 +759,28 @@ export class VFXDataConverter { } /** - * Convert Quarks geometries to VFX geometries (convert to left-handed) + * Convert Quarks geometries to geometries (convert to left-handed) */ - private _convertGeometries(quarksGeometries: QuarksGeometry[]): VFXGeometry[] { + private _convertGeometries(quarksGeometries: QuarksGeometry[]): Geometry[] { return quarksGeometries.map((quarks) => { if (quarks.type === "PlaneGeometry") { // PlaneGeometry - simple properties - const vfx: VFXGeometry = { + const geometry: Geometry = { uuid: quarks.uuid, type: "PlaneGeometry", width: (quarks as any).width ?? 1, height: (quarks as any).height ?? 1, }; - return vfx; + return geometry; } else if (quarks.type === "BufferGeometry") { // BufferGeometry - convert attributes to left-handed - const vfx: VFXGeometry = { + const geometry: Geometry = { uuid: quarks.uuid, type: "BufferGeometry", }; if (quarks.data?.attributes) { - const attributes: VFXGeometryData["attributes"] = {}; + const attributes: GeometryData["attributes"] = {}; const quarksAttrs = quarks.data.attributes; // Convert position (right-hand → left-hand: flip Z) @@ -829,7 +823,7 @@ export class VFXDataConverter { }; } - vfx.data = { + geometry.data = { attributes, }; @@ -842,13 +836,13 @@ export class VFXDataConverter { indices[i + 1] = indices[i + 2]; indices[i + 2] = temp; } - vfx.data.index = { + geometry.data.index = { array: indices, }; } } - return vfx; + return geometry; } // Unknown geometry type - return as-is diff --git a/tools/src/effect/parsers/parser.ts b/tools/src/effect/parsers/parser.ts new file mode 100644 index 000000000..d9766d329 --- /dev/null +++ b/tools/src/effect/parsers/parser.ts @@ -0,0 +1,125 @@ +import { Scene, TransformNode } from "babylonjs"; +import type { QuarksJSON } from "../types/quarksTypes"; +import type { LoaderOptions } from "../types/loader"; +import type { Data } from "../types/hierarchy"; +import { Logger } from "../loggers/logger"; +import { MaterialFactory, GeometryFactory, SystemFactory } from "../factories"; +import { DataConverter } from "./dataConverter"; +import type { EffectParticleSystem } from "../systems/effectParticleSystem"; +import type { EffectSolidParticleSystem } from "../systems/effectSolidParticleSystem"; + +/** + * Result of parsing JSON + */ +export interface ParseResult { + /** Created particle systems */ + systems: (EffectParticleSystem | EffectSolidParticleSystem)[]; + /** Converted data */ + Data: Data; + /** Map of group UUIDs to TransformNodes */ + groupNodesMap: Map; +} + +/** + * Main parser for Three.js particle JSON files + * Orchestrates the parsing process using modular components + */ +export class Parser { + private _logger: Logger; + private _materialFactory: MaterialFactory; + private _geometryFactory: GeometryFactory; + private _systemFactory: SystemFactory; + private _Data: Data; + private _groupNodesMap: Map; + private _options: LoaderOptions; + + constructor(scene: Scene, rootUrl: string, jsonData: QuarksJSON, options?: LoaderOptions) { + const opts = options || {}; + this._options = opts; + this._groupNodesMap = new Map(); + + this._logger = new Logger("[Parser]", opts); + + // Convert Quarks JSON to Data first + const dataConverter = new DataConverter(opts); + this._Data = dataConverter.convert(jsonData); + + // Create factories with Data instead of QuarksJSON + this._materialFactory = new MaterialFactory(scene, this._Data, rootUrl, opts); + this._geometryFactory = new GeometryFactory(this._Data, opts); + this._systemFactory = new SystemFactory(scene, opts, this._groupNodesMap, this._materialFactory, this._geometryFactory); + } + + /** + * Parse the JSON data and create particle systems + * Returns all necessary data for building the effect hierarchy + */ + public parse(): ParseResult { + this._logger.log("=== Starting Particle System Parsing ==="); + + if (!this._Data) { + this._logger.warn("Data is missing"); + return { + systems: [], + Data: this._Data, + groupNodesMap: this._groupNodesMap, + }; + } + + if (this._options.validate) { + this._validateJSONStructure(this._Data); + } + + const particleSystems = this._systemFactory.createSystems(this._Data); + + this._logger.log(`=== Parsing complete. Created ${particleSystems.length} particle system(s) ===`); + return { + systems: particleSystems, + Data: this._Data, + groupNodesMap: this._groupNodesMap, + }; + } + + /** + * Validate data structure + */ + private _validateJSONStructure(Data: Data): void { + this._logger.log("Validating data structure..."); + + if (!Data.root) { + this._logger.warn(" data missing 'root' property"); + } + + if (!Data.materials || Data.materials.length === 0) { + this._logger.warn(" data has no materials"); + } + + if (!Data.textures || Data.textures.length === 0) { + this._logger.warn(" data has no textures"); + } + + if (!Data.images || Data.images.length === 0) { + this._logger.warn(" data has no images"); + } + + if (!Data.geometries || Data.geometries.length === 0) { + this._logger.warn(" data has no geometries"); + } + + this._logger.log("Validation complete"); + } + + /** + * Get the material factory (for advanced use cases) + */ + public getMaterialFactory(): MaterialFactory { + return this._materialFactory; + } + + /** + * Get the geometry factory (for advanced use cases) + */ + public getGeometryFactory(): GeometryFactory { + return this._geometryFactory; + } +} diff --git a/editor/src/editor/windows/fx-editor/VFX/systems/VFXParticleSystem.ts b/tools/src/effect/systems/effectParticleSystem.ts similarity index 70% rename from editor/src/editor/windows/fx-editor/VFX/systems/VFXParticleSystem.ts rename to tools/src/effect/systems/effectParticleSystem.ts index 67bc88b9f..3bd94c9b6 100644 --- a/editor/src/editor/windows/fx-editor/VFX/systems/VFXParticleSystem.ts +++ b/tools/src/effect/systems/effectParticleSystem.ts @@ -1,26 +1,26 @@ import { Color4, ParticleSystem, Scene, Vector3, Matrix, Texture, AbstractMesh, TransformNode } from "babylonjs"; -import type { VFXPerParticleBehaviorFunction } from "../types/VFXBehaviorFunction"; -import type { IVFXSystem, ParticleWithSystem } from "../types/system"; +import type { PerParticleBehaviorFunction } from "../types/behaviors"; +import type { ISystem, ParticleWithSystem } from "../types/system"; import type { - VFXBehavior, - VFXColorOverLifeBehavior, - VFXSizeOverLifeBehavior, - VFXRotationOverLifeBehavior, - VFXForceOverLifeBehavior, - VFXGravityForceBehavior, - VFXSpeedOverLifeBehavior, - VFXFrameOverLifeBehavior, - VFXLimitSpeedOverLifeBehavior, - VFXColorBySpeedBehavior, - VFXSizeBySpeedBehavior, - VFXRotationBySpeedBehavior, - VFXOrbitOverLifeBehavior, + Behavior, + ColorOverLifeBehavior, + SizeOverLifeBehavior, + RotationOverLifeBehavior, + ForceOverLifeBehavior, + GravityForceBehavior, + SpeedOverLifeBehavior, + FrameOverLifeBehavior, + LimitSpeedOverLifeBehavior, + ColorBySpeedBehavior, + SizeBySpeedBehavior, + RotationBySpeedBehavior, + OrbitOverLifeBehavior, } from "../types/behaviors"; import type { Particle } from "babylonjs"; -import type { VFXShape } from "../types/shapes"; -import type { VFXParticleEmitterConfig, VFXEmissionBurst } from "../types/emitterConfig"; -import { VFXValueUtils } from "../utils/valueParser"; -import { VFXCapacityCalculator } from "../utils/capacityCalculator"; +import type { Shape } from "../types/shapes"; +import type { EmitterConfig, EmissionBurst } from "../types/emitter"; +import { ValueUtils } from "../utils/valueParser"; +import { CapacityCalculator } from "../utils/capacityCalculator"; import { applyColorOverLifePS, applySizeOverLifePS, @@ -37,21 +37,21 @@ import { } from "../behaviors"; /** - * Extended ParticleSystem with VFX behaviors support + * Extended ParticleSystem with behaviors support * Fully self-contained, no dependencies on parsers or factories */ -export class VFXParticleSystem extends ParticleSystem implements IVFXSystem { +export class EffectParticleSystem extends ParticleSystem implements ISystem { public startSize: number; public startSpeed: number; public startColor: Color4; public prewarm: boolean; - private _behaviors: VFXPerParticleBehaviorFunction[]; - public readonly behaviorConfigs: VFXBehavior[]; + private _behaviors: PerParticleBehaviorFunction[]; + public readonly behaviorConfigs: Behavior[]; constructor( name: string, scene: Scene, - config: VFXParticleEmitterConfig, + config: EmitterConfig, options?: { texture?: Texture; blendMode?: number; @@ -59,7 +59,7 @@ export class VFXParticleSystem extends ParticleSystem implements IVFXSystem { ) { // Calculate capacity const duration = config.duration || 5; - const capacity = VFXCapacityCalculator.calculateForParticleSystem(config.emissionOverTime, duration); + const capacity = CapacityCalculator.calculateForParticleSystem(config.emissionOverTime, duration); super(name, capacity, scene); this._behaviors = []; @@ -74,7 +74,7 @@ export class VFXParticleSystem extends ParticleSystem implements IVFXSystem { /** * Get the parent node (emitter) for hierarchy operations - * Required by IVFXSystem interface + * Required by ISystem interface */ public getParentNode(): AbstractMesh | TransformNode | null { // ParticleSystem.emitter can be AbstractMesh, Vector3, or null @@ -85,18 +85,18 @@ export class VFXParticleSystem extends ParticleSystem implements IVFXSystem { /** * Get behavior functions (internal use) */ - public get behaviors(): VFXPerParticleBehaviorFunction[] { + public get behaviors(): PerParticleBehaviorFunction[] { return this._behaviors; } /** * Create a proxy array that automatically updates behavior functions when configs change */ - private _createBehaviorConfigsProxy(configs: VFXBehavior[]): VFXBehavior[] { + private _createBehaviorConfigsProxy(configs: Behavior[]): Behavior[] { const self = this; // Wrap each behavior object in a proxy to detect property changes - const wrapBehavior = (behavior: VFXBehavior): VFXBehavior => { + const wrapBehavior = (behavior: Behavior): Behavior => { return new Proxy(behavior, { set(target, prop, value) { const result = Reflect.set(target, prop, value); @@ -118,7 +118,7 @@ export class VFXParticleSystem extends ParticleSystem implements IVFXSystem { if (property === "length" || typeof property === "number") { // If setting an element, wrap it in proxy if (typeof property === "number" && value && typeof value === "object") { - Reflect.set(target, property, wrapBehavior(value as VFXBehavior)); + Reflect.set(target, property, wrapBehavior(value as Behavior)); } self._updateBehaviorFunctions(); } @@ -147,7 +147,7 @@ export class VFXParticleSystem extends ParticleSystem implements IVFXSystem { for (let i = 0; i < args.length; i++) { if (args[i] && typeof args[i] === "object") { const index = property === "push" ? target.length - args.length + i : i; - Reflect.set(target, index, wrapBehavior(args[i] as VFXBehavior)); + Reflect.set(target, index, wrapBehavior(args[i] as Behavior)); } } } @@ -178,13 +178,13 @@ export class VFXParticleSystem extends ParticleSystem implements IVFXSystem { * Create per-particle behavior functions from configurations * Only creates functions for behaviors that depend on particle properties (speed, orbit) */ - private _createPerParticleBehaviorFunctions(behaviors: VFXBehavior[]): VFXPerParticleBehaviorFunction[] { - const functions: VFXPerParticleBehaviorFunction[] = []; + private _createPerParticleBehaviorFunctions(behaviors: Behavior[]): PerParticleBehaviorFunction[] { + const functions: PerParticleBehaviorFunction[] = []; for (const behavior of behaviors) { switch (behavior.type) { case "ColorBySpeed": { - const b = behavior as VFXColorBySpeedBehavior; + const b = behavior as ColorBySpeedBehavior; functions.push((particle: Particle) => { applyColorBySpeedPS(particle, b); }); @@ -192,7 +192,7 @@ export class VFXParticleSystem extends ParticleSystem implements IVFXSystem { } case "SizeBySpeed": { - const b = behavior as VFXSizeBySpeedBehavior; + const b = behavior as SizeBySpeedBehavior; functions.push((particle: Particle) => { applySizeBySpeedPS(particle, b); }); @@ -200,7 +200,7 @@ export class VFXParticleSystem extends ParticleSystem implements IVFXSystem { } case "RotationBySpeed": { - const b = behavior as VFXRotationBySpeedBehavior; + const b = behavior as RotationBySpeedBehavior; functions.push((particle: Particle) => { // Store reference to system in particle for behaviors that need it const particleWithSystem = particle as ParticleWithSystem; @@ -211,7 +211,7 @@ export class VFXParticleSystem extends ParticleSystem implements IVFXSystem { } case "OrbitOverLife": { - const b = behavior as VFXOrbitOverLifeBehavior; + const b = behavior as OrbitOverLifeBehavior; functions.push((particle: Particle) => { applyOrbitOverLifePS(particle, b); }); @@ -235,54 +235,54 @@ export class VFXParticleSystem extends ParticleSystem implements IVFXSystem { switch (behavior.type) { case "ColorOverLife": - applyColorOverLifePS(this, behavior as VFXColorOverLifeBehavior); + applyColorOverLifePS(this, behavior as ColorOverLifeBehavior); break; case "SizeOverLife": - applySizeOverLifePS(this, behavior as VFXSizeOverLifeBehavior); + applySizeOverLifePS(this, behavior as SizeOverLifeBehavior); break; case "RotationOverLife": case "Rotation3DOverLife": - applyRotationOverLifePS(this, behavior as VFXRotationOverLifeBehavior); + applyRotationOverLifePS(this, behavior as RotationOverLifeBehavior); break; case "ForceOverLife": case "ApplyForce": - applyForceOverLifePS(this, behavior as VFXForceOverLifeBehavior); + applyForceOverLifePS(this, behavior as ForceOverLifeBehavior); break; case "GravityForce": - applyGravityForcePS(this, behavior as VFXGravityForceBehavior); + applyGravityForcePS(this, behavior as GravityForceBehavior); break; case "SpeedOverLife": - applySpeedOverLifePS(this, behavior as VFXSpeedOverLifeBehavior); + applySpeedOverLifePS(this, behavior as SpeedOverLifeBehavior); break; case "FrameOverLife": - applyFrameOverLifePS(this, behavior as VFXFrameOverLifeBehavior); + applyFrameOverLifePS(this, behavior as FrameOverLifeBehavior); break; case "LimitSpeedOverLife": - applyLimitSpeedOverLifePS(this, behavior as VFXLimitSpeedOverLifeBehavior); + applyLimitSpeedOverLifePS(this, behavior as LimitSpeedOverLifeBehavior); break; } } } /** - * Configure particle system from VFX config (internal use) - * This method applies all configuration from VFXParticleEmitterConfig + * Configure particle system from config (internal use) + * This method applies all configuration from ParticleEmitterConfig */ private _configureFromConfig( - config: VFXParticleEmitterConfig, + config: EmitterConfig, options?: { texture?: Texture; blendMode?: number; - emitterShape?: { shape: VFXShape | undefined; cumulativeScale: Vector3; rotationMatrix: Matrix | null }; + emitterShape?: { shape: Shape | undefined; cumulativeScale: Vector3; rotationMatrix: Matrix | null }; } ): void { // Parse values - const emissionRate = config.emissionOverTime !== undefined ? VFXValueUtils.parseConstantValue(config.emissionOverTime) : 10; + const emissionRate = config.emissionOverTime !== undefined ? ValueUtils.parseConstantValue(config.emissionOverTime) : 10; const duration = config.duration || 5; - const lifeTime = config.startLife !== undefined ? VFXValueUtils.parseIntervalValue(config.startLife) : { min: 1, max: 1 }; - const speed = config.startSpeed !== undefined ? VFXValueUtils.parseIntervalValue(config.startSpeed) : { min: 1, max: 1 }; - const size = config.startSize !== undefined ? VFXValueUtils.parseIntervalValue(config.startSize) : { min: 1, max: 1 }; - const startColor = config.startColor !== undefined ? VFXValueUtils.parseConstantColor(config.startColor) : new Color4(1, 1, 1, 1); + const lifeTime = config.startLife !== undefined ? ValueUtils.parseIntervalValue(config.startLife) : { min: 1, max: 1 }; + const speed = config.startSpeed !== undefined ? ValueUtils.parseIntervalValue(config.startSpeed) : { min: 1, max: 1 }; + const size = config.startSize !== undefined ? ValueUtils.parseIntervalValue(config.startSize) : { min: 1, max: 1 }; + const startColor = config.startColor !== undefined ? ValueUtils.parseConstantColor(config.startColor) : new Color4(1, 1, 1, 1); // Configure basic properties this.targetStopDuration = duration; @@ -302,12 +302,12 @@ export class VFXParticleSystem extends ParticleSystem implements IVFXSystem { if (config.startRotation) { if (this._isEulerRotation(config.startRotation)) { if (config.startRotation.angleZ !== undefined) { - const angleZ = VFXValueUtils.parseIntervalValue(config.startRotation.angleZ); + const angleZ = ValueUtils.parseIntervalValue(config.startRotation.angleZ); this.minInitialRotation = angleZ.min; this.maxInitialRotation = angleZ.max; } } else { - const rotation = VFXValueUtils.parseIntervalValue(config.startRotation as any); + const rotation = ValueUtils.parseIntervalValue(config.startRotation as any); this.minInitialRotation = rotation.min; this.maxInitialRotation = rotation.max; } @@ -321,7 +321,7 @@ export class VFXParticleSystem extends ParticleSystem implements IVFXSystem { this.spriteCellHeight = config.vTileCount; if (config.startTileIndex !== undefined) { - const startTile = VFXValueUtils.parseConstantValue(config.startTileIndex); + const startTile = ValueUtils.parseConstantValue(config.startTileIndex); this.startSpriteCellID = Math.floor(startTile); this.endSpriteCellID = Math.floor(startTile); } @@ -365,7 +365,7 @@ export class VFXParticleSystem extends ParticleSystem implements IVFXSystem { this.targetStopDuration = config.looping ? 0 : duration; } - // Configure billboard mode (converted from renderMode in VFXDataConverter) + // Configure billboard mode (converted from renderMode in DataConverter) if (config.isBillboardBased !== undefined) { this.isBillboardBased = config.isBillboardBased; } @@ -378,7 +378,7 @@ export class VFXParticleSystem extends ParticleSystem implements IVFXSystem { this.disposeOnStop = config.autoDestroy; } - // Emitter shape is created in VFXSystemFactory after system creation + // Emitter shape is created in SystemFactory after system creation } /** @@ -391,14 +391,14 @@ export class VFXParticleSystem extends ParticleSystem implements IVFXSystem { /** * Apply emission bursts via emit rate gradients */ - private _applyEmissionBursts(bursts: VFXEmissionBurst[], baseEmitRate: number, duration: number): void { + private _applyEmissionBursts(bursts: EmissionBurst[], baseEmitRate: number, duration: number): void { for (const burst of bursts) { if (burst.time === undefined || burst.count === undefined) { continue; } - const burstTime = VFXValueUtils.parseConstantValue(burst.time); - const burstCount = VFXValueUtils.parseConstantValue(burst.count); + const burstTime = ValueUtils.parseConstantValue(burst.time); + const burstCount = ValueUtils.parseConstantValue(burst.count); const timeRatio = Math.min(Math.max(burstTime / duration, 0), 1); const windowSize = 0.02; const burstEmitRate = burstCount / windowSize; diff --git a/editor/src/editor/windows/fx-editor/VFX/systems/VFXSolidParticleSystem.ts b/tools/src/effect/systems/effectSolidParticleSystem.ts similarity index 67% rename from editor/src/editor/windows/fx-editor/VFX/systems/VFXSolidParticleSystem.ts rename to tools/src/effect/systems/effectSolidParticleSystem.ts index 5dcf0db4a..0565ac552 100644 --- a/editor/src/editor/windows/fx-editor/VFX/systems/VFXSolidParticleSystem.ts +++ b/tools/src/effect/systems/effectSolidParticleSystem.ts @@ -1,27 +1,28 @@ -import { Vector3, Quaternion, Matrix, Color4, SolidParticleSystem, SolidParticle, TransformNode, Mesh, AbstractMesh } from "babylonjs"; -import type { VFXParticleEmitterConfig, VFXEmissionBurst } from "../types/emitterConfig"; -import type { ISolidParticleEmitterType } from "../types/emitters"; -import { SolidPointParticleEmitter, SolidSphereParticleEmitter, SolidConeParticleEmitter } from "../types/emitters"; -import { VFXLogger } from "../loggers/VFXLogger"; -import type { VFXLoaderOptions } from "../types/loader"; -import type { VFXPerSolidParticleBehaviorFunction } from "../types/VFXBehaviorFunction"; -import type { IVFXSystem, SolidParticleWithSystem } from "../types/system"; -import type { - VFXBehavior, - VFXForceOverLifeBehavior, - VFXColorBySpeedBehavior, - VFXSizeBySpeedBehavior, - VFXRotationBySpeedBehavior, - VFXOrbitOverLifeBehavior, -} from "../types/behaviors"; -import type { VFXShape } from "../types/shapes"; -import type { VFXColor } from "../types/colors"; -import type { VFXValue } from "../types/values"; -import type { VFXRotation } from "../types/rotations"; -import { VFXValueUtils } from "../utils/valueParser"; -import { VFXCapacityCalculator } from "../utils/capacityCalculator"; +import { Vector3, Quaternion, Matrix, Color4, SolidParticle, TransformNode, Mesh, AbstractMesh, SolidParticleSystem } from "babylonjs"; +import type { EmitterConfig, EmissionBurst } from "../types/emitter"; +import type { ISolidParticleEmitterType } from "../types/emitter"; +import { SolidPointParticleEmitter, SolidSphereParticleEmitter, SolidConeParticleEmitter } from "../emitters"; +import type { PerSolidParticleBehaviorFunction } from "../types/behaviors"; +import type { ISystem, SolidParticleWithSystem } from "../types/system"; +import type { Behavior, ForceOverLifeBehavior, ColorBySpeedBehavior, SizeBySpeedBehavior, RotationBySpeedBehavior, OrbitOverLifeBehavior } from "../types/behaviors"; +import type { Shape } from "../types/shapes"; +import type { Color } from "../types/colors"; +import type { Value } from "../types/values"; +import type { Rotation } from "../types/rotations"; +import { ValueUtils } from "../utils/valueParser"; +import { CapacityCalculator } from "../utils/capacityCalculator"; import { ColorGradientSystem, NumberGradientSystem } from "../utils/gradientSystem"; -import { applyColorBySpeedSPS, applySizeBySpeedSPS, applyRotationBySpeedSPS, applyOrbitOverLifeSPS } from "../behaviors"; +import { + applyColorBySpeedSPS, + applySizeBySpeedSPS, + applyRotationBySpeedSPS, + applyOrbitOverLifeSPS, + applyColorOverLifeSPS, + applyLimitSpeedOverLifeSPS, + applyRotationOverLifeSPS, + applySizeOverLifeSPS, + applySpeedOverLifeSPS, +} from "../behaviors"; /** * Emission state matching three.quarks EmissionState structure @@ -42,14 +43,12 @@ interface EmissionState { * Extended SolidParticleSystem implementing three.quarks Mesh systemType (systemType = "solid") logic * This class replicates the exact behavior of three.quarks ParticleSystem with systemType = "solid" */ -export class VFXSolidParticleSystem extends SolidParticleSystem implements IVFXSystem { +export class EffectSolidParticleSystem extends SolidParticleSystem implements ISystem { private _emissionState: EmissionState; - private _behaviors: VFXPerSolidParticleBehaviorFunction[]; + private _behaviors: PerSolidParticleBehaviorFunction[]; public particleEmitterType: ISolidParticleEmitterType | null; private _parent: TransformNode | null; - private _vfxTransform: { position: Vector3; rotation: Quaternion; scale: Vector3 } | null; - private _logger: VFXLogger | null; - private _name: string; + private _transform: { position: Vector3; rotation: Quaternion; scale: Vector3 } | null; private _emitEnded: boolean; // Gradient systems for "OverLife" behaviors (similar to ParticleSystem native gradients) @@ -64,23 +63,23 @@ export class VFXSolidParticleSystem extends SolidParticleSystem implements IVFXS public isLooping: boolean; public duration: number; public prewarm: boolean; - public shape?: VFXShape; - public startLife?: VFXValue; - public startSpeed?: VFXValue; - public startRotation?: VFXRotation; - public startSize?: VFXValue; - public startColor?: VFXColor; - public emissionOverTime?: VFXValue; - public emissionOverDistance?: VFXValue; - public emissionBursts?: VFXEmissionBurst[]; + public shape?: Shape; + public startLife?: Value; + public startSpeed?: Value; + public startRotation?: Rotation; + public startSize?: Value; + public startColor?: Color; + public emissionOverTime?: Value; + public emissionOverDistance?: Value; + public emissionBursts?: EmissionBurst[]; public onlyUsedByOther: boolean; public instancingGeometry?: string; public renderOrder?: number; public rendererEmitterSettings?: Record; public material?: string; public layers?: number; - public isBillboardBased?: boolean; // Converted from renderMode (always false for Mesh) - public startTileIndex?: VFXValue; + public isBillboardBased?: boolean; + public startTileIndex?: Value; public uTileCount?: number; public vTileCount?: number; public blendTiles?: boolean; @@ -88,7 +87,7 @@ export class VFXSolidParticleSystem extends SolidParticleSystem implements IVFXS public softFarFade?: number; public softNearFade?: number; public worldSpace: boolean; - public readonly behaviorConfigs: VFXBehavior[]; + public readonly behaviorConfigs: Behavior[]; /** * Get/set parent transform node @@ -104,14 +103,14 @@ export class VFXSolidParticleSystem extends SolidParticleSystem implements IVFXS } /** - * Get/set minSize (compatible with VFXParticleSystem API) - * Works with startSize VFXValue under the hood + * Get/set minSize (compatible with ParticleSystem API) + * Works with startSize Value under the hood */ public get minSize(): number { if (!this.startSize) { return 1; } - return VFXValueUtils.parseIntervalValue(this.startSize).min; + return ValueUtils.parseIntervalValue(this.startSize).min; } public set minSize(value: number) { if (!this.startSize) { @@ -135,14 +134,14 @@ export class VFXSolidParticleSystem extends SolidParticleSystem implements IVFXS } /** - * Get/set maxSize (compatible with VFXParticleSystem API) - * Works with startSize VFXValue under the hood + * Get/set maxSize (compatible with ParticleSystem API) + * Works with startSize Value under the hood */ public get maxSize(): number { if (!this.startSize) { return 1; } - return VFXValueUtils.parseIntervalValue(this.startSize).max; + return ValueUtils.parseIntervalValue(this.startSize).max; } public set maxSize(value: number) { if (!this.startSize) { @@ -166,14 +165,14 @@ export class VFXSolidParticleSystem extends SolidParticleSystem implements IVFXS } /** - * Get/set minLifeTime (compatible with VFXParticleSystem API) - * Works with startLife VFXValue under the hood + * Get/set minLifeTime (compatible with ParticleSystem API) + * Works with startLife Value under the hood */ public get minLifeTime(): number { if (!this.startLife) { return 1; } - return VFXValueUtils.parseIntervalValue(this.startLife).min; + return ValueUtils.parseIntervalValue(this.startLife).min; } public set minLifeTime(value: number) { if (!this.startLife) { @@ -197,14 +196,14 @@ export class VFXSolidParticleSystem extends SolidParticleSystem implements IVFXS } /** - * Get/set maxLifeTime (compatible with VFXParticleSystem API) - * Works with startLife VFXValue under the hood + * Get/set maxLifeTime (compatible with ParticleSystem API) + * Works with startLife Value under the hood */ public get maxLifeTime(): number { if (!this.startLife) { return 1; } - return VFXValueUtils.parseIntervalValue(this.startLife).max; + return ValueUtils.parseIntervalValue(this.startLife).max; } public set maxLifeTime(value: number) { if (!this.startLife) { @@ -228,14 +227,14 @@ export class VFXSolidParticleSystem extends SolidParticleSystem implements IVFXS } /** - * Get/set minEmitPower (compatible with VFXParticleSystem API) - * Works with startSpeed VFXValue under the hood + * Get/set minEmitPower (compatible with ParticleSystem API) + * Works with startSpeed Value under the hood */ public get minEmitPower(): number { if (!this.startSpeed) { return 1; } - return VFXValueUtils.parseIntervalValue(this.startSpeed).min; + return ValueUtils.parseIntervalValue(this.startSpeed).min; } public set minEmitPower(value: number) { if (!this.startSpeed) { @@ -259,14 +258,14 @@ export class VFXSolidParticleSystem extends SolidParticleSystem implements IVFXS } /** - * Get/set maxEmitPower (compatible with VFXParticleSystem API) - * Works with startSpeed VFXValue under the hood + * Get/set maxEmitPower (compatible with ParticleSystem API) + * Works with startSpeed Value under the hood */ public get maxEmitPower(): number { if (!this.startSpeed) { return 1; } - return VFXValueUtils.parseIntervalValue(this.startSpeed).max; + return ValueUtils.parseIntervalValue(this.startSpeed).max; } public set maxEmitPower(value: number) { if (!this.startSpeed) { @@ -290,14 +289,14 @@ export class VFXSolidParticleSystem extends SolidParticleSystem implements IVFXS } /** - * Get/set color1 (compatible with VFXParticleSystem API) - * Works with startColor VFXColor under the hood + * Get/set color1 (compatible with ParticleSystem API) + * Works with startColor Color under the hood */ public get color1(): Color4 { if (!this.startColor) { return new Color4(1, 1, 1, 1); } - return VFXValueUtils.parseConstantColor(this.startColor); + return ValueUtils.parseConstantColor(this.startColor); } public set color1(value: Color4) { this.startColor = { @@ -307,8 +306,8 @@ export class VFXSolidParticleSystem extends SolidParticleSystem implements IVFXS } /** - * Get/set minInitialRotation (compatible with VFXParticleSystem API) - * Works with startRotation VFXRotation under the hood (uses angleZ) + * Get/set minInitialRotation (compatible with ParticleSystem API) + * Works with startRotation Rotation under the hood (uses angleZ) */ public get minInitialRotation(): number { if (!this.startRotation) { @@ -317,13 +316,13 @@ export class VFXSolidParticleSystem extends SolidParticleSystem implements IVFXS // Handle Euler rotation with angleZ if (typeof this.startRotation === "object" && "type" in this.startRotation && this.startRotation.type === "Euler") { if (this.startRotation.angleZ) { - return VFXValueUtils.parseIntervalValue(this.startRotation.angleZ).min; + return ValueUtils.parseIntervalValue(this.startRotation.angleZ).min; } return 0; } - // Handle simple VFXValue rotation + // Handle simple Value rotation if (typeof this.startRotation === "object" && "type" in this.startRotation) { - return VFXValueUtils.parseIntervalValue(this.startRotation as any).min; + return ValueUtils.parseIntervalValue(this.startRotation as any).min; } return typeof this.startRotation === "number" ? this.startRotation : 0; } @@ -337,7 +336,7 @@ export class VFXSolidParticleSystem extends SolidParticleSystem implements IVFXS if (!this.startRotation.angleZ) { this.startRotation.angleZ = { type: "IntervalValue", min: value, max: value }; } else { - const currentMax = VFXValueUtils.parseIntervalValue(this.startRotation.angleZ).max; + const currentMax = ValueUtils.parseIntervalValue(this.startRotation.angleZ).max; this.startRotation.angleZ = { type: "IntervalValue", min: value, max: currentMax }; } return; @@ -348,8 +347,8 @@ export class VFXSolidParticleSystem extends SolidParticleSystem implements IVFXS } /** - * Get/set maxInitialRotation (compatible with VFXParticleSystem API) - * Works with startRotation VFXRotation under the hood (uses angleZ) + * Get/set maxInitialRotation (compatible with ParticleSystem API) + * Works with startRotation Rotation under the hood (uses angleZ) */ public get maxInitialRotation(): number { if (!this.startRotation) { @@ -358,13 +357,13 @@ export class VFXSolidParticleSystem extends SolidParticleSystem implements IVFXS // Handle Euler rotation with angleZ if (typeof this.startRotation === "object" && "type" in this.startRotation && this.startRotation.type === "Euler") { if (this.startRotation.angleZ) { - return VFXValueUtils.parseIntervalValue(this.startRotation.angleZ).max; + return ValueUtils.parseIntervalValue(this.startRotation.angleZ).max; } return 0; } - // Handle simple VFXValue rotation + // Handle simple Value rotation if (typeof this.startRotation === "object" && "type" in this.startRotation) { - return VFXValueUtils.parseIntervalValue(this.startRotation as any).max; + return ValueUtils.parseIntervalValue(this.startRotation as any).max; } return typeof this.startRotation === "number" ? this.startRotation : 0; } @@ -378,7 +377,7 @@ export class VFXSolidParticleSystem extends SolidParticleSystem implements IVFXS if (!this.startRotation.angleZ) { this.startRotation.angleZ = { type: "IntervalValue", min: value, max: value }; } else { - const currentMin = VFXValueUtils.parseIntervalValue(this.startRotation.angleZ).min; + const currentMin = ValueUtils.parseIntervalValue(this.startRotation.angleZ).min; this.startRotation.angleZ = { type: "IntervalValue", min: currentMin, max: value }; } return; @@ -390,7 +389,7 @@ export class VFXSolidParticleSystem extends SolidParticleSystem implements IVFXS /** * Get the parent node (mesh) for hierarchy operations - * Implements IVFXSystem interface + * Implements ISystem interface */ public getParentNode(): AbstractMesh | TransformNode | null { return this.mesh || null; @@ -469,7 +468,7 @@ export class VFXSolidParticleSystem extends SolidParticleSystem implements IVFXS /** * Get behavior functions (internal use) */ - public get behaviors(): VFXPerSolidParticleBehaviorFunction[] { + public get behaviors(): PerSolidParticleBehaviorFunction[] { return this._behaviors; } @@ -525,42 +524,20 @@ export class VFXSolidParticleSystem extends SolidParticleSystem implements IVFXS */ private _initializeMesh(particleMesh: Mesh): void { if (!particleMesh) { - if (this._logger) { - this._logger.warn(`Cannot add shape to SPS: particleMesh is null`); - } return; } - // Calculate capacity from config - const capacity = VFXCapacityCalculator.calculateForSolidParticleSystem(this.emissionOverTime, this.duration, this.isLooping); - - if (this._logger) { - this._logger.log(`Adding shape to SPS: mesh name=${particleMesh.name}, hasMaterial=${!!particleMesh.material}, capacity=${capacity}`); - } - - // Add shape to SPS + const capacity = CapacityCalculator.calculateForSolidParticleSystem(this.emissionOverTime, this.duration, this.isLooping); this.addShape(particleMesh, capacity); - // Configure billboard mode (converted from renderMode in VFXDataConverter) - // For Mesh (systemType === "solid"), isBillboardBased is always false - // For other modes, use isBillboardBased from config if (this.isBillboardBased !== undefined) { - // For SolidParticleSystem, billboard property controls billboard mode - // isBillboardBased = false means mesh mode (always false for systemType === "solid") this.billboard = this.isBillboardBased; } else { - // Default: no billboard for mesh this.billboard = false; } - // Build mesh immediately after adding shape to ensure mesh is available this.buildMesh(); - - // Setup mesh properties (parent, transform, etc.) - // Call _setupMeshProperties to configure mesh properly this._setupMeshProperties(); - - // Dispose temporary mesh after adding to SPS particleMesh.dispose(); } @@ -569,9 +546,9 @@ export class VFXSolidParticleSystem extends SolidParticleSystem implements IVFXS */ public get emitRate(): number { if (!this.emissionOverTime) { - return 10; // Default + return 10; } - return VFXValueUtils.parseConstantValue(this.emissionOverTime); + return ValueUtils.parseConstantValue(this.emissionOverTime); } public set emitRate(value: number) { this.emissionOverTime = { type: "ConstantValue", value }; @@ -594,7 +571,7 @@ export class VFXSolidParticleSystem extends SolidParticleSystem implements IVFXS constructor( name: string, scene: any, - initialConfig: VFXParticleEmitterConfig, // Initial config for parsing + initialConfig: EmitterConfig, options?: { updatable?: boolean; isPickable?: boolean; @@ -602,19 +579,15 @@ export class VFXSolidParticleSystem extends SolidParticleSystem implements IVFXS particleIntersection?: boolean; useModelMaterial?: boolean; parentGroup?: TransformNode | null; - vfxTransform?: { position: Vector3; rotation: Quaternion; scale: Vector3 } | null; - logger?: VFXLogger | null; - loaderOptions?: VFXLoaderOptions; - particleMesh?: Mesh | null; // Pre-created mesh for initialization + transform?: { position: Vector3; rotation: Quaternion; scale: Vector3 } | null; + particleMesh?: Mesh | null; } ) { super(name, scene, options); - this._name = name; + this.name = name; this._behaviors = []; - this.particleEmitterType = null; // Will be set by create*Emitter methods - - // Initialize properties from initialConfig + this.particleEmitterType = null; this.isLooping = initialConfig.looping !== false; this.duration = initialConfig.duration || 5; this.prewarm = initialConfig.prewarm || false; @@ -633,7 +606,7 @@ export class VFXSolidParticleSystem extends SolidParticleSystem implements IVFXS this.rendererEmitterSettings = initialConfig.rendererEmitterSettings; this.material = initialConfig.material; this.layers = initialConfig.layers; - this.isBillboardBased = initialConfig.isBillboardBased; // Always false for Mesh (systemType === "solid") + this.isBillboardBased = initialConfig.isBillboardBased; this.startTileIndex = initialConfig.startTileIndex; this.uTileCount = initialConfig.uTileCount; this.vTileCount = initialConfig.vTileCount; @@ -643,7 +616,6 @@ export class VFXSolidParticleSystem extends SolidParticleSystem implements IVFXS this.softNearFade = initialConfig.softNearFade; this.worldSpace = initialConfig.worldSpace || false; - // Initialize gradient systems this._colorGradients = new ColorGradientSystem(); this._sizeGradients = new NumberGradientSystem(); this._velocityGradients = new NumberGradientSystem(); @@ -651,15 +623,12 @@ export class VFXSolidParticleSystem extends SolidParticleSystem implements IVFXS this._limitVelocityGradients = new NumberGradientSystem(); this._limitVelocityDamping = 0.1; - // Create proxy array for behavior configs this.behaviorConfigs = this._createBehaviorConfigsProxy(initialConfig.behaviors || []); - // Initialize behavior functions from config this._updateBehaviorFunctions(); this._parent = options?.parentGroup ?? null; - this._vfxTransform = options?.vfxTransform ?? null; - this._logger = options?.logger ?? null; + this._transform = options?.transform ?? null; this._emitEnded = false; this._normalMatrix = new Matrix(); this._tempVec = Vector3.Zero(); @@ -679,7 +648,6 @@ export class VFXSolidParticleSystem extends SolidParticleSystem implements IVFXS isBursting: false, }; - // Initialize mesh if provided if (options?.particleMesh) { this._initializeMesh(options.particleMesh); } @@ -687,7 +655,6 @@ export class VFXSolidParticleSystem extends SolidParticleSystem implements IVFXS /** * Find a dead particle for recycling - * Оптимизировано: кешируем particles и nbParticles */ private _findDeadParticle(): SolidParticle | null { const particles = this.particles; @@ -702,39 +669,35 @@ export class VFXSolidParticleSystem extends SolidParticleSystem implements IVFXS /** * Reset particle to initial state for recycling - * Оптимизировано: используем прямые присваивания вместо setAll где возможно */ private _resetParticle(particle: SolidParticle): void { particle.age = 0; particle.alive = true; particle.isVisible = true; - particle._stillInvisible = false; // Сбрасываем флаг невидимости + particle._stillInvisible = false; particle.position.setAll(0); particle.velocity.setAll(0); particle.rotation.setAll(0); particle.scaling.setAll(1); - // Оптимизация: создаем color только если его нет if (particle.color) { particle.color.set(1, 1, 1, 1); } else { particle.color = new Color4(1, 1, 1, 1); } - // Оптимизация: создаем props только если его нет const props = particle.props || (particle.props = {}); props.speedModifier = 1.0; } /** * Initialize particle color - * Оптимизировано: кешируем props и избегаем лишних созданий объектов */ private _initializeParticleColor(particle: SolidParticle): void { const props = particle.props!; if (this.startColor !== undefined) { - const startColor = VFXValueUtils.parseConstantColor(this.startColor); + const startColor = ValueUtils.parseConstantColor(this.startColor); props.startColor = startColor.clone(); if (particle.color) { particle.color.copyFrom(startColor); @@ -742,7 +705,6 @@ export class VFXSolidParticleSystem extends SolidParticleSystem implements IVFXS particle.color = startColor.clone(); } } else { - // Используем один объект для всех частиц без цвета (оптимизация памяти) if (!particle.color) { particle.color = new Color4(1, 1, 1, 1); } else { @@ -754,12 +716,11 @@ export class VFXSolidParticleSystem extends SolidParticleSystem implements IVFXS /** * Initialize particle speed - * Оптимизировано: normalizedTime передается как параметр (вычисляется один раз в _spawn) */ private _initializeParticleSpeed(particle: SolidParticle, normalizedTime: number): void { const props = particle.props!; if (this.startSpeed !== undefined) { - props.startSpeed = VFXValueUtils.parseValue(this.startSpeed, normalizedTime); + props.startSpeed = ValueUtils.parseValue(this.startSpeed, normalizedTime); } else { props.startSpeed = 0; } @@ -767,11 +728,10 @@ export class VFXSolidParticleSystem extends SolidParticleSystem implements IVFXS /** * Initialize particle lifetime - * Оптимизировано: normalizedTime передается как параметр (вычисляется один раз в _spawn) */ private _initializeParticleLife(particle: SolidParticle, normalizedTime: number): void { if (this.startLife !== undefined) { - particle.lifeTime = VFXValueUtils.parseValue(this.startLife, normalizedTime); + particle.lifeTime = ValueUtils.parseValue(this.startLife, normalizedTime); } else { particle.lifeTime = 1; } @@ -779,12 +739,11 @@ export class VFXSolidParticleSystem extends SolidParticleSystem implements IVFXS /** * Initialize particle size - * Оптимизировано: normalizedTime передается как параметр (вычисляется один раз в _spawn) */ private _initializeParticleSize(particle: SolidParticle, normalizedTime: number): void { const props = particle.props!; if (this.startSize !== undefined) { - const sizeValue = VFXValueUtils.parseValue(this.startSize, normalizedTime); + const sizeValue = ValueUtils.parseValue(this.startSize, normalizedTime); props.startSize = sizeValue; particle.scaling.setAll(sizeValue); } else { @@ -803,49 +762,42 @@ export class VFXSolidParticleSystem extends SolidParticleSystem implements IVFXS return; } - // Handle simple VFXValue (treat as angleZ for backward compatibility) if ( typeof this.startRotation === "number" || (typeof this.startRotation === "object" && "type" in this.startRotation && (this.startRotation.type === "ConstantValue" || this.startRotation.type === "IntervalValue" || this.startRotation.type === "PiecewiseBezier")) ) { - const angleZ = VFXValueUtils.parseValue(this.startRotation as VFXValue, normalizedTime); + const angleZ = ValueUtils.parseValue(this.startRotation as Value, normalizedTime); particle.rotation.set(0, 0, angleZ); return; } - // Handle Euler rotation if (this.startRotation.type === "Euler") { - const angleX = this.startRotation.angleX ? VFXValueUtils.parseValue(this.startRotation.angleX, normalizedTime) : 0; - const angleY = this.startRotation.angleY ? VFXValueUtils.parseValue(this.startRotation.angleY, normalizedTime) : 0; - const angleZ = this.startRotation.angleZ ? VFXValueUtils.parseValue(this.startRotation.angleZ, normalizedTime) : 0; + const angleX = this.startRotation.angleX ? ValueUtils.parseValue(this.startRotation.angleX, normalizedTime) : 0; + const angleY = this.startRotation.angleY ? ValueUtils.parseValue(this.startRotation.angleY, normalizedTime) : 0; + const angleZ = this.startRotation.angleZ ? ValueUtils.parseValue(this.startRotation.angleZ, normalizedTime) : 0; const order = this.startRotation.order || "xyz"; - // Convert Euler angles to quaternion based on order let quat: Quaternion; if (order === "xyz") { - // XYZ order: apply X, then Y, then Z quat = Quaternion.RotationYawPitchRoll(angleY, angleX, angleZ); } else { - // ZYX order: apply Z, then Y, then X const quatZ = Quaternion.RotationAxis(Vector3.Forward(), angleZ); const quatY = Quaternion.RotationAxis(Vector3.Up(), angleY); const quatX = Quaternion.RotationAxis(Vector3.Right(), angleX); quat = quatZ.multiply(quatY).multiply(quatX); } - // Convert quaternion to Euler for particle.rotation (Vector3) const euler = quat.toEulerAngles(); particle.rotation.set(euler.x, euler.y, euler.z); return; } - // Handle AxisAngle rotation if (this.startRotation.type === "AxisAngle") { - const axisX = this.startRotation.x ? VFXValueUtils.parseValue(this.startRotation.x, normalizedTime) : 0; - const axisY = this.startRotation.y ? VFXValueUtils.parseValue(this.startRotation.y, normalizedTime) : 0; - const axisZ = this.startRotation.z ? VFXValueUtils.parseValue(this.startRotation.z, normalizedTime) : 1; - const angle = this.startRotation.angle ? VFXValueUtils.parseValue(this.startRotation.angle, normalizedTime) : 0; + const axisX = this.startRotation.x ? ValueUtils.parseValue(this.startRotation.x, normalizedTime) : 0; + const axisY = this.startRotation.y ? ValueUtils.parseValue(this.startRotation.y, normalizedTime) : 0; + const axisZ = this.startRotation.z ? ValueUtils.parseValue(this.startRotation.z, normalizedTime) : 1; + const angle = this.startRotation.angle ? ValueUtils.parseValue(this.startRotation.angle, normalizedTime) : 0; const axis = new Vector3(axisX, axisY, axisZ); axis.normalize(); @@ -855,9 +807,7 @@ export class VFXSolidParticleSystem extends SolidParticleSystem implements IVFXS return; } - // Handle RandomQuat rotation if (this.startRotation.type === "RandomQuat") { - // Generate random quaternion (uniform distribution on unit sphere) const u1 = Math.random(); const u2 = Math.random(); const u3 = Math.random(); @@ -874,13 +824,11 @@ export class VFXSolidParticleSystem extends SolidParticleSystem implements IVFXS return; } - // Fallback: no rotation particle.rotation.setAll(0); } /** * Spawn particles from dead pool - * Оптимизировано: вычисляем матрицу эмиттера один раз для всех частиц */ private _spawn(count: number): void { if (count <= 0) { @@ -889,7 +837,6 @@ export class VFXSolidParticleSystem extends SolidParticleSystem implements IVFXS const emissionState = this._emissionState; - // Вычисляем матрицу эмиттера один раз для всех частиц const emitterMatrix = this._getEmitterMatrix(); const translation = this._tempVec; const quaternion = this._tempQuat; @@ -897,7 +844,6 @@ export class VFXSolidParticleSystem extends SolidParticleSystem implements IVFXS emitterMatrix.decompose(scale, quaternion, translation); emitterMatrix.toNormalMatrix(this._normalMatrix); - // Кешируем normalizedTime один раз для всех частиц в этом спавне const normalizedTime = this.duration > 0 ? this._emissionState.time / this.duration : 0; for (let i = 0; i < count; i++) { @@ -905,14 +851,9 @@ export class VFXSolidParticleSystem extends SolidParticleSystem implements IVFXS const particle = this._findDeadParticle(); if (!particle) { - // Логируем только один раз для избежания спама - if (i === 0 && this._logger) { - this._logger.warn(`No dead particles available for spawning. Capacity may be insufficient.`); - } - break; // Нет смысла продолжать, если нет мертвых частиц + break; } - // Вызываем методы напрямую (быстрее, чем bind) this._resetParticle(particle); this._initializeParticleColor(particle); this._initializeParticleSpeed(particle, normalizedTime); @@ -931,7 +872,6 @@ export class VFXSolidParticleSystem extends SolidParticleSystem implements IVFXS if (this.particleEmitterType) { this.particleEmitterType.initializeParticle(particle, startSpeed); } else { - // Fallback: default point emitter particle.position.setAll(0); particle.velocity.set(0, 1, 0); particle.velocity.scaleInPlace(startSpeed); @@ -1005,7 +945,7 @@ export class VFXSolidParticleSystem extends SolidParticleSystem implements IVFXS while (emissionState.burstIndex < this.emissionBursts.length && this._getBurstTime(this.emissionBursts[emissionState.burstIndex]) <= emissionState.time) { const burst = this.emissionBursts[emissionState.burstIndex]; - const burstCount = VFXValueUtils.parseConstantValue(burst.count); + const burstCount = ValueUtils.parseConstantValue(burst.count); emissionState.isBursting = true; emissionState.burstParticleCount = burstCount; this._spawn(burstCount); @@ -1021,11 +961,11 @@ export class VFXSolidParticleSystem extends SolidParticleSystem implements IVFXS return; } - const emissionRate = this.emissionOverTime !== undefined ? VFXValueUtils.parseConstantValue(this.emissionOverTime) : 10; + const emissionRate = this.emissionOverTime !== undefined ? ValueUtils.parseConstantValue(this.emissionOverTime) : 10; emissionState.waitEmiting += delta * emissionRate; if (this.emissionOverDistance !== undefined && this.mesh && this.mesh.position) { - const emitPerMeter = VFXValueUtils.parseConstantValue(this.emissionOverDistance); + const emitPerMeter = ValueUtils.parseConstantValue(this.emissionOverDistance); if (emitPerMeter > 0 && emissionState.previousWorldPos) { const distance = Vector3.Distance(emissionState.previousWorldPos, this.mesh.position); emissionState.travelDistance += distance; @@ -1043,18 +983,13 @@ export class VFXSolidParticleSystem extends SolidParticleSystem implements IVFXS } private _emit(delta: number): void { - // Сначала накапливаем эмиссию для текущего кадра this._accumulateEmission(delta); - - // Потом спавним частицы из накопленного waitEmiting this._spawnFromWaitEmiting(); - - // Спавним bursts this._spawnBursts(); } - private _getBurstTime(burst: VFXEmissionBurst): number { - return VFXValueUtils.parseConstantValue(burst.time); + private _getBurstTime(burst: EmissionBurst): number { + return ValueUtils.parseConstantValue(burst.time); } /** @@ -1064,13 +999,8 @@ export class VFXSolidParticleSystem extends SolidParticleSystem implements IVFXS public override buildMesh(): Mesh { const mesh = super.buildMesh(); - // Enable vertex colors and alpha for particle color support - // This is required for ColorOverLife behavior to work if (mesh) { mesh.hasVertexAlpha = true; - if (this._logger) { - this._logger.log(`Enabled hasVertexAlpha for SPS mesh: ${mesh.name}`); - } } return mesh; @@ -1078,61 +1008,29 @@ export class VFXSolidParticleSystem extends SolidParticleSystem implements IVFXS private _setupMeshProperties(): void { if (!this.mesh) { - if (this._logger) { - this._logger.warn(` SPS mesh is null in initParticles!`); - } return; } - // Ensure vertex alpha is enabled (in case mesh was already built) if (!this.mesh.hasVertexAlpha) { this.mesh.hasVertexAlpha = true; - if (this._logger) { - this._logger.log(`Enabled hasVertexAlpha for existing SPS mesh: ${this.mesh.name}`); - } - } - - if (this._logger) { - this._logger.log(` initParticles called for SPS: ${this._name}`); - this._logger.log(` SPS mesh exists: ${this.mesh.name}`); } if (this.renderOrder !== undefined) { this.mesh.renderingGroupId = this.renderOrder; - if (this._logger) { - this._logger.log(` Set SPS mesh renderingGroupId: ${this.renderOrder}`); - } } if (this.layers !== undefined) { this.mesh.layerMask = this.layers; - if (this._logger) { - this._logger.log(` Set SPS mesh layerMask: ${this.layers}`); - } } if (this._parent) { this.mesh.setParent(this._parent, false, true); - if (this._logger) { - this._logger.log(` Set SPS mesh parent to: ${this._parent.name}`); - } - } else if (this._logger) { - this._logger.log(` No parent group to set for SPS mesh`); } - if (this._vfxTransform) { - this.mesh.position.copyFrom(this._vfxTransform.position); - this.mesh.rotationQuaternion = this._vfxTransform.rotation.clone(); - this.mesh.scaling.copyFrom(this._vfxTransform.scale); - - if (this._logger) { - const rot = this.mesh.rotationQuaternion; - this._logger.log( - ` Applied VFX transform to SPS mesh: pos=(${this._vfxTransform.position.x.toFixed(2)}, ${this._vfxTransform.position.y.toFixed(2)}, ${this._vfxTransform.position.z.toFixed(2)}), rot=(${rot ? rot.x.toFixed(4) : 0}, ${rot ? rot.y.toFixed(4) : 0}, ${rot ? rot.z.toFixed(4) : 0}, ${rot ? rot.w.toFixed(4) : 1}), scale=(${this._vfxTransform.scale.x.toFixed(2)}, ${this._vfxTransform.scale.y.toFixed(2)}, ${this._vfxTransform.scale.z.toFixed(2)})` - ); - } - } else if (this._logger) { - this._logger.log(` No VFX transform to apply to SPS mesh`); + if (this._transform) { + this.mesh.position.copyFrom(this._transform.position); + this.mesh.rotationQuaternion = this._transform.rotation.clone(); + this.mesh.scaling.copyFrom(this._transform.scale); } } @@ -1179,33 +1077,28 @@ export class VFXSolidParticleSystem extends SolidParticleSystem implements IVFXS /** * Create a proxy array that automatically updates behavior functions when configs change */ - private _createBehaviorConfigsProxy(configs: VFXBehavior[]): VFXBehavior[] { + private _createBehaviorConfigsProxy(configs: Behavior[]): Behavior[] { const self = this; - // Wrap each behavior object in a proxy to detect property changes - const wrapBehavior = (behavior: VFXBehavior): VFXBehavior => { + const wrapBehavior = (behavior: Behavior): Behavior => { return new Proxy(behavior, { set(target, prop, value) { const result = Reflect.set(target, prop, value); - // When a behavior property changes, update functions self._updateBehaviorFunctions(); return result; }, }); }; - // Wrap all initial behaviors const wrappedConfigs = configs.map(wrapBehavior); return new Proxy(wrappedConfigs, { set(target, property, value) { const result = Reflect.set(target, property, value); - // Update functions when array is modified if (property === "length" || typeof property === "number") { - // If setting an element, wrap it in proxy if (typeof property === "number" && value && typeof value === "object") { - Reflect.set(target, property, wrapBehavior(value as VFXBehavior)); + Reflect.set(target, property, wrapBehavior(value as Behavior)); } self._updateBehaviorFunctions(); } @@ -1216,7 +1109,6 @@ export class VFXSolidParticleSystem extends SolidParticleSystem implements IVFXS get(target, property) { const value = Reflect.get(target, property); - // Intercept array methods that modify the array if ( typeof value === "function" && (property === "push" || @@ -1229,12 +1121,11 @@ export class VFXSolidParticleSystem extends SolidParticleSystem implements IVFXS ) { return function (...args: any[]) { const result = value.apply(target, args); - // Wrap any new behaviors added via push/unshift if (property === "push" || property === "unshift") { for (let i = 0; i < args.length; i++) { if (args[i] && typeof args[i] === "object") { const index = property === "push" ? target.length - args.length + i : i; - Reflect.set(target, index, wrapBehavior(args[i] as VFXBehavior)); + Reflect.set(target, index, wrapBehavior(args[i] as Behavior)); } } } @@ -1257,17 +1148,14 @@ export class VFXSolidParticleSystem extends SolidParticleSystem implements IVFXS * Applies both system-level behaviors (gradients) and per-particle behaviors */ private _updateBehaviorFunctions(): void { - // Clear all gradients this._colorGradients.clear(); this._sizeGradients.clear(); this._velocityGradients.clear(); this._angularSpeedGradients.clear(); this._limitVelocityGradients.clear(); - // Apply system-level behaviors (gradients) - these configure the system once this._applySystemLevelBehaviors(); - // Create per-particle behavior functions (BySpeed, OrbitOverLife, ForceOverLife, etc.) this._behaviors = this._createPerParticleBehaviorFunctions(this.behaviorConfigs); } @@ -1276,16 +1164,15 @@ export class VFXSolidParticleSystem extends SolidParticleSystem implements IVFXS * Only creates functions for behaviors that depend on particle properties (speed, orbit, force) * "OverLife" behaviors are handled by gradients (system-level) */ - private _createPerParticleBehaviorFunctions(behaviors: VFXBehavior[]): VFXPerSolidParticleBehaviorFunction[] { - const functions: VFXPerSolidParticleBehaviorFunction[] = []; + private _createPerParticleBehaviorFunctions(behaviors: Behavior[]): PerSolidParticleBehaviorFunction[] { + const functions: PerSolidParticleBehaviorFunction[] = []; for (const behavior of behaviors) { switch (behavior.type) { case "ForceOverLife": case "ApplyForce": { - const b = behavior as VFXForceOverLifeBehavior; + const b = behavior as ForceOverLifeBehavior; functions.push((particle: SolidParticle) => { - // Get updateSpeed from system (stored in particle.props or use default) const particleWithSystem = particle as SolidParticleWithSystem; const updateSpeed = particleWithSystem.system?.updateSpeed ?? 0.016; @@ -1293,9 +1180,9 @@ export class VFXSolidParticleSystem extends SolidParticleSystem implements IVFXS const forceY = b.y ?? b.force?.y; const forceZ = b.z ?? b.force?.z; if (forceX !== undefined || forceY !== undefined || forceZ !== undefined) { - const fx = forceX !== undefined ? VFXValueUtils.parseConstantValue(forceX) : 0; - const fy = forceY !== undefined ? VFXValueUtils.parseConstantValue(forceY) : 0; - const fz = forceZ !== undefined ? VFXValueUtils.parseConstantValue(forceZ) : 0; + const fx = forceX !== undefined ? ValueUtils.parseConstantValue(forceX) : 0; + const fy = forceY !== undefined ? ValueUtils.parseConstantValue(forceY) : 0; + const fz = forceZ !== undefined ? ValueUtils.parseConstantValue(forceZ) : 0; particle.velocity.x += fx * updateSpeed; particle.velocity.y += fy * updateSpeed; particle.velocity.z += fz * updateSpeed; @@ -1305,7 +1192,7 @@ export class VFXSolidParticleSystem extends SolidParticleSystem implements IVFXS } case "ColorBySpeed": { - const b = behavior as VFXColorBySpeedBehavior; + const b = behavior as ColorBySpeedBehavior; functions.push((particle: SolidParticle) => { applyColorBySpeedSPS(particle, b); }); @@ -1313,7 +1200,7 @@ export class VFXSolidParticleSystem extends SolidParticleSystem implements IVFXS } case "SizeBySpeed": { - const b = behavior as VFXSizeBySpeedBehavior; + const b = behavior as SizeBySpeedBehavior; functions.push((particle: SolidParticle) => { applySizeBySpeedSPS(particle, b); }); @@ -1321,7 +1208,7 @@ export class VFXSolidParticleSystem extends SolidParticleSystem implements IVFXS } case "RotationBySpeed": { - const b = behavior as VFXRotationBySpeedBehavior; + const b = behavior as RotationBySpeedBehavior; functions.push((particle: SolidParticle) => { applyRotationBySpeedSPS(particle, b); }); @@ -1329,7 +1216,7 @@ export class VFXSolidParticleSystem extends SolidParticleSystem implements IVFXS } case "OrbitOverLife": { - const b = behavior as VFXOrbitOverLifeBehavior; + const b = behavior as OrbitOverLifeBehavior; functions.push((particle: SolidParticle) => { applyOrbitOverLifeSPS(particle, b); }); @@ -1347,14 +1234,6 @@ export class VFXSolidParticleSystem extends SolidParticleSystem implements IVFXS * Similar to ParticleSystem native gradients */ private _applySystemLevelBehaviors(): void { - // Import behaviors dynamically to avoid circular dependencies - const behaviors = require("../behaviors"); - const applyColorOverLifeSPS = behaviors.applyColorOverLifeSPS; - const applySizeOverLifeSPS = behaviors.applySizeOverLifeSPS; - const applyRotationOverLifeSPS = behaviors.applyRotationOverLifeSPS; - const applySpeedOverLifeSPS = behaviors.applySpeedOverLifeSPS; - const applyLimitSpeedOverLifeSPS = behaviors.applyLimitSpeedOverLifeSPS; - for (const behavior of this.behaviorConfigs) { if (!behavior.type) { continue; @@ -1384,7 +1263,6 @@ export class VFXSolidParticleSystem extends SolidParticleSystem implements IVFXS public override beforeUpdateParticles(start?: number, stop?: number, update?: boolean): void { super.beforeUpdateParticles(start, stop, update); - // If system is stopped, hide all particles and return early if (this._stopped) { const particles = this.particles; const nbParticles = this.nbParticles; @@ -1403,62 +1281,43 @@ export class VFXSolidParticleSystem extends SolidParticleSystem implements IVFXS const deltaTime = this._scaledUpdateSpeed || 0.016; - // Сначала увеличиваем время this._emissionState.time += deltaTime; - // Потом эмиттим (внутри _emit будет накопление и спавн) this._emit(deltaTime); - // В конце обрабатываем looping (теперь time уже увеличен) this._handleEmissionLooping(); } private _updateParticle(particle: SolidParticle): SolidParticle { - // Ранний выход для мертвых частиц - базовый класс пропустит их обработку (continue) if (!particle.alive) { particle.isVisible = false; - // Обнуляем позиции только если частица еще не была помечена как невидимая - // Базовый класс обнуляет позиции для невидимых, но живых частиц в блоке else. - // Для мертвых частиц базовый класс делает continue до блока else, - // поэтому нам нужно обнулить позиции здесь, но только один раз. if (!particle._stillInvisible && this._positions32 && particle._model) { const shape = particle._model._shape; const startIdx = particle._pos; const positions32 = this._positions32; - // Оптимизированное обнуление: используем один цикл с прямым доступом for (let pt = 0, len = shape.length; pt < len; pt++) { const idx = startIdx + pt * 3; positions32[idx] = positions32[idx + 1] = positions32[idx + 2] = 0; } - particle._stillInvisible = true; // Помечаем как невидимую для оптимизации + particle._stillInvisible = true; } return particle; } - // Базовый класс уже обновил particle.age и проверил lifetime перед вызовом updateParticle - // Calculate lifeRatio for gradient interpolation const lifeRatio = particle.lifeTime > 0 ? particle.age / particle.lifeTime : 0; - // Apply "OverLife" gradients (similar to ParticleSystem native gradients) this._applyGradients(particle, lifeRatio); - // Store reference to system in particle for behaviors that need it - // Используем type assertion только один раз для оптимизации const particleWithSystem = particle as SolidParticleWithSystem; particleWithSystem.system = this; - // Apply per-particle behaviors (BySpeed, OrbitOverLife, etc.) - // These behaviors don't use gradients as they depend on particle properties, not lifeRatio - // Behavior config is captured in closure, so we only need to pass particle const behaviors = this._behaviors; for (let i = 0, len = behaviors.length; i < len; i++) { behaviors[i](particle); } - // Apply velocity with speed modifier - // Оптимизация: кешируем props и используем прямое обращение const props = particle.props; const speedModifier = props?.speedModifier ?? 1.0; const updateSpeed = this.updateSpeed; @@ -1469,21 +1328,15 @@ export class VFXSolidParticleSystem extends SolidParticleSystem implements IVFXS /** * Apply gradients to particle based on lifeRatio - * Оптимизировано для производительности: кешируем props и updateSpeed */ private _applyGradients(particle: SolidParticle, lifeRatio: number): void { - // Кешируем props и updateSpeed для избежания повторных обращений const props = particle.props || (particle.props = {}); const updateSpeed = this.updateSpeed; - // Apply color gradient const color = this._colorGradients.getValue(lifeRatio); if (color && particle.color) { - // Always apply gradient color directly to particle.color - // The base class will apply this to vertex colors if _computeParticleColor is enabled particle.color.copyFrom(color); - // Multiply with startColor if it exists (matching ParticleSystem behavior) const startColor = props.startColor; if (startColor) { particle.color.r *= startColor.r; @@ -1493,25 +1346,21 @@ export class VFXSolidParticleSystem extends SolidParticleSystem implements IVFXS } } - // Apply size gradient const size = this._sizeGradients.getValue(lifeRatio); if (size !== null && props.startSize !== undefined) { particle.scaling.setAll(props.startSize * size); } - // Apply velocity gradient (speed modifier) const velocity = this._velocityGradients.getValue(lifeRatio); if (velocity !== null) { props.speedModifier = velocity; } - // Apply angular speed gradient const angularSpeed = this._angularSpeedGradients.getValue(lifeRatio); if (angularSpeed !== null) { particle.rotation.z += angularSpeed * updateSpeed; } - // Apply limit velocity const limitVelocity = this._limitVelocityGradients.getValue(lifeRatio); if (limitVelocity !== null && this._limitVelocityDamping > 0) { const vel = particle.velocity; diff --git a/tools/src/effect/systems/index.ts b/tools/src/effect/systems/index.ts new file mode 100644 index 000000000..e0db1369e --- /dev/null +++ b/tools/src/effect/systems/index.ts @@ -0,0 +1,2 @@ +export { EffectParticleSystem } from "./effectParticleSystem"; +export { EffectSolidParticleSystem } from "./effectSolidParticleSystem"; diff --git a/tools/src/effect/types/behaviors.ts b/tools/src/effect/types/behaviors.ts new file mode 100644 index 000000000..618b2845f --- /dev/null +++ b/tools/src/effect/types/behaviors.ts @@ -0,0 +1,156 @@ +import type { Value } from "./values"; +import type { GradientKey } from "./gradients"; +import { Particle, ParticleSystem, SolidParticle, SolidParticleSystem } from "babylonjs"; + +/** + * Per-particle behavior function for ParticleSystem + * Behavior config is captured in closure, only particle is needed + */ +export type PerParticleBehaviorFunction = (particle: Particle) => void; + +/** + * Per-particle behavior function for SolidParticleSystem + * Behavior config is captured in closure, only particle is needed + */ +export type PerSolidParticleBehaviorFunction = (particle: SolidParticle) => void; + +/** + * System-level behavior function (applied once during initialization) + * Takes only system and behavior config - all data comes from system + */ +export type SystemBehaviorFunction = (system: ParticleSystem | SolidParticleSystem, behavior: Behavior) => void; + +/** + * behavior types (converted from Quarks) + */ +export interface ColorOverLifeBehavior { + type: "ColorOverLife"; + color?: { + color?: { + keys: GradientKey[]; + }; + alpha?: { + keys: GradientKey[]; + }; + keys?: GradientKey[]; + }; +} + +export interface SizeOverLifeBehavior { + type: "SizeOverLife"; + size?: { + keys?: GradientKey[]; + functions?: Array<{ + start: number; + function: { + p0?: number; + p3?: number; + }; + }>; + }; +} + +export interface RotationOverLifeBehavior { + type: "RotationOverLife" | "Rotation3DOverLife"; + angularVelocity?: Value; +} + +export interface ForceOverLifeBehavior { + type: "ForceOverLife" | "ApplyForce"; + force?: { + x?: Value; + y?: Value; + z?: Value; + }; + x?: Value; + y?: Value; + z?: Value; +} + +export interface GravityForceBehavior { + type: "GravityForce"; + gravity?: Value; +} + +export interface SpeedOverLifeBehavior { + type: "SpeedOverLife"; + speed?: + | { + keys?: GradientKey[]; + functions?: Array<{ + start: number; + function: { + p0?: number; + p3?: number; + }; + }>; + } + | Value; +} + +export interface FrameOverLifeBehavior { + type: "FrameOverLife"; + frame?: + | { + keys?: GradientKey[]; + } + | Value; +} + +export interface LimitSpeedOverLifeBehavior { + type: "LimitSpeedOverLife"; + maxSpeed?: Value; + speed?: Value | { keys?: GradientKey[] }; + dampen?: Value; +} + +export interface ColorBySpeedBehavior { + type: "ColorBySpeed"; + color?: { + keys: GradientKey[]; + }; + minSpeed?: Value; + maxSpeed?: Value; +} + +export interface SizeBySpeedBehavior { + type: "SizeBySpeed"; + size?: { + keys: GradientKey[]; + }; + minSpeed?: Value; + maxSpeed?: Value; +} + +export interface RotationBySpeedBehavior { + type: "RotationBySpeed"; + angularVelocity?: Value; + minSpeed?: Value; + maxSpeed?: Value; +} + +export interface OrbitOverLifeBehavior { + type: "OrbitOverLife"; + center?: { + x?: number; + y?: number; + z?: number; + }; + radius?: Value | { keys?: GradientKey[] }; + speed?: Value; +} + +export type Behavior = + | ColorOverLifeBehavior + | SizeOverLifeBehavior + | RotationOverLifeBehavior + | ForceOverLifeBehavior + | GravityForceBehavior + | SpeedOverLifeBehavior + | FrameOverLifeBehavior + | LimitSpeedOverLifeBehavior + | ColorBySpeedBehavior + | SizeBySpeedBehavior + | RotationBySpeedBehavior + | OrbitOverLifeBehavior + | { type: string; [key: string]: unknown }; // Fallback for unknown behaviors diff --git a/tools/src/effect/types/colors.ts b/tools/src/effect/types/colors.ts new file mode 100644 index 000000000..7045805b1 --- /dev/null +++ b/tools/src/effect/types/colors.ts @@ -0,0 +1,41 @@ +import type { GradientKey } from "./gradients"; + +/** + * color types (converted from Quarks) + */ +export interface ConstantColor { + type: "ConstantColor"; + value: [number, number, number, number]; // RGBA +} + +export interface ColorRange { + type: "ColorRange"; + colorA: [number, number, number, number]; // RGBA + colorB: [number, number, number, number]; // RGBA +} + +export interface GradientColor { + type: "Gradient"; + colorKeys: GradientKey[]; + alphaKeys?: GradientKey[]; +} + +export interface RandomColor { + type: "RandomColor"; + colorA: [number, number, number, number]; // RGBA + colorB: [number, number, number, number]; // RGBA +} + +export interface RandomColorBetweenGradient { + type: "RandomColorBetweenGradient"; + gradient1: { + colorKeys: GradientKey[]; + alphaKeys?: GradientKey[]; + }; + gradient2: { + colorKeys: GradientKey[]; + alphaKeys?: GradientKey[]; + }; +} + +export type Color = ConstantColor | ColorRange | GradientColor | RandomColor | RandomColorBetweenGradient | [number, number, number, number] | string; diff --git a/tools/src/effect/types/context.ts b/tools/src/effect/types/context.ts new file mode 100644 index 000000000..1c07edb1c --- /dev/null +++ b/tools/src/effect/types/context.ts @@ -0,0 +1,16 @@ +import { Scene, TransformNode } from "babylonjs"; +import type { QuarksJSON } from "./quarksTypes"; +import type { Data } from "./hierarchy"; +import type { LoaderOptions } from "./loader"; + +/** + * Context for parsing operations + */ +export interface ParseContext { + scene: Scene; + rootUrl: string; + jsonData: QuarksJSON; + options: LoaderOptions; + groupNodesMap: Map; + Data?: Data; +} diff --git a/tools/src/effect/types/emitter.ts b/tools/src/effect/types/emitter.ts new file mode 100644 index 000000000..b8e726356 --- /dev/null +++ b/tools/src/effect/types/emitter.ts @@ -0,0 +1,78 @@ +import { Nullable, SolidParticle, TransformNode, Vector3 } from "babylonjs"; +import type { Emitter } from "./hierarchy"; +import type { Value } from "./values"; +import type { Color } from "./colors"; +import type { Rotation } from "./rotations"; +import type { Shape } from "./shapes"; +import type { Behavior } from "./behaviors"; + +/** + * emission burst (converted from Quarks) + */ +export interface EmissionBurst { + time: Value; + count: Value; +} + +/** + * particle emitter configuration (converted from Quarks) + */ +export interface EmitterConfig { + version?: string; + autoDestroy?: boolean; + looping?: boolean; + prewarm?: boolean; + duration?: number; + shape?: Shape; + startLife?: Value; + startSpeed?: Value; + startRotation?: Rotation; + startSize?: Value; + startColor?: Color; + emissionOverTime?: Value; + emissionOverDistance?: Value; + emissionBursts?: EmissionBurst[]; + onlyUsedByOther?: boolean; + instancingGeometry?: string; + renderOrder?: number; + systemType: "solid" | "base"; + rendererEmitterSettings?: Record; + material?: string; + layers?: number; + isBillboardBased?: boolean; + billboardMode?: number; + startTileIndex?: Value; + uTileCount?: number; + vTileCount?: number; + blendTiles?: boolean; + softParticles?: boolean; + softFarFade?: number; + softNearFade?: number; + behaviors?: Behavior[]; + worldSpace?: boolean; +} + +/** + * Data structure for emitter creation + */ +export interface EmitterData { + name: string; + config: EmitterConfig; + materialId?: string; + matrix?: number[]; + position?: number[]; + parentGroup: Nullable; + cumulativeScale: Vector3; + Emitter?: Emitter; +} + +/** + * Interface for SolidParticleSystem emitter types + * Similar to IParticleEmitterType for ParticleSystem + */ +export interface ISolidParticleEmitterType { + /** + * Initialize particle position and velocity based on emitter shape + */ + initializeParticle(particle: SolidParticle, startSpeed: number): void; +} diff --git a/editor/src/editor/windows/fx-editor/VFX/types/factories.ts b/tools/src/effect/types/factories.ts similarity index 86% rename from editor/src/editor/windows/fx-editor/VFX/types/factories.ts rename to tools/src/effect/types/factories.ts index 43f886e95..54448a73a 100644 --- a/editor/src/editor/windows/fx-editor/VFX/types/factories.ts +++ b/tools/src/effect/types/factories.ts @@ -3,13 +3,13 @@ import { Nullable, Mesh, PBRMaterial, Texture, Scene } from "babylonjs"; /** * Factory interfaces for dependency injection */ -export interface IVFXMaterialFactory { +export interface IMaterialFactory { createMaterial(materialId: string, name: string): Nullable; createTexture(materialId: string): Nullable; getBlendMode(materialId: string): number | undefined; } -export interface IVFXGeometryFactory { +export interface IGeometryFactory { createMesh(geometryId: string, name: string, scene: Scene): Nullable; createParticleMesh(config: { instancingGeometry?: string }, name: string, scene: Scene): Nullable; } diff --git a/editor/src/editor/windows/fx-editor/VFX/types/gradients.ts b/tools/src/effect/types/gradients.ts similarity index 64% rename from editor/src/editor/windows/fx-editor/VFX/types/gradients.ts rename to tools/src/effect/types/gradients.ts index cb12d80d5..a277fc1dd 100644 --- a/editor/src/editor/windows/fx-editor/VFX/types/gradients.ts +++ b/tools/src/effect/types/gradients.ts @@ -1,7 +1,7 @@ /** - * VFX gradient key (converted from Quarks) + * gradient key (converted from Quarks) */ -export interface VFXGradientKey { +export interface GradientKey { time?: number; value: number | [number, number, number, number] | { r: number; g: number; b: number; a?: number }; pos?: number; diff --git a/tools/src/effect/types/hierarchy.ts b/tools/src/effect/types/hierarchy.ts new file mode 100644 index 000000000..2596e8ec9 --- /dev/null +++ b/tools/src/effect/types/hierarchy.ts @@ -0,0 +1,51 @@ +import { Vector3, Quaternion } from "babylonjs"; +import type { EmitterConfig } from "./emitter"; +import type { Material, Texture, Image, Geometry } from "./resources"; + +/** + * transform (converted from Quarks, left-handed coordinate system) + */ +export interface Transform { + position: Vector3; + rotation: Quaternion; + scale: Vector3; +} + +/** + * group (converted from Quarks) + */ +export interface Group { + uuid: string; + name: string; + transform: Transform; + children: (Group | Emitter)[]; +} + +/** + * emitter (converted from Quarks) + */ +export interface Emitter { + uuid: string; + name: string; + transform: Transform; + config: EmitterConfig; + materialId?: string; + parentUuid?: string; + systemType: "solid" | "base"; // Determined from renderMode: 2 = solid, otherwise base + matrix?: number[]; // Original Three.js matrix array for rotation extraction +} + +/** + * data (converted from Quarks) + * Contains the converted structure with groups, emitters, and resources + */ +export interface Data { + root: Group | Emitter | null; + groups: Map; + emitters: Map; + // Resources (converted from Quarks, ready for Babylon.js) + materials: Material[]; + textures: Texture[]; + images: Image[]; + geometries: Geometry[]; +} diff --git a/tools/src/effect/types/index.ts b/tools/src/effect/types/index.ts new file mode 100644 index 000000000..1da5384f7 --- /dev/null +++ b/tools/src/effect/types/index.ts @@ -0,0 +1,46 @@ +/** + * Types - Centralized type definitions + * + * This module exports all -related types organized by category. + * Import types directly from their specific modules for better tree-shaking. + */ + +// Loader types +export type { LoaderOptions } from "./loader"; + +// Emitter types +export type { EmitterData } from "./emitter"; + +// Factory interfaces +export type { IMaterialFactory, IGeometryFactory } from "./factories"; + +// Core types +export type { ConstantValue, IntervalValue, Value } from "./values"; +export type { ConstantColor, Color } from "./colors"; +export type { EulerRotation, Rotation } from "./rotations"; +export type { GradientKey } from "./gradients"; +export type { Shape } from "./shapes"; +export type { + ColorOverLifeBehavior, + SizeOverLifeBehavior, + RotationOverLifeBehavior, + ForceOverLifeBehavior, + GravityForceBehavior, + SpeedOverLifeBehavior, + FrameOverLifeBehavior, + LimitSpeedOverLifeBehavior, + ColorBySpeedBehavior, + SizeBySpeedBehavior, + RotationBySpeedBehavior, + OrbitOverLifeBehavior, + Behavior, +} from "./behaviors"; +export type { EmissionBurst, EmitterConfig } from "./emitter"; +export type { Transform, Group, Emitter, Data } from "./hierarchy"; +export type { Material, Texture, Image, Geometry } from "./resources"; +export type { ISolidParticleEmitterType } from "./emitter"; +export { SolidPointParticleEmitter, SolidSphereParticleEmitter, SolidConeParticleEmitter } from "../emitters"; +export type { QuarksJSON } from "./quarksTypes"; +export type { PerParticleBehaviorFunction, PerSolidParticleBehaviorFunction, SystemBehaviorFunction } from "./behaviors"; +export type { ISystem, ParticleWithSystem, SolidParticleWithSystem } from "./system"; +export { isSystem } from "./system"; diff --git a/editor/src/editor/windows/fx-editor/VFX/types/loader.ts b/tools/src/effect/types/loader.ts similarity index 85% rename from editor/src/editor/windows/fx-editor/VFX/types/loader.ts rename to tools/src/effect/types/loader.ts index 85db9d4a7..90357c9f0 100644 --- a/editor/src/editor/windows/fx-editor/VFX/types/loader.ts +++ b/tools/src/effect/types/loader.ts @@ -1,7 +1,7 @@ /** * Options for parsing Quarks/Three.js particle JSON */ -export interface VFXLoaderOptions { +export interface LoaderOptions { /** * Enable verbose logging for debugging */ diff --git a/editor/src/editor/windows/fx-editor/VFX/types/quarksTypes.ts b/tools/src/effect/types/quarksTypes.ts similarity index 98% rename from editor/src/editor/windows/fx-editor/VFX/types/quarksTypes.ts rename to tools/src/effect/types/quarksTypes.ts index 9b289f446..6a26cf613 100644 --- a/editor/src/editor/windows/fx-editor/VFX/types/quarksTypes.ts +++ b/tools/src/effect/types/quarksTypes.ts @@ -1,5 +1,5 @@ /** - * Type definitions for Quarks/Three.js VFX JSON structures + * Type definitions for Quarks/Three.js JSON structures * These represent the incoming format from Quarks/Three.js */ @@ -360,7 +360,7 @@ export interface QuarksGeometry { /** * Quarks/Three.js JSON structure */ -export interface QuarksVFXJSON { +export interface QuarksJSON { metadata?: { version?: number; type?: string; diff --git a/editor/src/editor/windows/fx-editor/VFX/types/resources.ts b/tools/src/effect/types/resources.ts similarity index 58% rename from editor/src/editor/windows/fx-editor/VFX/types/resources.ts rename to tools/src/effect/types/resources.ts index 81109ea4f..7f4ead381 100644 --- a/editor/src/editor/windows/fx-editor/VFX/types/resources.ts +++ b/tools/src/effect/types/resources.ts @@ -1,9 +1,9 @@ -import { Color3, Texture } from "babylonjs"; +import { Color3 } from "babylonjs"; /** - * VFX Material (converted from Quarks, ready for Babylon.js) + * Material (converted from Quarks, ready for Babylon.js) */ -export interface VFXMaterial { +export interface Material { uuid: string; type?: string; color?: Color3; // Converted from hex/array to Color3 @@ -16,9 +16,9 @@ export interface VFXMaterial { } /** - * VFX Texture (converted from Quarks, ready for Babylon.js) + * Texture (converted from Quarks, ready for Babylon.js) */ -export interface VFXTexture { +export interface Texture { uuid: string; image?: string; // Image UUID reference wrapU?: number; // Converted to Babylon.js wrap mode @@ -35,50 +35,50 @@ export interface VFXTexture { } /** - * VFX Image (converted from Quarks, normalized URL) + * Image (converted from Quarks, normalized URL) */ -export interface VFXImage { +export interface Image { uuid: string; url: string; // Normalized URL (ready for use) } /** - * VFX Geometry Attribute Data + * Geometry Attribute Data */ -export interface VFXGeometryAttribute { +export interface GeometryAttribute { array: number[]; itemSize?: number; } /** - * VFX Geometry Index Data + * Geometry Index Data */ -export interface VFXGeometryIndex { +export interface GeometryIndex { array: number[]; } /** - * VFX Geometry Data (converted from Quarks, left-handed coordinate system) + * Geometry Data (converted from Quarks, left-handed coordinate system) */ -export interface VFXGeometryData { +export interface GeometryData { attributes: { - position?: VFXGeometryAttribute; - normal?: VFXGeometryAttribute; - uv?: VFXGeometryAttribute; - color?: VFXGeometryAttribute; + position?: GeometryAttribute; + normal?: GeometryAttribute; + uv?: GeometryAttribute; + color?: GeometryAttribute; }; - index?: VFXGeometryIndex; + index?: GeometryIndex; } /** - * VFX Geometry (converted from Quarks, ready for Babylon.js) + * Geometry (converted from Quarks, ready for Babylon.js) */ -export interface VFXGeometry { +export interface Geometry { uuid: string; type: "PlaneGeometry" | "BufferGeometry"; // For PlaneGeometry width?: number; height?: number; // For BufferGeometry (already converted to left-handed) - data?: VFXGeometryData; + data?: GeometryData; } diff --git a/tools/src/effect/types/rotations.ts b/tools/src/effect/types/rotations.ts new file mode 100644 index 000000000..fd54a3260 --- /dev/null +++ b/tools/src/effect/types/rotations.ts @@ -0,0 +1,26 @@ +import type { Value } from "./values"; + +/** + * rotation types (converted from Quarks) + */ +export interface EulerRotation { + type: "Euler"; + angleX?: Value; + angleY?: Value; + angleZ?: Value; + order?: "xyz" | "zyx"; +} + +export interface AxisAngleRotation { + type: "AxisAngle"; + x?: Value; + y?: Value; + z?: Value; + angle?: Value; +} + +export interface RandomQuatRotation { + type: "RandomQuat"; +} + +export type Rotation = EulerRotation | AxisAngleRotation | RandomQuatRotation | Value; diff --git a/editor/src/editor/windows/fx-editor/VFX/types/shapes.ts b/tools/src/effect/types/shapes.ts similarity index 54% rename from editor/src/editor/windows/fx-editor/VFX/types/shapes.ts rename to tools/src/effect/types/shapes.ts index 88d8d8b2b..449ff2e8f 100644 --- a/editor/src/editor/windows/fx-editor/VFX/types/shapes.ts +++ b/tools/src/effect/types/shapes.ts @@ -1,9 +1,9 @@ -import type { VFXValue } from "./values"; +import type { Value } from "./values"; /** - * VFX shape configuration (converted from Quarks) + * shape configuration (converted from Quarks) */ -export interface VFXShape { +export interface Shape { type: string; radius?: number; arc?: number; @@ -11,7 +11,7 @@ export interface VFXShape { angle?: number; mode?: number; spread?: number; - speed?: VFXValue; + speed?: Value; size?: number[]; height?: number; } diff --git a/editor/src/editor/windows/fx-editor/VFX/types/system.ts b/tools/src/effect/types/system.ts similarity index 65% rename from editor/src/editor/windows/fx-editor/VFX/types/system.ts rename to tools/src/effect/types/system.ts index c8e4c9a84..aefdf6e95 100644 --- a/editor/src/editor/windows/fx-editor/VFX/types/system.ts +++ b/tools/src/effect/types/system.ts @@ -1,12 +1,12 @@ import { TransformNode, AbstractMesh, Particle, SolidParticle } from "babylonjs"; -import type { VFXParticleSystem } from "../systems/VFXParticleSystem"; -import type { VFXSolidParticleSystem } from "../systems/VFXSolidParticleSystem"; +import type { EffectParticleSystem } from "../systems/effectParticleSystem"; +import type { EffectSolidParticleSystem } from "../systems/effectSolidParticleSystem"; /** - * Common interface for all VFX particle systems + * Common interface for all particle systems * Provides type-safe access to common properties and methods */ -export interface IVFXSystem { +export interface ISystem { /** System name */ name: string; /** Get the parent node (mesh or emitter) for hierarchy operations */ @@ -25,7 +25,7 @@ export interface IVFXSystem { * Uses intersection type to add custom property without conflicting with base type */ export type ParticleWithSystem = Particle & { - particleSystem?: VFXParticleSystem; + particleSystem?: EffectParticleSystem; }; /** @@ -34,21 +34,21 @@ export type ParticleWithSystem = Particle & { * Uses intersection type to add custom property without conflicting with base type */ export type SolidParticleWithSystem = SolidParticle & { - system?: VFXSolidParticleSystem; + system?: EffectSolidParticleSystem; }; /** - * Type guard to check if a system implements IVFXSystem + * Type guard to check if a system implements ISystem */ -export function isVFXSystem(system: unknown): system is IVFXSystem { +export function isSystem(system: unknown): system is ISystem { return ( typeof system === "object" && system !== null && "getParentNode" in system && - typeof (system as IVFXSystem).getParentNode === "function" && + typeof (system as ISystem).getParentNode === "function" && "start" in system && - typeof (system as IVFXSystem).start === "function" && + typeof (system as ISystem).start === "function" && "stop" in system && - typeof (system as IVFXSystem).stop === "function" + typeof (system as ISystem).stop === "function" ); } diff --git a/editor/src/editor/windows/fx-editor/VFX/types/values.ts b/tools/src/effect/types/values.ts similarity index 51% rename from editor/src/editor/windows/fx-editor/VFX/types/values.ts rename to tools/src/effect/types/values.ts index 40de8f5ea..d8da55e4e 100644 --- a/editor/src/editor/windows/fx-editor/VFX/types/values.ts +++ b/tools/src/effect/types/values.ts @@ -1,18 +1,18 @@ /** - * VFX value types (converted from Quarks) + * value types (converted from Quarks) */ -export interface VFXConstantValue { +export interface ConstantValue { type: "ConstantValue"; value: number; } -export interface VFXIntervalValue { +export interface IntervalValue { type: "IntervalValue"; min: number; max: number; } -export interface VFXPiecewiseBezier { +export interface PiecewiseBezier { type: "PiecewiseBezier"; functions: Array<{ function: { @@ -24,4 +24,4 @@ export interface VFXPiecewiseBezier { start: number; }>; } -export type VFXValue = VFXConstantValue | VFXIntervalValue | VFXPiecewiseBezier | number; +export type Value = ConstantValue | IntervalValue | PiecewiseBezier | number; diff --git a/editor/src/editor/windows/fx-editor/VFX/utils/capacityCalculator.ts b/tools/src/effect/utils/capacityCalculator.ts similarity index 56% rename from editor/src/editor/windows/fx-editor/VFX/utils/capacityCalculator.ts rename to tools/src/effect/utils/capacityCalculator.ts index 74c27c560..df770dec0 100644 --- a/editor/src/editor/windows/fx-editor/VFX/utils/capacityCalculator.ts +++ b/tools/src/effect/utils/capacityCalculator.ts @@ -1,16 +1,16 @@ -import { VFXValueUtils } from "./valueParser"; -import type { VFXValue } from "../types/values"; +import { ValueUtils } from "./valueParser"; +import type { Value } from "../types/values"; /** * Utility for calculating particle system capacity */ -export class VFXCapacityCalculator { +export class CapacityCalculator { /** * Calculate capacity for ParticleSystem * Formula: emissionRate * duration * 2 (for non-looping systems) */ - public static calculateForParticleSystem(emissionOverTime: VFXValue | undefined, duration: number): number { - const emissionRate = emissionOverTime !== undefined ? VFXValueUtils.parseConstantValue(emissionOverTime) : 10; + public static calculateForParticleSystem(emissionOverTime: Value | undefined, duration: number): number { + const emissionRate = emissionOverTime !== undefined ? ValueUtils.parseConstantValue(emissionOverTime) : 10; return Math.ceil(emissionRate * duration * 2); } @@ -20,8 +20,8 @@ export class VFXCapacityCalculator { * - Looping: max(emissionRate * particleLifetime, 1) * - Non-looping: emissionRate * particleLifetime * 2 */ - public static calculateForSolidParticleSystem(emissionOverTime: VFXValue | undefined, duration: number, isLooping: boolean): number { - const emissionRate = emissionOverTime !== undefined ? VFXValueUtils.parseConstantValue(emissionOverTime) : 10; + public static calculateForSolidParticleSystem(emissionOverTime: Value | undefined, duration: number, isLooping: boolean): number { + const emissionRate = emissionOverTime !== undefined ? ValueUtils.parseConstantValue(emissionOverTime) : 10; const particleLifetime = duration || 5; if (isLooping) { diff --git a/editor/src/editor/windows/fx-editor/VFX/utils/gradientSystem.ts b/tools/src/effect/utils/gradientSystem.ts similarity index 97% rename from editor/src/editor/windows/fx-editor/VFX/utils/gradientSystem.ts rename to tools/src/effect/utils/gradientSystem.ts index 7f452bbb5..c844995cb 100644 --- a/editor/src/editor/windows/fx-editor/VFX/utils/gradientSystem.ts +++ b/tools/src/effect/utils/gradientSystem.ts @@ -74,7 +74,7 @@ export class GradientSystem { /** * Interpolate between two values (to be overridden by subclasses) */ - protected interpolate(value1: T, value2: T, t: number): T { + protected interpolate(value1: T, _value2: T, _t: number): T { // Default implementation - should be overridden return value1; } diff --git a/editor/src/editor/windows/fx-editor/VFX/utils/matrixUtils.ts b/tools/src/effect/utils/matrixUtils.ts similarity index 94% rename from editor/src/editor/windows/fx-editor/VFX/utils/matrixUtils.ts rename to tools/src/effect/utils/matrixUtils.ts index d437d04c4..2944e34c0 100644 --- a/editor/src/editor/windows/fx-editor/VFX/utils/matrixUtils.ts +++ b/tools/src/effect/utils/matrixUtils.ts @@ -3,7 +3,7 @@ import { Matrix } from "babylonjs"; /** * Utility functions for matrix operations */ -export class VFXMatrixUtils { +export class MatrixUtils { /** * Extracts rotation matrix from Three.js matrix array * @param matrix Three.js matrix array (16 elements) diff --git a/editor/src/editor/windows/fx-editor/VFX/utils/valueParser.ts b/tools/src/effect/utils/valueParser.ts similarity index 86% rename from editor/src/editor/windows/fx-editor/VFX/utils/valueParser.ts rename to tools/src/effect/utils/valueParser.ts index d671dbfb6..feaa99755 100644 --- a/editor/src/editor/windows/fx-editor/VFX/utils/valueParser.ts +++ b/tools/src/effect/utils/valueParser.ts @@ -1,17 +1,17 @@ import { Color4, ColorGradient } from "babylonjs"; -import type { VFXPiecewiseBezier, VFXValue } from "../types/values"; -import type { VFXColor } from "../types/colors"; -import type { VFXGradientKey } from "../types/gradients"; +import type { PiecewiseBezier, Value } from "../types/values"; +import type { Color } from "../types/colors"; +import type { GradientKey } from "../types/gradients"; /** - * Static utility functions for parsing VFX values + * Static utility functions for parsing values * These are stateless and don't require an instance */ -export class VFXValueUtils { +export class ValueUtils { /** * Parse a constant value */ - public static parseConstantValue(value: VFXValue): number { + public static parseConstantValue(value: Value): number { if (value && typeof value === "object" && value.type === "ConstantValue") { return value.value || 0; } @@ -21,7 +21,7 @@ export class VFXValueUtils { /** * Parse an interval value (returns min and max) */ - public static parseIntervalValue(value: VFXValue): { min: number; max: number } { + public static parseIntervalValue(value: Value): { min: number; max: number } { if (value && typeof value === "object" && "type" in value && value.type === "IntervalValue") { return { min: value.min ?? 0, @@ -35,7 +35,7 @@ export class VFXValueUtils { /** * Parse a constant color */ - public static parseConstantColor(value: VFXColor): Color4 { + public static parseConstantColor(value: Color): Color4 { if (value && typeof value === "object" && !Array.isArray(value)) { if ("type" in value && value.type === "ConstantColor") { if (value.value && Array.isArray(value.value)) { @@ -55,7 +55,7 @@ export class VFXValueUtils { * @param value The value to parse * @param normalizedTime Normalized time (0-1) for PiecewiseBezier, default is random for IntervalValue */ - public static parseValue(value: VFXValue, normalizedTime?: number): number { + public static parseValue(value: Value, normalizedTime?: number): number { if (!value || typeof value === "number") { return typeof value === "number" ? value : 0; } @@ -83,7 +83,7 @@ export class VFXValueUtils { /** * Evaluate PiecewiseBezier at normalized time t (0-1) */ - private static _evaluatePiecewiseBezier(bezier: VFXPiecewiseBezier, t: number): number { + private static _evaluatePiecewiseBezier(bezier: PiecewiseBezier, t: number): number { if (!bezier.functions || bezier.functions.length === 0) { return 0; } @@ -139,7 +139,7 @@ export class VFXValueUtils { /** * Parse gradient color keys */ - public static parseGradientColorKeys(keys: VFXGradientKey[]): ColorGradient[] { + public static parseGradientColorKeys(keys: GradientKey[]): ColorGradient[] { const gradients: ColorGradient[] = []; for (const key of keys) { const pos = key.pos ?? key.time ?? 0; @@ -164,7 +164,7 @@ export class VFXValueUtils { /** * Parse gradient alpha keys */ - public static parseGradientAlphaKeys(keys: VFXGradientKey[]): { gradient: number; factor: number }[] { + public static parseGradientAlphaKeys(keys: GradientKey[]): { gradient: number; factor: number }[] { const gradients: { gradient: number; factor: number }[] = []; for (const key of keys) { const pos = key.pos ?? key.time ?? 0; diff --git a/tools/src/index.ts b/tools/src/index.ts index acc015555..43e23c42d 100644 --- a/tools/src/index.ts +++ b/tools/src/index.ts @@ -33,3 +33,5 @@ export * from "./cinematic/typings"; export * from "./cinematic/generate"; export * from "./cinematic/guards"; export * from "./cinematic/cinematic"; + +export * from "./effect"; From 742ed1cc2e7833b7a8f688cc5df04e1636f1aa0e Mon Sep 17 00:00:00 2001 From: Mikalai Lazitski Date: Tue, 16 Dec 2025 11:01:30 +0300 Subject: [PATCH 35/62] feat: implement comprehensive effect editor features including animation, graph, layout, preview, and resource management components, enhancing user experience and functionality for VFX editing --- .../animation.tsx | 0 .../{fx-editor => effect-editor}/graph.tsx | 0 .../{fx-editor => effect-editor}/index.tsx | 3 - .../{fx-editor => effect-editor}/layout.tsx | 29 ++-- .../{fx-editor => effect-editor}/preview.tsx | 0 .../properties/behaviors.tsx | 0 .../behaviors/behavior-properties.tsx | 0 .../properties/behaviors/bezier-editor.tsx | 0 .../behaviors/color-function-editor.tsx | 0 .../properties/behaviors/function-editor.tsx | 0 .../properties/behaviors/registry.ts | 0 .../properties/color-editor.tsx | 0 .../properties/emission.tsx | 0 .../properties/emitter-shape.tsx | 0 .../properties/object.tsx | 0 .../properties/particle-initialization.tsx | 0 .../properties/particle-renderer.tsx | 0 .../properties/properties-tab.tsx | 109 ++++++++++++ .../properties/rotation-editor.tsx | 4 +- .../properties/value-editor.tsx | 7 +- .../resources.tsx | 0 .../{fx-editor => effect-editor}/toolbar.tsx | 0 .../editor/windows/fx-editor/properties.tsx | 159 ------------------ .../properties/behaviors-properties.tsx | 39 ----- .../properties/emission-properties.tsx | 40 ----- .../properties/emitter-properties.tsx | 40 ----- .../properties/initialization-properties.tsx | 40 ----- .../properties/object-properties.tsx | 49 ------ .../properties/renderer-properties.tsx | 42 ----- 29 files changed, 133 insertions(+), 428 deletions(-) rename editor/src/editor/windows/{fx-editor => effect-editor}/animation.tsx (100%) rename editor/src/editor/windows/{fx-editor => effect-editor}/graph.tsx (100%) rename editor/src/editor/windows/{fx-editor => effect-editor}/index.tsx (96%) rename editor/src/editor/windows/{fx-editor => effect-editor}/layout.tsx (91%) rename editor/src/editor/windows/{fx-editor => effect-editor}/preview.tsx (100%) rename editor/src/editor/windows/{fx-editor => effect-editor}/properties/behaviors.tsx (100%) rename editor/src/editor/windows/{fx-editor => effect-editor}/properties/behaviors/behavior-properties.tsx (100%) rename editor/src/editor/windows/{fx-editor => effect-editor}/properties/behaviors/bezier-editor.tsx (100%) rename editor/src/editor/windows/{fx-editor => effect-editor}/properties/behaviors/color-function-editor.tsx (100%) rename editor/src/editor/windows/{fx-editor => effect-editor}/properties/behaviors/function-editor.tsx (100%) rename editor/src/editor/windows/{fx-editor => effect-editor}/properties/behaviors/registry.ts (100%) rename editor/src/editor/windows/{fx-editor => effect-editor}/properties/color-editor.tsx (100%) rename editor/src/editor/windows/{fx-editor => effect-editor}/properties/emission.tsx (100%) rename editor/src/editor/windows/{fx-editor => effect-editor}/properties/emitter-shape.tsx (100%) rename editor/src/editor/windows/{fx-editor => effect-editor}/properties/object.tsx (100%) rename editor/src/editor/windows/{fx-editor => effect-editor}/properties/particle-initialization.tsx (100%) rename editor/src/editor/windows/{fx-editor => effect-editor}/properties/particle-renderer.tsx (100%) create mode 100644 editor/src/editor/windows/effect-editor/properties/properties-tab.tsx rename editor/src/editor/windows/{fx-editor => effect-editor}/properties/rotation-editor.tsx (97%) rename editor/src/editor/windows/{fx-editor => effect-editor}/properties/value-editor.tsx (96%) rename editor/src/editor/windows/{fx-editor => effect-editor}/resources.tsx (100%) rename editor/src/editor/windows/{fx-editor => effect-editor}/toolbar.tsx (100%) delete mode 100644 editor/src/editor/windows/fx-editor/properties.tsx delete mode 100644 editor/src/editor/windows/fx-editor/properties/behaviors-properties.tsx delete mode 100644 editor/src/editor/windows/fx-editor/properties/emission-properties.tsx delete mode 100644 editor/src/editor/windows/fx-editor/properties/emitter-properties.tsx delete mode 100644 editor/src/editor/windows/fx-editor/properties/initialization-properties.tsx delete mode 100644 editor/src/editor/windows/fx-editor/properties/object-properties.tsx delete mode 100644 editor/src/editor/windows/fx-editor/properties/renderer-properties.tsx diff --git a/editor/src/editor/windows/fx-editor/animation.tsx b/editor/src/editor/windows/effect-editor/animation.tsx similarity index 100% rename from editor/src/editor/windows/fx-editor/animation.tsx rename to editor/src/editor/windows/effect-editor/animation.tsx diff --git a/editor/src/editor/windows/fx-editor/graph.tsx b/editor/src/editor/windows/effect-editor/graph.tsx similarity index 100% rename from editor/src/editor/windows/fx-editor/graph.tsx rename to editor/src/editor/windows/effect-editor/graph.tsx diff --git a/editor/src/editor/windows/fx-editor/index.tsx b/editor/src/editor/windows/effect-editor/index.tsx similarity index 96% rename from editor/src/editor/windows/fx-editor/index.tsx rename to editor/src/editor/windows/effect-editor/index.tsx index 039c5fd2d..efaa90b8e 100644 --- a/editor/src/editor/windows/fx-editor/index.tsx +++ b/editor/src/editor/windows/effect-editor/index.tsx @@ -14,7 +14,6 @@ import { projectConfiguration, onProjectConfigurationChangedObservable, IProject import { EffectEditorAnimation } from "./animation"; import { EffectEditorGraph } from "./graph"; import { EffectEditorPreview } from "./preview"; -import { EffectEditorProperties } from "./properties"; import { EffectEditorResources } from "./resources"; export interface IEffectEditorWindowProps { @@ -31,7 +30,6 @@ export interface IEffectEditor { preview: EffectEditorPreview | null; graph: EffectEditorGraph | null; animation: EffectEditorAnimation | null; - properties: EffectEditorProperties | null; resources: EffectEditorResources | null; } export default class EffectEditorWindow extends Component { @@ -40,7 +38,6 @@ export default class EffectEditorWindow extends Component (this.props.editor.resources = r!)} resources={this.state.resources} />, animation: (this.props.editor.animation = r!)} filePath={this.props.filePath} editor={this.props.editor} />, "properties-object": ( - { // Update graph node names when name changes if (this.props.editor.graph) { @@ -242,43 +238,52 @@ export class EffectEditorLayout extends Component ), "properties-emitter": ( - this.props.editor.graph?.getNodeData(nodeId) || null} /> ), "properties-renderer": ( - this.props.editor.graph?.getNodeData(nodeId) || null} /> ), "properties-emission": ( - this.props.editor.graph?.getNodeData(nodeId) || null} /> ), "properties-initialization": ( - this.props.editor.graph?.getNodeData(nodeId) || null} /> ), "properties-behaviors": ( - this.props.editor.graph?.getNodeData(nodeId) || null} /> ), diff --git a/editor/src/editor/windows/fx-editor/preview.tsx b/editor/src/editor/windows/effect-editor/preview.tsx similarity index 100% rename from editor/src/editor/windows/fx-editor/preview.tsx rename to editor/src/editor/windows/effect-editor/preview.tsx diff --git a/editor/src/editor/windows/fx-editor/properties/behaviors.tsx b/editor/src/editor/windows/effect-editor/properties/behaviors.tsx similarity index 100% rename from editor/src/editor/windows/fx-editor/properties/behaviors.tsx rename to editor/src/editor/windows/effect-editor/properties/behaviors.tsx diff --git a/editor/src/editor/windows/fx-editor/properties/behaviors/behavior-properties.tsx b/editor/src/editor/windows/effect-editor/properties/behaviors/behavior-properties.tsx similarity index 100% rename from editor/src/editor/windows/fx-editor/properties/behaviors/behavior-properties.tsx rename to editor/src/editor/windows/effect-editor/properties/behaviors/behavior-properties.tsx diff --git a/editor/src/editor/windows/fx-editor/properties/behaviors/bezier-editor.tsx b/editor/src/editor/windows/effect-editor/properties/behaviors/bezier-editor.tsx similarity index 100% rename from editor/src/editor/windows/fx-editor/properties/behaviors/bezier-editor.tsx rename to editor/src/editor/windows/effect-editor/properties/behaviors/bezier-editor.tsx diff --git a/editor/src/editor/windows/fx-editor/properties/behaviors/color-function-editor.tsx b/editor/src/editor/windows/effect-editor/properties/behaviors/color-function-editor.tsx similarity index 100% rename from editor/src/editor/windows/fx-editor/properties/behaviors/color-function-editor.tsx rename to editor/src/editor/windows/effect-editor/properties/behaviors/color-function-editor.tsx diff --git a/editor/src/editor/windows/fx-editor/properties/behaviors/function-editor.tsx b/editor/src/editor/windows/effect-editor/properties/behaviors/function-editor.tsx similarity index 100% rename from editor/src/editor/windows/fx-editor/properties/behaviors/function-editor.tsx rename to editor/src/editor/windows/effect-editor/properties/behaviors/function-editor.tsx diff --git a/editor/src/editor/windows/fx-editor/properties/behaviors/registry.ts b/editor/src/editor/windows/effect-editor/properties/behaviors/registry.ts similarity index 100% rename from editor/src/editor/windows/fx-editor/properties/behaviors/registry.ts rename to editor/src/editor/windows/effect-editor/properties/behaviors/registry.ts diff --git a/editor/src/editor/windows/fx-editor/properties/color-editor.tsx b/editor/src/editor/windows/effect-editor/properties/color-editor.tsx similarity index 100% rename from editor/src/editor/windows/fx-editor/properties/color-editor.tsx rename to editor/src/editor/windows/effect-editor/properties/color-editor.tsx diff --git a/editor/src/editor/windows/fx-editor/properties/emission.tsx b/editor/src/editor/windows/effect-editor/properties/emission.tsx similarity index 100% rename from editor/src/editor/windows/fx-editor/properties/emission.tsx rename to editor/src/editor/windows/effect-editor/properties/emission.tsx diff --git a/editor/src/editor/windows/fx-editor/properties/emitter-shape.tsx b/editor/src/editor/windows/effect-editor/properties/emitter-shape.tsx similarity index 100% rename from editor/src/editor/windows/fx-editor/properties/emitter-shape.tsx rename to editor/src/editor/windows/effect-editor/properties/emitter-shape.tsx diff --git a/editor/src/editor/windows/fx-editor/properties/object.tsx b/editor/src/editor/windows/effect-editor/properties/object.tsx similarity index 100% rename from editor/src/editor/windows/fx-editor/properties/object.tsx rename to editor/src/editor/windows/effect-editor/properties/object.tsx diff --git a/editor/src/editor/windows/fx-editor/properties/particle-initialization.tsx b/editor/src/editor/windows/effect-editor/properties/particle-initialization.tsx similarity index 100% rename from editor/src/editor/windows/fx-editor/properties/particle-initialization.tsx rename to editor/src/editor/windows/effect-editor/properties/particle-initialization.tsx diff --git a/editor/src/editor/windows/fx-editor/properties/particle-renderer.tsx b/editor/src/editor/windows/effect-editor/properties/particle-renderer.tsx similarity index 100% rename from editor/src/editor/windows/fx-editor/properties/particle-renderer.tsx rename to editor/src/editor/windows/effect-editor/properties/particle-renderer.tsx diff --git a/editor/src/editor/windows/effect-editor/properties/properties-tab.tsx b/editor/src/editor/windows/effect-editor/properties/properties-tab.tsx new file mode 100644 index 000000000..bbfeb73e8 --- /dev/null +++ b/editor/src/editor/windows/effect-editor/properties/properties-tab.tsx @@ -0,0 +1,109 @@ +import { Component, ReactNode } from "react"; +import type { EffectNode } from "babylonjs-editor-tools"; +import { IEffectEditor } from ".."; +import { EffectEditorObjectProperties } from "./object"; +import { EffectEditorEmitterShapeProperties } from "./emitter-shape"; +import { EffectEditorParticleRendererProperties } from "./particle-renderer"; +import { EffectEditorEmissionProperties } from "./emission"; +import { EffectEditorParticleInitializationProperties } from "./particle-initialization"; +import { EffectEditorBehaviorsProperties } from "./behaviors"; + +export interface IEffectEditorPropertiesTabProps { + filePath: string | null; + selectedNodeId: string | number | null; + editor: IEffectEditor; + tabType: "object" | "emitter" | "renderer" | "emission" | "initialization" | "behaviors"; + onNameChanged?: () => void; + getNodeData: (nodeId: string | number) => EffectNode | null; +} + +export class EffectEditorPropertiesTab extends Component { + public render(): ReactNode { + const { selectedNodeId, tabType, getNodeData, editor, onNameChanged } = this.props; + + if (!selectedNodeId) { + return ( +
+

{tabType === "object" ? "No node selected" : "No particle selected"}

+
+ ); + } + + const nodeData = getNodeData(selectedNodeId); + + if (!nodeData) { + return ( +
+

Node not found

+
+ ); + } + + // For groups, only show object properties + if (nodeData.type === "group" && tabType !== "object") { + return ( +
+

Select a particle system

+
+ ); + } + + // For particles, check if system exists + if (nodeData.type === "particle" && !nodeData.system && tabType !== "object") { + return ( +
+

Select a particle system

+
+ ); + } + + const commonProps = { + nodeData, + onChange: () => { + this.forceUpdate(); + onNameChanged?.(); + }, + }; + + switch (tabType) { + case "object": + return ( +
+ +
+ ); + case "emitter": + return ( +
+ +
+ ); + case "renderer": + return ( +
+ +
+ ); + case "emission": + return ( +
+ +
+ ); + case "initialization": + return ( +
+ +
+ ); + case "behaviors": + return ( +
+ +
+ ); + default: + return null; + } + } +} diff --git a/editor/src/editor/windows/fx-editor/properties/rotation-editor.tsx b/editor/src/editor/windows/effect-editor/properties/rotation-editor.tsx similarity index 97% rename from editor/src/editor/windows/fx-editor/properties/rotation-editor.tsx rename to editor/src/editor/windows/effect-editor/properties/rotation-editor.tsx index fbb4e1fe9..2f7e7565a 100644 --- a/editor/src/editor/windows/fx-editor/properties/rotation-editor.tsx +++ b/editor/src/editor/windows/effect-editor/properties/rotation-editor.tsx @@ -3,10 +3,10 @@ import { ReactNode } from "react"; import { EditorInspectorListField } from "../../../layout/inspector/fields/list"; import { EditorInspectorBlockField } from "../../../layout/inspector/fields/block"; -import { type Rotation, ValueUtils, type Value } from "babylonjs-editor-tools"; +import { type Rotation, type EulerRotation, type AxisAngleRotation, type RandomQuatRotation, ValueUtils, type Value } from "babylonjs-editor-tools"; import { EffectValueEditor } from "./value-editor"; -export type EffectRotationType = "Euler" | "AxisAngle" | "RandomQuat"; +export type EffectRotationType = EulerRotation["type"] | AxisAngleRotation["type"] | RandomQuatRotation["type"]; export interface IEffectRotationEditorProps { value: Rotation | undefined; diff --git a/editor/src/editor/windows/fx-editor/properties/value-editor.tsx b/editor/src/editor/windows/effect-editor/properties/value-editor.tsx similarity index 96% rename from editor/src/editor/windows/fx-editor/properties/value-editor.tsx rename to editor/src/editor/windows/effect-editor/properties/value-editor.tsx index 3056df9cd..7330fc6c8 100644 --- a/editor/src/editor/windows/fx-editor/properties/value-editor.tsx +++ b/editor/src/editor/windows/effect-editor/properties/value-editor.tsx @@ -4,10 +4,13 @@ import { EditorInspectorNumberField } from "../../../layout/inspector/fields/num import { EditorInspectorListField } from "../../../layout/inspector/fields/list"; import { EditorInspectorBlockField } from "../../../layout/inspector/fields/block"; -import { type Value, ValueUtils } from "babylonjs-editor-tools"; +import { type Value, type ConstantValue, type IntervalValue, ValueUtils } from "babylonjs-editor-tools"; + +type PiecewiseBezier = Extract; import { BezierEditor } from "./behaviors/bezier-editor"; -export type EffectValueType = "ConstantValue" | "IntervalValue" | "PiecewiseBezier" | "Vec3Function"; +// Vec3Function is a custom editor extension, not part of the core Value type +export type EffectValueType = ConstantValue["type"] | IntervalValue["type"] | PiecewiseBezier["type"] | "Vec3Function"; export interface IVec3Function { type: "Vec3Function"; diff --git a/editor/src/editor/windows/fx-editor/resources.tsx b/editor/src/editor/windows/effect-editor/resources.tsx similarity index 100% rename from editor/src/editor/windows/fx-editor/resources.tsx rename to editor/src/editor/windows/effect-editor/resources.tsx diff --git a/editor/src/editor/windows/fx-editor/toolbar.tsx b/editor/src/editor/windows/effect-editor/toolbar.tsx similarity index 100% rename from editor/src/editor/windows/fx-editor/toolbar.tsx rename to editor/src/editor/windows/effect-editor/toolbar.tsx diff --git a/editor/src/editor/windows/fx-editor/properties.tsx b/editor/src/editor/windows/fx-editor/properties.tsx deleted file mode 100644 index cc51d1253..000000000 --- a/editor/src/editor/windows/fx-editor/properties.tsx +++ /dev/null @@ -1,159 +0,0 @@ -import { Component, ReactNode } from "react"; - -import { Tabs, TabsContent, TabsList, TabsTrigger } from "../../../ui/shadcn/ui/tabs"; - -import { EffectEditorObjectProperties } from "./properties/object"; -import { EffectEditorEmitterShapeProperties } from "./properties/emitter-shape"; -import { EffectEditorParticleRendererProperties } from "./properties/particle-renderer"; -import { EffectEditorEmissionProperties } from "./properties/emission"; -import { EffectEditorParticleInitializationProperties } from "./properties/particle-initialization"; -import { EffectEditorBehaviorsProperties } from "./properties/behaviors"; -import { IEffectEditor } from "."; -import type { EffectNode } from "babylonjs-editor-tools"; - -export interface IEffectEditorPropertiesProps { - filePath: string | null; - selectedNodeId: string | number | null; - editor: IEffectEditor; - onNameChanged?: () => void; - getNodeData: (nodeId: string | number) => EffectNode | null; -} - -export interface IEffectEditorPropertiesState {} - -export class EffectEditorProperties extends Component { - public constructor(props: IEffectEditorPropertiesProps) { - super(props); - this.state = {}; - } - - public componentDidUpdate(prevProps: IEffectEditorPropertiesProps): void { - // Force update when selectedNodeId changes to ensure we show the correct node's properties - if (prevProps.selectedNodeId !== this.props.selectedNodeId) { - // Use setTimeout to ensure the update happens after flexlayout-react processes the change - setTimeout(() => { - this.forceUpdate(); - }, 0); - } - } - - public componentDidMount(): void { - // Force update on mount if a node is already selected - if (this.props.selectedNodeId) { - this.forceUpdate(); - } - } - - public render(): ReactNode { - const nodeId = this.props.selectedNodeId; - - if (!nodeId) { - return ( -
-

No particle selected

-
- ); - } - - // Get node data from graph - const nodeData = this.props.getNodeData(nodeId); - - if (!nodeData) { - return ( -
-

Node not found

-
- ); - } - - // For groups, show only Object properties - if (nodeData.type === "group" && nodeData.group) { - return ( -
- - - - Object - - - - { - this.forceUpdate(); - this.props.onNameChanged?.(); - }} - /> - - -
- ); - } - - // For particles, show all properties in tabs - if (nodeData.type === "particle" && nodeData.system) { - return ( -
- - - - Object - - - Emitter - - - Renderer - - - Emission - - - Initialization - - - Behaviors - - - - - { - this.forceUpdate(); - this.props.onNameChanged?.(); - }} - /> - - - - this.forceUpdate()} /> - - - - this.forceUpdate()} /> - - - - this.forceUpdate()} /> - - - - this.forceUpdate()} /> - - - - this.forceUpdate()} /> - - -
- ); - } - - return ( -
-

Invalid node type

-
- ); - } -} diff --git a/editor/src/editor/windows/fx-editor/properties/behaviors-properties.tsx b/editor/src/editor/windows/fx-editor/properties/behaviors-properties.tsx deleted file mode 100644 index 9b49edd35..000000000 --- a/editor/src/editor/windows/fx-editor/properties/behaviors-properties.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import { Component, ReactNode } from "react"; -import type { EffectNode } from "babylonjs-editor-tools"; -import { EffectEditorBehaviorsProperties } from "./behaviors"; - -export interface IEffectEditorBehaviorsPropertiesTabProps { - filePath: string | null; - selectedNodeId: string | number | null; - getNodeData: (nodeId: string | number) => EffectNode | null; -} - -export class EffectEditorBehaviorsPropertiesTab extends Component { - public render(): ReactNode { - const nodeId = this.props.selectedNodeId; - - if (!nodeId) { - return ( -
-

No particle selected

-
- ); - } - - const nodeData = this.props.getNodeData(nodeId); - - if (!nodeData || nodeData.type !== "particle" || !nodeData.system) { - return ( -
-

Select a particle system

-
- ); - } - - return ( -
- this.forceUpdate()} /> -
- ); - } -} diff --git a/editor/src/editor/windows/fx-editor/properties/emission-properties.tsx b/editor/src/editor/windows/fx-editor/properties/emission-properties.tsx deleted file mode 100644 index 45594064c..000000000 --- a/editor/src/editor/windows/fx-editor/properties/emission-properties.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import { Component, ReactNode } from "react"; - -import { EffectEditorEmissionProperties } from "./emission"; -import type { EffectNode } from "babylonjs-editor-tools"; - -export interface IEffectEditorEmissionPropertiesTabProps { - filePath: string | null; - selectedNodeId: string | number | null; - getNodeData: (nodeId: string | number) => EffectNode | null; -} - -export class EffectEditorEmissionPropertiesTab extends Component { - public render(): ReactNode { - const nodeId = this.props.selectedNodeId; - - if (!nodeId) { - return ( -
-

No particle selected

-
- ); - } - - const nodeData = this.props.getNodeData(nodeId); - - if (!nodeData || nodeData.type !== "particle" || !nodeData.system) { - return ( -
-

Select a particle system

-
- ); - } - - return ( -
- this.forceUpdate()} /> -
- ); - } -} diff --git a/editor/src/editor/windows/fx-editor/properties/emitter-properties.tsx b/editor/src/editor/windows/fx-editor/properties/emitter-properties.tsx deleted file mode 100644 index 884ac8b7e..000000000 --- a/editor/src/editor/windows/fx-editor/properties/emitter-properties.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import { Component, ReactNode } from "react"; - -import { EffectEditorEmitterShapeProperties } from "./emitter-shape"; -import type { EffectNode } from "babylonjs-editor-tools"; - -export interface IEffectEditorEmitterPropertiesTabProps { - filePath: string | null; - selectedNodeId: string | number | null; - getNodeData: (nodeId: string | number) => EffectNode | null; -} - -export class EffectEditorEmitterPropertiesTab extends Component { - public render(): ReactNode { - const nodeId = this.props.selectedNodeId; - - if (!nodeId) { - return ( -
-

No particle selected

-
- ); - } - - const nodeData = this.props.getNodeData(nodeId); - - if (!nodeData || nodeData.type !== "particle" || !nodeData.system) { - return ( -
-

Select a particle system

-
- ); - } - - return ( -
- this.forceUpdate()} /> -
- ); - } -} diff --git a/editor/src/editor/windows/fx-editor/properties/initialization-properties.tsx b/editor/src/editor/windows/fx-editor/properties/initialization-properties.tsx deleted file mode 100644 index 01231fab3..000000000 --- a/editor/src/editor/windows/fx-editor/properties/initialization-properties.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import { Component, ReactNode } from "react"; - -import { EffectEditorParticleInitializationProperties } from "./particle-initialization"; -import type { EffectNode } from "babylonjs-editor-tools"; - -export interface IEffectEditorInitializationPropertiesTabProps { - filePath: string | null; - selectedNodeId: string | number | null; - getNodeData: (nodeId: string | number) => EffectNode | null; -} - -export class EffectEditorInitializationPropertiesTab extends Component { - public render(): ReactNode { - const nodeId = this.props.selectedNodeId; - - if (!nodeId) { - return ( -
-

No particle selected

-
- ); - } - - const nodeData = this.props.getNodeData(nodeId); - - if (!nodeData || nodeData.type !== "particle" || !nodeData.system) { - return ( -
-

Select a particle system

-
- ); - } - - return ( -
- this.forceUpdate()} /> -
- ); - } -} diff --git a/editor/src/editor/windows/fx-editor/properties/object-properties.tsx b/editor/src/editor/windows/fx-editor/properties/object-properties.tsx deleted file mode 100644 index 9527e9d0c..000000000 --- a/editor/src/editor/windows/fx-editor/properties/object-properties.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import { Component, ReactNode } from "react"; - -import { EffectEditorObjectProperties } from "./object"; -import { IEffectEditor } from ".."; -import type { EffectNode } from "babylonjs-editor-tools"; - -export interface IEffectEditorObjectPropertiesTabProps { - filePath: string | null; - selectedNodeId: string | number | null; - editor: IEffectEditor; - onNameChanged?: () => void; - getNodeData: (nodeId: string | number) => EffectNode | null; -} - -export class EffectEditorObjectPropertiesTab extends Component { - public render(): ReactNode { - const nodeId = this.props.selectedNodeId; - - if (!nodeId) { - return ( -
-

No node selected

-
- ); - } - - const nodeData = this.props.getNodeData(nodeId); - - if (!nodeData) { - return ( -
-

Node not found

-
- ); - } - - return ( -
- { - this.forceUpdate(); - this.props.onNameChanged?.(); - }} - /> -
- ); - } -} diff --git a/editor/src/editor/windows/fx-editor/properties/renderer-properties.tsx b/editor/src/editor/windows/fx-editor/properties/renderer-properties.tsx deleted file mode 100644 index c5a2315b0..000000000 --- a/editor/src/editor/windows/fx-editor/properties/renderer-properties.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import { Component, ReactNode } from "react"; - -import { EffectEditorParticleRendererProperties } from "./particle-renderer"; -import { IEffectEditor } from ".."; -import type { EffectNode } from "babylonjs-editor-tools"; - -export interface IEffectEditorRendererPropertiesTabProps { - filePath: string | null; - selectedNodeId: string | number | null; - editor: IEffectEditor; - getNodeData: (nodeId: string | number) => EffectNode | null; -} - -export class EffectEditorRendererPropertiesTab extends Component { - public render(): ReactNode { - const nodeId = this.props.selectedNodeId; - - if (!nodeId) { - return ( -
-

No particle selected

-
- ); - } - - const nodeData = this.props.getNodeData(nodeId); - - if (!nodeData || nodeData.type !== "particle" || !nodeData.system) { - return ( -
-

Select a particle system

-
- ); - } - - return ( -
- this.forceUpdate()} /> -
- ); - } -} From e8fa8505d6850044d99e8754d6d5d1eeefc07400 Mon Sep 17 00:00:00 2001 From: Mikalai Lazitski Date: Tue, 16 Dec 2025 13:41:03 +0300 Subject: [PATCH 36/62] refactor: update FX editor components by renaming properties for consistency, enhancing gradient field integration, and removing deprecated emitter shape properties to streamline the editor's functionality and improve maintainability --- .../layout/inspector/fields/gradient.tsx | 1 - .../bezier-editor.tsx => editors/bezier.tsx} | 0 .../color-function.tsx} | 10 +- .../color-editor.tsx => editors/color.tsx} | 0 .../function.tsx} | 8 +- .../windows/effect-editor/editors/index.ts | 6 + .../rotation.tsx} | 2 +- .../value-editor.tsx => editors/value.tsx} | 2 +- .../editor/windows/effect-editor/layout.tsx | 26 +- .../effect-editor/properties/behaviors.tsx | 479 +++++++++++++++++- .../behaviors/behavior-properties.tsx | 124 ----- .../properties/behaviors/registry.ts | 356 ------------- .../effect-editor/properties/emission.tsx | 374 ++++++++++++-- .../properties/emitter-shape.tsx | 266 ---------- ...-initialization.tsx => initialization.tsx} | 6 +- .../effect-editor/properties/object.tsx | 2 +- .../{particle-renderer.tsx => renderer.tsx} | 2 +- .../{properties-tab.tsx => tab.tsx} | 17 +- tools/src/effect/factories/emitterFactory.ts | 18 +- tools/src/effect/factories/geometryFactory.ts | 14 +- tools/src/effect/factories/materialFactory.ts | 26 +- tools/src/effect/index.ts | 10 +- tools/src/effect/loggers/index.ts | 1 + tools/src/effect/parsers/dataConverter.ts | 30 +- tools/src/effect/parsers/index.ts | 2 + .../effect/systems/effectParticleSystem.ts | 4 +- .../systems/effectSolidParticleSystem.ts | 4 +- tools/src/effect/types/context.ts | 16 - tools/src/effect/types/emitter.ts | 4 +- tools/src/effect/types/hierarchy.ts | 10 +- tools/src/effect/types/index.ts | 59 +-- tools/src/effect/types/resources.ts | 26 +- tools/src/effect/types/rotations.ts | 8 +- tools/src/effect/types/shapes.ts | 2 +- tools/src/effect/types/values.ts | 8 +- tools/src/effect/utils/capacityCalculator.ts | 3 +- tools/src/effect/utils/gradientSystem.ts | 32 +- tools/src/effect/utils/index.ts | 4 + tools/src/effect/utils/valueParser.ts | 4 +- 39 files changed, 940 insertions(+), 1026 deletions(-) rename editor/src/editor/windows/effect-editor/{properties/behaviors/bezier-editor.tsx => editors/bezier.tsx} (100%) rename editor/src/editor/windows/effect-editor/{properties/behaviors/color-function-editor.tsx => editors/color-function.tsx} (94%) rename editor/src/editor/windows/effect-editor/{properties/color-editor.tsx => editors/color.tsx} (100%) rename editor/src/editor/windows/effect-editor/{properties/behaviors/function-editor.tsx => editors/function.tsx} (92%) create mode 100644 editor/src/editor/windows/effect-editor/editors/index.ts rename editor/src/editor/windows/effect-editor/{properties/rotation-editor.tsx => editors/rotation.tsx} (99%) rename editor/src/editor/windows/effect-editor/{properties/value-editor.tsx => editors/value.tsx} (99%) delete mode 100644 editor/src/editor/windows/effect-editor/properties/behaviors/behavior-properties.tsx delete mode 100644 editor/src/editor/windows/effect-editor/properties/behaviors/registry.ts delete mode 100644 editor/src/editor/windows/effect-editor/properties/emitter-shape.tsx rename editor/src/editor/windows/effect-editor/properties/{particle-initialization.tsx => initialization.tsx} (97%) rename editor/src/editor/windows/effect-editor/properties/{particle-renderer.tsx => renderer.tsx} (99%) rename editor/src/editor/windows/effect-editor/properties/{properties-tab.tsx => tab.tsx} (87%) create mode 100644 tools/src/effect/loggers/index.ts create mode 100644 tools/src/effect/parsers/index.ts delete mode 100644 tools/src/effect/types/context.ts create mode 100644 tools/src/effect/utils/index.ts diff --git a/editor/src/editor/layout/inspector/fields/gradient.tsx b/editor/src/editor/layout/inspector/fields/gradient.tsx index 6c7980ca7..2ce21d666 100644 --- a/editor/src/editor/layout/inspector/fields/gradient.tsx +++ b/editor/src/editor/layout/inspector/fields/gradient.tsx @@ -176,4 +176,3 @@ export function EditorInspectorColorGradientField(props: IEditorInspectorColorGr ); } - diff --git a/editor/src/editor/windows/effect-editor/properties/behaviors/bezier-editor.tsx b/editor/src/editor/windows/effect-editor/editors/bezier.tsx similarity index 100% rename from editor/src/editor/windows/effect-editor/properties/behaviors/bezier-editor.tsx rename to editor/src/editor/windows/effect-editor/editors/bezier.tsx diff --git a/editor/src/editor/windows/effect-editor/properties/behaviors/color-function-editor.tsx b/editor/src/editor/windows/effect-editor/editors/color-function.tsx similarity index 94% rename from editor/src/editor/windows/effect-editor/properties/behaviors/color-function-editor.tsx rename to editor/src/editor/windows/effect-editor/editors/color-function.tsx index fbd425c8c..be471f9ee 100644 --- a/editor/src/editor/windows/effect-editor/properties/behaviors/color-function-editor.tsx +++ b/editor/src/editor/windows/effect-editor/editors/color-function.tsx @@ -1,11 +1,11 @@ import { ReactNode } from "react"; import { Color4, Vector3 } from "babylonjs"; -import { EditorInspectorColorField } from "../../../../layout/inspector/fields/color"; -import { EditorInspectorColorGradientField } from "../../../../layout/inspector/fields/gradient"; -import { EditorInspectorListField } from "../../../../layout/inspector/fields/list"; -import { EditorInspectorBlockField } from "../../../../layout/inspector/fields/block"; -import type { IGradientKey } from "../../../../../ui/gradient-picker"; +import { EditorInspectorColorField } from "../../../layout/inspector/fields/color"; +import { EditorInspectorColorGradientField } from "../../../layout/inspector/fields/gradient"; +import { EditorInspectorListField } from "../../../layout/inspector/fields/list"; +import { EditorInspectorBlockField } from "../../../layout/inspector/fields/block"; +import type { IGradientKey } from "../../../../ui/gradient-picker"; export type ColorFunctionType = "ConstantColor" | "ColorRange" | "Gradient" | "RandomColor" | "RandomColorBetweenGradient"; diff --git a/editor/src/editor/windows/effect-editor/properties/color-editor.tsx b/editor/src/editor/windows/effect-editor/editors/color.tsx similarity index 100% rename from editor/src/editor/windows/effect-editor/properties/color-editor.tsx rename to editor/src/editor/windows/effect-editor/editors/color.tsx diff --git a/editor/src/editor/windows/effect-editor/properties/behaviors/function-editor.tsx b/editor/src/editor/windows/effect-editor/editors/function.tsx similarity index 92% rename from editor/src/editor/windows/effect-editor/properties/behaviors/function-editor.tsx rename to editor/src/editor/windows/effect-editor/editors/function.tsx index 0e97eb15e..88acb9d27 100644 --- a/editor/src/editor/windows/effect-editor/properties/behaviors/function-editor.tsx +++ b/editor/src/editor/windows/effect-editor/editors/function.tsx @@ -1,10 +1,10 @@ import { ReactNode } from "react"; -import { EditorInspectorNumberField } from "../../../../layout/inspector/fields/number"; -import { EditorInspectorListField } from "../../../../layout/inspector/fields/list"; -import { EditorInspectorBlockField } from "../../../../layout/inspector/fields/block"; +import { EditorInspectorNumberField } from "../../../layout/inspector/fields/number"; +import { EditorInspectorListField } from "../../../layout/inspector/fields/list"; +import { EditorInspectorBlockField } from "../../../layout/inspector/fields/block"; -import { BezierEditor } from "./bezier-editor"; +import { BezierEditor } from "./bezier"; export type FunctionType = "ConstantValue" | "IntervalValue" | "PiecewiseBezier" | "Vector3Function"; diff --git a/editor/src/editor/windows/effect-editor/editors/index.ts b/editor/src/editor/windows/effect-editor/editors/index.ts new file mode 100644 index 000000000..15478b643 --- /dev/null +++ b/editor/src/editor/windows/effect-editor/editors/index.ts @@ -0,0 +1,6 @@ +export * from "./value"; +export * from "./color"; +export * from "./rotation"; +export * from "./function"; +export * from "./color-function"; +export * from "./bezier"; diff --git a/editor/src/editor/windows/effect-editor/properties/rotation-editor.tsx b/editor/src/editor/windows/effect-editor/editors/rotation.tsx similarity index 99% rename from editor/src/editor/windows/effect-editor/properties/rotation-editor.tsx rename to editor/src/editor/windows/effect-editor/editors/rotation.tsx index 2f7e7565a..2997c9d13 100644 --- a/editor/src/editor/windows/effect-editor/properties/rotation-editor.tsx +++ b/editor/src/editor/windows/effect-editor/editors/rotation.tsx @@ -4,7 +4,7 @@ import { EditorInspectorListField } from "../../../layout/inspector/fields/list" import { EditorInspectorBlockField } from "../../../layout/inspector/fields/block"; import { type Rotation, type EulerRotation, type AxisAngleRotation, type RandomQuatRotation, ValueUtils, type Value } from "babylonjs-editor-tools"; -import { EffectValueEditor } from "./value-editor"; +import { EffectValueEditor } from "./value"; export type EffectRotationType = EulerRotation["type"] | AxisAngleRotation["type"] | RandomQuatRotation["type"]; diff --git a/editor/src/editor/windows/effect-editor/properties/value-editor.tsx b/editor/src/editor/windows/effect-editor/editors/value.tsx similarity index 99% rename from editor/src/editor/windows/effect-editor/properties/value-editor.tsx rename to editor/src/editor/windows/effect-editor/editors/value.tsx index 7330fc6c8..9140053eb 100644 --- a/editor/src/editor/windows/effect-editor/properties/value-editor.tsx +++ b/editor/src/editor/windows/effect-editor/editors/value.tsx @@ -7,7 +7,7 @@ import { EditorInspectorBlockField } from "../../../layout/inspector/fields/bloc import { type Value, type ConstantValue, type IntervalValue, ValueUtils } from "babylonjs-editor-tools"; type PiecewiseBezier = Extract; -import { BezierEditor } from "./behaviors/bezier-editor"; +import { BezierEditor } from "./bezier"; // Vec3Function is a custom editor extension, not part of the core Value type export type EffectValueType = ConstantValue["type"] | IntervalValue["type"] | PiecewiseBezier["type"] | "Vec3Function"; diff --git a/editor/src/editor/windows/effect-editor/layout.tsx b/editor/src/editor/windows/effect-editor/layout.tsx index c2ae0e676..c762da8b8 100644 --- a/editor/src/editor/windows/effect-editor/layout.tsx +++ b/editor/src/editor/windows/effect-editor/layout.tsx @@ -6,7 +6,7 @@ import { waitNextAnimationFrame } from "../../../tools/tools"; import { EffectEditorPreview } from "./preview"; import { EffectEditorGraph } from "./graph"; import { EffectEditorAnimation } from "./animation"; -import { EffectEditorPropertiesTab } from "./properties/properties-tab"; +import { EffectEditorPropertiesTab } from "./properties/tab"; import { EffectEditorResources } from "./resources"; import { IEffectEditor } from "."; @@ -97,9 +97,9 @@ const layoutModel: IJsonModel = { }, { type: "tab", - id: "properties-emitter", - name: "Emitter", - component: "properties-emitter", + id: "properties-emission", + name: "Emission", + component: "properties-emission", enableClose: false, enableRenderOnDemand: false, }, @@ -111,14 +111,6 @@ const layoutModel: IJsonModel = { enableClose: false, enableRenderOnDemand: false, }, - { - type: "tab", - id: "properties-emission", - name: "Emission", - component: "properties-emission", - enableClose: false, - enableRenderOnDemand: false, - }, { type: "tab", id: "properties-initialization", @@ -237,16 +229,6 @@ export class EffectEditorLayout extends Component this.props.editor.graph?.getNodeData(nodeId) || null} /> ), - "properties-emitter": ( - this.props.editor.graph?.getNodeData(nodeId) || null} - /> - ), "properties-renderer": ( ; + functionTypes?: FunctionType[]; + colorFunctionTypes?: ColorFunctionType[]; +} + +export interface IBehaviorDefinition { + type: string; + label: string; + properties: IBehaviorProperty[]; +} + +// Behavior Registry +export const BehaviorRegistry: { [key: string]: IBehaviorDefinition } = { + ApplyForce: { + type: "ApplyForce", + label: "Apply Force", + properties: [ + { name: "direction", type: "vector3", label: "Direction", default: { x: 0, y: 1, z: 0 } }, + { + name: "magnitude", + type: "function", + label: "Magnitude", + default: null, + functionTypes: ["ConstantValue", "IntervalValue"], + }, + ], + }, + Noise: { + type: "Noise", + label: "Noise", + properties: [ + { + name: "frequency", + type: "function", + label: "Frequency", + default: 1.0, + functionTypes: ["ConstantValue", "IntervalValue"], + }, + { + name: "power", + type: "function", + label: "Power", + default: 1.0, + functionTypes: ["ConstantValue", "IntervalValue"], + }, + { + name: "positionAmount", + type: "function", + label: "Position Amount", + default: 1.0, + functionTypes: ["ConstantValue", "IntervalValue"], + }, + { + name: "rotationAmount", + type: "function", + label: "Rotation Amount", + default: 0.0, + functionTypes: ["ConstantValue", "IntervalValue"], + }, + ], + }, + TurbulenceField: { + type: "TurbulenceField", + label: "Turbulence Field", + properties: [ + { name: "scale", type: "vector3", label: "Scale", default: { x: 1, y: 1, z: 1 } }, + { name: "octaves", type: "number", label: "Octaves", default: 1 }, + { name: "velocityMultiplier", type: "vector3", label: "Velocity Multiplier", default: { x: 1, y: 1, z: 1 } }, + { name: "timeScale", type: "vector3", label: "Time Scale", default: { x: 1, y: 1, z: 1 } }, + ], + }, + GravityForce: { + type: "GravityForce", + label: "Gravity Force", + properties: [ + { name: "center", type: "vector3", label: "Center", default: { x: 0, y: 0, z: 0 } }, + { name: "magnitude", type: "number", label: "Magnitude", default: 1.0 }, + ], + }, + ColorOverLife: { + type: "ColorOverLife", + label: "Color Over Life", + properties: [ + { + name: "color", + type: "colorFunction", + label: "Color", + default: null, + colorFunctionTypes: ["ConstantColor", "ColorRange", "Gradient", "RandomColorBetweenGradient"], + }, + ], + }, + RotationOverLife: { + type: "RotationOverLife", + label: "Rotation Over Life", + properties: [ + { + name: "angularVelocity", + type: "function", + label: "Angular Velocity", + default: null, + functionTypes: ["ConstantValue", "IntervalValue", "PiecewiseBezier"], + }, + ], + }, + Rotation3DOverLife: { + type: "Rotation3DOverLife", + label: "Rotation 3D Over Life", + properties: [ + { + name: "angularVelocity", + type: "function", + label: "Angular Velocity", + default: null, + functionTypes: ["ConstantValue", "IntervalValue", "PiecewiseBezier"], + }, + ], + }, + SizeOverLife: { + type: "SizeOverLife", + label: "Size Over Life", + properties: [ + { + name: "size", + type: "function", + label: "Size", + default: null, + functionTypes: ["ConstantValue", "IntervalValue", "PiecewiseBezier", "Vector3Function"], + }, + ], + }, + ColorBySpeed: { + type: "ColorBySpeed", + label: "Color By Speed", + properties: [ + { + name: "color", + type: "colorFunction", + label: "Color", + default: null, + colorFunctionTypes: ["ConstantColor", "ColorRange", "Gradient", "RandomColorBetweenGradient"], + }, + { name: "speedRange", type: "range", label: "Speed Range", default: { min: 0, max: 10 } }, + ], + }, + RotationBySpeed: { + type: "RotationBySpeed", + label: "Rotation By Speed", + properties: [ + { + name: "angularVelocity", + type: "function", + label: "Angular Velocity", + default: null, + functionTypes: ["ConstantValue", "IntervalValue", "PiecewiseBezier"], + }, + { name: "speedRange", type: "range", label: "Speed Range", default: { min: 0, max: 10 } }, + ], + }, + SizeBySpeed: { + type: "SizeBySpeed", + label: "Size By Speed", + properties: [ + { + name: "size", + type: "function", + label: "Size", + default: null, + functionTypes: ["ConstantValue", "IntervalValue", "PiecewiseBezier", "Vector3Function"], + }, + { name: "speedRange", type: "range", label: "Speed Range", default: { min: 0, max: 10 } }, + ], + }, + SpeedOverLife: { + type: "SpeedOverLife", + label: "Speed Over Life", + properties: [ + { + name: "speed", + type: "function", + label: "Speed", + default: null, + functionTypes: ["ConstantValue", "IntervalValue", "PiecewiseBezier"], + }, + ], + }, + FrameOverLife: { + type: "FrameOverLife", + label: "Frame Over Life", + properties: [ + { + name: "frame", + type: "function", + label: "Frame", + default: null, + functionTypes: ["ConstantValue", "IntervalValue", "PiecewiseBezier"], + }, + ], + }, + ForceOverLife: { + type: "ForceOverLife", + label: "Force Over Life", + properties: [ + { + name: "x", + type: "function", + label: "X", + default: null, + functionTypes: ["ConstantValue", "IntervalValue", "PiecewiseBezier"], + }, + { + name: "y", + type: "function", + label: "Y", + default: null, + functionTypes: ["ConstantValue", "IntervalValue", "PiecewiseBezier"], + }, + { + name: "z", + type: "function", + label: "Z", + default: null, + functionTypes: ["ConstantValue", "IntervalValue", "PiecewiseBezier"], + }, + ], + }, + OrbitOverLife: { + type: "OrbitOverLife", + label: "Orbit Over Life", + properties: [ + { + name: "orbitSpeed", + type: "function", + label: "Orbit Speed", + default: null, + functionTypes: ["ConstantValue", "IntervalValue", "PiecewiseBezier"], + }, + { name: "axis", type: "vector3", label: "Axis", default: { x: 0, y: 1, z: 0 } }, + ], + }, + WidthOverLength: { + type: "WidthOverLength", + label: "Width Over Length", + properties: [ + { + name: "width", + type: "function", + label: "Width", + default: null, + functionTypes: ["ConstantValue", "IntervalValue", "PiecewiseBezier"], + }, + ], + }, + ChangeEmitDirection: { + type: "ChangeEmitDirection", + label: "Change Emit Direction", + properties: [ + { + name: "angle", + type: "function", + label: "Angle", + default: 0.0, + functionTypes: ["ConstantValue", "IntervalValue"], + }, + ], + }, + EmitSubParticleSystem: { + type: "EmitSubParticleSystem", + label: "Emit Sub Particle System", + properties: [ + { name: "subParticleSystem", type: "string", label: "Sub Particle System", default: "" }, + { name: "useVelocityAsBasis", type: "boolean", label: "Use Velocity As Basis", default: false }, + { + name: "mode", + type: "enum", + label: "Mode", + default: 0, + enumItems: [ + { text: "Death", value: 0 }, + { text: "Birth", value: 1 }, + { text: "Frame", value: 2 }, + ], + }, + { name: "emitProbability", type: "number", label: "Emit Probability", default: 1.0 }, + ], + }, + LimitSpeedOverLife: { + type: "LimitSpeedOverLife", + label: "Limit Speed Over Life", + properties: [ + { + name: "speed", + type: "function", + label: "Speed", + default: null, + functionTypes: ["ConstantValue", "IntervalValue", "PiecewiseBezier"], + }, + { name: "dampen", type: "number", label: "Dampen", default: 0.0 }, + ], + }, +}; + +// Utility functions +export function getBehaviorDefinition(type: string): IBehaviorDefinition | undefined { + return BehaviorRegistry[type]; +} + +export function createDefaultBehaviorData(type: string): any { + const definition = BehaviorRegistry[type]; + if (!definition) { + return { type }; + } + + const data: any = { type }; + for (const prop of definition.properties) { + if (prop.type === "function") { + data[prop.name] = { + functionType: prop.functionTypes?.[0] || "ConstantValue", + data: {}, + }; + if (data[prop.name].functionType === "ConstantValue") { + data[prop.name].data.value = prop.default !== undefined ? prop.default : 1.0; + } else if (data[prop.name].functionType === "IntervalValue") { + data[prop.name].data.min = 0; + data[prop.name].data.max = 1; + } + } else if (prop.type === "colorFunction") { + data[prop.name] = { + colorFunctionType: prop.colorFunctionTypes?.[0] || "ConstantColor", + data: {}, + }; + } else if (prop.default !== undefined) { + if (prop.type === "vector3") { + data[prop.name] = { x: prop.default.x, y: prop.default.y, z: prop.default.z }; + } else if (prop.type === "range") { + data[prop.name] = { min: prop.default.min, max: prop.default.max }; + } else { + data[prop.name] = prop.default; + } + } + } + return data; +} + +// Helper function to render a single property +function renderProperty(prop: IBehaviorProperty, behavior: any, onChange: () => void): ReactNode { + switch (prop.type) { + case "vector3": + if (!behavior[prop.name]) { + const defaultVal = prop.default || { x: 0, y: 0, z: 0 }; + behavior[prop.name] = new Vector3(defaultVal.x, defaultVal.y, defaultVal.z); + } else if (!(behavior[prop.name] instanceof Vector3)) { + const obj = behavior[prop.name]; + behavior[prop.name] = new Vector3(obj.x || 0, obj.y || 0, obj.z || 0); + } + return ; + + case "number": + if (behavior[prop.name] === undefined) { + behavior[prop.name] = prop.default !== undefined ? prop.default : 0; + } + return ; + + case "color": + if (!behavior[prop.name]) { + behavior[prop.name] = prop.default ? new Color4(prop.default.r, prop.default.g, prop.default.b, prop.default.a) : new Color4(1, 1, 1, 1); + } + return ; + + case "range": + if (!behavior[prop.name]) { + behavior[prop.name] = prop.default ? { ...prop.default } : { min: 0, max: 1 }; + } + return ( + +
{prop.label}
+
+ + +
+
+ ); + + case "boolean": + if (behavior[prop.name] === undefined) { + behavior[prop.name] = prop.default !== undefined ? prop.default : false; + } + return ; + + case "string": + if (behavior[prop.name] === undefined) { + behavior[prop.name] = prop.default !== undefined ? prop.default : ""; + } + return ; + + case "enum": + if (behavior[prop.name] === undefined) { + behavior[prop.name] = prop.default !== undefined ? prop.default : (prop.enumItems?.[0]?.value ?? 0); + } + if (!prop.enumItems || prop.enumItems.length === 0) { + return null; + } + return ; + + case "colorFunction": + if (!behavior[prop.name]) { + behavior[prop.name] = { + colorFunctionType: prop.colorFunctionTypes?.[0] || "ConstantColor", + data: {}, + }; + } + return ; + + case "function": + if (!behavior[prop.name]) { + behavior[prop.name] = { + functionType: prop.functionTypes?.[0] || "ConstantValue", + data: {}, + }; + } + return ; + + default: + return null; + } +} + +// Component to render behavior properties +interface IBehaviorPropertiesProps { + behavior: any; + onChange: () => void; +} + +function BehaviorProperties(props: IBehaviorPropertiesProps): ReactNode { + const { behavior, onChange } = props; + const definition = getBehaviorDefinition(behavior.type); + + if (!definition) { + return null; + } + + return <>{definition.properties.map((prop) => renderProperty(prop, behavior, onChange))}; +} + +// Main component export interface IEffectEditorBehaviorsPropertiesProps { nodeData: EffectNode; onChange: () => void; @@ -24,33 +485,21 @@ export function EffectEditorBehaviorsProperties(props: IEffectEditorBehaviorsPro } const system = nodeData.system; - - // Get behavior configurations from system - let behaviorConfigs: any[] = []; - if (system instanceof EffectParticleSystem) { - behaviorConfigs = system.behaviorConfigs || []; - } else if (system instanceof EffectSolidParticleSystem) { - behaviorConfigs = system.behaviorConfigs || []; - } + const behaviorConfigs: any[] = system instanceof EffectParticleSystem || system instanceof EffectSolidParticleSystem ? system.behaviorConfigs || [] : []; const handleAddBehavior = (behaviorType: string): void => { const newBehavior = createDefaultBehaviorData(behaviorType); newBehavior.id = `behavior-${Date.now()}-${Math.random()}`; - - // Directly modify the array - proxy will automatically update functions behaviorConfigs.push(newBehavior); onChange(); }; const handleRemoveBehavior = (index: number): void => { - // Directly modify the array - proxy will automatically update functions behaviorConfigs.splice(index, 1); onChange(); }; const handleBehaviorChange = (): void => { - // When behavior properties change, the proxy automatically detects it - // and updates the behavior functions. We just need to trigger UI update. onChange(); }; diff --git a/editor/src/editor/windows/effect-editor/properties/behaviors/behavior-properties.tsx b/editor/src/editor/windows/effect-editor/properties/behaviors/behavior-properties.tsx deleted file mode 100644 index 6a83bb5b5..000000000 --- a/editor/src/editor/windows/effect-editor/properties/behaviors/behavior-properties.tsx +++ /dev/null @@ -1,124 +0,0 @@ -import { ReactNode } from "react"; -import { Vector3, Color4 } from "babylonjs"; - -import { EditorInspectorNumberField } from "../../../../layout/inspector/fields/number"; -import { EditorInspectorVectorField } from "../../../../layout/inspector/fields/vector"; -import { EditorInspectorColorField } from "../../../../layout/inspector/fields/color"; -import { EditorInspectorSwitchField } from "../../../../layout/inspector/fields/switch"; -import { EditorInspectorStringField } from "../../../../layout/inspector/fields/string"; -import { EditorInspectorListField } from "../../../../layout/inspector/fields/list"; -import { EditorInspectorBlockField } from "../../../../layout/inspector/fields/block"; - -import { getBehaviorDefinition } from "./registry"; -import { FunctionEditor } from "./function-editor"; -import { ColorFunctionEditor } from "./color-function-editor"; - -export interface IBehaviorPropertiesProps { - behavior: any; - onChange: () => void; -} - -export function BehaviorProperties(props: IBehaviorPropertiesProps): ReactNode { - const { behavior, onChange } = props; - const definition = getBehaviorDefinition(behavior.type); - - if (!definition) { - return null; - } - - return ( - <> - {definition.properties.map((prop) => { - if (prop.type === "vector3") { - // Ensure vector3 property exists and is a Vector3 or object - if (!behavior[prop.name]) { - const defaultVal = prop.default || { x: 0, y: 0, z: 0 }; - behavior[prop.name] = new Vector3(defaultVal.x, defaultVal.y, defaultVal.z); - } else if (!(behavior[prop.name] instanceof Vector3)) { - // Convert to Vector3 if it's an object - const obj = behavior[prop.name]; - behavior[prop.name] = new Vector3(obj.x || 0, obj.y || 0, obj.z || 0); - } - return ; - } - - if (prop.type === "number") { - if (behavior[prop.name] === undefined) { - behavior[prop.name] = prop.default !== undefined ? prop.default : 0; - } - return ; - } - - if (prop.type === "color") { - if (!behavior[prop.name]) { - behavior[prop.name] = prop.default ? new Color4(prop.default.r, prop.default.g, prop.default.b, prop.default.a) : new Color4(1, 1, 1, 1); - } - return ; - } - - if (prop.type === "range") { - if (!behavior[prop.name]) { - behavior[prop.name] = prop.default ? { ...prop.default } : { min: 0, max: 1 }; - } - return ( - -
{prop.label}
-
- - -
-
- ); - } - - if (prop.type === "boolean") { - if (behavior[prop.name] === undefined) { - behavior[prop.name] = prop.default !== undefined ? prop.default : false; - } - return ; - } - - if (prop.type === "string") { - if (behavior[prop.name] === undefined) { - behavior[prop.name] = prop.default !== undefined ? prop.default : ""; - } - return ; - } - - if (prop.type === "enum") { - if (behavior[prop.name] === undefined) { - behavior[prop.name] = prop.default !== undefined ? prop.default : (prop.enumItems?.[0]?.value ?? 0); - } - if (!prop.enumItems || prop.enumItems.length === 0) { - return null; - } - return ; - } - - if (prop.type === "colorFunction") { - // Initialize color function value if not set - if (!behavior[prop.name]) { - behavior[prop.name] = { - colorFunctionType: prop.colorFunctionTypes?.[0] || "ConstantColor", - data: {}, - }; - } - return ; - } - - if (prop.type === "function") { - // Initialize function value if not set - if (!behavior[prop.name]) { - behavior[prop.name] = { - functionType: prop.functionTypes?.[0] || "ConstantValue", - data: {}, - }; - } - return ; - } - - return null; - })} - - ); -} diff --git a/editor/src/editor/windows/effect-editor/properties/behaviors/registry.ts b/editor/src/editor/windows/effect-editor/properties/behaviors/registry.ts deleted file mode 100644 index 302861c6b..000000000 --- a/editor/src/editor/windows/effect-editor/properties/behaviors/registry.ts +++ /dev/null @@ -1,356 +0,0 @@ -import { ReactNode } from "react"; - -export type FunctionType = "ConstantValue" | "IntervalValue" | "PiecewiseBezier" | "Vector3Function"; -export type ColorFunctionType = "ConstantColor" | "ColorRange" | "Gradient" | "RandomColor" | "RandomColorBetweenGradient"; - -export interface IBehaviorProperty { - name: string; - type: "vector3" | "number" | "color" | "range" | "boolean" | "string" | "function" | "enum" | "colorFunction"; - label: string; - default?: any; - enumItems?: Array<{ text: string; value: any }>; - functionTypes?: FunctionType[]; - colorFunctionTypes?: ColorFunctionType[]; -} - -export interface IBehaviorDefinition { - type: string; - label: string; - properties: IBehaviorProperty[]; - component?: (props: { behavior: any; onChange: () => void }) => ReactNode; -} - -export const BehaviorRegistry: { [key: string]: IBehaviorDefinition } = { - ApplyForce: { - type: "ApplyForce", - label: "Apply Force", - properties: [ - { name: "direction", type: "vector3", label: "Direction", default: { x: 0, y: 1, z: 0 } }, - { - name: "magnitude", - type: "function", - label: "Magnitude", - default: null, - functionTypes: ["ConstantValue", "IntervalValue"], - }, - ], - }, - Noise: { - type: "Noise", - label: "Noise", - properties: [ - { - name: "frequency", - type: "function", - label: "Frequency", - default: 1.0, - functionTypes: ["ConstantValue", "IntervalValue"], - }, - { - name: "power", - type: "function", - label: "Power", - default: 1.0, - functionTypes: ["ConstantValue", "IntervalValue"], - }, - { - name: "positionAmount", - type: "function", - label: "Position Amount", - default: 1.0, - functionTypes: ["ConstantValue", "IntervalValue"], - }, - { - name: "rotationAmount", - type: "function", - label: "Rotation Amount", - default: 0.0, - functionTypes: ["ConstantValue", "IntervalValue"], - }, - ], - }, - TurbulenceField: { - type: "TurbulenceField", - label: "Turbulence Field", - properties: [ - { name: "scale", type: "vector3", label: "Scale", default: { x: 1, y: 1, z: 1 } }, - { name: "octaves", type: "number", label: "Octaves", default: 1 }, - { name: "velocityMultiplier", type: "vector3", label: "Velocity Multiplier", default: { x: 1, y: 1, z: 1 } }, - { name: "timeScale", type: "vector3", label: "Time Scale", default: { x: 1, y: 1, z: 1 } }, - ], - }, - GravityForce: { - type: "GravityForce", - label: "Gravity Force", - properties: [ - { name: "center", type: "vector3", label: "Center", default: { x: 0, y: 0, z: 0 } }, - { name: "magnitude", type: "number", label: "Magnitude", default: 1.0 }, - ], - }, - ColorOverLife: { - type: "ColorOverLife", - label: "Color Over Life", - properties: [ - { - name: "color", - type: "colorFunction", - label: "Color", - default: null, - colorFunctionTypes: ["ConstantColor", "ColorRange", "Gradient", "RandomColorBetweenGradient"], - }, - ], - }, - RotationOverLife: { - type: "RotationOverLife", - label: "Rotation Over Life", - properties: [ - { - name: "angularVelocity", - type: "function", - label: "Angular Velocity", - default: null, - functionTypes: ["ConstantValue", "IntervalValue", "PiecewiseBezier"], - }, - ], - }, - Rotation3DOverLife: { - type: "Rotation3DOverLife", - label: "Rotation 3D Over Life", - properties: [ - { - name: "angularVelocity", - type: "function", - label: "Angular Velocity", - default: null, - functionTypes: ["ConstantValue", "IntervalValue", "PiecewiseBezier"], - }, - ], - }, - SizeOverLife: { - type: "SizeOverLife", - label: "Size Over Life", - properties: [ - { - name: "size", - type: "function", - label: "Size", - default: null, - functionTypes: ["ConstantValue", "IntervalValue", "PiecewiseBezier", "Vector3Function"], - }, - ], - }, - ColorBySpeed: { - type: "ColorBySpeed", - label: "Color By Speed", - properties: [ - { - name: "color", - type: "colorFunction", - label: "Color", - default: null, - colorFunctionTypes: ["ConstantColor", "ColorRange", "Gradient", "RandomColorBetweenGradient"], - }, - { name: "speedRange", type: "range", label: "Speed Range", default: { min: 0, max: 10 } }, - ], - }, - RotationBySpeed: { - type: "RotationBySpeed", - label: "Rotation By Speed", - properties: [ - { - name: "angularVelocity", - type: "function", - label: "Angular Velocity", - default: null, - functionTypes: ["ConstantValue", "IntervalValue", "PiecewiseBezier"], - }, - { name: "speedRange", type: "range", label: "Speed Range", default: { min: 0, max: 10 } }, - ], - }, - SizeBySpeed: { - type: "SizeBySpeed", - label: "Size By Speed", - properties: [ - { - name: "size", - type: "function", - label: "Size", - default: null, - functionTypes: ["ConstantValue", "IntervalValue", "PiecewiseBezier", "Vector3Function"], - }, - { name: "speedRange", type: "range", label: "Speed Range", default: { min: 0, max: 10 } }, - ], - }, - SpeedOverLife: { - type: "SpeedOverLife", - label: "Speed Over Life", - properties: [ - { - name: "speed", - type: "function", - label: "Speed", - default: null, - functionTypes: ["ConstantValue", "IntervalValue", "PiecewiseBezier"], - }, - ], - }, - FrameOverLife: { - type: "FrameOverLife", - label: "Frame Over Life", - properties: [ - { - name: "frame", - type: "function", - label: "Frame", - default: null, - functionTypes: ["ConstantValue", "IntervalValue", "PiecewiseBezier"], - }, - ], - }, - ForceOverLife: { - type: "ForceOverLife", - label: "Force Over Life", - properties: [ - { - name: "x", - type: "function", - label: "X", - default: null, - functionTypes: ["ConstantValue", "IntervalValue", "PiecewiseBezier"], - }, - { - name: "y", - type: "function", - label: "Y", - default: null, - functionTypes: ["ConstantValue", "IntervalValue", "PiecewiseBezier"], - }, - { - name: "z", - type: "function", - label: "Z", - default: null, - functionTypes: ["ConstantValue", "IntervalValue", "PiecewiseBezier"], - }, - ], - }, - OrbitOverLife: { - type: "OrbitOverLife", - label: "Orbit Over Life", - properties: [ - { - name: "orbitSpeed", - type: "function", - label: "Orbit Speed", - default: null, - functionTypes: ["ConstantValue", "IntervalValue", "PiecewiseBezier"], - }, - { name: "axis", type: "vector3", label: "Axis", default: { x: 0, y: 1, z: 0 } }, - ], - }, - WidthOverLength: { - type: "WidthOverLength", - label: "Width Over Length", - properties: [ - { - name: "width", - type: "function", - label: "Width", - default: null, - functionTypes: ["ConstantValue", "IntervalValue", "PiecewiseBezier"], - }, - ], - }, - ChangeEmitDirection: { - type: "ChangeEmitDirection", - label: "Change Emit Direction", - properties: [ - { - name: "angle", - type: "function", - label: "Angle", - default: 0.0, - functionTypes: ["ConstantValue", "IntervalValue"], - }, - ], - }, - EmitSubParticleSystem: { - type: "EmitSubParticleSystem", - label: "Emit Sub Particle System", - properties: [ - { name: "subParticleSystem", type: "string", label: "Sub Particle System", default: "" }, - { name: "useVelocityAsBasis", type: "boolean", label: "Use Velocity As Basis", default: false }, - { - name: "mode", - type: "enum", - label: "Mode", - default: 0, - enumItems: [ - { text: "Death", value: 0 }, - { text: "Birth", value: 1 }, - { text: "Frame", value: 2 }, - ], - }, - { name: "emitProbability", type: "number", label: "Emit Probability", default: 1.0 }, - ], - }, - LimitSpeedOverLife: { - type: "LimitSpeedOverLife", - label: "Limit Speed Over Life", - properties: [ - { - name: "speed", - type: "function", - label: "Speed", - default: null, - functionTypes: ["ConstantValue", "IntervalValue", "PiecewiseBezier"], - }, - { name: "dampen", type: "number", label: "Dampen", default: 0.0 }, - ], - }, -}; - -export function getBehaviorDefinition(type: string): IBehaviorDefinition | undefined { - return BehaviorRegistry[type]; -} - -export function createDefaultBehaviorData(type: string): any { - const definition = BehaviorRegistry[type]; - if (!definition) { - return { type }; - } - - const data: any = { type }; - for (const prop of definition.properties) { - if (prop.type === "function") { - // Initialize function with default type - data[prop.name] = { - functionType: prop.functionTypes?.[0] || "ConstantValue", - data: {}, - }; - // Set default value for ConstantValue - if (data[prop.name].functionType === "ConstantValue") { - data[prop.name].data.value = prop.default !== undefined ? prop.default : 1.0; - } else if (data[prop.name].functionType === "IntervalValue") { - data[prop.name].data.min = 0; - data[prop.name].data.max = 1; - } - } else if (prop.type === "colorFunction") { - // Initialize color function with default type - data[prop.name] = { - colorFunctionType: prop.colorFunctionTypes?.[0] || "ConstantColor", - data: {}, - }; - } else if (prop.default !== undefined) { - if (prop.type === "vector3") { - // Store as object, will be converted to Vector3 in behavior-properties.tsx - data[prop.name] = { x: prop.default.x, y: prop.default.y, z: prop.default.z }; - } else if (prop.type === "range") { - data[prop.name] = { min: prop.default.min, max: prop.default.max }; - } else { - data[prop.name] = prop.default; - } - } - } - return data; -} diff --git a/editor/src/editor/windows/effect-editor/properties/emission.tsx b/editor/src/editor/windows/effect-editor/properties/emission.tsx index f121c71e1..f8084539f 100644 --- a/editor/src/editor/windows/effect-editor/properties/emission.tsx +++ b/editor/src/editor/windows/effect-editor/properties/emission.tsx @@ -1,83 +1,278 @@ import { ReactNode } from "react"; +import { Vector3 } from "babylonjs"; import { EditorInspectorNumberField } from "../../../layout/inspector/fields/number"; +import { EditorInspectorVectorField } from "../../../layout/inspector/fields/vector"; import { EditorInspectorSwitchField } from "../../../layout/inspector/fields/switch"; +import { EditorInspectorListField } from "../../../layout/inspector/fields/list"; import { EditorInspectorBlockField } from "../../../layout/inspector/fields/block"; import { EditorInspectorSectionField } from "../../../layout/inspector/fields/section"; -import { type EffectNode, EffectSolidParticleSystem, EffectParticleSystem, EmissionBurst, Value } from "babylonjs-editor-tools"; -import { EffectValueEditor } from "./value-editor"; +import { + type EffectNode, + EffectSolidParticleSystem, + EffectParticleSystem, + SolidSphereParticleEmitter, + SolidConeParticleEmitter, + EmissionBurst, + Value, +} from "babylonjs-editor-tools"; +import { EffectValueEditor } from "../editors/value"; export interface IEffectEditorEmissionPropertiesProps { nodeData: EffectNode; onChange: () => void; } -export function EffectEditorEmissionProperties(props: IEffectEditorEmissionPropertiesProps): ReactNode { - const { nodeData, onChange } = props; +/** + * Renders emitter shape properties for SolidParticleSystem + */ +function renderSolidParticleSystemEmitter(system: EffectSolidParticleSystem, onChange: () => void): ReactNode { + const emitter = system.particleEmitterType; + const emitterType = emitter ? emitter.constructor.name : "Point"; - if (nodeData.type !== "particle" || !nodeData.system) { - return null; + const emitterTypeMap: Record = { + SolidPointParticleEmitter: "Point", + SolidSphereParticleEmitter: "Sphere", + SolidConeParticleEmitter: "Cone", + }; + + const currentType = emitterTypeMap[emitterType] || "Point"; + const emitterTypes = [ + { text: "Point", value: "point" }, + { text: "Sphere", value: "sphere" }, + { text: "Cone", value: "cone" }, + ]; + + return ( + <> + ({ text: t.text, value: t.value }))} + onChange={(value) => { + const currentRadius = emitter instanceof SolidSphereParticleEmitter || emitter instanceof SolidConeParticleEmitter ? emitter.radius : 1; + const currentArc = emitter instanceof SolidSphereParticleEmitter || emitter instanceof SolidConeParticleEmitter ? emitter.arc : Math.PI * 2; + const currentThickness = emitter instanceof SolidSphereParticleEmitter || emitter instanceof SolidConeParticleEmitter ? emitter.thickness : 1; + const currentAngle = emitter instanceof SolidConeParticleEmitter ? emitter.angle : Math.PI / 6; + + switch (value) { + case "point": + system.createPointEmitter(); + break; + case "sphere": + system.createSphereEmitter(currentRadius, currentArc, currentThickness); + break; + case "cone": + system.createConeEmitter(currentRadius, currentArc, currentThickness, currentAngle); + break; + } + onChange(); + }} + /> + + {emitter instanceof SolidSphereParticleEmitter && ( + <> + + + + + )} + + {emitter instanceof SolidConeParticleEmitter && ( + <> + + + + + + )} + + ); +} + +/** + * Renders emitter shape properties for ParticleSystem + */ +function renderParticleSystemEmitter(system: EffectParticleSystem, onChange: () => void): ReactNode { + const emitter = system.particleEmitterType; + if (!emitter) { + return
No emitter found.
; } - const system = nodeData.system; + const emitterType = emitter.getClassName(); + const emitterTypeMap: Record = { + PointParticleEmitter: "point", + BoxParticleEmitter: "box", + SphereParticleEmitter: "sphere", + SphereDirectedParticleEmitter: "sphere", + ConeParticleEmitter: "cone", + ConeDirectedParticleEmitter: "cone", + HemisphericParticleEmitter: "hemisphere", + CylinderParticleEmitter: "cylinder", + CylinderDirectedParticleEmitter: "cylinder", + }; + + const currentType = emitterTypeMap[emitterType] || "point"; + const emitterTypes = [ + { text: "Point", value: "point" }, + { text: "Box", value: "box" }, + { text: "Sphere", value: "sphere" }, + { text: "Cone", value: "cone" }, + { text: "Hemisphere", value: "hemisphere" }, + { text: "Cylinder", value: "cylinder" }, + ]; return ( <> - {/* Looping / Duration / Prewarm / OnlyUsedByOther */} - - ({ text: t.text, value: t.value }))} + onChange={(value) => { + const currentRadius = "radius" in emitter ? (emitter as any).radius : 1; + const currentAngle = "angle" in emitter ? (emitter as any).angle : Math.PI / 6; + const currentHeight = "height" in emitter ? (emitter as any).height : 1; + const currentDirection1 = "direction1" in emitter ? (emitter as any).direction1?.clone() : Vector3.Zero(); + const currentDirection2 = "direction2" in emitter ? (emitter as any).direction2?.clone() : Vector3.Zero(); + const currentMinEmitBox = "minEmitBox" in emitter ? (emitter as any).minEmitBox?.clone() : new Vector3(-0.5, -0.5, -0.5); + const currentMaxEmitBox = "maxEmitBox" in emitter ? (emitter as any).maxEmitBox?.clone() : new Vector3(0.5, 0.5, 0.5); + + switch (value) { + case "point": + system.createPointEmitter(currentDirection1, currentDirection2); + break; + case "box": + system.createBoxEmitter(currentDirection1, currentDirection2, currentMinEmitBox, currentMaxEmitBox); + break; + case "sphere": + system.createSphereEmitter(currentRadius); + break; + case "cone": + system.createConeEmitter(currentRadius, currentAngle); + break; + case "hemisphere": + system.createHemisphericEmitter(currentRadius); + break; + case "cylinder": + system.createCylinderEmitter(currentRadius, currentHeight); + break; + } + onChange(); + }} /> - - - {/* Emit Over Time */} - - { - (system as any).emissionOverTime = val; - onChange(); - }} - /> - + {emitterType === "BoxParticleEmitter" && ( + <> + +
Direction
+ + +
+ +
Emit Box
+ + +
+ + )} - {/* Emit Over Distance */} - - { - (system as any).emissionOverDistance = val; - onChange(); - }} - /> - + {(emitterType === "ConeParticleEmitter" || emitterType === "ConeDirectedParticleEmitter") && ( + <> + + + + + - {/* Emit Power (min/max) - только для base (есть min/maxEmitPower) */} - {system instanceof EffectParticleSystem && ( + {emitterType === "ConeDirectedParticleEmitter" && ( + +
Direction
+ + +
+ )} + + )} + + {(emitterType === "CylinderParticleEmitter" || emitterType === "CylinderDirectedParticleEmitter") && ( + <> + + + + + + {emitterType === "CylinderDirectedParticleEmitter" && ( + +
Direction
+ + +
+ )} + + )} + + {(emitterType === "SphereParticleEmitter" || emitterType === "SphereDirectedParticleEmitter") && ( + <> + + + + + {emitterType === "SphereDirectedParticleEmitter" && ( + +
Direction
+ + +
+ )} + + )} + + {emitterType === "PointParticleEmitter" && ( -
Emit Power
-
- - -
+
Direction
+ +
)} - {/* Bursts */} - {renderBursts(system as any, onChange)} + {emitterType === "HemisphericParticleEmitter" && ( + <> + + + + + )} ); } +/** + * Renders emitter shape properties + */ +function renderEmitterShape(nodeData: EffectNode, onChange: () => void): ReactNode { + if (nodeData.type !== "particle" || !nodeData.system) { + return null; + } + + const system = nodeData.system; + + if (system instanceof EffectSolidParticleSystem) { + return renderSolidParticleSystemEmitter(system, onChange); + } + + if (system instanceof EffectParticleSystem) { + return renderParticleSystemEmitter(system, onChange); + } + + return null; +} + +/** + * Renders emission bursts + */ function renderBursts(system: EffectParticleSystem | EffectSolidParticleSystem, onChange: () => void): ReactNode { const bursts: (EmissionBurst & { cycle?: number; interval?: number; probability?: number })[] = Array.isArray((system as any).emissionBursts) ? (system as any).emissionBursts @@ -142,3 +337,84 @@ function renderBursts(system: EffectParticleSystem | EffectSolidParticleSystem,
); } + +/** + * Renders emission parameters (looping, duration, emit over time/distance, bursts) + */ +function renderEmissionParameters(nodeData: EffectNode, onChange: () => void): ReactNode { + if (nodeData.type !== "particle" || !nodeData.system) { + return null; + } + + const system = nodeData.system; + + return ( + <> + + + + + + + { + (system as any).emissionOverTime = val; + onChange(); + }} + /> + + + + { + (system as any).emissionOverDistance = val; + onChange(); + }} + /> + + + {system instanceof EffectParticleSystem && ( + +
Emit Power
+
+ + +
+
+ )} + + {renderBursts(system as any, onChange)} + + ); +} + +/** + * Combined emission properties component + * Includes both emitter shape and emission parameters + */ +export function EffectEditorEmissionProperties(props: IEffectEditorEmissionPropertiesProps): ReactNode { + const { nodeData, onChange } = props; + + if (nodeData.type !== "particle" || !nodeData.system) { + return null; + } + + return ( + <> + {renderEmitterShape(nodeData, onChange)} + + {renderEmissionParameters(nodeData, onChange)} + + ); +} diff --git a/editor/src/editor/windows/effect-editor/properties/emitter-shape.tsx b/editor/src/editor/windows/effect-editor/properties/emitter-shape.tsx deleted file mode 100644 index 0b98fd7de..000000000 --- a/editor/src/editor/windows/effect-editor/properties/emitter-shape.tsx +++ /dev/null @@ -1,266 +0,0 @@ -import { Component, ReactNode } from "react"; -import { Vector3 } from "babylonjs"; - -import { EditorInspectorVectorField } from "../../../layout/inspector/fields/vector"; -import { EditorInspectorNumberField } from "../../../layout/inspector/fields/number"; -import { EditorInspectorSwitchField } from "../../../layout/inspector/fields/switch"; -import { EditorInspectorListField } from "../../../layout/inspector/fields/list"; -import { EditorInspectorBlockField } from "../../../layout/inspector/fields/block"; - -import { type EffectNode, EffectSolidParticleSystem, EffectParticleSystem, SolidSphereParticleEmitter, SolidConeParticleEmitter } from "babylonjs-editor-tools"; - -export interface IEffectEditorEmitterShapePropertiesProps { - nodeData: EffectNode; - onChange: () => void; -} - -export class EffectEditorEmitterShapeProperties extends Component { - public render(): ReactNode { - const { nodeData, onChange } = this.props; - - if (nodeData.type !== "particle" || !nodeData.system) { - return null; - } - - const system = nodeData.system; - - // For VEffectSolidParticleSystem - if (system instanceof EffectSolidParticleSystem) { - return this._renderSolidParticleSystemEmitter(system, onChange); - } - - // For VEffectParticleSystem - if (system instanceof EffectParticleSystem) { - return this._renderParticleSystemEmitter(system, onChange); - } - - return null; - } - - private _renderSolidParticleSystemEmitter(system: EffectSolidParticleSystem, onChange: () => void): ReactNode { - const emitter = system.particleEmitterType; - const emitterType = emitter ? emitter.constructor.name : "Point"; - - // Map emitter class names to display names - const emitterTypeMap: Record = { - SolidPointParticleEmitter: "Point", - SolidSphereParticleEmitter: "Sphere", - SolidConeParticleEmitter: "Cone", - }; - - const currentType = emitterTypeMap[emitterType] || "Point"; - - // Available emitter types for SolidParticleSystem - const emitterTypes = [ - { text: "Point", value: "point" }, - { text: "Sphere", value: "sphere" }, - { text: "Cone", value: "cone" }, - ]; - - return ( - <> - ({ text: t.text, value: t.value }))} - onChange={(value) => { - // Save current properties - const currentRadius = emitter instanceof SolidSphereParticleEmitter || emitter instanceof SolidConeParticleEmitter ? emitter.radius : 1; - const currentArc = emitter instanceof SolidSphereParticleEmitter || emitter instanceof SolidConeParticleEmitter ? emitter.arc : Math.PI * 2; - const currentThickness = emitter instanceof SolidSphereParticleEmitter || emitter instanceof SolidConeParticleEmitter ? emitter.thickness : 1; - const currentAngle = emitter instanceof SolidConeParticleEmitter ? emitter.angle : Math.PI / 6; - - switch (value) { - case "point": - system.createPointEmitter(); - break; - case "sphere": - system.createSphereEmitter(currentRadius, currentArc, currentThickness); - break; - case "cone": - system.createConeEmitter(currentRadius, currentArc, currentThickness, currentAngle); - break; - } - onChange(); - }} - /> - - {emitter instanceof SolidSphereParticleEmitter && ( - <> - - - - - )} - - {emitter instanceof SolidConeParticleEmitter && ( - <> - - - - - - )} - - ); - } - - private _renderParticleSystemEmitter(system: EffectParticleSystem, onChange: () => void): ReactNode { - const emitter = system.particleEmitterType; - if (!emitter) { - return
No emitter found.
; - } - - const emitterType = emitter.getClassName(); - const emitterTypeMap: Record = { - PointParticleEmitter: "point", - BoxParticleEmitter: "box", - SphereParticleEmitter: "sphere", - SphereDirectedParticleEmitter: "sphere", - ConeParticleEmitter: "cone", - ConeDirectedParticleEmitter: "cone", - HemisphericParticleEmitter: "hemisphere", - CylinderParticleEmitter: "cylinder", - CylinderDirectedParticleEmitter: "cylinder", - }; - - const currentType = emitterTypeMap[emitterType] || "point"; - - // Available emitter types for ParticleSystem - const emitterTypes = [ - { text: "Point", value: "point" }, - { text: "Box", value: "box" }, - { text: "Sphere", value: "sphere" }, - { text: "Cone", value: "cone" }, - { text: "Hemisphere", value: "hemisphere" }, - { text: "Cylinder", value: "cylinder" }, - ]; - - return ( - <> - ({ text: t.text, value: t.value }))} - onChange={(value) => { - // Save current properties that might be common - const currentRadius = "radius" in emitter ? (emitter as any).radius : 1; - const currentAngle = "angle" in emitter ? (emitter as any).angle : Math.PI / 6; - const currentHeight = "height" in emitter ? (emitter as any).height : 1; - const currentDirection1 = "direction1" in emitter ? (emitter as any).direction1?.clone() : Vector3.Zero(); - const currentDirection2 = "direction2" in emitter ? (emitter as any).direction2?.clone() : Vector3.Zero(); - const currentMinEmitBox = "minEmitBox" in emitter ? (emitter as any).minEmitBox?.clone() : new Vector3(-0.5, -0.5, -0.5); - const currentMaxEmitBox = "maxEmitBox" in emitter ? (emitter as any).maxEmitBox?.clone() : new Vector3(0.5, 0.5, 0.5); - - switch (value) { - case "point": - system.createPointEmitter(currentDirection1, currentDirection2); - break; - case "box": - system.createBoxEmitter(currentDirection1, currentDirection2, currentMinEmitBox, currentMaxEmitBox); - break; - case "sphere": - system.createSphereEmitter(currentRadius); - break; - case "cone": - system.createConeEmitter(currentRadius, currentAngle); - break; - case "hemisphere": - system.createHemisphericEmitter(currentRadius); - break; - case "cylinder": - system.createCylinderEmitter(currentRadius, currentHeight); - break; - } - - onChange(); - }} - /> - - {emitterType === "BoxParticleEmitter" && ( - <> - -
Direction
- - -
- -
Emit Box
- - -
- - )} - - {(emitterType === "ConeParticleEmitter" || emitterType === "ConeDirectedParticleEmitter") && ( - <> - - - - - - - {emitterType === "ConeDirectedParticleEmitter" && ( - -
Direction
- - -
- )} - - )} - - {(emitterType === "CylinderParticleEmitter" || emitterType === "CylinderDirectedParticleEmitter") && ( - <> - - - - - - {emitterType === "CylinderDirectedParticleEmitter" && ( - -
Direction
- - -
- )} - - )} - - {(emitterType === "SphereParticleEmitter" || emitterType === "SphereDirectedParticleEmitter") && ( - <> - - - - - {emitterType === "SphereDirectedParticleEmitter" && ( - -
Direction
- - -
- )} - - )} - - {emitterType === "PointParticleEmitter" && ( - -
Direction
- - -
- )} - - {emitterType === "HemisphericParticleEmitter" && ( - <> - - - - - )} - - ); - } -} diff --git a/editor/src/editor/windows/effect-editor/properties/particle-initialization.tsx b/editor/src/editor/windows/effect-editor/properties/initialization.tsx similarity index 97% rename from editor/src/editor/windows/effect-editor/properties/particle-initialization.tsx rename to editor/src/editor/windows/effect-editor/properties/initialization.tsx index ea1518cfc..0192f615c 100644 --- a/editor/src/editor/windows/effect-editor/properties/particle-initialization.tsx +++ b/editor/src/editor/windows/effect-editor/properties/initialization.tsx @@ -3,9 +3,9 @@ import { ReactNode } from "react"; import { EditorInspectorBlockField } from "../../../layout/inspector/fields/block"; import { type EffectNode, EffectSolidParticleSystem, EffectParticleSystem, ValueUtils, Value, Color, Rotation } from "babylonjs-editor-tools"; -import { EffectValueEditor, type IVec3Function } from "./value-editor"; -import { EffectColorEditor } from "./color-editor"; -import { EffectRotationEditor } from "./rotation-editor"; +import { EffectValueEditor, type IVec3Function } from "../editors/value"; +import { EffectColorEditor } from "../editors/color"; +import { EffectRotationEditor } from "../editors/rotation"; export interface IEffectEditorParticleInitializationPropertiesProps { nodeData: EffectNode; diff --git a/editor/src/editor/windows/effect-editor/properties/object.tsx b/editor/src/editor/windows/effect-editor/properties/object.tsx index 1917f99a8..00b79ff3f 100644 --- a/editor/src/editor/windows/effect-editor/properties/object.tsx +++ b/editor/src/editor/windows/effect-editor/properties/object.tsx @@ -5,7 +5,7 @@ import { EditorInspectorStringField } from "../../../layout/inspector/fields/str import { EditorInspectorVectorField } from "../../../layout/inspector/fields/vector"; import { EditorInspectorSwitchField } from "../../../layout/inspector/fields/switch"; -import { type EffectNode, EffectSolidParticleSystem, EffectParticleSystem } from "babylonjs-editor-tools"; +import { type EffectNode, EffectSolidParticleSystem, EffectParticleSystem } from "babylonjs-editor-tools"; export interface IEffectEditorObjectPropertiesProps { nodeData: EffectNode; diff --git a/editor/src/editor/windows/effect-editor/properties/particle-renderer.tsx b/editor/src/editor/windows/effect-editor/properties/renderer.tsx similarity index 99% rename from editor/src/editor/windows/effect-editor/properties/particle-renderer.tsx rename to editor/src/editor/windows/effect-editor/properties/renderer.tsx index 9019ecd18..6658aeb59 100644 --- a/editor/src/editor/windows/effect-editor/properties/particle-renderer.tsx +++ b/editor/src/editor/windows/effect-editor/properties/renderer.tsx @@ -26,7 +26,7 @@ import { EditorGradientMaterialInspector } from "../../../layout/inspector/mater import { type EffectNode, EffectSolidParticleSystem, EffectParticleSystem } from "babylonjs-editor-tools"; import { IEffectEditor } from ".."; import { Mesh } from "babylonjs"; -import { EffectValueEditor } from "./value-editor"; +import { EffectValueEditor } from "../editors/value"; import { CellMaterial, FireMaterial, GradientMaterial, GridMaterial, LavaMaterial, NormalMaterial, SkyMaterial, TriPlanarMaterial, WaterMaterial } from "babylonjs-materials"; export interface IEffectEditorParticleRendererPropertiesProps { diff --git a/editor/src/editor/windows/effect-editor/properties/properties-tab.tsx b/editor/src/editor/windows/effect-editor/properties/tab.tsx similarity index 87% rename from editor/src/editor/windows/effect-editor/properties/properties-tab.tsx rename to editor/src/editor/windows/effect-editor/properties/tab.tsx index bbfeb73e8..a6e01c9d7 100644 --- a/editor/src/editor/windows/effect-editor/properties/properties-tab.tsx +++ b/editor/src/editor/windows/effect-editor/properties/tab.tsx @@ -2,17 +2,16 @@ import { Component, ReactNode } from "react"; import type { EffectNode } from "babylonjs-editor-tools"; import { IEffectEditor } from ".."; import { EffectEditorObjectProperties } from "./object"; -import { EffectEditorEmitterShapeProperties } from "./emitter-shape"; -import { EffectEditorParticleRendererProperties } from "./particle-renderer"; +import { EffectEditorParticleRendererProperties } from "./renderer"; import { EffectEditorEmissionProperties } from "./emission"; -import { EffectEditorParticleInitializationProperties } from "./particle-initialization"; +import { EffectEditorParticleInitializationProperties } from "./initialization"; import { EffectEditorBehaviorsProperties } from "./behaviors"; export interface IEffectEditorPropertiesTabProps { filePath: string | null; selectedNodeId: string | number | null; editor: IEffectEditor; - tabType: "object" | "emitter" | "renderer" | "emission" | "initialization" | "behaviors"; + tabType: "object" | "emission" | "renderer" | "initialization" | "behaviors"; onNameChanged?: () => void; getNodeData: (nodeId: string | number) => EffectNode | null; } @@ -72,10 +71,10 @@ export class EffectEditorPropertiesTab extends Component ); - case "emitter": + case "emission": return (
- +
); case "renderer": @@ -84,12 +83,6 @@ export class EffectEditorPropertiesTab extends Component ); - case "emission": - return ( -
- -
- ); case "initialization": return (
diff --git a/tools/src/effect/factories/emitterFactory.ts b/tools/src/effect/factories/emitterFactory.ts index 2821e5b16..bc10cb64e 100644 --- a/tools/src/effect/factories/emitterFactory.ts +++ b/tools/src/effect/factories/emitterFactory.ts @@ -1,5 +1,5 @@ import { Vector3, Matrix } from "babylonjs"; -import type { Shape } from "../types/shapes"; +import type { IShape } from "../types/shapes"; import type { EffectSolidParticleSystem } from "../systems/effectSolidParticleSystem"; import type { EffectParticleSystem } from "../systems/effectParticleSystem"; @@ -12,14 +12,14 @@ export class EmitterFactory { * Create emitter for ParticleSystem * Applies emitter shape to the particle system */ - public createParticleSystemEmitter(particleSystem: EffectParticleSystem, shape: Shape | undefined, cumulativeScale: Vector3, rotationMatrix: Matrix | null): void { + public createParticleSystemEmitter(particleSystem: EffectParticleSystem, shape: IShape | undefined, cumulativeScale: Vector3, rotationMatrix: Matrix | null): void { if (!shape || !shape.type) { this._createPointEmitter(particleSystem, Vector3.Zero(), Vector3.Zero()); return; } const shapeType = shape.type.toLowerCase(); - const shapeHandlers: Record void> = { + const shapeHandlers: Record void> = { cone: this._createConeEmitter.bind(this, particleSystem), sphere: this._createSphereEmitter.bind(this, particleSystem), point: this._createPointEmitter.bind(this, particleSystem), @@ -40,7 +40,7 @@ export class EmitterFactory { * Create emitter for SolidParticleSystem * Creates emitter using system's create*Emitter methods (similar to ParticleSystem) */ - public createSolidParticleSystemEmitter(sps: EffectSolidParticleSystem, shape: Shape | undefined): void { + public createSolidParticleSystemEmitter(sps: EffectSolidParticleSystem, shape: IShape | undefined): void { if (!shape || !shape.type) { sps.createPointEmitter(); return; @@ -84,7 +84,7 @@ export class EmitterFactory { /** * Creates cone emitter for ParticleSystem */ - private _createConeEmitter(particleSystem: EffectParticleSystem, shape: Shape, scale: Vector3, rotationMatrix: Matrix | null): void { + private _createConeEmitter(particleSystem: EffectParticleSystem, shape: IShape, scale: Vector3, rotationMatrix: Matrix | null): void { const radius = ((shape as any).radius || 1) * ((scale.x + scale.z) / 2); const angle = (shape as any).angle !== undefined ? (shape as any).angle : Math.PI / 4; const defaultDir = new Vector3(0, 1, 0); @@ -100,7 +100,7 @@ export class EmitterFactory { /** * Creates sphere emitter for ParticleSystem */ - private _createSphereEmitter(particleSystem: EffectParticleSystem, shape: Shape, scale: Vector3, rotationMatrix: Matrix | null): void { + private _createSphereEmitter(particleSystem: EffectParticleSystem, shape: IShape, scale: Vector3, rotationMatrix: Matrix | null): void { const radius = ((shape as any).radius || 1) * ((scale.x + scale.y + scale.z) / 3); const defaultDir = new Vector3(0, 1, 0); const rotatedDir = this._applyRotationToDirection(defaultDir, rotationMatrix); @@ -122,7 +122,7 @@ export class EmitterFactory { /** * Creates box emitter for ParticleSystem */ - private _createBoxEmitter(particleSystem: EffectParticleSystem, shape: Shape, scale: Vector3, rotationMatrix: Matrix | null): void { + private _createBoxEmitter(particleSystem: EffectParticleSystem, shape: IShape, scale: Vector3, rotationMatrix: Matrix | null): void { const boxSize = ((shape as any).size || [1, 1, 1]).map((s: number, i: number) => s * [scale.x, scale.y, scale.z][i]); const minBox = new Vector3(-boxSize[0] / 2, -boxSize[1] / 2, -boxSize[2] / 2); const maxBox = new Vector3(boxSize[0] / 2, boxSize[1] / 2, boxSize[2] / 2); @@ -139,7 +139,7 @@ export class EmitterFactory { /** * Creates hemisphere emitter for ParticleSystem */ - private _createHemisphereEmitter(particleSystem: EffectParticleSystem, shape: Shape, scale: Vector3, _rotationMatrix: Matrix | null): void { + private _createHemisphereEmitter(particleSystem: EffectParticleSystem, shape: IShape, scale: Vector3, _rotationMatrix: Matrix | null): void { const radius = ((shape as any).radius || 1) * ((scale.x + scale.y + scale.z) / 3); particleSystem.createHemisphericEmitter(radius); } @@ -147,7 +147,7 @@ export class EmitterFactory { /** * Creates cylinder emitter for ParticleSystem */ - private _createCylinderEmitter(particleSystem: EffectParticleSystem, shape: Shape, scale: Vector3, rotationMatrix: Matrix | null): void { + private _createCylinderEmitter(particleSystem: EffectParticleSystem, shape: IShape, scale: Vector3, rotationMatrix: Matrix | null): void { const radius = ((shape as any).radius || 1) * ((scale.x + scale.z) / 2); const height = ((shape as any).height || 1) * scale.y; const defaultDir = new Vector3(0, 1, 0); diff --git a/tools/src/effect/factories/geometryFactory.ts b/tools/src/effect/factories/geometryFactory.ts index c5305faed..4980b338f 100644 --- a/tools/src/effect/factories/geometryFactory.ts +++ b/tools/src/effect/factories/geometryFactory.ts @@ -2,7 +2,7 @@ import { Mesh, VertexData, CreatePlane, Nullable, Scene } from "babylonjs"; import type { IGeometryFactory } from "../types/factories"; import { Logger } from "../loggers/logger"; import type { Data } from "../types/hierarchy"; -import type { Geometry } from "../types/resources"; +import type { IGeometry } from "../types/resources"; import type { LoaderOptions } from "../types/loader"; /** @@ -86,7 +86,7 @@ export class GeometryFactory implements IGeometryFactory { /** * Finds geometry by UUID */ - private _findGeometry(geometryId: string): Geometry | null { + private _findGeometry(geometryId: string): IGeometry | null { if (!this._Data.geometries || this._Data.geometries.length === 0) { this._logger.warn("No geometries data available"); return null; @@ -104,10 +104,10 @@ export class GeometryFactory implements IGeometryFactory { /** * Creates mesh from geometry data based on type */ - private _createMeshFromGeometry(geometryData: Geometry, name: string, scene: Scene): Nullable { + private _createMeshFromGeometry(geometryData: IGeometry, name: string, scene: Scene): Nullable { this._logger.log(`createMeshFromGeometry: type=${geometryData.type}, name=${name}`); - const geometryTypeHandlers: Record Nullable> = { + const geometryTypeHandlers: Record Nullable> = { PlaneGeometry: (data, meshName, scene) => this._createPlaneGeometry(data, meshName, scene), BufferGeometry: (data, meshName, scene) => this._createBufferGeometry(data, meshName, scene), }; @@ -124,7 +124,7 @@ export class GeometryFactory implements IGeometryFactory { /** * Creates plane geometry mesh */ - private _createPlaneGeometry(geometryData: Geometry, name: string, scene: Scene): Nullable { + private _createPlaneGeometry(geometryData: IGeometry, name: string, scene: Scene): Nullable { const width = geometryData.width ?? 1; const height = geometryData.height ?? 1; @@ -143,7 +143,7 @@ export class GeometryFactory implements IGeometryFactory { /** * Creates buffer geometry mesh (already converted to left-handed) */ - private _createBufferGeometry(geometryData: Geometry, name: string, scene: Scene): Nullable { + private _createBufferGeometry(geometryData: IGeometry, name: string, scene: Scene): Nullable { if (!geometryData.data?.attributes) { this._logger.warn("BufferGeometry missing data or attributes"); return null; @@ -164,7 +164,7 @@ export class GeometryFactory implements IGeometryFactory { /** * Creates VertexData from BufferGeometry attributes (already converted to left-handed) */ - private _createVertexDataFromAttributes(geometryData: Geometry): Nullable { + private _createVertexDataFromAttributes(geometryData: IGeometry): Nullable { if (!geometryData.data?.attributes) { return null; } diff --git a/tools/src/effect/factories/materialFactory.ts b/tools/src/effect/factories/materialFactory.ts index 90a03664f..3454436e2 100644 --- a/tools/src/effect/factories/materialFactory.ts +++ b/tools/src/effect/factories/materialFactory.ts @@ -3,7 +3,7 @@ import type { IMaterialFactory } from "../types/factories"; import { Logger } from "../loggers/logger"; import type { LoaderOptions } from "../types/loader"; import type { Data } from "../types/hierarchy"; -import type { Material, Texture, Image } from "../types/resources"; +import type { IMaterial, ITexture, IImage } from "../types/resources"; /** * Factory for creating materials and textures from Three.js JSON data @@ -57,7 +57,7 @@ export class MaterialFactory implements IMaterialFactory { /** * Resolves material, texture, and image data from material ID */ - private _resolveTextureData(materialId: string): { material: Material; texture: Texture; image: Image } | null { + private _resolveTextureData(materialId: string): { material: IMaterial; texture: ITexture; image: IImage } | null { if (!this._hasRequiredData()) { this._logger.warn(`Missing materials/textures/images data for material ${materialId}`); return null; @@ -91,7 +91,7 @@ export class MaterialFactory implements IMaterialFactory { /** * Finds material by UUID */ - private _findMaterial(materialId: string): Material | null { + private _findMaterial(materialId: string): IMaterial | null { const material = this._data.materials?.find((m) => m.uuid === materialId); if (!material) { this._logger.warn(`Material not found: ${materialId}`); @@ -103,7 +103,7 @@ export class MaterialFactory implements IMaterialFactory { /** * Finds texture by UUID */ - private _findTexture(textureId: string): Texture | null { + private _findTexture(textureId: string): ITexture | null { const texture = this._data.textures?.find((t) => t.uuid === textureId); if (!texture) { this._logger.warn(`Texture not found: ${textureId}`); @@ -115,7 +115,7 @@ export class MaterialFactory implements IMaterialFactory { /** * Finds image by UUID */ - private _findImage(imageId: string): Image | null { + private _findImage(imageId: string): IImage | null { const image = this._data.images?.find((img) => img.uuid === imageId); if (!image) { this._logger.warn(`Image not found: ${imageId}`); @@ -127,7 +127,7 @@ export class MaterialFactory implements IMaterialFactory { /** * Builds texture URL from image data */ - private _buildTextureUrl(image: Image): string { + private _buildTextureUrl(image: IImage): string { if (!image.url) { return ""; } @@ -138,7 +138,7 @@ export class MaterialFactory implements IMaterialFactory { /** * Applies texture properties from texture data to Babylon.js texture */ - private _applyTextureProperties(babylonTexture: BabylonTexture, texture: Texture): void { + private _applyTextureProperties(babylonTexture: BabylonTexture, texture: ITexture): void { if (texture.wrapU !== undefined) { babylonTexture.wrapU = texture.wrapU; } @@ -168,7 +168,7 @@ export class MaterialFactory implements IMaterialFactory { /** * Creates Babylon.js texture from texture data */ - private _createTextureFromData(textureUrl: string, texture: Texture): BabylonTexture { + private _createTextureFromData(textureUrl: string, texture: ITexture): BabylonTexture { const samplingMode = texture.samplingMode ?? BabylonTexture.TRILINEAR_SAMPLINGMODE; const babylonTexture = new BabylonTexture(textureUrl, this._scene, { @@ -214,7 +214,7 @@ export class MaterialFactory implements IMaterialFactory { /** * Creates unlit material (MeshBasicMaterial equivalent) */ - private _createUnlitMaterial(name: string, material: Material, texture: BabylonTexture, color: Color3): PBRMaterial { + private _createUnlitMaterial(name: string, material: IMaterial, texture: BabylonTexture, color: Color3): PBRMaterial { const unlitMaterial = new PBRMaterial(name + "_material", this._scene); unlitMaterial.unlit = true; @@ -235,7 +235,7 @@ export class MaterialFactory implements IMaterialFactory { /** * Applies transparency settings to material */ - private _applyTransparency(material: PBRMaterial, Material: Material, texture: BabylonTexture): void { + private _applyTransparency(material: PBRMaterial, Material: IMaterial, texture: BabylonTexture): void { if (Material.transparent) { material.transparencyMode = BabylonMaterial.MATERIAL_ALPHABLEND; material.needDepthPrePass = false; @@ -251,7 +251,7 @@ export class MaterialFactory implements IMaterialFactory { /** * Applies depth write settings to material */ - private _applyDepthWrite(material: PBRMaterial, Material: Material): void { + private _applyDepthWrite(material: PBRMaterial, Material: IMaterial): void { if (Material.depthWrite !== undefined) { material.disableDepthWrite = !Material.depthWrite; this._logger.log(`Set disableDepthWrite: ${!Material.depthWrite}`); @@ -263,7 +263,7 @@ export class MaterialFactory implements IMaterialFactory { /** * Applies side orientation settings to material */ - private _applySideSettings(material: PBRMaterial, Material: Material): void { + private _applySideSettings(material: PBRMaterial, Material: IMaterial): void { material.backFaceCulling = false; if (Material.side !== undefined) { @@ -275,7 +275,7 @@ export class MaterialFactory implements IMaterialFactory { /** * Applies blend mode to material */ - private _applyBlendMode(material: PBRMaterial, Material: Material): void { + private _applyBlendMode(material: PBRMaterial, Material: IMaterial): void { if (Material.blending === undefined) { return; } diff --git a/tools/src/effect/index.ts b/tools/src/effect/index.ts index 2c961f588..02285d8d8 100644 --- a/tools/src/effect/index.ts +++ b/tools/src/effect/index.ts @@ -1,11 +1,9 @@ export * from "./types"; -export * from "./parsers/parser"; -export * from "./parsers/dataConverter"; +export * from "./parsers"; export * from "./factories"; -export * from "./utils/capacityCalculator"; -export * from "./utils/matrixUtils"; +export * from "./utils"; export * from "./systems"; -export * from "./loggers/logger"; export * from "./effect"; -export * from "./utils/valueParser"; export * from "./emitters"; +export * from "./behaviors"; +export * from "./loggers"; diff --git a/tools/src/effect/loggers/index.ts b/tools/src/effect/loggers/index.ts new file mode 100644 index 000000000..41c7bf273 --- /dev/null +++ b/tools/src/effect/loggers/index.ts @@ -0,0 +1 @@ +export * from "./logger"; diff --git a/tools/src/effect/parsers/dataConverter.ts b/tools/src/effect/parsers/dataConverter.ts index 3c69e8b10..13ec6a7f0 100644 --- a/tools/src/effect/parsers/dataConverter.ts +++ b/tools/src/effect/parsers/dataConverter.ts @@ -24,7 +24,7 @@ import type { QuarksOrbitOverLifeBehavior, } from "../types/quarksTypes"; import type { Transform, Group, Emitter, Data } from "../types/hierarchy"; -import type { Material, Texture, Image, Geometry, GeometryData } from "../types/resources"; +import type { IMaterial, ITexture, IImage, IGeometry, IGeometryData } from "../types/resources"; import type { EmitterConfig } from "../types/emitter"; import type { Behavior, @@ -40,7 +40,7 @@ import type { Value } from "../types/values"; import type { Color } from "../types/colors"; import type { Rotation } from "../types/rotations"; import type { GradientKey } from "../types/gradients"; -import type { Shape } from "../types/shapes"; +import type { IShape } from "../types/shapes"; import { Logger } from "../loggers/logger"; /** @@ -75,10 +75,10 @@ export class DataConverter { } // Convert all resources with error handling - let materials: Material[] = []; - let textures: Texture[] = []; - let images: Image[] = []; - let geometries: Geometry[] = []; + let materials: IMaterial[] = []; + let textures: ITexture[] = []; + let images: IImage[] = []; + let geometries: IGeometry[] = []; try { materials = this._convertMaterials(quarksData.materials || []); @@ -449,8 +449,8 @@ export class DataConverter { /** * Convert Quarks shape to shape */ - private _convertShape(quarksShape: QuarksShape): Shape { - const Shape: Shape = { + private _convertShape(quarksShape: QuarksShape): IShape { + const Shape: IShape = { type: quarksShape.type, radius: quarksShape.radius, arc: quarksShape.arc, @@ -650,9 +650,9 @@ export class DataConverter { /** * Convert Quarks materials to materials */ - private _convertMaterials(quarksMaterials: QuarksMaterial[]): Material[] { + private _convertMaterials(quarksMaterials: QuarksMaterial[]): IMaterial[] { return quarksMaterials.map((quarks) => { - const material: Material = { + const material: IMaterial = { uuid: quarks.uuid, type: quarks.type, transparent: quarks.transparent, @@ -687,9 +687,9 @@ export class DataConverter { /** * Convert Quarks textures to textures */ - private _convertTextures(quarksTextures: QuarksTexture[]): Texture[] { + private _convertTextures(quarksTextures: QuarksTexture[]): ITexture[] { return quarksTextures.map((quarks) => { - const texture: Texture = { + const texture: ITexture = { uuid: quarks.uuid, image: quarks.image, generateMipmaps: quarks.generateMipmaps, @@ -751,7 +751,7 @@ export class DataConverter { /** * Convert Quarks images to images (normalize URLs) */ - private _convertImages(quarksImages: QuarksImage[]): Image[] { + private _convertImages(quarksImages: QuarksImage[]): IImage[] { return quarksImages.map((quarks) => ({ uuid: quarks.uuid, url: quarks.url || "", @@ -774,13 +774,13 @@ export class DataConverter { return geometry; } else if (quarks.type === "BufferGeometry") { // BufferGeometry - convert attributes to left-handed - const geometry: Geometry = { + const geometry: IGeometry = { uuid: quarks.uuid, type: "BufferGeometry", }; if (quarks.data?.attributes) { - const attributes: GeometryData["attributes"] = {}; + const attributes: IGeometryData["attributes"] = {}; const quarksAttrs = quarks.data.attributes; // Convert position (right-hand → left-hand: flip Z) diff --git a/tools/src/effect/parsers/index.ts b/tools/src/effect/parsers/index.ts new file mode 100644 index 000000000..9d5a42bab --- /dev/null +++ b/tools/src/effect/parsers/index.ts @@ -0,0 +1,2 @@ +export * from "./parser"; +export * from "./dataConverter"; diff --git a/tools/src/effect/systems/effectParticleSystem.ts b/tools/src/effect/systems/effectParticleSystem.ts index 3bd94c9b6..b6ff87df7 100644 --- a/tools/src/effect/systems/effectParticleSystem.ts +++ b/tools/src/effect/systems/effectParticleSystem.ts @@ -17,7 +17,7 @@ import type { OrbitOverLifeBehavior, } from "../types/behaviors"; import type { Particle } from "babylonjs"; -import type { Shape } from "../types/shapes"; +import type { IShape } from "../types/shapes"; import type { EmitterConfig, EmissionBurst } from "../types/emitter"; import { ValueUtils } from "../utils/valueParser"; import { CapacityCalculator } from "../utils/capacityCalculator"; @@ -273,7 +273,7 @@ export class EffectParticleSystem extends ParticleSystem implements ISystem { options?: { texture?: Texture; blendMode?: number; - emitterShape?: { shape: Shape | undefined; cumulativeScale: Vector3; rotationMatrix: Matrix | null }; + emitterShape?: { shape: IShape | undefined; cumulativeScale: Vector3; rotationMatrix: Matrix | null }; } ): void { // Parse values diff --git a/tools/src/effect/systems/effectSolidParticleSystem.ts b/tools/src/effect/systems/effectSolidParticleSystem.ts index 0565ac552..45d8805a4 100644 --- a/tools/src/effect/systems/effectSolidParticleSystem.ts +++ b/tools/src/effect/systems/effectSolidParticleSystem.ts @@ -5,7 +5,7 @@ import { SolidPointParticleEmitter, SolidSphereParticleEmitter, SolidConeParticl import type { PerSolidParticleBehaviorFunction } from "../types/behaviors"; import type { ISystem, SolidParticleWithSystem } from "../types/system"; import type { Behavior, ForceOverLifeBehavior, ColorBySpeedBehavior, SizeBySpeedBehavior, RotationBySpeedBehavior, OrbitOverLifeBehavior } from "../types/behaviors"; -import type { Shape } from "../types/shapes"; +import type { IShape } from "../types/shapes"; import type { Color } from "../types/colors"; import type { Value } from "../types/values"; import type { Rotation } from "../types/rotations"; @@ -63,7 +63,7 @@ export class EffectSolidParticleSystem extends SolidParticleSystem implements IS public isLooping: boolean; public duration: number; public prewarm: boolean; - public shape?: Shape; + public shape?: IShape; public startLife?: Value; public startSpeed?: Value; public startRotation?: Rotation; diff --git a/tools/src/effect/types/context.ts b/tools/src/effect/types/context.ts deleted file mode 100644 index 1c07edb1c..000000000 --- a/tools/src/effect/types/context.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { Scene, TransformNode } from "babylonjs"; -import type { QuarksJSON } from "./quarksTypes"; -import type { Data } from "./hierarchy"; -import type { LoaderOptions } from "./loader"; - -/** - * Context for parsing operations - */ -export interface ParseContext { - scene: Scene; - rootUrl: string; - jsonData: QuarksJSON; - options: LoaderOptions; - groupNodesMap: Map; - Data?: Data; -} diff --git a/tools/src/effect/types/emitter.ts b/tools/src/effect/types/emitter.ts index b8e726356..cc044d535 100644 --- a/tools/src/effect/types/emitter.ts +++ b/tools/src/effect/types/emitter.ts @@ -3,7 +3,7 @@ import type { Emitter } from "./hierarchy"; import type { Value } from "./values"; import type { Color } from "./colors"; import type { Rotation } from "./rotations"; -import type { Shape } from "./shapes"; +import type { IShape } from "./shapes"; import type { Behavior } from "./behaviors"; /** @@ -23,7 +23,7 @@ export interface EmitterConfig { looping?: boolean; prewarm?: boolean; duration?: number; - shape?: Shape; + shape?: IShape; startLife?: Value; startSpeed?: Value; startRotation?: Rotation; diff --git a/tools/src/effect/types/hierarchy.ts b/tools/src/effect/types/hierarchy.ts index 2596e8ec9..254bfe36d 100644 --- a/tools/src/effect/types/hierarchy.ts +++ b/tools/src/effect/types/hierarchy.ts @@ -1,6 +1,6 @@ import { Vector3, Quaternion } from "babylonjs"; import type { EmitterConfig } from "./emitter"; -import type { Material, Texture, Image, Geometry } from "./resources"; +import type { IMaterial, ITexture, IImage, IGeometry } from "./resources"; /** * transform (converted from Quarks, left-handed coordinate system) @@ -44,8 +44,8 @@ export interface Data { groups: Map; emitters: Map; // Resources (converted from Quarks, ready for Babylon.js) - materials: Material[]; - textures: Texture[]; - images: Image[]; - geometries: Geometry[]; + materials: IMaterial[]; + textures: ITexture[]; + images: IImage[]; + geometries: IGeometry[]; } diff --git a/tools/src/effect/types/index.ts b/tools/src/effect/types/index.ts index 1da5384f7..4a9c51822 100644 --- a/tools/src/effect/types/index.ts +++ b/tools/src/effect/types/index.ts @@ -1,46 +1,13 @@ -/** - * Types - Centralized type definitions - * - * This module exports all -related types organized by category. - * Import types directly from their specific modules for better tree-shaking. - */ - -// Loader types -export type { LoaderOptions } from "./loader"; - -// Emitter types -export type { EmitterData } from "./emitter"; - -// Factory interfaces -export type { IMaterialFactory, IGeometryFactory } from "./factories"; - -// Core types -export type { ConstantValue, IntervalValue, Value } from "./values"; -export type { ConstantColor, Color } from "./colors"; -export type { EulerRotation, Rotation } from "./rotations"; -export type { GradientKey } from "./gradients"; -export type { Shape } from "./shapes"; -export type { - ColorOverLifeBehavior, - SizeOverLifeBehavior, - RotationOverLifeBehavior, - ForceOverLifeBehavior, - GravityForceBehavior, - SpeedOverLifeBehavior, - FrameOverLifeBehavior, - LimitSpeedOverLifeBehavior, - ColorBySpeedBehavior, - SizeBySpeedBehavior, - RotationBySpeedBehavior, - OrbitOverLifeBehavior, - Behavior, -} from "./behaviors"; -export type { EmissionBurst, EmitterConfig } from "./emitter"; -export type { Transform, Group, Emitter, Data } from "./hierarchy"; -export type { Material, Texture, Image, Geometry } from "./resources"; -export type { ISolidParticleEmitterType } from "./emitter"; -export { SolidPointParticleEmitter, SolidSphereParticleEmitter, SolidConeParticleEmitter } from "../emitters"; -export type { QuarksJSON } from "./quarksTypes"; -export type { PerParticleBehaviorFunction, PerSolidParticleBehaviorFunction, SystemBehaviorFunction } from "./behaviors"; -export type { ISystem, ParticleWithSystem, SolidParticleWithSystem } from "./system"; -export { isSystem } from "./system"; +export * from "./values"; +export * from "./colors"; +export * from "./rotations"; +export * from "./gradients"; +export * from "./shapes"; +export * from "./behaviors"; +export * from "./emitter"; +export * from "./system"; +export * from "./quarksTypes"; +export * from "./loader"; +export * from "./hierarchy"; +export * from "./resources"; +export * from "./factories"; diff --git a/tools/src/effect/types/resources.ts b/tools/src/effect/types/resources.ts index 7f4ead381..64810f8a1 100644 --- a/tools/src/effect/types/resources.ts +++ b/tools/src/effect/types/resources.ts @@ -3,7 +3,7 @@ import { Color3 } from "babylonjs"; /** * Material (converted from Quarks, ready for Babylon.js) */ -export interface Material { +export interface IMaterial { uuid: string; type?: string; color?: Color3; // Converted from hex/array to Color3 @@ -18,7 +18,7 @@ export interface Material { /** * Texture (converted from Quarks, ready for Babylon.js) */ -export interface Texture { +export interface ITexture { uuid: string; image?: string; // Image UUID reference wrapU?: number; // Converted to Babylon.js wrap mode @@ -37,7 +37,7 @@ export interface Texture { /** * Image (converted from Quarks, normalized URL) */ -export interface Image { +export interface IImage { uuid: string; url: string; // Normalized URL (ready for use) } @@ -45,7 +45,7 @@ export interface Image { /** * Geometry Attribute Data */ -export interface GeometryAttribute { +export interface IGeometryAttribute { array: number[]; itemSize?: number; } @@ -53,32 +53,32 @@ export interface GeometryAttribute { /** * Geometry Index Data */ -export interface GeometryIndex { +export interface IGeometryIndex { array: number[]; } /** * Geometry Data (converted from Quarks, left-handed coordinate system) */ -export interface GeometryData { +export interface IGeometryData { attributes: { - position?: GeometryAttribute; - normal?: GeometryAttribute; - uv?: GeometryAttribute; - color?: GeometryAttribute; + position?: IGeometryAttribute; + normal?: IGeometryAttribute; + uv?: IGeometryAttribute; + color?: IGeometryAttribute; }; - index?: GeometryIndex; + index?: IGeometryIndex; } /** * Geometry (converted from Quarks, ready for Babylon.js) */ -export interface Geometry { +export interface IGeometry { uuid: string; type: "PlaneGeometry" | "BufferGeometry"; // For PlaneGeometry width?: number; height?: number; // For BufferGeometry (already converted to left-handed) - data?: GeometryData; + data?: IGeometryData; } diff --git a/tools/src/effect/types/rotations.ts b/tools/src/effect/types/rotations.ts index fd54a3260..92b7a6a56 100644 --- a/tools/src/effect/types/rotations.ts +++ b/tools/src/effect/types/rotations.ts @@ -3,7 +3,7 @@ import type { Value } from "./values"; /** * rotation types (converted from Quarks) */ -export interface EulerRotation { +export interface IEulerRotation { type: "Euler"; angleX?: Value; angleY?: Value; @@ -11,7 +11,7 @@ export interface EulerRotation { order?: "xyz" | "zyx"; } -export interface AxisAngleRotation { +export interface IAxisAngleRotation { type: "AxisAngle"; x?: Value; y?: Value; @@ -19,8 +19,8 @@ export interface AxisAngleRotation { angle?: Value; } -export interface RandomQuatRotation { +export interface IRandomQuatRotation { type: "RandomQuat"; } -export type Rotation = EulerRotation | AxisAngleRotation | RandomQuatRotation | Value; +export type Rotation = IEulerRotation | IAxisAngleRotation | IRandomQuatRotation | Value; diff --git a/tools/src/effect/types/shapes.ts b/tools/src/effect/types/shapes.ts index 449ff2e8f..04232da8f 100644 --- a/tools/src/effect/types/shapes.ts +++ b/tools/src/effect/types/shapes.ts @@ -3,7 +3,7 @@ import type { Value } from "./values"; /** * shape configuration (converted from Quarks) */ -export interface Shape { +export interface IShape { type: string; radius?: number; arc?: number; diff --git a/tools/src/effect/types/values.ts b/tools/src/effect/types/values.ts index d8da55e4e..3b300e578 100644 --- a/tools/src/effect/types/values.ts +++ b/tools/src/effect/types/values.ts @@ -1,18 +1,18 @@ /** * value types (converted from Quarks) */ -export interface ConstantValue { +export interface IConstantValue { type: "ConstantValue"; value: number; } -export interface IntervalValue { +export interface IIntervalValue { type: "IntervalValue"; min: number; max: number; } -export interface PiecewiseBezier { +export interface IPiecewiseBezier { type: "PiecewiseBezier"; functions: Array<{ function: { @@ -24,4 +24,4 @@ export interface PiecewiseBezier { start: number; }>; } -export type Value = ConstantValue | IntervalValue | PiecewiseBezier | number; +export type Value = IConstantValue | IIntervalValue | IPiecewiseBezier | number; diff --git a/tools/src/effect/utils/capacityCalculator.ts b/tools/src/effect/utils/capacityCalculator.ts index df770dec0..1d88bc640 100644 --- a/tools/src/effect/utils/capacityCalculator.ts +++ b/tools/src/effect/utils/capacityCalculator.ts @@ -26,8 +26,7 @@ export class CapacityCalculator { if (isLooping) { return Math.max(Math.ceil(emissionRate * particleLifetime), 1); - } else { - return Math.ceil(emissionRate * particleLifetime * 2); } + return Math.ceil(emissionRate * particleLifetime * 2); } } diff --git a/tools/src/effect/utils/gradientSystem.ts b/tools/src/effect/utils/gradientSystem.ts index c844995cb..ec75d1c50 100644 --- a/tools/src/effect/utils/gradientSystem.ts +++ b/tools/src/effect/utils/gradientSystem.ts @@ -5,10 +5,10 @@ import { Color4 } from "babylonjs"; * Similar to Babylon.js native gradients but for SolidParticleSystem */ export class GradientSystem { - private gradients: Array<{ gradient: number; value: T }>; + private _gradients: Array<{ gradient: number; value: T }>; constructor() { - this.gradients = []; + this._gradients = []; } /** @@ -16,11 +16,11 @@ export class GradientSystem { */ public addGradient(gradient: number, value: T): void { // Insert in sorted order - const index = this.gradients.findIndex((g) => g.gradient > gradient); + const index = this._gradients.findIndex((g) => g.gradient > gradient); if (index === -1) { - this.gradients.push({ gradient, value }); + this._gradients.push({ gradient, value }); } else { - this.gradients.splice(index, 0, { gradient, value }); + this._gradients.splice(index, 0, { gradient, value }); } } @@ -28,21 +28,21 @@ export class GradientSystem { * Get interpolated value at given gradient position (0-1) */ public getValue(gradient: number): T | null { - if (this.gradients.length === 0) { + if (this._gradients.length === 0) { return null; } - if (this.gradients.length === 1) { - return this.gradients[0].value; + if (this._gradients.length === 1) { + return this._gradients[0].value; } // Clamp gradient to [0, 1] const clampedGradient = Math.max(0, Math.min(1, gradient)); // Find the two gradients to interpolate between - for (let i = 0; i < this.gradients.length - 1; i++) { - const g1 = this.gradients[i]; - const g2 = this.gradients[i + 1]; + for (let i = 0; i < this._gradients.length - 1; i++) { + const g1 = this._gradients[i]; + const g2 = this._gradients[i + 1]; if (clampedGradient >= g1.gradient && clampedGradient <= g2.gradient) { const t = g2.gradient - g1.gradient !== 0 ? (clampedGradient - g1.gradient) / (g2.gradient - g1.gradient) : 0; @@ -51,24 +51,24 @@ export class GradientSystem { } // Clamp to first or last gradient - if (clampedGradient <= this.gradients[0].gradient) { - return this.gradients[0].value; + if (clampedGradient <= this._gradients[0].gradient) { + return this._gradients[0].value; } - return this.gradients[this.gradients.length - 1].value; + return this._gradients[this._gradients.length - 1].value; } /** * Clear all gradients */ public clear(): void { - this.gradients = []; + this._gradients = []; } /** * Get all gradients (for debugging) */ public getGradients(): Array<{ gradient: number; value: T }> { - return [...this.gradients]; + return [...this._gradients]; } /** diff --git a/tools/src/effect/utils/index.ts b/tools/src/effect/utils/index.ts new file mode 100644 index 000000000..ff40dd376 --- /dev/null +++ b/tools/src/effect/utils/index.ts @@ -0,0 +1,4 @@ +export * from "./valueParser"; +export * from "./capacityCalculator"; +export * from "./matrixUtils"; +export * from "./gradientSystem"; diff --git a/tools/src/effect/utils/valueParser.ts b/tools/src/effect/utils/valueParser.ts index feaa99755..c0db7bd2c 100644 --- a/tools/src/effect/utils/valueParser.ts +++ b/tools/src/effect/utils/valueParser.ts @@ -1,5 +1,5 @@ import { Color4, ColorGradient } from "babylonjs"; -import type { PiecewiseBezier, Value } from "../types/values"; +import type { IPiecewiseBezier, Value } from "../types/values"; import type { Color } from "../types/colors"; import type { GradientKey } from "../types/gradients"; @@ -83,7 +83,7 @@ export class ValueUtils { /** * Evaluate PiecewiseBezier at normalized time t (0-1) */ - private static _evaluatePiecewiseBezier(bezier: PiecewiseBezier, t: number): number { + private static _evaluatePiecewiseBezier(bezier: IPiecewiseBezier, t: number): number { if (!bezier.functions || bezier.functions.length === 0) { return 0; } From 4a3116291116498a2c2bbf839bc94f25907f3fb9 Mon Sep 17 00:00:00 2001 From: Mikalai Lazitski Date: Tue, 16 Dec 2025 14:00:19 +0300 Subject: [PATCH 37/62] refactor: consolidate imports in geometry and renderer components, enhance gradient handling in gradient picker, and update type definitions for Quarks to improve code clarity and maintainability --- .../layout/inspector/fields/geometry.tsx | 3 +- .../layout/inspector/fields/gradient.tsx | 4 +- .../effect-editor/editors/color-function.tsx | 8 +- .../editor/windows/effect-editor/graph.tsx | 4 +- .../editor/windows/effect-editor/preview.tsx | 10 +- .../effect-editor/properties/renderer.tsx | 3 +- editor/src/ui/gradient-picker.tsx | 84 ++-- tools/src/effect/parsers/dataConverter.ts | 469 +++++++++--------- tools/src/effect/parsers/parser.ts | 9 +- .../effect/systems/effectParticleSystem.ts | 15 +- .../systems/effectSolidParticleSystem.ts | 31 +- tools/src/effect/types/quarksTypes.ts | 200 ++++---- 12 files changed, 435 insertions(+), 405 deletions(-) diff --git a/editor/src/editor/layout/inspector/fields/geometry.tsx b/editor/src/editor/layout/inspector/fields/geometry.tsx index 487acf71f..0edb73784 100644 --- a/editor/src/editor/layout/inspector/fields/geometry.tsx +++ b/editor/src/editor/layout/inspector/fields/geometry.tsx @@ -11,8 +11,7 @@ import { Scene, Mesh } from "babylonjs"; import { isScene } from "../../../../tools/guards/scene"; import { registerUndoRedo } from "../../../../tools/undoredo"; -import { configureImportedNodeIds } from "../../preview/import/import"; -import { loadImportedSceneFile } from "../../preview/import/import"; +import { configureImportedNodeIds, loadImportedSceneFile } from "../../preview/import/import"; import { EditorInspectorNumberField } from "./number"; import { isMesh } from "babylonjs-editor-tools"; diff --git a/editor/src/editor/layout/inspector/fields/gradient.tsx b/editor/src/editor/layout/inspector/fields/gradient.tsx index 2ce21d666..6df5c39cd 100644 --- a/editor/src/editor/layout/inspector/fields/gradient.tsx +++ b/editor/src/editor/layout/inspector/fields/gradient.tsx @@ -43,7 +43,9 @@ export function EditorInspectorColorGradientField(props: IEditorInspectorColorGr // Generate preview gradient CSS const generatePreview = (): string => { const sorted = [...value.colorKeys].sort((a, b) => (a.pos || 0) - (b.pos || 0)); - if (sorted.length === 0) return "linear-gradient(to right, rgba(0, 0, 0, 1) 0%, rgba(1, 1, 1, 1) 100%)"; + if (sorted.length === 0) { + return "linear-gradient(to right, rgba(0, 0, 0, 1) 0%, rgba(1, 1, 1, 1) 100%)"; + } const stops = sorted.map((key) => { const pos = (key.pos || 0) * 100; diff --git a/editor/src/editor/windows/effect-editor/editors/color-function.tsx b/editor/src/editor/windows/effect-editor/editors/color-function.tsx index be471f9ee..dcfa09ecf 100644 --- a/editor/src/editor/windows/effect-editor/editors/color-function.tsx +++ b/editor/src/editor/windows/effect-editor/editors/color-function.tsx @@ -221,8 +221,12 @@ export function ColorFunctionEditor(props: IColorFunctionEditorProps): ReactNode })); }; - if (!value.data.gradient1) value.data.gradient1 = {}; - if (!value.data.gradient2) value.data.gradient2 = {}; + if (!value.data.gradient1) { + value.data.gradient1 = {}; + } + if (!value.data.gradient2) { + value.data.gradient2 = {}; + } const wrapperGradient1 = { colorKeys: convertColorKeys(value.data.gradient1.colorKeys), diff --git a/editor/src/editor/windows/effect-editor/graph.tsx b/editor/src/editor/windows/effect-editor/graph.tsx index 7eefaedd7..27691d0aa 100644 --- a/editor/src/editor/windows/effect-editor/graph.tsx +++ b/editor/src/editor/windows/effect-editor/graph.tsx @@ -32,7 +32,7 @@ export interface IEffectEditorGraphState { selectedNodeId: string | number | null; } -interface EffectInfo { +interface IEffectInfo { id: string; name: string; effect: Effect; @@ -40,7 +40,7 @@ interface EffectInfo { } export class EffectEditorGraph extends Component { - private _effects: Map = new Map(); + private _effects: Map = new Map(); /** Map of node instances to unique IDs for tree nodes */ private _nodeIdMap: Map = new Map(); diff --git a/editor/src/editor/windows/effect-editor/preview.tsx b/editor/src/editor/windows/effect-editor/preview.tsx index 8b64f65ad..825b6c22f 100644 --- a/editor/src/editor/windows/effect-editor/preview.tsx +++ b/editor/src/editor/windows/effect-editor/preview.tsx @@ -282,13 +282,11 @@ export class EffectEditorPreview extends Component { + if (keys.length === 0) { + return new Color4(1, 1, 1, 1); + } + if (keys.length === 1) { + return getColorFromKey(keys[0]); + } + + for (let i = 0; i < keys.length - 1; i++) { + const key1 = keys[i]; + const key2 = keys[i + 1]; + const pos1 = key1.pos || 0; + const pos2 = key2.pos || 0; + + if (pos >= pos1 && pos <= pos2) { + const t = (pos - pos1) / (pos2 - pos1); + const color1 = getColorFromKey(key1); + const color2 = getColorFromKey(key2); + return new Color4( + color1.r + (color2.r - color1.r) * t, + color1.g + (color2.g - color1.g) * t, + color1.b + (color2.b - color1.b) * t, + color1.a + (color2.a - color1.a) * t + ); + } + } + + // Outside range, return nearest + if (pos <= (keys[0].pos || 0)) { + return getColorFromKey(keys[0]); + } + return getColorFromKey(keys[keys.length - 1]); + }; + // Handle click on gradient bar to add/select key const handleGradientClick = (e: MouseEvent, isAlpha: boolean = false) => { const rect = isAlpha ? alphaRef.current?.getBoundingClientRect() : gradientRef.current?.getBoundingClientRect(); - if (!rect) return; + if (!rect) { + return; + } const x = e.clientX - rect.left; const pos = Math.max(0, Math.min(1, x / rect.width)); @@ -112,35 +149,6 @@ export function GradientPicker(props: IGradientPickerProps): ReactNode { } }; - // Interpolate color at position - const interpolateColorAtPosition = (keys: IGradientKey[], pos: number): Color4 => { - if (keys.length === 0) return new Color4(1, 1, 1, 1); - if (keys.length === 1) return getColorFromKey(keys[0]); - - for (let i = 0; i < keys.length - 1; i++) { - const key1 = keys[i]; - const key2 = keys[i + 1]; - const pos1 = key1.pos || 0; - const pos2 = key2.pos || 0; - - if (pos >= pos1 && pos <= pos2) { - const t = (pos - pos1) / (pos2 - pos1); - const color1 = getColorFromKey(key1); - const color2 = getColorFromKey(key2); - return new Color4( - color1.r + (color2.r - color1.r) * t, - color1.g + (color2.g - color1.g) * t, - color1.b + (color2.b - color1.b) * t, - color1.a + (color2.a - color1.a) * t - ); - } - } - - // Outside range, return nearest - if (pos <= (keys[0].pos || 0)) return getColorFromKey(keys[0]); - return getColorFromKey(keys[keys.length - 1]); - }; - // Handle mouse down on key stop const handleKeyMouseDown = (e: MouseEvent, index: number, isAlpha: boolean) => { e.stopPropagation(); @@ -211,7 +219,9 @@ export function GradientPicker(props: IGradientPickerProps): ReactNode { // Handle color change for selected key const handleColorChange = (color: Color3 | Color4) => { - if (selectedKeyIndex === null) return; + if (selectedKeyIndex === null) { + return; + } const key = sortedColorKeys[selectedKeyIndex]; const originalIndex = colorKeys.findIndex((k) => k === key); @@ -227,7 +237,9 @@ export function GradientPicker(props: IGradientPickerProps): ReactNode { // Handle alpha change for selected alpha key const handleAlphaChange = (value: number) => { - if (selectedAlphaIndex === null) return; + if (selectedAlphaIndex === null) { + return; + } const key = sortedAlphaKeys[selectedAlphaIndex]; const originalIndex = alphaKeys.findIndex((k) => k === key); @@ -241,12 +253,16 @@ export function GradientPicker(props: IGradientPickerProps): ReactNode { // Handle delete key const handleDeleteKey = (index: number, isAlpha: boolean) => { if (isAlpha) { - if (alphaKeys.length <= 2) return; // Keep at least 2 keys + if (alphaKeys.length <= 2) { + return; // Keep at least 2 keys + } const newAlphaKeys = alphaKeys.filter((_, i) => i !== index); setSelectedAlphaIndex(null); onChange(colorKeys, newAlphaKeys); } else { - if (colorKeys.length <= 2) return; // Keep at least 2 keys + if (colorKeys.length <= 2) { + return; // Keep at least 2 keys + } const newColorKeys = colorKeys.filter((_, i) => i !== index); setSelectedKeyIndex(null); onChange(newColorKeys, alphaKeys); diff --git a/tools/src/effect/parsers/dataConverter.ts b/tools/src/effect/parsers/dataConverter.ts index 13ec6a7f0..141be6c5e 100644 --- a/tools/src/effect/parsers/dataConverter.ts +++ b/tools/src/effect/parsers/dataConverter.ts @@ -1,27 +1,31 @@ import { Vector3, Matrix, Quaternion, Color3, Texture as BabylonTexture, ParticleSystem } from "babylonjs"; import type { LoaderOptions } from "../types/loader"; -import type { QuarksJSON, QuarksMaterial, QuarksTexture, QuarksImage, QuarksGeometry } from "../types/quarksTypes"; import type { - QuarksObject, - QuarksParticleEmitterConfig, - QuarksBehavior, - QuarksValue, - QuarksColor, - QuarksRotation, - QuarksGradientKey, - QuarksShape, - QuarksColorOverLifeBehavior, - QuarksSizeOverLifeBehavior, - QuarksRotationOverLifeBehavior, - QuarksForceOverLifeBehavior, - QuarksGravityForceBehavior, - QuarksSpeedOverLifeBehavior, - QuarksFrameOverLifeBehavior, - QuarksLimitSpeedOverLifeBehavior, - QuarksColorBySpeedBehavior, - QuarksSizeBySpeedBehavior, - QuarksRotationBySpeedBehavior, - QuarksOrbitOverLifeBehavior, + IQuarksJSON, + IQuarksMaterial, + IQuarksTexture, + IQuarksImage, + IQuarksGeometry, + IQuarksObject, + IQuarksParticleEmitterConfig, + IQuarksBehavior, + IQuarksValue, + IQuarksColor, + IQuarksRotation, + IQuarksGradientKey, + IQuarksShape, + IQuarksColorOverLifeBehavior, + IQuarksSizeOverLifeBehavior, + IQuarksRotationOverLifeBehavior, + IQuarksForceOverLifeBehavior, + IQuarksGravityForceBehavior, + IQuarksSpeedOverLifeBehavior, + IQuarksFrameOverLifeBehavior, + IQuarksLimitSpeedOverLifeBehavior, + IQuarksColorBySpeedBehavior, + IQuarksSizeBySpeedBehavior, + IQuarksRotationBySpeedBehavior, + IQuarksOrbitOverLifeBehavior, } from "../types/quarksTypes"; import type { Transform, Group, Emitter, Data } from "../types/hierarchy"; import type { IMaterial, ITexture, IImage, IGeometry, IGeometryData } from "../types/resources"; @@ -44,7 +48,7 @@ import type { IShape } from "../types/shapes"; import { Logger } from "../loggers/logger"; /** - * Converts Quarks/Three.js JSON (right-handed) to Babylon.js format (left-handed) + * Converts IQuarks/Three.js JSON (right-handed) to Babylon.js format (left-handed) * All coordinate system conversions happen here, once */ export class DataConverter { @@ -55,11 +59,11 @@ export class DataConverter { } /** - * Convert Quarks/Three.js JSON to Babylon.js format + * Convert IQuarks/Three.js JSON to Babylon.js format * Handles errors gracefully and returns partial data if conversion fails */ - public convert(quarksData: QuarksJSON): Data { - this._logger.log("=== Converting Quarks to Babylon.js format ==="); + public convert(IQuarksData: IQuarksJSON): Data { + this._logger.log("=== Converting IQuarks to Babylon.js format ==="); const groups = new Map(); const emitters = new Map(); @@ -67,8 +71,8 @@ export class DataConverter { let root: Group | Emitter | null = null; try { - if (quarksData.object) { - root = this._convertObject(quarksData.object, null, groups, emitters, 0); + if (IQuarksData.object) { + root = this._convertObject(IQuarksData.object, null, groups, emitters, 0); } } catch (error) { this._logger.error(`Failed to convert root object: ${error instanceof Error ? error.message : String(error)}`); @@ -81,25 +85,25 @@ export class DataConverter { let geometries: IGeometry[] = []; try { - materials = this._convertMaterials(quarksData.materials || []); + materials = this._convertMaterials(IQuarksData.materials || []); } catch (error) { this._logger.error(`Failed to convert materials: ${error instanceof Error ? error.message : String(error)}`); } try { - textures = this._convertTextures(quarksData.textures || []); + textures = this._convertTextures(IQuarksData.textures || []); } catch (error) { this._logger.error(`Failed to convert textures: ${error instanceof Error ? error.message : String(error)}`); } try { - images = this._convertImages(quarksData.images || []); + images = this._convertImages(IQuarksData.images || []); } catch (error) { this._logger.error(`Failed to convert images: ${error instanceof Error ? error.message : String(error)}`); } try { - geometries = this._convertGeometries(quarksData.geometries || []); + geometries = this._convertGeometries(IQuarksData.geometries || []); } catch (error) { this._logger.error(`Failed to convert geometries: ${error instanceof Error ? error.message : String(error)}`); } @@ -120,9 +124,9 @@ export class DataConverter { } /** - * Convert a Quarks/Three.js object to Babylon.js format + * Convert a IQuarks/Three.js object to Babylon.js format */ - private _convertObject(obj: QuarksObject, parentUuid: string | null, groups: Map, emitters: Map, depth: number): Group | Emitter | null { + private _convertObject(obj: IQuarksObject, parentUuid: string | null, groups: Map, emitters: Map, depth: number): Group | Emitter | null { const indent = " ".repeat(depth); if (!obj || typeof obj !== "object") { @@ -162,7 +166,7 @@ export class DataConverter { this._logger.log(`${indent}Converted Group: ${group.name} (uuid: ${group.uuid})`); return group; } else if (obj.type === "ParticleEmitter" && obj.ps) { - // Convert emitter config from Quarks to format + // Convert emitter config from IQuarks to format const Config = this._convertEmitterConfig(obj.ps); const emitter: Emitter = { @@ -185,7 +189,7 @@ export class DataConverter { } /** - * Convert transform from Quarks/Three.js (right-handed) to Babylon.js (left-handed) + * Convert transform from IQuarks/Three.js (right-handed) to Babylon.js (left-handed) * This is the ONLY place where handedness conversion happens */ private _convertTransform(matrixArray?: number[], positionArray?: number[], rotationArray?: number[], scaleArray?: number[]): Transform { @@ -240,104 +244,104 @@ export class DataConverter { } /** - * Convert emitter config from Quarks to format + * Convert emitter config from IQuarks to format */ - private _convertEmitterConfig(quarksConfig: QuarksParticleEmitterConfig): EmitterConfig { + private _convertEmitterConfig(IQuarksConfig: IQuarksParticleEmitterConfig): EmitterConfig { // Determine system type based on renderMode: 2 = solid, otherwise base - const systemType: "solid" | "base" = quarksConfig.renderMode === 2 ? "solid" : "base"; + const systemType: "solid" | "base" = IQuarksConfig.renderMode === 2 ? "solid" : "base"; const Config: EmitterConfig = { - version: quarksConfig.version, - autoDestroy: quarksConfig.autoDestroy, - looping: quarksConfig.looping, - prewarm: quarksConfig.prewarm, - duration: quarksConfig.duration, - onlyUsedByOther: quarksConfig.onlyUsedByOther, - instancingGeometry: quarksConfig.instancingGeometry, - renderOrder: quarksConfig.renderOrder, + version: IQuarksConfig.version, + autoDestroy: IQuarksConfig.autoDestroy, + looping: IQuarksConfig.looping, + prewarm: IQuarksConfig.prewarm, + duration: IQuarksConfig.duration, + onlyUsedByOther: IQuarksConfig.onlyUsedByOther, + instancingGeometry: IQuarksConfig.instancingGeometry, + renderOrder: IQuarksConfig.renderOrder, systemType, - rendererEmitterSettings: quarksConfig.rendererEmitterSettings, - material: quarksConfig.material, - layers: quarksConfig.layers, - uTileCount: quarksConfig.uTileCount, - vTileCount: quarksConfig.vTileCount, - blendTiles: quarksConfig.blendTiles, - softParticles: quarksConfig.softParticles, - softFarFade: quarksConfig.softFarFade, - softNearFade: quarksConfig.softNearFade, - worldSpace: quarksConfig.worldSpace, + rendererEmitterSettings: IQuarksConfig.rendererEmitterSettings, + material: IQuarksConfig.material, + layers: IQuarksConfig.layers, + uTileCount: IQuarksConfig.uTileCount, + vTileCount: IQuarksConfig.vTileCount, + blendTiles: IQuarksConfig.blendTiles, + softParticles: IQuarksConfig.softParticles, + softFarFade: IQuarksConfig.softFarFade, + softNearFade: IQuarksConfig.softNearFade, + worldSpace: IQuarksConfig.worldSpace, }; // Convert values - if (quarksConfig.startLife !== undefined) { - Config.startLife = this._convertValue(quarksConfig.startLife); + if (IQuarksConfig.startLife !== undefined) { + Config.startLife = this._convertValue(IQuarksConfig.startLife); } - if (quarksConfig.startSpeed !== undefined) { - Config.startSpeed = this._convertValue(quarksConfig.startSpeed); + if (IQuarksConfig.startSpeed !== undefined) { + Config.startSpeed = this._convertValue(IQuarksConfig.startSpeed); } - if (quarksConfig.startRotation !== undefined) { - Config.startRotation = this._convertRotation(quarksConfig.startRotation); + if (IQuarksConfig.startRotation !== undefined) { + Config.startRotation = this._convertRotation(IQuarksConfig.startRotation); } - if (quarksConfig.startSize !== undefined) { - Config.startSize = this._convertValue(quarksConfig.startSize); + if (IQuarksConfig.startSize !== undefined) { + Config.startSize = this._convertValue(IQuarksConfig.startSize); } - if (quarksConfig.startColor !== undefined) { - Config.startColor = this._convertColor(quarksConfig.startColor); + if (IQuarksConfig.startColor !== undefined) { + Config.startColor = this._convertColor(IQuarksConfig.startColor); } - if (quarksConfig.emissionOverTime !== undefined) { - Config.emissionOverTime = this._convertValue(quarksConfig.emissionOverTime); + if (IQuarksConfig.emissionOverTime !== undefined) { + Config.emissionOverTime = this._convertValue(IQuarksConfig.emissionOverTime); } - if (quarksConfig.emissionOverDistance !== undefined) { - Config.emissionOverDistance = this._convertValue(quarksConfig.emissionOverDistance); + if (IQuarksConfig.emissionOverDistance !== undefined) { + Config.emissionOverDistance = this._convertValue(IQuarksConfig.emissionOverDistance); } - if (quarksConfig.startTileIndex !== undefined) { - Config.startTileIndex = this._convertValue(quarksConfig.startTileIndex); + if (IQuarksConfig.startTileIndex !== undefined) { + Config.startTileIndex = this._convertValue(IQuarksConfig.startTileIndex); } // Convert shape - if (quarksConfig.shape !== undefined) { - Config.shape = this._convertShape(quarksConfig.shape); + if (IQuarksConfig.shape !== undefined) { + Config.shape = this._convertShape(IQuarksConfig.shape); } // Convert emission bursts - if (quarksConfig.emissionBursts !== undefined && Array.isArray(quarksConfig.emissionBursts)) { - Config.emissionBursts = quarksConfig.emissionBursts.map((burst) => ({ + if (IQuarksConfig.emissionBursts !== undefined && Array.isArray(IQuarksConfig.emissionBursts)) { + Config.emissionBursts = IQuarksConfig.emissionBursts.map((burst) => ({ time: this._convertValue(burst.time), count: this._convertValue(burst.count), })); } // Convert behaviors - if (quarksConfig.behaviors !== undefined && Array.isArray(quarksConfig.behaviors)) { - Config.behaviors = quarksConfig.behaviors.map((behavior) => this._convertBehavior(behavior)); + if (IQuarksConfig.behaviors !== undefined && Array.isArray(IQuarksConfig.behaviors)) { + Config.behaviors = IQuarksConfig.behaviors.map((behavior) => this._convertBehavior(behavior)); } // Convert renderMode to systemType, billboardMode and isBillboardBased - // Quarks RenderMode: + // IQuarks RenderMode: // 0 = BillBoard → systemType = "base", isBillboardBased = true, billboardMode = ALL (default) // 1 = StretchedBillBoard → systemType = "base", isBillboardBased = true, billboardMode = STRETCHED // 2 = Mesh → systemType = "solid", isBillboardBased = false (always) // 3 = Trail → systemType = "base", isBillboardBased = true, billboardMode = ALL (not directly supported, treat as billboard) // 4 = HorizontalBillBoard → systemType = "base", isBillboardBased = true, billboardMode = Y // 5 = VerticalBillBoard → systemType = "base", isBillboardBased = true, billboardMode = Y (same as horizontal) - if (quarksConfig.renderMode !== undefined) { - if (quarksConfig.renderMode === 0) { + if (IQuarksConfig.renderMode !== undefined) { + if (IQuarksConfig.renderMode === 0) { // BillBoard Config.isBillboardBased = true; Config.billboardMode = ParticleSystem.BILLBOARDMODE_ALL; - } else if (quarksConfig.renderMode === 1) { + } else if (IQuarksConfig.renderMode === 1) { // StretchedBillBoard Config.isBillboardBased = true; Config.billboardMode = ParticleSystem.BILLBOARDMODE_STRETCHED; - } else if (quarksConfig.renderMode === 2) { + } else if (IQuarksConfig.renderMode === 2) { // Mesh (SolidParticleSystem) - always false Config.isBillboardBased = false; // billboardMode not applicable for mesh - } else if (quarksConfig.renderMode === 3) { + } else if (IQuarksConfig.renderMode === 3) { // Trail - not directly supported, treat as billboard Config.isBillboardBased = true; Config.billboardMode = ParticleSystem.BILLBOARDMODE_ALL; - } else if (quarksConfig.renderMode === 4 || quarksConfig.renderMode === 5) { + } else if (IQuarksConfig.renderMode === 4 || IQuarksConfig.renderMode === 5) { // HorizontalBillBoard or VerticalBillBoard Config.isBillboardBased = true; Config.billboardMode = ParticleSystem.BILLBOARDMODE_Y; @@ -356,54 +360,54 @@ export class DataConverter { } /** - * Convert Quarks value to value + * Convert IQuarks value to value */ - private _convertValue(quarksValue: QuarksValue): Value { - if (typeof quarksValue === "number") { - return quarksValue; + private _convertValue(IQuarksValue: IQuarksValue): Value { + if (typeof IQuarksValue === "number") { + return IQuarksValue; } - if (quarksValue.type === "ConstantValue") { + if (IQuarksValue.type === "ConstantValue") { return { type: "ConstantValue", - value: quarksValue.value, + value: IQuarksValue.value, }; } - if (quarksValue.type === "IntervalValue") { + if (IQuarksValue.type === "IntervalValue") { return { type: "IntervalValue", - min: quarksValue.a ?? 0, - max: quarksValue.b ?? 0, + min: IQuarksValue.a ?? 0, + max: IQuarksValue.b ?? 0, }; } - if (quarksValue.type === "PiecewiseBezier") { + if (IQuarksValue.type === "PiecewiseBezier") { return { type: "PiecewiseBezier", - functions: quarksValue.functions.map((f) => ({ + functions: IQuarksValue.functions.map((f) => ({ function: f.function, start: f.start, })), }; } - return quarksValue; + return IQuarksValue; } /** - * Convert Quarks color to color + * Convert IQuarks color to color */ - private _convertColor(quarksColor: QuarksColor): Color { - if (typeof quarksColor === "string" || Array.isArray(quarksColor)) { - return quarksColor; + private _convertColor(IQuarksColor: IQuarksColor): Color { + if (typeof IQuarksColor === "string" || Array.isArray(IQuarksColor)) { + return IQuarksColor; } - if (quarksColor.type === "ConstantColor") { - if (quarksColor.value && Array.isArray(quarksColor.value)) { + if (IQuarksColor.type === "ConstantColor") { + if (IQuarksColor.value && Array.isArray(IQuarksColor.value)) { return { type: "ConstantColor", - value: quarksColor.value, + value: IQuarksColor.value, }; - } else if (quarksColor.color) { + } else if (IQuarksColor.color) { return { type: "ConstantColor", - value: [quarksColor.color.r || 0, quarksColor.color.g || 0, quarksColor.color.b || 0, quarksColor.color.a !== undefined ? quarksColor.color.a : 1], + value: [IQuarksColor.color.r || 0, IQuarksColor.color.g || 0, IQuarksColor.color.b || 0, IQuarksColor.color.a !== undefined ? IQuarksColor.color.a : 1], }; } else { // Fallback: return default color if neither value nor color is present @@ -413,67 +417,70 @@ export class DataConverter { }; } } - return quarksColor as Color; + return IQuarksColor as Color; } /** - * Convert Quarks rotation to rotation + * Convert IQuarks rotation to rotation */ - private _convertRotation(quarksRotation: QuarksRotation): Rotation { - if (typeof quarksRotation === "number" || (typeof quarksRotation === "object" && quarksRotation !== null && "type" in quarksRotation && quarksRotation.type !== "Euler")) { - return this._convertValue(quarksRotation as QuarksValue); + private _convertRotation(IQuarksRotation: IQuarksRotation): Rotation { + if ( + typeof IQuarksRotation === "number" || + (typeof IQuarksRotation === "object" && IQuarksRotation !== null && "type" in IQuarksRotation && IQuarksRotation.type !== "Euler") + ) { + return this._convertValue(IQuarksRotation as IQuarksValue); } - if (typeof quarksRotation === "object" && quarksRotation !== null && "type" in quarksRotation && quarksRotation.type === "Euler") { + if (typeof IQuarksRotation === "object" && IQuarksRotation !== null && "type" in IQuarksRotation && IQuarksRotation.type === "Euler") { return { type: "Euler", - angleX: quarksRotation.angleX !== undefined ? this._convertValue(quarksRotation.angleX) : undefined, - angleY: quarksRotation.angleY !== undefined ? this._convertValue(quarksRotation.angleY) : undefined, - angleZ: quarksRotation.angleZ !== undefined ? this._convertValue(quarksRotation.angleZ) : undefined, - order: (quarksRotation as any).order || "xyz", // Default to xyz if not specified + angleX: IQuarksRotation.angleX !== undefined ? this._convertValue(IQuarksRotation.angleX) : undefined, + angleY: IQuarksRotation.angleY !== undefined ? this._convertValue(IQuarksRotation.angleY) : undefined, + angleZ: IQuarksRotation.angleZ !== undefined ? this._convertValue(IQuarksRotation.angleZ) : undefined, + order: (IQuarksRotation as any).order || "xyz", // Default to xyz if not specified }; } - return this._convertValue(quarksRotation as QuarksValue); + return this._convertValue(IQuarksRotation as IQuarksValue); } /** - * Convert Quarks gradient key to gradient key + * Convert IQuarks gradient key to gradient key */ - private _convertGradientKey(quarksKey: QuarksGradientKey): GradientKey { + private _convertGradientKey(IQuarksKey: IQuarksGradientKey): GradientKey { return { - time: quarksKey.time, - value: quarksKey.value, - pos: quarksKey.pos, + time: IQuarksKey.time, + value: IQuarksKey.value, + pos: IQuarksKey.pos, }; } /** - * Convert Quarks shape to shape + * Convert IQuarks shape to shape */ - private _convertShape(quarksShape: QuarksShape): IShape { + private _convertShape(IQuarksShape: IQuarksShape): IShape { const Shape: IShape = { - type: quarksShape.type, - radius: quarksShape.radius, - arc: quarksShape.arc, - thickness: quarksShape.thickness, - angle: quarksShape.angle, - mode: quarksShape.mode, - spread: quarksShape.spread, - size: quarksShape.size, - height: quarksShape.height, + type: IQuarksShape.type, + radius: IQuarksShape.radius, + arc: IQuarksShape.arc, + thickness: IQuarksShape.thickness, + angle: IQuarksShape.angle, + mode: IQuarksShape.mode, + spread: IQuarksShape.spread, + size: IQuarksShape.size, + height: IQuarksShape.height, }; - if (quarksShape.speed !== undefined) { - Shape.speed = this._convertValue(quarksShape.speed); + if (IQuarksShape.speed !== undefined) { + Shape.speed = this._convertValue(IQuarksShape.speed); } return Shape; } /** - * Convert Quarks behavior to behavior + * Convert IQuarks behavior to behavior */ - private _convertBehavior(quarksBehavior: QuarksBehavior): Behavior { - switch (quarksBehavior.type) { + private _convertBehavior(IQuarksBehavior: IQuarksBehavior): Behavior { + switch (IQuarksBehavior.type) { case "ColorOverLife": { - const behavior = quarksBehavior as QuarksColorOverLifeBehavior; + const behavior = IQuarksBehavior as IQuarksColorOverLifeBehavior; if (behavior.color) { const Color: ColorOverLifeBehavior["color"] = {}; if (behavior.color.color?.keys) { @@ -491,11 +498,11 @@ export class DataConverter { } case "SizeOverLife": { - const behavior = quarksBehavior as QuarksSizeOverLifeBehavior; + const behavior = IQuarksBehavior as IQuarksSizeOverLifeBehavior; if (behavior.size) { const Size: SizeOverLifeBehavior["size"] = {}; if (behavior.size.keys) { - Size.keys = behavior.size.keys.map((k: QuarksGradientKey) => this._convertGradientKey(k)); + Size.keys = behavior.size.keys.map((k: IQuarksGradientKey) => this._convertGradientKey(k)); } if (behavior.size.functions) { Size.functions = behavior.size.functions; @@ -507,7 +514,7 @@ export class DataConverter { case "RotationOverLife": case "Rotation3DOverLife": { - const behavior = quarksBehavior as QuarksRotationOverLifeBehavior; + const behavior = IQuarksBehavior as IQuarksRotationOverLifeBehavior; return { type: behavior.type, angularVelocity: behavior.angularVelocity !== undefined ? this._convertValue(behavior.angularVelocity) : undefined, @@ -516,7 +523,7 @@ export class DataConverter { case "ForceOverLife": case "ApplyForce": { - const behavior = quarksBehavior as QuarksForceOverLifeBehavior; + const behavior = IQuarksBehavior as IQuarksForceOverLifeBehavior; const Behavior: ForceOverLifeBehavior = { type: behavior.type }; if (behavior.force) { Behavior.force = { @@ -532,7 +539,7 @@ export class DataConverter { } case "GravityForce": { - const behavior = quarksBehavior as QuarksGravityForceBehavior; + const behavior = IQuarksBehavior as IQuarksGravityForceBehavior; const Behavior: { type: string; gravity?: Value } = { type: "GravityForce", gravity: behavior.gravity !== undefined ? this._convertValue(behavior.gravity) : undefined, @@ -541,50 +548,50 @@ export class DataConverter { } case "SpeedOverLife": { - const behavior = quarksBehavior as QuarksSpeedOverLifeBehavior; + const behavior = IQuarksBehavior as IQuarksSpeedOverLifeBehavior; if (behavior.speed) { if (typeof behavior.speed === "object" && behavior.speed !== null && "keys" in behavior.speed) { const Speed: SpeedOverLifeBehavior["speed"] = {}; if (behavior.speed.keys) { - Speed.keys = behavior.speed.keys.map((k: QuarksGradientKey) => this._convertGradientKey(k)); + Speed.keys = behavior.speed.keys.map((k: IQuarksGradientKey) => this._convertGradientKey(k)); } if (behavior.speed.functions) { Speed.functions = behavior.speed.functions; } return { type: "SpeedOverLife", speed: Speed }; } else if (typeof behavior.speed === "number" || (typeof behavior.speed === "object" && behavior.speed !== null && "type" in behavior.speed)) { - return { type: "SpeedOverLife", speed: this._convertValue(behavior.speed as QuarksValue) }; + return { type: "SpeedOverLife", speed: this._convertValue(behavior.speed as IQuarksValue) }; } } return { type: "SpeedOverLife" }; } case "FrameOverLife": { - const behavior = quarksBehavior as QuarksFrameOverLifeBehavior; + const behavior = IQuarksBehavior as IQuarksFrameOverLifeBehavior; const Behavior: { type: string; frame?: Value | { keys?: GradientKey[] } } = { type: "FrameOverLife" }; if (behavior.frame) { if (typeof behavior.frame === "object" && behavior.frame !== null && "keys" in behavior.frame) { Behavior.frame = { - keys: behavior.frame.keys?.map((k: QuarksGradientKey) => this._convertGradientKey(k)), + keys: behavior.frame.keys?.map((k: IQuarksGradientKey) => this._convertGradientKey(k)), }; } else if (typeof behavior.frame === "number" || (typeof behavior.frame === "object" && behavior.frame !== null && "type" in behavior.frame)) { - Behavior.frame = this._convertValue(behavior.frame as QuarksValue); + Behavior.frame = this._convertValue(behavior.frame as IQuarksValue); } } return Behavior as Behavior; } case "LimitSpeedOverLife": { - const behavior = quarksBehavior as QuarksLimitSpeedOverLifeBehavior; + const behavior = IQuarksBehavior as IQuarksLimitSpeedOverLifeBehavior; const Behavior: LimitSpeedOverLifeBehavior = { type: "LimitSpeedOverLife" }; if (behavior.maxSpeed !== undefined) { Behavior.maxSpeed = this._convertValue(behavior.maxSpeed); } if (behavior.speed !== undefined) { if (typeof behavior.speed === "object" && behavior.speed !== null && "keys" in behavior.speed) { - Behavior.speed = { keys: behavior.speed.keys?.map((k: QuarksGradientKey) => this._convertGradientKey(k)) }; + Behavior.speed = { keys: behavior.speed.keys?.map((k: IQuarksGradientKey) => this._convertGradientKey(k)) }; } else if (typeof behavior.speed === "number" || (typeof behavior.speed === "object" && behavior.speed !== null && "type" in behavior.speed)) { - Behavior.speed = this._convertValue(behavior.speed as QuarksValue); + Behavior.speed = this._convertValue(behavior.speed as IQuarksValue); } } if (behavior.dampen !== undefined) { @@ -594,33 +601,33 @@ export class DataConverter { } case "ColorBySpeed": { - const behavior = quarksBehavior as QuarksColorBySpeedBehavior; + const behavior = IQuarksBehavior as IQuarksColorBySpeedBehavior; const Behavior: ColorBySpeedBehavior = { type: "ColorBySpeed", minSpeed: behavior.minSpeed !== undefined ? this._convertValue(behavior.minSpeed) : undefined, maxSpeed: behavior.maxSpeed !== undefined ? this._convertValue(behavior.maxSpeed) : undefined, }; if (behavior.color?.keys) { - Behavior.color = { keys: behavior.color.keys.map((k: QuarksGradientKey) => this._convertGradientKey(k)) }; + Behavior.color = { keys: behavior.color.keys.map((k: IQuarksGradientKey) => this._convertGradientKey(k)) }; } return Behavior; } case "SizeBySpeed": { - const behavior = quarksBehavior as QuarksSizeBySpeedBehavior; + const behavior = IQuarksBehavior as IQuarksSizeBySpeedBehavior; const Behavior: SizeBySpeedBehavior = { type: "SizeBySpeed", minSpeed: behavior.minSpeed !== undefined ? this._convertValue(behavior.minSpeed) : undefined, maxSpeed: behavior.maxSpeed !== undefined ? this._convertValue(behavior.maxSpeed) : undefined, }; if (behavior.size?.keys) { - Behavior.size = { keys: behavior.size.keys.map((k: QuarksGradientKey) => this._convertGradientKey(k)) }; + Behavior.size = { keys: behavior.size.keys.map((k: IQuarksGradientKey) => this._convertGradientKey(k)) }; } return Behavior; } case "RotationBySpeed": { - const behavior = quarksBehavior as QuarksRotationBySpeedBehavior; + const behavior = IQuarksBehavior as IQuarksRotationBySpeedBehavior; const Behavior: { type: string; angularVelocity?: Value; minSpeed?: Value; maxSpeed?: Value } = { type: "RotationBySpeed", angularVelocity: behavior.angularVelocity !== undefined ? this._convertValue(behavior.angularVelocity) : undefined, @@ -631,7 +638,7 @@ export class DataConverter { } case "OrbitOverLife": { - const behavior = quarksBehavior as QuarksOrbitOverLifeBehavior; + const behavior = IQuarksBehavior as IQuarksOrbitOverLifeBehavior; const Behavior: { type: string; center?: { x?: number; y?: number; z?: number }; radius?: Value; speed?: Value } = { type: "OrbitOverLife", center: behavior.center, @@ -643,27 +650,27 @@ export class DataConverter { default: // Fallback for unknown behaviors - copy as-is - return quarksBehavior as Behavior; + return IQuarksBehavior as Behavior; } } /** - * Convert Quarks materials to materials + * Convert IQuarks materials to materials */ - private _convertMaterials(quarksMaterials: QuarksMaterial[]): IMaterial[] { - return quarksMaterials.map((quarks) => { + private _convertMaterials(IQuarksMaterials: IQuarksMaterial[]): IMaterial[] { + return IQuarksMaterials.map((IQuarks) => { const material: IMaterial = { - uuid: quarks.uuid, - type: quarks.type, - transparent: quarks.transparent, - depthWrite: quarks.depthWrite, - side: quarks.side, - map: quarks.map, + uuid: IQuarks.uuid, + type: IQuarks.type, + transparent: IQuarks.transparent, + depthWrite: IQuarks.depthWrite, + side: IQuarks.side, + map: IQuarks.map, }; // Convert color from hex to Color3 - if (quarks.color !== undefined) { - const colorHex = typeof quarks.color === "number" ? quarks.color : parseInt(String(quarks.color).replace("#", ""), 16) || 0xffffff; + if (IQuarks.color !== undefined) { + const colorHex = typeof IQuarks.color === "number" ? IQuarks.color : parseInt(String(IQuarks.color).replace("#", ""), 16) || 0xffffff; const r = ((colorHex >> 16) & 0xff) / 255; const g = ((colorHex >> 8) & 0xff) / 255; const b = (colorHex & 0xff) / 255; @@ -671,13 +678,13 @@ export class DataConverter { } // Convert blending mode (Three.js → Babylon.js) - if (quarks.blending !== undefined) { + if (IQuarks.blending !== undefined) { const blendModeMap: Record = { 0: 0, // NoBlending → ALPHA_DISABLE 1: 1, // NormalBlending → ALPHA_COMBINE 2: 2, // AdditiveBlending → ALPHA_ADD }; - material.blending = blendModeMap[quarks.blending] ?? quarks.blending; + material.blending = blendModeMap[IQuarks.blending] ?? IQuarks.blending; } return material; @@ -685,61 +692,61 @@ export class DataConverter { } /** - * Convert Quarks textures to textures + * Convert IQuarks textures to textures */ - private _convertTextures(quarksTextures: QuarksTexture[]): ITexture[] { - return quarksTextures.map((quarks) => { + private _convertTextures(IQuarksTextures: IQuarksTexture[]): ITexture[] { + return IQuarksTextures.map((IQuarks) => { const texture: ITexture = { - uuid: quarks.uuid, - image: quarks.image, - generateMipmaps: quarks.generateMipmaps, - flipY: quarks.flipY, + uuid: IQuarks.uuid, + image: IQuarks.image, + generateMipmaps: IQuarks.generateMipmaps, + flipY: IQuarks.flipY, }; // Convert wrap mode (Three.js → Babylon.js) - if (quarks.wrap && Array.isArray(quarks.wrap)) { + if (IQuarks.wrap && Array.isArray(IQuarks.wrap)) { const wrapModeMap: Record = { 1000: BabylonTexture.WRAP_ADDRESSMODE, // RepeatWrapping 1001: BabylonTexture.CLAMP_ADDRESSMODE, // ClampToEdgeWrapping 1002: BabylonTexture.MIRROR_ADDRESSMODE, // MirroredRepeatWrapping }; - texture.wrapU = wrapModeMap[quarks.wrap[0]] ?? BabylonTexture.WRAP_ADDRESSMODE; - texture.wrapV = wrapModeMap[quarks.wrap[1]] ?? BabylonTexture.WRAP_ADDRESSMODE; + texture.wrapU = wrapModeMap[IQuarks.wrap[0]] ?? BabylonTexture.WRAP_ADDRESSMODE; + texture.wrapV = wrapModeMap[IQuarks.wrap[1]] ?? BabylonTexture.WRAP_ADDRESSMODE; } // Convert repeat to scale - if (quarks.repeat && Array.isArray(quarks.repeat)) { - texture.uScale = quarks.repeat[0] || 1; - texture.vScale = quarks.repeat[1] || 1; + if (IQuarks.repeat && Array.isArray(IQuarks.repeat)) { + texture.uScale = IQuarks.repeat[0] || 1; + texture.vScale = IQuarks.repeat[1] || 1; } // Convert offset - if (quarks.offset && Array.isArray(quarks.offset)) { - texture.uOffset = quarks.offset[0] || 0; - texture.vOffset = quarks.offset[1] || 0; + if (IQuarks.offset && Array.isArray(IQuarks.offset)) { + texture.uOffset = IQuarks.offset[0] || 0; + texture.vOffset = IQuarks.offset[1] || 0; } // Convert rotation - if (quarks.rotation !== undefined) { - texture.uAng = quarks.rotation; + if (IQuarks.rotation !== undefined) { + texture.uAng = IQuarks.rotation; } // Convert channel - if (typeof quarks.channel === "number") { - texture.coordinatesIndex = quarks.channel; + if (typeof IQuarks.channel === "number") { + texture.coordinatesIndex = IQuarks.channel; } // Convert sampling mode (Three.js filters → Babylon.js sampling mode) - if (quarks.minFilter !== undefined) { - if (quarks.minFilter === 1008 || quarks.minFilter === 1009) { + if (IQuarks.minFilter !== undefined) { + if (IQuarks.minFilter === 1008 || IQuarks.minFilter === 1009) { texture.samplingMode = BabylonTexture.TRILINEAR_SAMPLINGMODE; - } else if (quarks.minFilter === 1007 || quarks.minFilter === 1006) { + } else if (IQuarks.minFilter === 1007 || IQuarks.minFilter === 1006) { texture.samplingMode = BabylonTexture.BILINEAR_SAMPLINGMODE; } else { texture.samplingMode = BabylonTexture.NEAREST_SAMPLINGMODE; } - } else if (quarks.magFilter !== undefined) { - texture.samplingMode = quarks.magFilter === 1006 ? BabylonTexture.BILINEAR_SAMPLINGMODE : BabylonTexture.NEAREST_SAMPLINGMODE; + } else if (IQuarks.magFilter !== undefined) { + texture.samplingMode = IQuarks.magFilter === 1006 ? BabylonTexture.BILINEAR_SAMPLINGMODE : BabylonTexture.NEAREST_SAMPLINGMODE; } else { texture.samplingMode = BabylonTexture.TRILINEAR_SAMPLINGMODE; } @@ -749,77 +756,77 @@ export class DataConverter { } /** - * Convert Quarks images to images (normalize URLs) + * Convert IQuarks images to images (normalize URLs) */ - private _convertImages(quarksImages: QuarksImage[]): IImage[] { - return quarksImages.map((quarks) => ({ - uuid: quarks.uuid, - url: quarks.url || "", + private _convertImages(IQuarksImages: IQuarksImage[]): IImage[] { + return IQuarksImages.map((IQuarks) => ({ + uuid: IQuarks.uuid, + url: IQuarks.url || "", })); } /** - * Convert Quarks geometries to geometries (convert to left-handed) + * Convert IQuarks geometries to geometries (convert to left-handed) */ - private _convertGeometries(quarksGeometries: QuarksGeometry[]): Geometry[] { - return quarksGeometries.map((quarks) => { - if (quarks.type === "PlaneGeometry") { + private _convertGeometries(IQuarksGeometries: IQuarksGeometry[]): IGeometry[] { + return IQuarksGeometries.map((IQuarks) => { + if (IQuarks.type === "PlaneGeometry") { // PlaneGeometry - simple properties - const geometry: Geometry = { - uuid: quarks.uuid, + const geometry: IGeometry = { + uuid: IQuarks.uuid, type: "PlaneGeometry", - width: (quarks as any).width ?? 1, - height: (quarks as any).height ?? 1, + width: (IQuarks as any).width ?? 1, + height: (IQuarks as any).height ?? 1, }; return geometry; - } else if (quarks.type === "BufferGeometry") { + } else if (IQuarks.type === "BufferGeometry") { // BufferGeometry - convert attributes to left-handed const geometry: IGeometry = { - uuid: quarks.uuid, + uuid: IQuarks.uuid, type: "BufferGeometry", }; - if (quarks.data?.attributes) { + if (IQuarks.data?.attributes) { const attributes: IGeometryData["attributes"] = {}; - const quarksAttrs = quarks.data.attributes; + const IQuarksAttrs = IQuarks.data.attributes; // Convert position (right-hand → left-hand: flip Z) - if (quarksAttrs.position) { - const positions = Array.from(quarksAttrs.position.array); + if (IQuarksAttrs.position) { + const positions = Array.from(IQuarksAttrs.position.array); // Flip Z coordinate for left-handed system for (let i = 2; i < positions.length; i += 3) { positions[i] = -positions[i]; } attributes.position = { array: positions, - itemSize: quarksAttrs.position.itemSize, + itemSize: IQuarksAttrs.position.itemSize, }; } // Convert normal (right-hand → left-hand: flip Z) - if (quarksAttrs.normal) { - const normals = Array.from(quarksAttrs.normal.array); + if (IQuarksAttrs.normal) { + const normals = Array.from(IQuarksAttrs.normal.array); for (let i = 2; i < normals.length; i += 3) { normals[i] = -normals[i]; } attributes.normal = { array: normals, - itemSize: quarksAttrs.normal.itemSize, + itemSize: IQuarksAttrs.normal.itemSize, }; } // UV and color - no conversion needed - if (quarksAttrs.uv) { + if (IQuarksAttrs.uv) { attributes.uv = { - array: Array.from(quarksAttrs.uv.array), - itemSize: quarksAttrs.uv.itemSize, + array: Array.from(IQuarksAttrs.uv.array), + itemSize: IQuarksAttrs.uv.itemSize, }; } - if (quarksAttrs.color) { + if (IQuarksAttrs.color) { attributes.color = { - array: Array.from(quarksAttrs.color.array), - itemSize: quarksAttrs.color.itemSize, + array: Array.from(IQuarksAttrs.color.array), + itemSize: IQuarksAttrs.color.itemSize, }; } @@ -828,8 +835,8 @@ export class DataConverter { }; // Convert indices (reverse winding order for left-handed) - if (quarks.data.index) { - const indices = Array.from(quarks.data.index.array); + if (IQuarks.data.index) { + const indices = Array.from(IQuarks.data.index.array); // Reverse winding: swap every 2nd and 3rd index in each triangle for (let i = 0; i < indices.length; i += 3) { const temp = indices[i + 1]; @@ -847,8 +854,8 @@ export class DataConverter { // Unknown geometry type - return as-is return { - uuid: quarks.uuid, - type: quarks.type as "PlaneGeometry" | "BufferGeometry", + uuid: IQuarks.uuid, + type: IQuarks.type as "PlaneGeometry" | "BufferGeometry", }; }); } diff --git a/tools/src/effect/parsers/parser.ts b/tools/src/effect/parsers/parser.ts index d9766d329..5c3893ce3 100644 --- a/tools/src/effect/parsers/parser.ts +++ b/tools/src/effect/parsers/parser.ts @@ -1,12 +1,9 @@ import { Scene, TransformNode } from "babylonjs"; -import type { QuarksJSON } from "../types/quarksTypes"; -import type { LoaderOptions } from "../types/loader"; -import type { Data } from "../types/hierarchy"; +import type { IQuarksJSON, LoaderOptions, Data } from "../types"; import { Logger } from "../loggers/logger"; import { MaterialFactory, GeometryFactory, SystemFactory } from "../factories"; import { DataConverter } from "./dataConverter"; -import type { EffectParticleSystem } from "../systems/effectParticleSystem"; -import type { EffectSolidParticleSystem } from "../systems/effectSolidParticleSystem"; +import type { EffectParticleSystem, EffectSolidParticleSystem } from "../systems"; /** * Result of parsing JSON @@ -33,7 +30,7 @@ export class Parser { private _groupNodesMap: Map; private _options: LoaderOptions; - constructor(scene: Scene, rootUrl: string, jsonData: QuarksJSON, options?: LoaderOptions) { + constructor(scene: Scene, rootUrl: string, jsonData: IQuarksJSON, options?: LoaderOptions) { const opts = options || {}; this._options = opts; this._groupNodesMap = new Map(); diff --git a/tools/src/effect/systems/effectParticleSystem.ts b/tools/src/effect/systems/effectParticleSystem.ts index b6ff87df7..d19d0ad0a 100644 --- a/tools/src/effect/systems/effectParticleSystem.ts +++ b/tools/src/effect/systems/effectParticleSystem.ts @@ -1,6 +1,4 @@ -import { Color4, ParticleSystem, Scene, Vector3, Matrix, Texture, AbstractMesh, TransformNode } from "babylonjs"; -import type { PerParticleBehaviorFunction } from "../types/behaviors"; -import type { ISystem, ParticleWithSystem } from "../types/system"; +import { Color4, ParticleSystem, Scene, Vector3, Matrix, Texture, AbstractMesh, TransformNode, Particle } from "babylonjs"; import type { Behavior, ColorOverLifeBehavior, @@ -15,10 +13,13 @@ import type { SizeBySpeedBehavior, RotationBySpeedBehavior, OrbitOverLifeBehavior, -} from "../types/behaviors"; -import type { Particle } from "babylonjs"; -import type { IShape } from "../types/shapes"; -import type { EmitterConfig, EmissionBurst } from "../types/emitter"; + PerParticleBehaviorFunction, + ISystem, + ParticleWithSystem, + IShape, + EmitterConfig, + EmissionBurst, +} from "../types"; import { ValueUtils } from "../utils/valueParser"; import { CapacityCalculator } from "../utils/capacityCalculator"; import { diff --git a/tools/src/effect/systems/effectSolidParticleSystem.ts b/tools/src/effect/systems/effectSolidParticleSystem.ts index 45d8805a4..bbf9875f0 100644 --- a/tools/src/effect/systems/effectSolidParticleSystem.ts +++ b/tools/src/effect/systems/effectSolidParticleSystem.ts @@ -1,17 +1,24 @@ import { Vector3, Quaternion, Matrix, Color4, SolidParticle, TransformNode, Mesh, AbstractMesh, SolidParticleSystem } from "babylonjs"; -import type { EmitterConfig, EmissionBurst } from "../types/emitter"; -import type { ISolidParticleEmitterType } from "../types/emitter"; +import type { + Behavior, + ForceOverLifeBehavior, + ColorBySpeedBehavior, + SizeBySpeedBehavior, + RotationBySpeedBehavior, + OrbitOverLifeBehavior, + EmitterConfig, + EmissionBurst, + ISolidParticleEmitterType, + PerSolidParticleBehaviorFunction, + ISystem, + SolidParticleWithSystem, + IShape, + Color, + Value, + Rotation, +} from "../types"; import { SolidPointParticleEmitter, SolidSphereParticleEmitter, SolidConeParticleEmitter } from "../emitters"; -import type { PerSolidParticleBehaviorFunction } from "../types/behaviors"; -import type { ISystem, SolidParticleWithSystem } from "../types/system"; -import type { Behavior, ForceOverLifeBehavior, ColorBySpeedBehavior, SizeBySpeedBehavior, RotationBySpeedBehavior, OrbitOverLifeBehavior } from "../types/behaviors"; -import type { IShape } from "../types/shapes"; -import type { Color } from "../types/colors"; -import type { Value } from "../types/values"; -import type { Rotation } from "../types/rotations"; -import { ValueUtils } from "../utils/valueParser"; -import { CapacityCalculator } from "../utils/capacityCalculator"; -import { ColorGradientSystem, NumberGradientSystem } from "../utils/gradientSystem"; +import { ValueUtils, CapacityCalculator, ColorGradientSystem, NumberGradientSystem } from "../utils"; import { applyColorBySpeedSPS, applySizeBySpeedSPS, diff --git a/tools/src/effect/types/quarksTypes.ts b/tools/src/effect/types/quarksTypes.ts index 6a26cf613..308b8f8b4 100644 --- a/tools/src/effect/types/quarksTypes.ts +++ b/tools/src/effect/types/quarksTypes.ts @@ -6,18 +6,18 @@ /** * Quarks/Three.js value types */ -export interface QuarksConstantValue { +export interface IQuarksConstantValue { type: "ConstantValue"; value: number; } -export interface QuarksIntervalValue { +export interface IQuarksIntervalValue { type: "IntervalValue"; a: number; // min b: number; // max } -export interface QuarksPiecewiseBezier { +export interface IQuarksPiecewiseBezier { type: "PiecewiseBezier"; functions: Array<{ function: { @@ -30,12 +30,12 @@ export interface QuarksPiecewiseBezier { }>; } -export type QuarksValue = QuarksConstantValue | QuarksIntervalValue | QuarksPiecewiseBezier | number; +export type IQuarksValue = IQuarksConstantValue | IQuarksIntervalValue | IQuarksPiecewiseBezier | number; /** * Quarks/Three.js color types */ -export interface QuarksConstantColor { +export interface IQuarksConstantColor { type: "ConstantColor"; color?: { r: number; @@ -46,24 +46,24 @@ export interface QuarksConstantColor { value?: [number, number, number, number]; // RGBA array alternative } -export type QuarksColor = QuarksConstantColor | [number, number, number, number] | string; +export type IQuarksColor = IQuarksConstantColor | [number, number, number, number] | string; /** * Quarks/Three.js rotation types */ -export interface QuarksEulerRotation { +export interface IQuarksEulerRotation { type: "Euler"; - angleX?: QuarksValue; - angleY?: QuarksValue; - angleZ?: QuarksValue; + angleX?: IQuarksValue; + angleY?: IQuarksValue; + angleZ?: IQuarksValue; } -export type QuarksRotation = QuarksEulerRotation | QuarksValue; +export type IQuarksRotation = IQuarksEulerRotation | IQuarksValue; /** * Quarks/Three.js gradient key */ -export interface QuarksGradientKey { +export interface IQuarksGradientKey { time?: number; value: number | [number, number, number, number] | { r: number; g: number; b: number; a?: number }; pos?: number; @@ -72,7 +72,7 @@ export interface QuarksGradientKey { /** * Quarks/Three.js shape configuration */ -export interface QuarksShape { +export interface IQuarksShape { type: string; radius?: number; arc?: number; @@ -80,7 +80,7 @@ export interface QuarksShape { angle?: number; mode?: number; spread?: number; - speed?: QuarksValue; + speed?: IQuarksValue; size?: number[]; height?: number; } @@ -88,31 +88,31 @@ export interface QuarksShape { /** * Quarks/Three.js emission burst */ -export interface QuarksEmissionBurst { - time: QuarksValue; - count: QuarksValue; +export interface IQuarksEmissionBurst { + time: IQuarksValue; + count: IQuarksValue; } /** * Quarks/Three.js behavior types */ -export interface QuarksColorOverLifeBehavior { +export interface IQuarksColorOverLifeBehavior { type: "ColorOverLife"; color?: { color?: { - keys: QuarksGradientKey[]; + keys: IQuarksGradientKey[]; }; alpha?: { - keys: QuarksGradientKey[]; + keys: IQuarksGradientKey[]; }; - keys?: QuarksGradientKey[]; + keys?: IQuarksGradientKey[]; }; } -export interface QuarksSizeOverLifeBehavior { +export interface IQuarksSizeOverLifeBehavior { type: "SizeOverLife"; size?: { - keys?: QuarksGradientKey[]; + keys?: IQuarksGradientKey[]; functions?: Array<{ start: number; function: { @@ -123,33 +123,33 @@ export interface QuarksSizeOverLifeBehavior { }; } -export interface QuarksRotationOverLifeBehavior { +export interface IQuarksRotationOverLifeBehavior { type: "RotationOverLife" | "Rotation3DOverLife"; - angularVelocity?: QuarksValue; + angularVelocity?: IQuarksValue; } -export interface QuarksForceOverLifeBehavior { +export interface IQuarksForceOverLifeBehavior { type: "ForceOverLife" | "ApplyForce"; force?: { - x?: QuarksValue; - y?: QuarksValue; - z?: QuarksValue; + x?: IQuarksValue; + y?: IQuarksValue; + z?: IQuarksValue; }; - x?: QuarksValue; - y?: QuarksValue; - z?: QuarksValue; + x?: IQuarksValue; + y?: IQuarksValue; + z?: IQuarksValue; } -export interface QuarksGravityForceBehavior { +export interface IQuarksGravityForceBehavior { type: "GravityForce"; - gravity?: QuarksValue; + gravity?: IQuarksValue; } -export interface QuarksSpeedOverLifeBehavior { +export interface IQuarksSpeedOverLifeBehavior { type: "SpeedOverLife"; speed?: | { - keys?: QuarksGradientKey[]; + keys?: IQuarksGradientKey[]; functions?: Array<{ start: number; function: { @@ -158,94 +158,94 @@ export interface QuarksSpeedOverLifeBehavior { }; }>; } - | QuarksValue; + | IQuarksValue; } -export interface QuarksFrameOverLifeBehavior { +export interface IQuarksFrameOverLifeBehavior { type: "FrameOverLife"; frame?: | { - keys?: QuarksGradientKey[]; + keys?: IQuarksGradientKey[]; } - | QuarksValue; + | IQuarksValue; } -export interface QuarksLimitSpeedOverLifeBehavior { +export interface IQuarksLimitSpeedOverLifeBehavior { type: "LimitSpeedOverLife"; - maxSpeed?: QuarksValue; - speed?: QuarksValue | { keys?: QuarksGradientKey[] }; - dampen?: QuarksValue; + maxSpeed?: IQuarksValue; + speed?: IQuarksValue | { keys?: IQuarksGradientKey[] }; + dampen?: IQuarksValue; } -export interface QuarksColorBySpeedBehavior { +export interface IQuarksColorBySpeedBehavior { type: "ColorBySpeed"; color?: { - keys: QuarksGradientKey[]; + keys: IQuarksGradientKey[]; }; - minSpeed?: QuarksValue; - maxSpeed?: QuarksValue; + minSpeed?: IQuarksValue; + maxSpeed?: IQuarksValue; } -export interface QuarksSizeBySpeedBehavior { +export interface IQuarksSizeBySpeedBehavior { type: "SizeBySpeed"; size?: { - keys: QuarksGradientKey[]; + keys: IQuarksGradientKey[]; }; - minSpeed?: QuarksValue; - maxSpeed?: QuarksValue; + minSpeed?: IQuarksValue; + maxSpeed?: IQuarksValue; } -export interface QuarksRotationBySpeedBehavior { +export interface IQuarksRotationBySpeedBehavior { type: "RotationBySpeed"; - angularVelocity?: QuarksValue; - minSpeed?: QuarksValue; - maxSpeed?: QuarksValue; + angularVelocity?: IQuarksValue; + minSpeed?: IQuarksValue; + maxSpeed?: IQuarksValue; } -export interface QuarksOrbitOverLifeBehavior { +export interface IQuarksOrbitOverLifeBehavior { type: "OrbitOverLife"; center?: { x?: number; y?: number; z?: number; }; - radius?: QuarksValue; - speed?: QuarksValue; + radius?: IQuarksValue; + speed?: IQuarksValue; } -export type QuarksBehavior = - | QuarksColorOverLifeBehavior - | QuarksSizeOverLifeBehavior - | QuarksRotationOverLifeBehavior - | QuarksForceOverLifeBehavior - | QuarksGravityForceBehavior - | QuarksSpeedOverLifeBehavior - | QuarksFrameOverLifeBehavior - | QuarksLimitSpeedOverLifeBehavior - | QuarksColorBySpeedBehavior - | QuarksSizeBySpeedBehavior - | QuarksRotationBySpeedBehavior - | QuarksOrbitOverLifeBehavior +export type IQuarksBehavior = + | IQuarksColorOverLifeBehavior + | IQuarksSizeOverLifeBehavior + | IQuarksRotationOverLifeBehavior + | IQuarksForceOverLifeBehavior + | IQuarksGravityForceBehavior + | IQuarksSpeedOverLifeBehavior + | IQuarksFrameOverLifeBehavior + | IQuarksLimitSpeedOverLifeBehavior + | IQuarksColorBySpeedBehavior + | IQuarksSizeBySpeedBehavior + | IQuarksRotationBySpeedBehavior + | IQuarksOrbitOverLifeBehavior | { type: string; [key: string]: unknown }; // Fallback for unknown behaviors /** * Quarks/Three.js particle emitter configuration */ -export interface QuarksParticleEmitterConfig { +export interface IQuarksParticleEmitterConfig { version?: string; autoDestroy?: boolean; looping?: boolean; prewarm?: boolean; duration?: number; - shape?: QuarksShape; - startLife?: QuarksValue; - startSpeed?: QuarksValue; - startRotation?: QuarksRotation; - startSize?: QuarksValue; - startColor?: QuarksColor; - emissionOverTime?: QuarksValue; - emissionOverDistance?: QuarksValue; - emissionBursts?: QuarksEmissionBurst[]; + shape?: IQuarksShape; + startLife?: IQuarksValue; + startSpeed?: IQuarksValue; + startRotation?: IQuarksRotation; + startSize?: IQuarksValue; + startColor?: IQuarksColor; + emissionOverTime?: IQuarksValue; + emissionOverDistance?: IQuarksValue; + emissionBursts?: IQuarksEmissionBurst[]; onlyUsedByOther?: boolean; instancingGeometry?: string; renderOrder?: number; @@ -253,21 +253,21 @@ export interface QuarksParticleEmitterConfig { rendererEmitterSettings?: Record; material?: string; layers?: number; - startTileIndex?: QuarksValue; + startTileIndex?: IQuarksValue; uTileCount?: number; vTileCount?: number; blendTiles?: boolean; softParticles?: boolean; softFarFade?: number; softNearFade?: number; - behaviors?: QuarksBehavior[]; + behaviors?: IQuarksBehavior[]; worldSpace?: boolean; } /** * Quarks/Three.js object types */ -export interface QuarksGroup { +export interface IQuarksGroup { uuid: string; type: "Group"; name: string; @@ -275,10 +275,10 @@ export interface QuarksGroup { position?: number[]; rotation?: number[]; scale?: number[]; - children?: QuarksObject[]; + children?: IQuarksObject[]; } -export interface QuarksParticleEmitter { +export interface IQuarksParticleEmitter { uuid: string; type: "ParticleEmitter"; name: string; @@ -286,16 +286,16 @@ export interface QuarksParticleEmitter { position?: number[]; rotation?: number[]; scale?: number[]; - ps: QuarksParticleEmitterConfig; - children?: QuarksObject[]; + ps: IQuarksParticleEmitterConfig; + children?: IQuarksObject[]; } -export type QuarksObject = QuarksGroup | QuarksParticleEmitter; +export type IQuarksObject = IQuarksGroup | IQuarksParticleEmitter; /** * Quarks/Three.js material */ -export interface QuarksMaterial { +export interface IQuarksMaterial { uuid: string; type: string; name?: string; @@ -310,7 +310,7 @@ export interface QuarksMaterial { /** * Quarks/Three.js texture */ -export interface QuarksTexture { +export interface IQuarksTexture { uuid: string; name?: string; image?: string; @@ -330,7 +330,7 @@ export interface QuarksTexture { /** * Quarks/Three.js image */ -export interface QuarksImage { +export interface IQuarksImage { uuid: string; url?: string; } @@ -338,7 +338,7 @@ export interface QuarksImage { /** * Quarks/Three.js geometry */ -export interface QuarksGeometry { +export interface IQuarksGeometry { uuid: string; type: string; data?: { @@ -360,15 +360,15 @@ export interface QuarksGeometry { /** * Quarks/Three.js JSON structure */ -export interface QuarksJSON { +export interface IQuarksJSON { metadata?: { version?: number; type?: string; generator?: string; }; - geometries?: QuarksGeometry[]; - materials?: QuarksMaterial[]; - textures?: QuarksTexture[]; - images?: QuarksImage[]; - object?: QuarksObject; + geometries?: IQuarksGeometry[]; + materials?: IQuarksMaterial[]; + textures?: IQuarksTexture[]; + images?: IQuarksImage[]; + object?: IQuarksObject; } From cd0c0b96ed77963995e72152cd5cca2d3a3ac7b5 Mon Sep 17 00:00:00 2001 From: Mikalai Lazitski Date: Tue, 16 Dec 2025 14:25:40 +0300 Subject: [PATCH 38/62] refactor: update type definitions across effect components to improve clarity and maintainability, transitioning from generic types to prefixed interfaces for better consistency in the codebase --- tools/src/effect/behaviors/utils.ts | 6 +-- tools/src/effect/effect.ts | 22 +++++------ tools/src/effect/factories/geometryFactory.ts | 8 ++-- tools/src/effect/factories/materialFactory.ts | 8 ++-- tools/src/effect/factories/systemFactory.ts | 34 ++++++++--------- tools/src/effect/loggers/logger.ts | 6 +-- tools/src/effect/parsers/dataConverter.ts | 38 +++++++++---------- tools/src/effect/parsers/parser.ts | 12 +++--- .../effect/systems/effectParticleSystem.ts | 10 ++--- .../systems/effectSolidParticleSystem.ts | 10 ++--- tools/src/effect/types/behaviors.ts | 18 ++++----- tools/src/effect/types/colors.ts | 26 ++++++------- tools/src/effect/types/emitter.ts | 14 +++---- tools/src/effect/types/gradients.ts | 2 +- tools/src/effect/types/hierarchy.ts | 24 ++++++------ tools/src/effect/types/loader.ts | 2 +- tools/src/effect/utils/valueParser.ts | 6 +-- 17 files changed, 123 insertions(+), 123 deletions(-) diff --git a/tools/src/effect/behaviors/utils.ts b/tools/src/effect/behaviors/utils.ts index 3d0b84aba..17d1d73cf 100644 --- a/tools/src/effect/behaviors/utils.ts +++ b/tools/src/effect/behaviors/utils.ts @@ -1,4 +1,4 @@ -import type { GradientKey } from "../types/gradients"; +import type { IGradientKey } from "../types/gradients"; /** * Extract RGB color from gradient key value @@ -77,7 +77,7 @@ export function extractNumberFromValue(value: number | number[] | { r: number; g * Interpolate between two gradient keys */ export function interpolateGradientKeys( - keys: GradientKey[], + keys: IGradientKey[], ratio: number, extractValue: (value: number | number[] | { r: number; g: number; b: number; a?: number } | undefined) => number ): number { @@ -112,7 +112,7 @@ export function interpolateGradientKeys( /** * Interpolate color between two gradient keys */ -export function interpolateColorKeys(keys: GradientKey[], ratio: number): { r: number; g: number; b: number; a: number } { +export function interpolateColorKeys(keys: IGradientKey[], ratio: number): { r: number; g: number; b: number; a: number } { if (!keys || keys.length === 0) { return { r: 1, g: 1, b: 1, a: 1 }; } diff --git a/tools/src/effect/effect.ts b/tools/src/effect/effect.ts index cd14c8429..6c2a49c11 100644 --- a/tools/src/effect/effect.ts +++ b/tools/src/effect/effect.ts @@ -1,11 +1,11 @@ import { Scene, Tools, IDisposable, TransformNode, Vector3, CreatePlane, MeshBuilder, Texture } from "babylonjs"; import type { QuarksJSON } from "./types/quarksTypes"; -import type { LoaderOptions } from "./types/loader"; +import type { ILoaderOptions } from "./types/loader"; import { Parser } from "./parsers/parser"; import { EffectParticleSystem } from "./systems/effectParticleSystem"; import { EffectSolidParticleSystem } from "./systems/effectSolidParticleSystem"; -import type { Group, Emitter, Data } from "./types/hierarchy"; -import type { EmitterConfig } from "./types/emitter"; +import type { IGroup, IEmitter, IData } from "./types/hierarchy"; +import type { IEmitterConfig } from "./types/emitter"; import { isSystem } from "./types/system"; import { EmitterFactory } from "./factories/emitterFactory"; @@ -80,7 +80,7 @@ export class Effect implements IDisposable { * @param options Optional parsing options * @returns Promise that resolves to a Effect */ - public static async LoadAsync(url: string, scene: Scene, rootUrl: string = "", options?: LoaderOptions): Promise { + public static async LoadAsync(url: string, scene: Scene, rootUrl: string = "", options?: ILoaderOptions): Promise { return new Promise((resolve, reject) => { Tools.LoadFile( url, @@ -111,7 +111,7 @@ export class Effect implements IDisposable { * @param options Optional parsing options * @returns A Effect containing all particle systems */ - public static Parse(jsonData: QuarksJSON, scene: Scene, rootUrl: string = "", options?: LoaderOptions): Effect { + public static Parse(jsonData: QuarksJSON, scene: Scene, rootUrl: string = "", options?: ILoaderOptions): Effect { return new Effect(jsonData, scene, rootUrl, options); } @@ -122,7 +122,7 @@ export class Effect implements IDisposable { * @param rootUrl Root URL for loading textures * @param options Optional parsing options */ - constructor(jsonData?: QuarksJSON, scene?: Scene, rootUrl: string = "", options?: LoaderOptions) { + constructor(jsonData?: QuarksJSON, scene?: Scene, rootUrl: string = "", options?: ILoaderOptions) { this._scene = scene || null; if (jsonData && scene) { const parser = new Parser(scene, rootUrl, jsonData, options); @@ -143,7 +143,7 @@ export class Effect implements IDisposable { * Build hierarchy from data and group nodes map * Handles errors gracefully and continues building partial hierarchy if errors occur */ - private _buildHierarchy(Data: Data, groupNodesMap: Map, systems: (EffectParticleSystem | EffectSolidParticleSystem)[]): void { + private _buildHierarchy(Data: IData, groupNodesMap: Map, systems: (EffectParticleSystem | EffectSolidParticleSystem)[]): void { if (!Data || !Data.root) { return; } @@ -161,7 +161,7 @@ export class Effect implements IDisposable { * Recursively build nodes from hierarchy */ private _buildNodeFromHierarchy( - obj: Group | Emitter, + obj: IGroup | IEmitter, parent: EffectNode | null, groupNodesMap: Map, systems: (EffectParticleSystem | EffectSolidParticleSystem)[] @@ -181,7 +181,7 @@ export class Effect implements IDisposable { if (node.type === "particle") { // Find system by name - const emitter = obj as Emitter; + const emitter = obj as IEmitter; const system = systems.find((s) => s.name === emitter.name); if (system) { node.system = system; @@ -192,7 +192,7 @@ export class Effect implements IDisposable { } } else { // Find group TransformNode - const group = obj as Group; + const group = obj as IGroup; const groupNode = group.uuid ? groupNodesMap.get(group.uuid) : null; if (groupNode) { node.group = groupNode; @@ -632,7 +632,7 @@ export class Effect implements IDisposable { const systemUuid = Tools.RandomId(); // Create default config - const config: EmitterConfig = { + const config: IEmitterConfig = { systemType, looping: true, duration: 5, diff --git a/tools/src/effect/factories/geometryFactory.ts b/tools/src/effect/factories/geometryFactory.ts index 4980b338f..11682791c 100644 --- a/tools/src/effect/factories/geometryFactory.ts +++ b/tools/src/effect/factories/geometryFactory.ts @@ -1,18 +1,18 @@ import { Mesh, VertexData, CreatePlane, Nullable, Scene } from "babylonjs"; import type { IGeometryFactory } from "../types/factories"; import { Logger } from "../loggers/logger"; -import type { Data } from "../types/hierarchy"; +import type { IData } from "../types/hierarchy"; import type { IGeometry } from "../types/resources"; -import type { LoaderOptions } from "../types/loader"; +import type { ILoaderOptions } from "../types/loader"; /** * Factory for creating meshes from Three.js geometry data */ export class GeometryFactory implements IGeometryFactory { private _logger: Logger; - private _Data: Data; + private _Data: IData; - constructor(Data: Data, options: LoaderOptions) { + constructor(Data: IData, options: ILoaderOptions) { this._Data = Data; this._logger = new Logger("[GeometryFactory]", options); } diff --git a/tools/src/effect/factories/materialFactory.ts b/tools/src/effect/factories/materialFactory.ts index 3454436e2..aa470f4aa 100644 --- a/tools/src/effect/factories/materialFactory.ts +++ b/tools/src/effect/factories/materialFactory.ts @@ -1,8 +1,8 @@ import { Nullable, Texture as BabylonTexture, PBRMaterial, Material as BabylonMaterial, Constants, Tools, Scene, Color3 } from "babylonjs"; import type { IMaterialFactory } from "../types/factories"; import { Logger } from "../loggers/logger"; -import type { LoaderOptions } from "../types/loader"; -import type { Data } from "../types/hierarchy"; +import type { ILoaderOptions } from "../types/loader"; +import type { IData } from "../types/hierarchy"; import type { IMaterial, ITexture, IImage } from "../types/resources"; /** @@ -11,10 +11,10 @@ import type { IMaterial, ITexture, IImage } from "../types/resources"; export class MaterialFactory implements IMaterialFactory { private _logger: Logger; private _scene: Scene; - private _data: Data; + private _data: IData; private _rootUrl: string; - constructor(scene: Scene, data: Data, rootUrl: string, options: LoaderOptions) { + constructor(scene: Scene, data: IData, rootUrl: string, options: ILoaderOptions) { this._scene = scene; this._data = data; this._rootUrl = rootUrl; diff --git a/tools/src/effect/factories/systemFactory.ts b/tools/src/effect/factories/systemFactory.ts index d3244048b..dd54c5410 100644 --- a/tools/src/effect/factories/systemFactory.ts +++ b/tools/src/effect/factories/systemFactory.ts @@ -1,12 +1,12 @@ import { Nullable, Vector3, TransformNode, Texture, Scene } from "babylonjs"; import { EffectParticleSystem } from "../systems/effectParticleSystem"; import { EffectSolidParticleSystem } from "../systems/effectSolidParticleSystem"; -import type { Data, Group, Emitter, Transform } from "../types/hierarchy"; +import type { IData, IGroup, IEmitter, ITransform } from "../types/hierarchy"; import { Logger } from "../loggers/logger"; import { MatrixUtils } from "../utils/matrixUtils"; import { EmitterFactory } from "./emitterFactory"; import type { IMaterialFactory, IGeometryFactory } from "../types/factories"; -import type { LoaderOptions } from "../types/loader"; +import type { ILoaderOptions } from "../types/loader"; /** * Factory for creating particle systems from data @@ -20,7 +20,7 @@ export class SystemFactory { private _geometryFactory: IGeometryFactory; private _emitterFactory: EmitterFactory; - constructor(scene: Scene, options: LoaderOptions, groupNodesMap: Map, materialFactory: IMaterialFactory, geometryFactory: IGeometryFactory) { + constructor(scene: Scene, options: ILoaderOptions, groupNodesMap: Map, materialFactory: IMaterialFactory, geometryFactory: IGeometryFactory) { this._scene = scene; this._groupNodesMap = groupNodesMap; this._logger = new Logger("[SystemFactory]", options); @@ -33,7 +33,7 @@ export class SystemFactory { * Create particle systems from data * Creates all nodes, sets parents, and applies transformations in one pass */ - public createSystems(Data: Data): (EffectParticleSystem | EffectSolidParticleSystem)[] { + public createSystems(Data: IData): (EffectParticleSystem | EffectSolidParticleSystem)[] { if (!Data.root) { this._logger.warn("No root object found in data"); return []; @@ -50,11 +50,11 @@ export class SystemFactory { * Creates nodes, sets parents, and applies transformations in one pass */ private _processObject( - Obj: Group | Emitter, + Obj: IGroup | IEmitter, parentGroup: Nullable, depth: number, particleSystems: (EffectParticleSystem | EffectSolidParticleSystem)[], - Data: Data + Data: IData ): void { this._logger.log(`${" ".repeat(depth)}Processing object: ${Obj.name}`); @@ -69,11 +69,11 @@ export class SystemFactory { * Process a Group object */ private _processGroup( - Group: Group, + Group: IGroup, parentGroup: Nullable, depth: number, particleSystems: (EffectParticleSystem | EffectSolidParticleSystem)[], - Data: Data + Data: IData ): void { const groupNode = this._createGroupNode(Group, parentGroup, depth); this._processChildren(Group.children, groupNode, depth, particleSystems, Data); @@ -82,7 +82,7 @@ export class SystemFactory { /** * Process a Emitter object */ - private _processEmitter(Emitter: Emitter, parentGroup: Nullable, depth: number, particleSystems: (EffectParticleSystem | EffectSolidParticleSystem)[]): void { + private _processEmitter(Emitter: IEmitter, parentGroup: Nullable, depth: number, particleSystems: (EffectParticleSystem | EffectSolidParticleSystem)[]): void { const particleSystem = this._createParticleSystem(Emitter, parentGroup, depth); if (particleSystem) { particleSystems.push(particleSystem); @@ -93,11 +93,11 @@ export class SystemFactory { * Process children of a group recursively */ private _processChildren( - children: (Group | Emitter)[] | undefined, + children: (IGroup | IEmitter)[] | undefined, parentGroup: TransformNode, depth: number, particleSystems: (EffectParticleSystem | EffectSolidParticleSystem)[], - Data: Data + Data: IData ): void { if (!children || children.length === 0) { return; @@ -112,7 +112,7 @@ export class SystemFactory { /** * Create a TransformNode for a Group */ - private _createGroupNode(Group: Group, parentGroup: Nullable, depth: number): TransformNode { + private _createGroupNode(Group: IGroup, parentGroup: Nullable, depth: number): TransformNode { const groupNode = new TransformNode(Group.name, this._scene); groupNode.id = Group.uuid; @@ -129,7 +129,7 @@ export class SystemFactory { /** * Create a particle system from a Emitter */ - private _createParticleSystem(Emitter: Emitter, parentGroup: Nullable, depth: number): Nullable { + private _createParticleSystem(Emitter: IEmitter, parentGroup: Nullable, depth: number): Nullable { const indent = " ".repeat(depth); const parentName = parentGroup ? parentGroup.name : "none"; this._logger.log(`${indent}Processing emitter: ${Emitter.name} (parent: ${parentName})`); @@ -200,7 +200,7 @@ export class SystemFactory { /** * Create a ParticleSystem instance */ - private _createParticleSystemInstance(Emitter: Emitter, _parentGroup: Nullable, cumulativeScale: Vector3, _depth: number): Nullable { + private _createParticleSystemInstance(Emitter: IEmitter, _parentGroup: Nullable, cumulativeScale: Vector3, _depth: number): Nullable { const { name, config } = Emitter; this._logger.log(`Creating ParticleSystem: ${name}`); @@ -228,7 +228,7 @@ export class SystemFactory { /** * Create a SolidParticleSystem instance */ - private _createSolidParticleSystem(Emitter: Emitter, parentGroup: Nullable): Nullable { + private _createSolidParticleSystem(Emitter: IEmitter, parentGroup: Nullable): Nullable { const { name, config } = Emitter; this._logger.log(`Creating SolidParticleSystem: ${name}`); @@ -287,14 +287,14 @@ export class SystemFactory { } // Type guards - private _isGroup(Obj: Group | Emitter): Obj is Group { + private _isGroup(Obj: IGroup | IEmitter): Obj is IGroup { return "children" in Obj; } /** * Apply transform to a node */ - private _applyTransform(node: TransformNode, transform: Transform, depth: number): void { + private _applyTransform(node: TransformNode, transform: ITransform, depth: number): void { if (!transform) { this._logger.warn(`Transform is undefined for node: ${node.name}`); return; diff --git a/tools/src/effect/loggers/logger.ts b/tools/src/effect/loggers/logger.ts index a5d3da191..9170caf16 100644 --- a/tools/src/effect/loggers/logger.ts +++ b/tools/src/effect/loggers/logger.ts @@ -1,14 +1,14 @@ import { Logger as BabylonLogger } from "babylonjs"; -import type { LoaderOptions } from "../types"; +import type { ILoaderOptions } from "../types"; /** * Logger utility for operations */ export class Logger { private _prefix: string; - private _options?: LoaderOptions; + private _options?: ILoaderOptions; - constructor(prefix: string = "[]", options?: LoaderOptions) { + constructor(prefix: string = "[]", options?: ILoaderOptions) { this._prefix = prefix; this._options = options; } diff --git a/tools/src/effect/parsers/dataConverter.ts b/tools/src/effect/parsers/dataConverter.ts index 141be6c5e..14b016b8b 100644 --- a/tools/src/effect/parsers/dataConverter.ts +++ b/tools/src/effect/parsers/dataConverter.ts @@ -1,5 +1,5 @@ import { Vector3, Matrix, Quaternion, Color3, Texture as BabylonTexture, ParticleSystem } from "babylonjs"; -import type { LoaderOptions } from "../types/loader"; +import type { ILoaderOptions } from "../types/loader"; import type { IQuarksJSON, IQuarksMaterial, @@ -27,9 +27,9 @@ import type { IQuarksRotationBySpeedBehavior, IQuarksOrbitOverLifeBehavior, } from "../types/quarksTypes"; -import type { Transform, Group, Emitter, Data } from "../types/hierarchy"; +import type { ITransform, IGroup, IEmitter, IData } from "../types/hierarchy"; import type { IMaterial, ITexture, IImage, IGeometry, IGeometryData } from "../types/resources"; -import type { EmitterConfig } from "../types/emitter"; +import type { IEmitterConfig } from "../types/emitter"; import type { Behavior, ColorOverLifeBehavior, @@ -43,7 +43,7 @@ import type { import type { Value } from "../types/values"; import type { Color } from "../types/colors"; import type { Rotation } from "../types/rotations"; -import type { GradientKey } from "../types/gradients"; +import type { IGradientKey } from "../types/gradients"; import type { IShape } from "../types/shapes"; import { Logger } from "../loggers/logger"; @@ -54,7 +54,7 @@ import { Logger } from "../loggers/logger"; export class DataConverter { private _logger: Logger; - constructor(options?: LoaderOptions) { + constructor(options?: ILoaderOptions) { this._logger = new Logger("[DataConverter]", options); } @@ -62,13 +62,13 @@ export class DataConverter { * Convert IQuarks/Three.js JSON to Babylon.js format * Handles errors gracefully and returns partial data if conversion fails */ - public convert(IQuarksData: IQuarksJSON): Data { + public convert(IQuarksData: IQuarksJSON): IData { this._logger.log("=== Converting IQuarks to Babylon.js format ==="); - const groups = new Map(); - const emitters = new Map(); + const groups = new Map(); + const emitters = new Map(); - let root: Group | Emitter | null = null; + let root: IGroup | IEmitter | null = null; try { if (IQuarksData.object) { @@ -126,7 +126,7 @@ export class DataConverter { /** * Convert a IQuarks/Three.js object to Babylon.js format */ - private _convertObject(obj: IQuarksObject, parentUuid: string | null, groups: Map, emitters: Map, depth: number): Group | Emitter | null { + private _convertObject(obj: IQuarksObject, parentUuid: string | null, groups: Map, emitters: Map, depth: number): IGroup | IEmitter | null { const indent = " ".repeat(depth); if (!obj || typeof obj !== "object") { @@ -139,7 +139,7 @@ export class DataConverter { const transform = this._convertTransform(obj.matrix, obj.position, obj.rotation, obj.scale); if (obj.type === "Group") { - const group: Group = { + const group: IGroup = { uuid: obj.uuid || `group_${groups.size}`, name: obj.name || "Group", transform, @@ -153,10 +153,10 @@ export class DataConverter { if (convertedChild) { if ("config" in convertedChild) { // It's an emitter - group.children.push(convertedChild as Emitter); + group.children.push(convertedChild as IEmitter); } else { // It's a group - group.children.push(convertedChild as Group); + group.children.push(convertedChild as IGroup); } } } @@ -169,7 +169,7 @@ export class DataConverter { // Convert emitter config from IQuarks to format const Config = this._convertEmitterConfig(obj.ps); - const emitter: Emitter = { + const emitter: IEmitter = { uuid: obj.uuid || `emitter_${emitters.size}`, name: obj.name || "ParticleEmitter", transform, @@ -192,7 +192,7 @@ export class DataConverter { * Convert transform from IQuarks/Three.js (right-handed) to Babylon.js (left-handed) * This is the ONLY place where handedness conversion happens */ - private _convertTransform(matrixArray?: number[], positionArray?: number[], rotationArray?: number[], scaleArray?: number[]): Transform { + private _convertTransform(matrixArray?: number[], positionArray?: number[], rotationArray?: number[], scaleArray?: number[]): ITransform { const position = Vector3.Zero(); const rotation = Quaternion.Identity(); const scale = Vector3.One(); @@ -246,11 +246,11 @@ export class DataConverter { /** * Convert emitter config from IQuarks to format */ - private _convertEmitterConfig(IQuarksConfig: IQuarksParticleEmitterConfig): EmitterConfig { + private _convertEmitterConfig(IQuarksConfig: IQuarksParticleEmitterConfig): IEmitterConfig { // Determine system type based on renderMode: 2 = solid, otherwise base const systemType: "solid" | "base" = IQuarksConfig.renderMode === 2 ? "solid" : "base"; - const Config: EmitterConfig = { + const Config: IEmitterConfig = { version: IQuarksConfig.version, autoDestroy: IQuarksConfig.autoDestroy, looping: IQuarksConfig.looping, @@ -445,7 +445,7 @@ export class DataConverter { /** * Convert IQuarks gradient key to gradient key */ - private _convertGradientKey(IQuarksKey: IQuarksGradientKey): GradientKey { + private _convertGradientKey(IQuarksKey: IQuarksGradientKey): IGradientKey { return { time: IQuarksKey.time, value: IQuarksKey.value, @@ -568,7 +568,7 @@ export class DataConverter { case "FrameOverLife": { const behavior = IQuarksBehavior as IQuarksFrameOverLifeBehavior; - const Behavior: { type: string; frame?: Value | { keys?: GradientKey[] } } = { type: "FrameOverLife" }; + const Behavior: { type: string; frame?: Value | { keys?: IGradientKey[] } } = { type: "FrameOverLife" }; if (behavior.frame) { if (typeof behavior.frame === "object" && behavior.frame !== null && "keys" in behavior.frame) { Behavior.frame = { diff --git a/tools/src/effect/parsers/parser.ts b/tools/src/effect/parsers/parser.ts index 5c3893ce3..e1d41d2d2 100644 --- a/tools/src/effect/parsers/parser.ts +++ b/tools/src/effect/parsers/parser.ts @@ -1,5 +1,5 @@ import { Scene, TransformNode } from "babylonjs"; -import type { IQuarksJSON, LoaderOptions, Data } from "../types"; +import type { IQuarksJSON, ILoaderOptions, IData } from "../types"; import { Logger } from "../loggers/logger"; import { MaterialFactory, GeometryFactory, SystemFactory } from "../factories"; import { DataConverter } from "./dataConverter"; @@ -12,7 +12,7 @@ export interface ParseResult { /** Created particle systems */ systems: (EffectParticleSystem | EffectSolidParticleSystem)[]; /** Converted data */ - Data: Data; + Data: IData; /** Map of group UUIDs to TransformNodes */ groupNodesMap: Map; } @@ -26,11 +26,11 @@ export class Parser { private _materialFactory: MaterialFactory; private _geometryFactory: GeometryFactory; private _systemFactory: SystemFactory; - private _Data: Data; + private _Data: IData; private _groupNodesMap: Map; - private _options: LoaderOptions; + private _options: ILoaderOptions; - constructor(scene: Scene, rootUrl: string, jsonData: IQuarksJSON, options?: LoaderOptions) { + constructor(scene: Scene, rootUrl: string, jsonData: IQuarksJSON, options?: ILoaderOptions) { const opts = options || {}; this._options = opts; this._groupNodesMap = new Map(); @@ -80,7 +80,7 @@ export class Parser { /** * Validate data structure */ - private _validateJSONStructure(Data: Data): void { + private _validateJSONStructure(Data: IData): void { this._logger.log("Validating data structure..."); if (!Data.root) { diff --git a/tools/src/effect/systems/effectParticleSystem.ts b/tools/src/effect/systems/effectParticleSystem.ts index d19d0ad0a..ff4a679ce 100644 --- a/tools/src/effect/systems/effectParticleSystem.ts +++ b/tools/src/effect/systems/effectParticleSystem.ts @@ -17,8 +17,8 @@ import type { ISystem, ParticleWithSystem, IShape, - EmitterConfig, - EmissionBurst, + IEmitterConfig, + IEmissionBurst, } from "../types"; import { ValueUtils } from "../utils/valueParser"; import { CapacityCalculator } from "../utils/capacityCalculator"; @@ -52,7 +52,7 @@ export class EffectParticleSystem extends ParticleSystem implements ISystem { constructor( name: string, scene: Scene, - config: EmitterConfig, + config: IEmitterConfig, options?: { texture?: Texture; blendMode?: number; @@ -270,7 +270,7 @@ export class EffectParticleSystem extends ParticleSystem implements ISystem { * This method applies all configuration from ParticleEmitterConfig */ private _configureFromConfig( - config: EmitterConfig, + config: IEmitterConfig, options?: { texture?: Texture; blendMode?: number; @@ -392,7 +392,7 @@ export class EffectParticleSystem extends ParticleSystem implements ISystem { /** * Apply emission bursts via emit rate gradients */ - private _applyEmissionBursts(bursts: EmissionBurst[], baseEmitRate: number, duration: number): void { + private _applyEmissionBursts(bursts: IEmissionBurst[], baseEmitRate: number, duration: number): void { for (const burst of bursts) { if (burst.time === undefined || burst.count === undefined) { continue; diff --git a/tools/src/effect/systems/effectSolidParticleSystem.ts b/tools/src/effect/systems/effectSolidParticleSystem.ts index bbf9875f0..313488f84 100644 --- a/tools/src/effect/systems/effectSolidParticleSystem.ts +++ b/tools/src/effect/systems/effectSolidParticleSystem.ts @@ -6,8 +6,8 @@ import type { SizeBySpeedBehavior, RotationBySpeedBehavior, OrbitOverLifeBehavior, - EmitterConfig, - EmissionBurst, + IEmitterConfig, + IEmissionBurst, ISolidParticleEmitterType, PerSolidParticleBehaviorFunction, ISystem, @@ -78,7 +78,7 @@ export class EffectSolidParticleSystem extends SolidParticleSystem implements IS public startColor?: Color; public emissionOverTime?: Value; public emissionOverDistance?: Value; - public emissionBursts?: EmissionBurst[]; + public emissionBursts?: IEmissionBurst[]; public onlyUsedByOther: boolean; public instancingGeometry?: string; public renderOrder?: number; @@ -578,7 +578,7 @@ export class EffectSolidParticleSystem extends SolidParticleSystem implements IS constructor( name: string, scene: any, - initialConfig: EmitterConfig, + initialConfig: IEmitterConfig, options?: { updatable?: boolean; isPickable?: boolean; @@ -995,7 +995,7 @@ export class EffectSolidParticleSystem extends SolidParticleSystem implements IS this._spawnBursts(); } - private _getBurstTime(burst: EmissionBurst): number { + private _getBurstTime(burst: IEmissionBurst): number { return ValueUtils.parseConstantValue(burst.time); } diff --git a/tools/src/effect/types/behaviors.ts b/tools/src/effect/types/behaviors.ts index 618b2845f..2964fec27 100644 --- a/tools/src/effect/types/behaviors.ts +++ b/tools/src/effect/types/behaviors.ts @@ -1,5 +1,5 @@ import type { Value } from "./values"; -import type { GradientKey } from "./gradients"; +import type { IGradientKey } from "./gradients"; import { Particle, ParticleSystem, SolidParticle, SolidParticleSystem } from "babylonjs"; /** @@ -27,19 +27,19 @@ export interface ColorOverLifeBehavior { type: "ColorOverLife"; color?: { color?: { - keys: GradientKey[]; + keys: IGradientKey[]; }; alpha?: { - keys: GradientKey[]; + keys: IGradientKey[]; }; - keys?: GradientKey[]; + keys?: IGradientKey[]; }; } export interface SizeOverLifeBehavior { type: "SizeOverLife"; size?: { - keys?: GradientKey[]; + keys?: IGradientKey[]; functions?: Array<{ start: number; function: { @@ -76,7 +76,7 @@ export interface SpeedOverLifeBehavior { type: "SpeedOverLife"; speed?: | { - keys?: GradientKey[]; + keys?: IGradientKey[]; functions?: Array<{ start: number; function: { @@ -92,7 +92,7 @@ export interface FrameOverLifeBehavior { type: "FrameOverLife"; frame?: | { - keys?: GradientKey[]; + keys?: IGradientKey[]; } | Value; } @@ -100,7 +100,7 @@ export interface FrameOverLifeBehavior { export interface LimitSpeedOverLifeBehavior { type: "LimitSpeedOverLife"; maxSpeed?: Value; - speed?: Value | { keys?: GradientKey[] }; + speed?: Value | { keys?: IGradientKey[] }; dampen?: Value; } @@ -136,7 +136,7 @@ export interface OrbitOverLifeBehavior { y?: number; z?: number; }; - radius?: Value | { keys?: GradientKey[] }; + radius?: Value | { keys?: IGradientKey[] }; speed?: Value; } diff --git a/tools/src/effect/types/colors.ts b/tools/src/effect/types/colors.ts index 7045805b1..304f99201 100644 --- a/tools/src/effect/types/colors.ts +++ b/tools/src/effect/types/colors.ts @@ -1,41 +1,41 @@ -import type { GradientKey } from "./gradients"; +import type { IGradientKey } from "./gradients"; /** * color types (converted from Quarks) */ -export interface ConstantColor { +export interface IConstantColor { type: "ConstantColor"; value: [number, number, number, number]; // RGBA } -export interface ColorRange { +export interface IColorRange { type: "ColorRange"; colorA: [number, number, number, number]; // RGBA colorB: [number, number, number, number]; // RGBA } -export interface GradientColor { +export interface IGradientColor { type: "Gradient"; - colorKeys: GradientKey[]; - alphaKeys?: GradientKey[]; + colorKeys: IGradientKey[]; + alphaKeys?: IGradientKey[]; } -export interface RandomColor { +export interface IRandomColor { type: "RandomColor"; colorA: [number, number, number, number]; // RGBA colorB: [number, number, number, number]; // RGBA } -export interface RandomColorBetweenGradient { +export interface IRandomColorBetweenGradient { type: "RandomColorBetweenGradient"; gradient1: { - colorKeys: GradientKey[]; - alphaKeys?: GradientKey[]; + colorKeys: IGradientKey[]; + alphaKeys?: IGradientKey[]; }; gradient2: { - colorKeys: GradientKey[]; - alphaKeys?: GradientKey[]; + colorKeys: IGradientKey[]; + alphaKeys?: IGradientKey[]; }; } -export type Color = ConstantColor | ColorRange | GradientColor | RandomColor | RandomColorBetweenGradient | [number, number, number, number] | string; +export type Color = IConstantColor | IColorRange | IGradientColor | IRandomColor | IRandomColorBetweenGradient | [number, number, number, number] | string; diff --git a/tools/src/effect/types/emitter.ts b/tools/src/effect/types/emitter.ts index cc044d535..f93cd279d 100644 --- a/tools/src/effect/types/emitter.ts +++ b/tools/src/effect/types/emitter.ts @@ -1,5 +1,5 @@ import { Nullable, SolidParticle, TransformNode, Vector3 } from "babylonjs"; -import type { Emitter } from "./hierarchy"; +import type { IEmitter } from "./hierarchy"; import type { Value } from "./values"; import type { Color } from "./colors"; import type { Rotation } from "./rotations"; @@ -9,7 +9,7 @@ import type { Behavior } from "./behaviors"; /** * emission burst (converted from Quarks) */ -export interface EmissionBurst { +export interface IEmissionBurst { time: Value; count: Value; } @@ -17,7 +17,7 @@ export interface EmissionBurst { /** * particle emitter configuration (converted from Quarks) */ -export interface EmitterConfig { +export interface IEmitterConfig { version?: string; autoDestroy?: boolean; looping?: boolean; @@ -31,7 +31,7 @@ export interface EmitterConfig { startColor?: Color; emissionOverTime?: Value; emissionOverDistance?: Value; - emissionBursts?: EmissionBurst[]; + emissionBursts?: IEmissionBurst[]; onlyUsedByOther?: boolean; instancingGeometry?: string; renderOrder?: number; @@ -55,15 +55,15 @@ export interface EmitterConfig { /** * Data structure for emitter creation */ -export interface EmitterData { +export interface IEmitterData { name: string; - config: EmitterConfig; + config: IEmitterConfig; materialId?: string; matrix?: number[]; position?: number[]; parentGroup: Nullable; cumulativeScale: Vector3; - Emitter?: Emitter; + Emitter?: IEmitter; } /** diff --git a/tools/src/effect/types/gradients.ts b/tools/src/effect/types/gradients.ts index a277fc1dd..ff5467855 100644 --- a/tools/src/effect/types/gradients.ts +++ b/tools/src/effect/types/gradients.ts @@ -1,7 +1,7 @@ /** * gradient key (converted from Quarks) */ -export interface GradientKey { +export interface IGradientKey { time?: number; value: number | [number, number, number, number] | { r: number; g: number; b: number; a?: number }; pos?: number; diff --git a/tools/src/effect/types/hierarchy.ts b/tools/src/effect/types/hierarchy.ts index 254bfe36d..a7a2d128e 100644 --- a/tools/src/effect/types/hierarchy.ts +++ b/tools/src/effect/types/hierarchy.ts @@ -1,11 +1,11 @@ import { Vector3, Quaternion } from "babylonjs"; -import type { EmitterConfig } from "./emitter"; +import type { IEmitterConfig } from "./emitter"; import type { IMaterial, ITexture, IImage, IGeometry } from "./resources"; /** * transform (converted from Quarks, left-handed coordinate system) */ -export interface Transform { +export interface ITransform { position: Vector3; rotation: Quaternion; scale: Vector3; @@ -14,21 +14,21 @@ export interface Transform { /** * group (converted from Quarks) */ -export interface Group { +export interface IGroup { uuid: string; name: string; - transform: Transform; - children: (Group | Emitter)[]; + transform: ITransform; + children: (IGroup | IEmitter)[]; } /** * emitter (converted from Quarks) */ -export interface Emitter { +export interface IEmitter { uuid: string; name: string; - transform: Transform; - config: EmitterConfig; + transform: ITransform; + config: IEmitterConfig; materialId?: string; parentUuid?: string; systemType: "solid" | "base"; // Determined from renderMode: 2 = solid, otherwise base @@ -39,10 +39,10 @@ export interface Emitter { * data (converted from Quarks) * Contains the converted structure with groups, emitters, and resources */ -export interface Data { - root: Group | Emitter | null; - groups: Map; - emitters: Map; +export interface IData { + root: IGroup | IEmitter | null; + groups: Map; + emitters: Map; // Resources (converted from Quarks, ready for Babylon.js) materials: IMaterial[]; textures: ITexture[]; diff --git a/tools/src/effect/types/loader.ts b/tools/src/effect/types/loader.ts index 90357c9f0..5aaf2cd91 100644 --- a/tools/src/effect/types/loader.ts +++ b/tools/src/effect/types/loader.ts @@ -1,7 +1,7 @@ /** * Options for parsing Quarks/Three.js particle JSON */ -export interface LoaderOptions { +export interface ILoaderOptions { /** * Enable verbose logging for debugging */ diff --git a/tools/src/effect/utils/valueParser.ts b/tools/src/effect/utils/valueParser.ts index c0db7bd2c..42281b849 100644 --- a/tools/src/effect/utils/valueParser.ts +++ b/tools/src/effect/utils/valueParser.ts @@ -1,7 +1,7 @@ import { Color4, ColorGradient } from "babylonjs"; import type { IPiecewiseBezier, Value } from "../types/values"; import type { Color } from "../types/colors"; -import type { GradientKey } from "../types/gradients"; +import type { IGradientKey } from "../types/gradients"; /** * Static utility functions for parsing values @@ -139,7 +139,7 @@ export class ValueUtils { /** * Parse gradient color keys */ - public static parseGradientColorKeys(keys: GradientKey[]): ColorGradient[] { + public static parseGradientColorKeys(keys: IGradientKey[]): ColorGradient[] { const gradients: ColorGradient[] = []; for (const key of keys) { const pos = key.pos ?? key.time ?? 0; @@ -164,7 +164,7 @@ export class ValueUtils { /** * Parse gradient alpha keys */ - public static parseGradientAlphaKeys(keys: GradientKey[]): { gradient: number; factor: number }[] { + public static parseGradientAlphaKeys(keys: IGradientKey[]): { gradient: number; factor: number }[] { const gradients: { gradient: number; factor: number }[] = []; for (const key of keys) { const pos = key.pos ?? key.time ?? 0; From 8bf8710842f5839ae4a7814bd4ee45b8d8aaf34f Mon Sep 17 00:00:00 2001 From: Mikalai Lazitski Date: Tue, 16 Dec 2025 14:53:31 +0300 Subject: [PATCH 39/62] refactor: standardize type definitions across effect behaviors and systems by transitioning to prefixed interfaces, enhancing code clarity and maintainability --- tools/src/effect/behaviors/colorBySpeed.ts | 6 +-- tools/src/effect/behaviors/colorOverLife.ts | 6 +-- tools/src/effect/behaviors/forceOverLife.ts | 6 +-- tools/src/effect/behaviors/frameOverLife.ts | 10 ++-- .../effect/behaviors/limitSpeedOverLife.ts | 6 +-- tools/src/effect/behaviors/orbitOverLife.ts | 6 +-- tools/src/effect/behaviors/rotationBySpeed.ts | 6 +-- .../src/effect/behaviors/rotationOverLife.ts | 6 +-- tools/src/effect/behaviors/sizeBySpeed.ts | 6 +-- tools/src/effect/behaviors/sizeOverLife.ts | 6 +-- tools/src/effect/behaviors/speedOverLife.ts | 6 +-- tools/src/effect/effect.ts | 46 ++++++++-------- tools/src/effect/factories/geometryFactory.ts | 10 ++-- tools/src/effect/parsers/dataConverter.ts | 54 ++++++++++--------- tools/src/effect/parsers/parser.ts | 44 +++++++-------- .../effect/systems/effectParticleSystem.ts | 48 ++++++++--------- .../systems/effectSolidParticleSystem.ts | 28 +++++----- tools/src/effect/types/behaviors.ts | 52 +++++++++--------- 18 files changed, 179 insertions(+), 173 deletions(-) diff --git a/tools/src/effect/behaviors/colorBySpeed.ts b/tools/src/effect/behaviors/colorBySpeed.ts index 49b753972..73adf0133 100644 --- a/tools/src/effect/behaviors/colorBySpeed.ts +++ b/tools/src/effect/behaviors/colorBySpeed.ts @@ -1,5 +1,5 @@ import { SolidParticle, Particle, Vector3 } from "babylonjs"; -import type { ColorBySpeedBehavior } from "../types/behaviors"; +import type { IIColorBySpeedBehavior } from "../types/behaviors"; import { interpolateColorKeys } from "./utils"; import { ValueUtils } from "../utils/valueParser"; @@ -7,7 +7,7 @@ import { ValueUtils } from "../utils/valueParser"; * Apply ColorBySpeed behavior to Particle * Gets currentSpeed from particle.velocity magnitude */ -export function applyColorBySpeedPS(particle: Particle, behavior: ColorBySpeedBehavior): void { +export function applyColorBySpeedPS(particle: Particle, behavior: IColorBySpeedBehavior): void { if (!behavior.color || !behavior.color.keys || !particle.color || !particle.direction) { return; } @@ -40,7 +40,7 @@ export function applyColorBySpeedPS(particle: Particle, behavior: ColorBySpeedBe * Apply ColorBySpeed behavior to SolidParticle * Gets currentSpeed from particle.velocity magnitude */ -export function applyColorBySpeedSPS(particle: SolidParticle, behavior: ColorBySpeedBehavior): void { +export function applyColorBySpeedSPS(particle: SolidParticle, behavior: IColorBySpeedBehavior): void { if (!behavior.color || !behavior.color.keys || !particle.color) { return; } diff --git a/tools/src/effect/behaviors/colorOverLife.ts b/tools/src/effect/behaviors/colorOverLife.ts index e16fb7173..18a6344a1 100644 --- a/tools/src/effect/behaviors/colorOverLife.ts +++ b/tools/src/effect/behaviors/colorOverLife.ts @@ -1,5 +1,5 @@ import { Color4 } from "babylonjs"; -import type { ColorOverLifeBehavior } from "../types/behaviors"; +import type { IIColorOverLifeBehavior } from "../types/behaviors"; import { extractColorFromValue, extractAlphaFromValue } from "./utils"; import type { EffectSolidParticleSystem } from "../systems/effectSolidParticleSystem"; import type { EffectParticleSystem } from "../systems/effectParticleSystem"; @@ -7,7 +7,7 @@ import type { EffectParticleSystem } from "../systems/effectParticleSystem"; /** * Apply ColorOverLife behavior to ParticleSystem */ -export function applyColorOverLifePS(particleSystem: EffectParticleSystem, behavior: ColorOverLifeBehavior): void { +export function applyColorOverLifePS(particleSystem: EffectParticleSystem, behavior: IColorOverLifeBehavior): void { if (behavior.color && behavior.color.color && behavior.color.color.keys) { const colorKeys = behavior.color.color.keys; for (const key of colorKeys) { @@ -44,7 +44,7 @@ export function applyColorOverLifePS(particleSystem: EffectParticleSystem, behav * Adds color gradients to the system (similar to ParticleSystem native gradients) * Properly combines color and alpha keys even when they have different positions */ -export function applyColorOverLifeSPS(system: EffectSolidParticleSystem, behavior: ColorOverLifeBehavior): void { +export function applyColorOverLifeSPS(system: EffectSolidParticleSystem, behavior: IColorOverLifeBehavior): void { if (!behavior.color) { return; } diff --git a/tools/src/effect/behaviors/forceOverLife.ts b/tools/src/effect/behaviors/forceOverLife.ts index 93557d74e..52f4b0164 100644 --- a/tools/src/effect/behaviors/forceOverLife.ts +++ b/tools/src/effect/behaviors/forceOverLife.ts @@ -1,11 +1,11 @@ import { Vector3 } from "babylonjs"; -import type { ForceOverLifeBehavior, GravityForceBehavior } from "../types/behaviors"; +import type { IIForceOverLifeBehavior, IIGravityForceBehavior } from "../types/behaviors"; import { ValueUtils } from "../utils/valueParser"; import type { EffectParticleSystem } from "../systems/effectParticleSystem"; /** * Apply ForceOverLife behavior to ParticleSystem */ -export function applyForceOverLifePS(particleSystem: EffectParticleSystem, behavior: ForceOverLifeBehavior): void { +export function applyForceOverLifePS(particleSystem: EffectParticleSystem, behavior: IForceOverLifeBehavior): void { if (behavior.force) { const forceX = behavior.force.x !== undefined ? ValueUtils.parseConstantValue(behavior.force.x) : 0; const forceY = behavior.force.y !== undefined ? ValueUtils.parseConstantValue(behavior.force.y) : 0; @@ -26,7 +26,7 @@ export function applyForceOverLifePS(particleSystem: EffectParticleSystem, behav /** * Apply GravityForce behavior to ParticleSystem */ -export function applyGravityForcePS(particleSystem: EffectParticleSystem, behavior: GravityForceBehavior): void { +export function applyGravityForcePS(particleSystem: EffectParticleSystem, behavior: IGravityForceBehavior): void { if (behavior.gravity !== undefined) { const gravity = ValueUtils.parseConstantValue(behavior.gravity); particleSystem.gravity = new Vector3(0, -gravity, 0); diff --git a/tools/src/effect/behaviors/frameOverLife.ts b/tools/src/effect/behaviors/frameOverLife.ts index 257765762..960d539a8 100644 --- a/tools/src/effect/behaviors/frameOverLife.ts +++ b/tools/src/effect/behaviors/frameOverLife.ts @@ -1,10 +1,10 @@ -import type { FrameOverLifeBehavior } from "../types/behaviors"; +import type { IFrameOverLifeBehavior } from "../types/behaviors"; import { ValueUtils } from "../utils/valueParser"; import type { EffectParticleSystem } from "../systems/effectParticleSystem"; /** * Apply FrameOverLife behavior to ParticleSystem */ -export function applyFrameOverLifePS(particleSystem: EffectParticleSystem, behavior: FrameOverLifeBehavior): void { +export function applyFrameOverLifePS(particleSystem: EffectParticleSystem, behavior: IFrameOverLifeBehavior): void { if (!behavior.frame) { return; } @@ -16,11 +16,11 @@ export function applyFrameOverLifePS(particleSystem: EffectParticleSystem, behav const pos = k.pos ?? k.time ?? 0; if (typeof val === "number") { return val; - } else if (Array.isArray(val)) { + } + if (Array.isArray(val)) { return val[0] || 0; - } else { - return pos; } + return pos; }); if (frames.length > 0) { particleSystem.startSpriteCellID = Math.floor(frames[0]); diff --git a/tools/src/effect/behaviors/limitSpeedOverLife.ts b/tools/src/effect/behaviors/limitSpeedOverLife.ts index f834d10e4..b5efc92a1 100644 --- a/tools/src/effect/behaviors/limitSpeedOverLife.ts +++ b/tools/src/effect/behaviors/limitSpeedOverLife.ts @@ -1,4 +1,4 @@ -import type { LimitSpeedOverLifeBehavior } from "../types/behaviors"; +import type { IILimitSpeedOverLifeBehavior } from "../types/behaviors"; import { extractNumberFromValue } from "./utils"; import { ValueUtils } from "../utils/valueParser"; import type { EffectSolidParticleSystem } from "../systems/effectSolidParticleSystem"; @@ -6,7 +6,7 @@ import type { EffectParticleSystem } from "../systems/effectParticleSystem"; /** * Apply LimitSpeedOverLife behavior to ParticleSystem */ -export function applyLimitSpeedOverLifePS(particleSystem: EffectParticleSystem, behavior: LimitSpeedOverLifeBehavior): void { +export function applyLimitSpeedOverLifePS(particleSystem: EffectParticleSystem, behavior: ILimitSpeedOverLifeBehavior): void { if (behavior.dampen !== undefined) { const dampen = ValueUtils.parseConstantValue(behavior.dampen); particleSystem.limitVelocityDamping = dampen; @@ -38,7 +38,7 @@ export function applyLimitSpeedOverLifePS(particleSystem: EffectParticleSystem, * Apply LimitSpeedOverLife behavior to SolidParticleSystem * Adds limit velocity gradients to the system (similar to ParticleSystem native gradients) */ -export function applyLimitSpeedOverLifeSPS(system: EffectSolidParticleSystem, behavior: LimitSpeedOverLifeBehavior): void { +export function applyLimitSpeedOverLifeSPS(system: EffectSolidParticleSystem, behavior: ILimitSpeedOverLifeBehavior): void { if (behavior.dampen !== undefined) { const dampen = ValueUtils.parseConstantValue(behavior.dampen); system.limitVelocityDamping = dampen; diff --git a/tools/src/effect/behaviors/orbitOverLife.ts b/tools/src/effect/behaviors/orbitOverLife.ts index 560b245a2..cded859c6 100644 --- a/tools/src/effect/behaviors/orbitOverLife.ts +++ b/tools/src/effect/behaviors/orbitOverLife.ts @@ -1,5 +1,5 @@ import { Particle, SolidParticle } from "babylonjs"; -import type { OrbitOverLifeBehavior } from "../types/behaviors"; +import type { IIOrbitOverLifeBehavior } from "../types/behaviors"; import { extractNumberFromValue, interpolateGradientKeys } from "./utils"; import { ValueUtils } from "../utils/valueParser"; import type { Value } from "../types/values"; @@ -8,7 +8,7 @@ import type { Value } from "../types/values"; * Apply OrbitOverLife behavior to Particle * Gets lifeRatio from particle (age / lifeTime) */ -export function applyOrbitOverLifePS(particle: Particle, behavior: OrbitOverLifeBehavior): void { +export function applyOrbitOverLifePS(particle: Particle, behavior: IOrbitOverLifeBehavior): void { if (!behavior.radius || particle.lifeTime <= 0) { return; } @@ -60,7 +60,7 @@ export function applyOrbitOverLifePS(particle: Particle, behavior: OrbitOverLife * Apply OrbitOverLife behavior to SolidParticle * Gets lifeRatio from particle (age / lifeTime) */ -export function applyOrbitOverLifeSPS(particle: SolidParticle, behavior: OrbitOverLifeBehavior): void { +export function applyOrbitOverLifeSPS(particle: SolidParticle, behavior: IOrbitOverLifeBehavior): void { if (!behavior.radius || particle.lifeTime <= 0) { return; } diff --git a/tools/src/effect/behaviors/rotationBySpeed.ts b/tools/src/effect/behaviors/rotationBySpeed.ts index 63b51e4c6..0686753ea 100644 --- a/tools/src/effect/behaviors/rotationBySpeed.ts +++ b/tools/src/effect/behaviors/rotationBySpeed.ts @@ -1,5 +1,5 @@ import { Particle, SolidParticle, Vector3 } from "babylonjs"; -import type { RotationBySpeedBehavior } from "../types/behaviors"; +import type { IIRotationBySpeedBehavior } from "../types/behaviors"; import { extractNumberFromValue, interpolateGradientKeys } from "./utils"; import { ValueUtils } from "../utils/valueParser"; import { ParticleWithSystem, SolidParticleWithSystem } from "../types/system"; @@ -8,7 +8,7 @@ import { ParticleWithSystem, SolidParticleWithSystem } from "../types/system"; * Apply RotationBySpeed behavior to Particle * Gets currentSpeed from particle.direction magnitude and updateSpeed from system */ -export function applyRotationBySpeedPS(particle: Particle, behavior: RotationBySpeedBehavior): void { +export function applyRotationBySpeedPS(particle: Particle, behavior: IRotationBySpeedBehavior): void { if (!behavior.angularVelocity || !particle.direction) { return; } @@ -45,7 +45,7 @@ export function applyRotationBySpeedPS(particle: Particle, behavior: RotationByS * Apply RotationBySpeed behavior to SolidParticle * Gets currentSpeed from particle.velocity magnitude and updateSpeed from system */ -export function applyRotationBySpeedSPS(particle: SolidParticle, behavior: RotationBySpeedBehavior): void { +export function applyRotationBySpeedSPS(particle: SolidParticle, behavior: IRotationBySpeedBehavior): void { if (!behavior.angularVelocity) { return; } diff --git a/tools/src/effect/behaviors/rotationOverLife.ts b/tools/src/effect/behaviors/rotationOverLife.ts index 4ea4a7e90..6992207c7 100644 --- a/tools/src/effect/behaviors/rotationOverLife.ts +++ b/tools/src/effect/behaviors/rotationOverLife.ts @@ -1,4 +1,4 @@ -import type { RotationOverLifeBehavior } from "../types/behaviors"; +import type { IIRotationOverLifeBehavior } from "../types/behaviors"; import { ValueUtils } from "../utils/valueParser"; import { extractNumberFromValue } from "./utils"; import type { EffectSolidParticleSystem } from "../systems/effectSolidParticleSystem"; @@ -7,7 +7,7 @@ import type { EffectParticleSystem } from "../systems/effectParticleSystem"; * Apply RotationOverLife behavior to ParticleSystem * Uses addAngularSpeedGradient for gradient support (Babylon.js native) */ -export function applyRotationOverLifePS(particleSystem: EffectParticleSystem, behavior: RotationOverLifeBehavior): void { +export function applyRotationOverLifePS(particleSystem: EffectParticleSystem, behavior: IRotationOverLifeBehavior): void { if (!behavior.angularVelocity) { return; } @@ -59,7 +59,7 @@ export function applyRotationOverLifePS(particleSystem: EffectParticleSystem, be * Apply RotationOverLife behavior to SolidParticleSystem * Adds angular speed gradients to the system (similar to ParticleSystem native gradients) */ -export function applyRotationOverLifeSPS(system: EffectSolidParticleSystem, behavior: RotationOverLifeBehavior): void { +export function applyRotationOverLifeSPS(system: EffectSolidParticleSystem, behavior: IRotationOverLifeBehavior): void { if (!behavior.angularVelocity) { return; } diff --git a/tools/src/effect/behaviors/sizeBySpeed.ts b/tools/src/effect/behaviors/sizeBySpeed.ts index ea0718aca..ea88b6396 100644 --- a/tools/src/effect/behaviors/sizeBySpeed.ts +++ b/tools/src/effect/behaviors/sizeBySpeed.ts @@ -1,5 +1,5 @@ import { Particle, SolidParticle, Vector3 } from "babylonjs"; -import type { SizeBySpeedBehavior } from "../types/behaviors"; +import type { IISizeBySpeedBehavior } from "../types/behaviors"; import { extractNumberFromValue, interpolateGradientKeys } from "./utils"; import { ValueUtils } from "../utils/valueParser"; @@ -7,7 +7,7 @@ import { ValueUtils } from "../utils/valueParser"; * Apply SizeBySpeed behavior to Particle * Gets currentSpeed from particle.direction magnitude */ -export function applySizeBySpeedPS(particle: Particle, behavior: SizeBySpeedBehavior): void { +export function applySizeBySpeedPS(particle: Particle, behavior: ISizeBySpeedBehavior): void { if (!behavior.size || !behavior.size.keys || !particle.direction) { return; } @@ -29,7 +29,7 @@ export function applySizeBySpeedPS(particle: Particle, behavior: SizeBySpeedBeha * Apply SizeBySpeed behavior to SolidParticle * Gets currentSpeed from particle.velocity magnitude */ -export function applySizeBySpeedSPS(particle: SolidParticle, behavior: SizeBySpeedBehavior): void { +export function applySizeBySpeedSPS(particle: SolidParticle, behavior: ISizeBySpeedBehavior): void { if (!behavior.size || !behavior.size.keys) { return; } diff --git a/tools/src/effect/behaviors/sizeOverLife.ts b/tools/src/effect/behaviors/sizeOverLife.ts index cc7339502..20c151871 100644 --- a/tools/src/effect/behaviors/sizeOverLife.ts +++ b/tools/src/effect/behaviors/sizeOverLife.ts @@ -1,4 +1,4 @@ -import type { SizeOverLifeBehavior } from "../types/behaviors"; +import type { IISizeOverLifeBehavior } from "../types/behaviors"; import { extractNumberFromValue } from "./utils"; import type { EffectSolidParticleSystem } from "../systems/effectSolidParticleSystem"; import type { EffectParticleSystem } from "../systems/effectParticleSystem"; @@ -7,7 +7,7 @@ import type { EffectParticleSystem } from "../systems/effectParticleSystem"; * In Quarks, SizeOverLife values are multipliers relative to initial particle size * In Babylon.js, sizeGradients are absolute values, so we multiply by average initial size */ -export function applySizeOverLifePS(particleSystem: EffectParticleSystem, behavior: SizeOverLifeBehavior): void { +export function applySizeOverLifePS(particleSystem: EffectParticleSystem, behavior: ISizeOverLifeBehavior): void { // Get average initial size from minSize/maxSize to use as base for multipliers const avgInitialSize = (particleSystem.minSize + particleSystem.maxSize) / 2; @@ -39,7 +39,7 @@ export function applySizeOverLifePS(particleSystem: EffectParticleSystem, behavi * Apply SizeOverLife behavior to SolidParticleSystem * Adds size gradients to the system (similar to ParticleSystem native gradients) */ -export function applySizeOverLifeSPS(system: EffectSolidParticleSystem, behavior: SizeOverLifeBehavior): void { +export function applySizeOverLifeSPS(system: EffectSolidParticleSystem, behavior: ISizeOverLifeBehavior): void { if (!behavior.size) { return; } diff --git a/tools/src/effect/behaviors/speedOverLife.ts b/tools/src/effect/behaviors/speedOverLife.ts index 3bd7cdea8..095744e5f 100644 --- a/tools/src/effect/behaviors/speedOverLife.ts +++ b/tools/src/effect/behaviors/speedOverLife.ts @@ -1,4 +1,4 @@ -import type { SpeedOverLifeBehavior } from "../types/behaviors"; +import type { IISpeedOverLifeBehavior } from "../types/behaviors"; import { extractNumberFromValue } from "./utils"; import { ValueUtils } from "../utils/valueParser"; import type { EffectSolidParticleSystem } from "../systems/effectSolidParticleSystem"; @@ -6,7 +6,7 @@ import type { EffectParticleSystem } from "../systems/effectParticleSystem"; /** * Apply SpeedOverLife behavior to ParticleSystem */ -export function applySpeedOverLifePS(particleSystem: EffectParticleSystem, behavior: SpeedOverLifeBehavior): void { +export function applySpeedOverLifePS(particleSystem: EffectParticleSystem, behavior: ISpeedOverLifeBehavior): void { if (behavior.speed) { if (typeof behavior.speed === "object" && behavior.speed !== null && "keys" in behavior.speed && behavior.speed.keys && Array.isArray(behavior.speed.keys)) { for (const key of behavior.speed.keys) { @@ -46,7 +46,7 @@ export function applySpeedOverLifePS(particleSystem: EffectParticleSystem, behav * Apply SpeedOverLife behavior to SolidParticleSystem * Adds velocity gradients to the system (similar to ParticleSystem native gradients) */ -export function applySpeedOverLifeSPS(system: EffectSolidParticleSystem, behavior: SpeedOverLifeBehavior): void { +export function applySpeedOverLifeSPS(system: EffectSolidParticleSystem, behavior: ISpeedOverLifeBehavior): void { if (!behavior.speed) { return; } diff --git a/tools/src/effect/effect.ts b/tools/src/effect/effect.ts index 6c2a49c11..3bf542455 100644 --- a/tools/src/effect/effect.ts +++ b/tools/src/effect/effect.ts @@ -12,7 +12,7 @@ import { EmitterFactory } from "./factories/emitterFactory"; /** * Effect Node - represents either a particle system or a group */ -export interface EffectNode { +export interface IEffectNode { /** Node name */ name: string; /** Node UUID from original JSON */ @@ -22,9 +22,9 @@ export interface EffectNode { /** Transform node (if this is a group) */ group?: TransformNode; /** Parent node */ - parent?: EffectNode; + parent?: IEffectNode; /** Child nodes */ - children: EffectNode[]; + children: IEffectNode[]; /** Node type */ type: "particle" | "group"; } @@ -38,7 +38,7 @@ export class Effect implements IDisposable { private _systems: (EffectParticleSystem | EffectSolidParticleSystem)[] = []; /** Root node of the effect hierarchy */ - private _root: EffectNode | null = null; + private _root: IEffectNode | null = null; /** * Get all particle systems in this effect @@ -50,7 +50,7 @@ export class Effect implements IDisposable { /** * Get root node of the effect hierarchy */ - public get root(): EffectNode | null { + public get root(): IEffectNode | null { return this._root; } @@ -67,7 +67,7 @@ export class Effect implements IDisposable { private readonly _groupsByUuid = new Map(); /** All nodes in the hierarchy */ - private readonly _nodes = new Map(); + private readonly _nodes = new Map(); /** Scene reference for creating new systems */ private _scene: Scene | null = null; @@ -129,8 +129,8 @@ export class Effect implements IDisposable { const parseResult = parser.parse(); this._systems.push(...parseResult.systems); - if (parseResult.Data && parseResult.groupNodesMap) { - this._buildHierarchy(parseResult.Data, parseResult.groupNodesMap, parseResult.systems); + if (parseResult.data && parseResult.groupNodesMap) { + this._buildHierarchy(parseResult.data, parseResult.groupNodesMap, parseResult.systems); } } else if (scene) { // Create empty effect with root group @@ -162,16 +162,16 @@ export class Effect implements IDisposable { */ private _buildNodeFromHierarchy( obj: IGroup | IEmitter, - parent: EffectNode | null, + parent: IEffectNode | null, groupNodesMap: Map, systems: (EffectParticleSystem | EffectSolidParticleSystem)[] - ): EffectNode | null { + ): IEffectNode | null { if (!obj) { return null; } try { - const node: EffectNode = { + const node: IEffectNode = { name: obj.name, uuid: obj.uuid, parent: parent || undefined, @@ -263,14 +263,14 @@ export class Effect implements IDisposable { /** * Find a node (system or group) by name */ - public findNodeByName(name: string): EffectNode | null { + public findNodeByName(name: string): IEffectNode | null { return this._nodes.get(name) || null; } /** * Find a node (system or group) by UUID */ - public findNodeByUuid(uuid: string): EffectNode | null { + public findNodeByUuid(uuid: string): IEffectNode | null { return this._nodes.get(uuid) || null; } @@ -365,7 +365,7 @@ export class Effect implements IDisposable { /** * Start a node (system or group) */ - public startNode(node: EffectNode): void { + public startNode(node: IEffectNode): void { if (node.type === "particle" && node.system) { node.system.start(); } else if (node.type === "group" && node.group) { @@ -380,7 +380,7 @@ export class Effect implements IDisposable { /** * Stop a node (system or group) */ - public stopNode(node: EffectNode): void { + public stopNode(node: IEffectNode): void { if (node.type === "particle" && node.system) { node.system.stop(); } else if (node.type === "group" && node.group) { @@ -395,7 +395,7 @@ export class Effect implements IDisposable { /** * Reset a node (system or group) */ - public resetNode(node: EffectNode): void { + public resetNode(node: IEffectNode): void { if (node.type === "particle" && node.system) { node.system.reset(); } else if (node.type === "group" && node.group) { @@ -410,7 +410,7 @@ export class Effect implements IDisposable { /** * Check if a node is started (system or group) */ - public isNodeStarted(node: EffectNode): boolean { + public isNodeStarted(node: IEffectNode): boolean { if (node.type === "particle" && node.system) { if (node.system instanceof EffectParticleSystem) { return (node.system as any).isStarted ? (node.system as any).isStarted() : false; @@ -436,7 +436,7 @@ export class Effect implements IDisposable { /** * Get all systems in a node recursively */ - private _getSystemsInNode(node: EffectNode): (EffectParticleSystem | EffectSolidParticleSystem)[] { + private _getSystemsInNode(node: IEffectNode): (EffectParticleSystem | EffectSolidParticleSystem)[] { const systems: (EffectParticleSystem | EffectSolidParticleSystem)[] = []; if (node.type === "particle" && node.system) { @@ -531,7 +531,7 @@ export class Effect implements IDisposable { const rootUuid = Tools.RandomId(); rootGroup.id = rootUuid; - const rootNode: EffectNode = { + const rootNode: IEffectNode = { name: "Root", uuid: rootUuid, group: rootGroup, @@ -552,7 +552,7 @@ export class Effect implements IDisposable { * @param name Optional name (defaults to "Group") * @returns Created group node */ - public createGroup(parentNode: EffectNode | null = null, name: string = "Group"): EffectNode | null { + public createGroup(parentNode: IEffectNode | null = null, name: string = "Group"): IEffectNode | null { if (!this._scene) { console.error("Cannot create group: scene is not available"); return null; @@ -581,7 +581,7 @@ export class Effect implements IDisposable { groupNode.setParent(parent.group, false, true); } - const newNode: EffectNode = { + const newNode: IEffectNode = { name: uniqueName, uuid: groupUuid, group: groupNode, @@ -609,7 +609,7 @@ export class Effect implements IDisposable { * @param name Optional name (defaults to "ParticleSystem") * @returns Created particle system node */ - public createParticleSystem(parentNode: EffectNode | null = null, systemType: "solid" | "base" = "base", name: string = "ParticleSystem"): EffectNode | null { + public createParticleSystem(parentNode: IEffectNode | null = null, systemType: "solid" | "base" = "base", name: string = "ParticleSystem"): IEffectNode | null { if (!this._scene) { console.error("Cannot create particle system: scene is not available"); return null; @@ -686,7 +686,7 @@ export class Effect implements IDisposable { // Set system name system.name = uniqueName; - const newNode: EffectNode = { + const newNode: IEffectNode = { name: uniqueName, uuid: systemUuid, system, diff --git a/tools/src/effect/factories/geometryFactory.ts b/tools/src/effect/factories/geometryFactory.ts index 11682791c..a86a6b9f3 100644 --- a/tools/src/effect/factories/geometryFactory.ts +++ b/tools/src/effect/factories/geometryFactory.ts @@ -10,10 +10,10 @@ import type { ILoaderOptions } from "../types/loader"; */ export class GeometryFactory implements IGeometryFactory { private _logger: Logger; - private _Data: IData; + private _data: IData; - constructor(Data: IData, options: ILoaderOptions) { - this._Data = Data; + constructor(data: IData, options: ILoaderOptions) { + this._data = data; this._logger = new Logger("[GeometryFactory]", options); } @@ -87,12 +87,12 @@ export class GeometryFactory implements IGeometryFactory { * Finds geometry by UUID */ private _findGeometry(geometryId: string): IGeometry | null { - if (!this._Data.geometries || this._Data.geometries.length === 0) { + if (!this._data.geometries || this._data.geometries.length === 0) { this._logger.warn("No geometries data available"); return null; } - const geometry = this._Data.geometries.find((g) => g.uuid === geometryId); + const geometry = this._data.geometries.find((g) => g.uuid === geometryId); if (!geometry) { this._logger.warn(`Geometry not found: ${geometryId}`); return null; diff --git a/tools/src/effect/parsers/dataConverter.ts b/tools/src/effect/parsers/dataConverter.ts index 14b016b8b..1662cbe1c 100644 --- a/tools/src/effect/parsers/dataConverter.ts +++ b/tools/src/effect/parsers/dataConverter.ts @@ -32,13 +32,13 @@ import type { IMaterial, ITexture, IImage, IGeometry, IGeometryData } from "../t import type { IEmitterConfig } from "../types/emitter"; import type { Behavior, - ColorOverLifeBehavior, - SizeOverLifeBehavior, - ForceOverLifeBehavior, - SpeedOverLifeBehavior, - LimitSpeedOverLifeBehavior, - ColorBySpeedBehavior, - SizeBySpeedBehavior, + IColorOverLifeBehavior, + ISizeOverLifeBehavior, + IForceOverLifeBehavior, + ISpeedOverLifeBehavior, + ILimitSpeedOverLifeBehavior, + IColorBySpeedBehavior, + ISizeBySpeedBehavior, } from "../types/behaviors"; import type { Value } from "../types/values"; import type { Color } from "../types/colors"; @@ -404,18 +404,18 @@ export class DataConverter { type: "ConstantColor", value: IQuarksColor.value, }; - } else if (IQuarksColor.color) { + } + if (IQuarksColor.color) { return { type: "ConstantColor", value: [IQuarksColor.color.r || 0, IQuarksColor.color.g || 0, IQuarksColor.color.b || 0, IQuarksColor.color.a !== undefined ? IQuarksColor.color.a : 1], }; - } else { - // Fallback: return default color if neither value nor color is present - return { - type: "ConstantColor", - value: [1, 1, 1, 1], - }; } + // Fallback: return default color if neither value nor color is present + return { + type: "ConstantColor", + value: [1, 1, 1, 1], + }; } return IQuarksColor as Color; } @@ -482,7 +482,7 @@ export class DataConverter { case "ColorOverLife": { const behavior = IQuarksBehavior as IQuarksColorOverLifeBehavior; if (behavior.color) { - const Color: ColorOverLifeBehavior["color"] = {}; + const Color: IColorOverLifeBehavior["color"] = {}; if (behavior.color.color?.keys) { Color.color = { keys: behavior.color.color.keys.map((k) => this._convertGradientKey(k)) }; } @@ -500,7 +500,7 @@ export class DataConverter { case "SizeOverLife": { const behavior = IQuarksBehavior as IQuarksSizeOverLifeBehavior; if (behavior.size) { - const Size: SizeOverLifeBehavior["size"] = {}; + const Size: ISizeOverLifeBehavior["size"] = {}; if (behavior.size.keys) { Size.keys = behavior.size.keys.map((k: IQuarksGradientKey) => this._convertGradientKey(k)); } @@ -524,7 +524,7 @@ export class DataConverter { case "ForceOverLife": case "ApplyForce": { const behavior = IQuarksBehavior as IQuarksForceOverLifeBehavior; - const Behavior: ForceOverLifeBehavior = { type: behavior.type }; + const Behavior: IForceOverLifeBehavior = { type: behavior.type }; if (behavior.force) { Behavior.force = { x: behavior.force.x !== undefined ? this._convertValue(behavior.force.x) : undefined, @@ -532,9 +532,15 @@ export class DataConverter { z: behavior.force.z !== undefined ? this._convertValue(behavior.force.z) : undefined, }; } - if (behavior.x !== undefined) Behavior.x = this._convertValue(behavior.x); - if (behavior.y !== undefined) Behavior.y = this._convertValue(behavior.y); - if (behavior.z !== undefined) Behavior.z = this._convertValue(behavior.z); + if (behavior.x !== undefined) { + Behavior.x = this._convertValue(behavior.x); + } + if (behavior.y !== undefined) { + Behavior.y = this._convertValue(behavior.y); + } + if (behavior.z !== undefined) { + Behavior.z = this._convertValue(behavior.z); + } return Behavior; } @@ -551,7 +557,7 @@ export class DataConverter { const behavior = IQuarksBehavior as IQuarksSpeedOverLifeBehavior; if (behavior.speed) { if (typeof behavior.speed === "object" && behavior.speed !== null && "keys" in behavior.speed) { - const Speed: SpeedOverLifeBehavior["speed"] = {}; + const Speed: ISpeedOverLifeBehavior["speed"] = {}; if (behavior.speed.keys) { Speed.keys = behavior.speed.keys.map((k: IQuarksGradientKey) => this._convertGradientKey(k)); } @@ -583,7 +589,7 @@ export class DataConverter { case "LimitSpeedOverLife": { const behavior = IQuarksBehavior as IQuarksLimitSpeedOverLifeBehavior; - const Behavior: LimitSpeedOverLifeBehavior = { type: "LimitSpeedOverLife" }; + const Behavior: ILimitSpeedOverLifeBehavior = { type: "LimitSpeedOverLife" }; if (behavior.maxSpeed !== undefined) { Behavior.maxSpeed = this._convertValue(behavior.maxSpeed); } @@ -602,7 +608,7 @@ export class DataConverter { case "ColorBySpeed": { const behavior = IQuarksBehavior as IQuarksColorBySpeedBehavior; - const Behavior: ColorBySpeedBehavior = { + const Behavior: IColorBySpeedBehavior = { type: "ColorBySpeed", minSpeed: behavior.minSpeed !== undefined ? this._convertValue(behavior.minSpeed) : undefined, maxSpeed: behavior.maxSpeed !== undefined ? this._convertValue(behavior.maxSpeed) : undefined, @@ -615,7 +621,7 @@ export class DataConverter { case "SizeBySpeed": { const behavior = IQuarksBehavior as IQuarksSizeBySpeedBehavior; - const Behavior: SizeBySpeedBehavior = { + const Behavior: ISizeBySpeedBehavior = { type: "SizeBySpeed", minSpeed: behavior.minSpeed !== undefined ? this._convertValue(behavior.minSpeed) : undefined, maxSpeed: behavior.maxSpeed !== undefined ? this._convertValue(behavior.maxSpeed) : undefined, diff --git a/tools/src/effect/parsers/parser.ts b/tools/src/effect/parsers/parser.ts index e1d41d2d2..a94d4aa85 100644 --- a/tools/src/effect/parsers/parser.ts +++ b/tools/src/effect/parsers/parser.ts @@ -8,11 +8,11 @@ import type { EffectParticleSystem, EffectSolidParticleSystem } from "../systems /** * Result of parsing JSON */ -export interface ParseResult { +export interface IParseResult { /** Created particle systems */ systems: (EffectParticleSystem | EffectSolidParticleSystem)[]; /** Converted data */ - Data: IData; + data: IData; /** Map of group UUIDs to TransformNodes */ groupNodesMap: Map; } @@ -26,24 +26,24 @@ export class Parser { private _materialFactory: MaterialFactory; private _geometryFactory: GeometryFactory; private _systemFactory: SystemFactory; - private _Data: IData; + private _data: IData; private _groupNodesMap: Map; private _options: ILoaderOptions; - constructor(scene: Scene, rootUrl: string, jsonData: IQuarksJSON, options?: ILoaderOptions) { + constructor(scene: Scene, rootUrl: string, jsondata: IQuarksJSON, options?: ILoaderOptions) { const opts = options || {}; this._options = opts; this._groupNodesMap = new Map(); this._logger = new Logger("[Parser]", opts); - // Convert Quarks JSON to Data first + // Convert Quarks JSON to data first const dataConverter = new DataConverter(opts); - this._Data = dataConverter.convert(jsonData); + this._data = dataConverter.convert(jsondata); - // Create factories with Data instead of QuarksJSON - this._materialFactory = new MaterialFactory(scene, this._Data, rootUrl, opts); - this._geometryFactory = new GeometryFactory(this._Data, opts); + // Create factories with data instead of QuarksJSON + this._materialFactory = new MaterialFactory(scene, this._data, rootUrl, opts); + this._geometryFactory = new GeometryFactory(this._data, opts); this._systemFactory = new SystemFactory(scene, opts, this._groupNodesMap, this._materialFactory, this._geometryFactory); } @@ -51,28 +51,28 @@ export class Parser { * Parse the JSON data and create particle systems * Returns all necessary data for building the effect hierarchy */ - public parse(): ParseResult { + public parse(): IParseResult { this._logger.log("=== Starting Particle System Parsing ==="); - if (!this._Data) { - this._logger.warn("Data is missing"); + if (!this._data) { + this._logger.warn("data is missing"); return { systems: [], - Data: this._Data, + data: this._data, groupNodesMap: this._groupNodesMap, }; } if (this._options.validate) { - this._validateJSONStructure(this._Data); + this._validateJSONStructure(this._data); } - const particleSystems = this._systemFactory.createSystems(this._Data); + const particleSystems = this._systemFactory.createSystems(this._data); this._logger.log(`=== Parsing complete. Created ${particleSystems.length} particle system(s) ===`); return { systems: particleSystems, - Data: this._Data, + data: this._data, groupNodesMap: this._groupNodesMap, }; } @@ -80,26 +80,26 @@ export class Parser { /** * Validate data structure */ - private _validateJSONStructure(Data: IData): void { + private _validateJSONStructure(data: IData): void { this._logger.log("Validating data structure..."); - if (!Data.root) { + if (!data.root) { this._logger.warn(" data missing 'root' property"); } - if (!Data.materials || Data.materials.length === 0) { + if (!data.materials || data.materials.length === 0) { this._logger.warn(" data has no materials"); } - if (!Data.textures || Data.textures.length === 0) { + if (!data.textures || data.textures.length === 0) { this._logger.warn(" data has no textures"); } - if (!Data.images || Data.images.length === 0) { + if (!data.images || data.images.length === 0) { this._logger.warn(" data has no images"); } - if (!Data.geometries || Data.geometries.length === 0) { + if (!data.geometries || data.geometries.length === 0) { this._logger.warn(" data has no geometries"); } diff --git a/tools/src/effect/systems/effectParticleSystem.ts b/tools/src/effect/systems/effectParticleSystem.ts index ff4a679ce..d381573a9 100644 --- a/tools/src/effect/systems/effectParticleSystem.ts +++ b/tools/src/effect/systems/effectParticleSystem.ts @@ -1,18 +1,18 @@ import { Color4, ParticleSystem, Scene, Vector3, Matrix, Texture, AbstractMesh, TransformNode, Particle } from "babylonjs"; import type { Behavior, - ColorOverLifeBehavior, - SizeOverLifeBehavior, - RotationOverLifeBehavior, - ForceOverLifeBehavior, - GravityForceBehavior, - SpeedOverLifeBehavior, - FrameOverLifeBehavior, - LimitSpeedOverLifeBehavior, - ColorBySpeedBehavior, - SizeBySpeedBehavior, - RotationBySpeedBehavior, - OrbitOverLifeBehavior, + IColorOverLifeBehavior, + ISizeOverLifeBehavior, + IRotationOverLifeBehavior, + IForceOverLifeBehavior, + IGravityForceBehavior, + ISpeedOverLifeBehavior, + IFrameOverLifeBehavior, + ILimitSpeedOverLifeBehavior, + IColorBySpeedBehavior, + ISizeBySpeedBehavior, + IRotationBySpeedBehavior, + IOrbitOverLifeBehavior, PerParticleBehaviorFunction, ISystem, ParticleWithSystem, @@ -185,7 +185,7 @@ export class EffectParticleSystem extends ParticleSystem implements ISystem { for (const behavior of behaviors) { switch (behavior.type) { case "ColorBySpeed": { - const b = behavior as ColorBySpeedBehavior; + const b = behavior as IColorBySpeedBehavior; functions.push((particle: Particle) => { applyColorBySpeedPS(particle, b); }); @@ -193,7 +193,7 @@ export class EffectParticleSystem extends ParticleSystem implements ISystem { } case "SizeBySpeed": { - const b = behavior as SizeBySpeedBehavior; + const b = behavior as ISizeBySpeedBehavior; functions.push((particle: Particle) => { applySizeBySpeedPS(particle, b); }); @@ -201,7 +201,7 @@ export class EffectParticleSystem extends ParticleSystem implements ISystem { } case "RotationBySpeed": { - const b = behavior as RotationBySpeedBehavior; + const b = behavior as IRotationBySpeedBehavior; functions.push((particle: Particle) => { // Store reference to system in particle for behaviors that need it const particleWithSystem = particle as ParticleWithSystem; @@ -212,7 +212,7 @@ export class EffectParticleSystem extends ParticleSystem implements ISystem { } case "OrbitOverLife": { - const b = behavior as OrbitOverLifeBehavior; + const b = behavior as IOrbitOverLifeBehavior; functions.push((particle: Particle) => { applyOrbitOverLifePS(particle, b); }); @@ -236,30 +236,30 @@ export class EffectParticleSystem extends ParticleSystem implements ISystem { switch (behavior.type) { case "ColorOverLife": - applyColorOverLifePS(this, behavior as ColorOverLifeBehavior); + applyColorOverLifePS(this, behavior as IColorOverLifeBehavior); break; case "SizeOverLife": - applySizeOverLifePS(this, behavior as SizeOverLifeBehavior); + applySizeOverLifePS(this, behavior as ISizeOverLifeBehavior); break; case "RotationOverLife": case "Rotation3DOverLife": - applyRotationOverLifePS(this, behavior as RotationOverLifeBehavior); + applyRotationOverLifePS(this, behavior as IRotationOverLifeBehavior); break; case "ForceOverLife": case "ApplyForce": - applyForceOverLifePS(this, behavior as ForceOverLifeBehavior); + applyForceOverLifePS(this, behavior as IForceOverLifeBehavior); break; case "GravityForce": - applyGravityForcePS(this, behavior as GravityForceBehavior); + applyGravityForcePS(this, behavior as IGravityForceBehavior); break; case "SpeedOverLife": - applySpeedOverLifePS(this, behavior as SpeedOverLifeBehavior); + applySpeedOverLifePS(this, behavior as ISpeedOverLifeBehavior); break; case "FrameOverLife": - applyFrameOverLifePS(this, behavior as FrameOverLifeBehavior); + applyFrameOverLifePS(this, behavior as IFrameOverLifeBehavior); break; case "LimitSpeedOverLife": - applyLimitSpeedOverLifePS(this, behavior as LimitSpeedOverLifeBehavior); + applyLimitSpeedOverLifePS(this, behavior as ILimitSpeedOverLifeBehavior); break; } } diff --git a/tools/src/effect/systems/effectSolidParticleSystem.ts b/tools/src/effect/systems/effectSolidParticleSystem.ts index 313488f84..9aa305a1e 100644 --- a/tools/src/effect/systems/effectSolidParticleSystem.ts +++ b/tools/src/effect/systems/effectSolidParticleSystem.ts @@ -1,11 +1,11 @@ import { Vector3, Quaternion, Matrix, Color4, SolidParticle, TransformNode, Mesh, AbstractMesh, SolidParticleSystem } from "babylonjs"; import type { Behavior, - ForceOverLifeBehavior, - ColorBySpeedBehavior, - SizeBySpeedBehavior, - RotationBySpeedBehavior, - OrbitOverLifeBehavior, + IForceOverLifeBehavior, + IColorBySpeedBehavior, + ISizeBySpeedBehavior, + IRotationBySpeedBehavior, + IOrbitOverLifeBehavior, IEmitterConfig, IEmissionBurst, ISolidParticleEmitterType, @@ -34,7 +34,7 @@ import { /** * Emission state matching three.quarks EmissionState structure */ -interface EmissionState { +interface IEmissionState { time: number; waitEmiting: number; travelDistance: number; @@ -51,7 +51,7 @@ interface EmissionState { * This class replicates the exact behavior of three.quarks ParticleSystem with systemType = "solid" */ export class EffectSolidParticleSystem extends SolidParticleSystem implements ISystem { - private _emissionState: EmissionState; + private _emissionState: IEmissionState; private _behaviors: PerSolidParticleBehaviorFunction[]; public particleEmitterType: ISolidParticleEmitterType | null; private _parent: TransformNode | null; @@ -693,7 +693,7 @@ export class EffectSolidParticleSystem extends SolidParticleSystem implements IS particle.color = new Color4(1, 1, 1, 1); } - const props = particle.props || (particle.props = {}); + const props = (particle.props ||= {}); props.speedModifier = 1.0; } @@ -1178,7 +1178,7 @@ export class EffectSolidParticleSystem extends SolidParticleSystem implements IS switch (behavior.type) { case "ForceOverLife": case "ApplyForce": { - const b = behavior as ForceOverLifeBehavior; + const b = behavior as IForceOverLifeBehavior; functions.push((particle: SolidParticle) => { const particleWithSystem = particle as SolidParticleWithSystem; const updateSpeed = particleWithSystem.system?.updateSpeed ?? 0.016; @@ -1199,7 +1199,7 @@ export class EffectSolidParticleSystem extends SolidParticleSystem implements IS } case "ColorBySpeed": { - const b = behavior as ColorBySpeedBehavior; + const b = behavior as IColorBySpeedBehavior; functions.push((particle: SolidParticle) => { applyColorBySpeedSPS(particle, b); }); @@ -1207,7 +1207,7 @@ export class EffectSolidParticleSystem extends SolidParticleSystem implements IS } case "SizeBySpeed": { - const b = behavior as SizeBySpeedBehavior; + const b = behavior as ISizeBySpeedBehavior; functions.push((particle: SolidParticle) => { applySizeBySpeedSPS(particle, b); }); @@ -1215,7 +1215,7 @@ export class EffectSolidParticleSystem extends SolidParticleSystem implements IS } case "RotationBySpeed": { - const b = behavior as RotationBySpeedBehavior; + const b = behavior as IRotationBySpeedBehavior; functions.push((particle: SolidParticle) => { applyRotationBySpeedSPS(particle, b); }); @@ -1223,7 +1223,7 @@ export class EffectSolidParticleSystem extends SolidParticleSystem implements IS } case "OrbitOverLife": { - const b = behavior as OrbitOverLifeBehavior; + const b = behavior as IOrbitOverLifeBehavior; functions.push((particle: SolidParticle) => { applyOrbitOverLifeSPS(particle, b); }); @@ -1337,7 +1337,7 @@ export class EffectSolidParticleSystem extends SolidParticleSystem implements IS * Apply gradients to particle based on lifeRatio */ private _applyGradients(particle: SolidParticle, lifeRatio: number): void { - const props = particle.props || (particle.props = {}); + const props = (particle.props ||= {}); const updateSpeed = this.updateSpeed; const color = this._colorGradients.getValue(lifeRatio); diff --git a/tools/src/effect/types/behaviors.ts b/tools/src/effect/types/behaviors.ts index 2964fec27..3c668751e 100644 --- a/tools/src/effect/types/behaviors.ts +++ b/tools/src/effect/types/behaviors.ts @@ -23,7 +23,7 @@ export type SystemBehaviorFunction = (system: ParticleSystem | SolidParticleSyst /** * behavior types (converted from Quarks) */ -export interface ColorOverLifeBehavior { +export interface IColorOverLifeBehavior { type: "ColorOverLife"; color?: { color?: { @@ -36,7 +36,7 @@ export interface ColorOverLifeBehavior { }; } -export interface SizeOverLifeBehavior { +export interface ISizeOverLifeBehavior { type: "SizeOverLife"; size?: { keys?: IGradientKey[]; @@ -50,12 +50,12 @@ export interface SizeOverLifeBehavior { }; } -export interface RotationOverLifeBehavior { +export interface IRotationOverLifeBehavior { type: "RotationOverLife" | "Rotation3DOverLife"; angularVelocity?: Value; } -export interface ForceOverLifeBehavior { +export interface IForceOverLifeBehavior { type: "ForceOverLife" | "ApplyForce"; force?: { x?: Value; @@ -67,12 +67,12 @@ export interface ForceOverLifeBehavior { z?: Value; } -export interface GravityForceBehavior { +export interface IGravityForceBehavior { type: "GravityForce"; gravity?: Value; } -export interface SpeedOverLifeBehavior { +export interface ISpeedOverLifeBehavior { type: "SpeedOverLife"; speed?: | { @@ -88,7 +88,7 @@ export interface SpeedOverLifeBehavior { | Value; } -export interface FrameOverLifeBehavior { +export interface IFrameOverLifeBehavior { type: "FrameOverLife"; frame?: | { @@ -97,39 +97,39 @@ export interface FrameOverLifeBehavior { | Value; } -export interface LimitSpeedOverLifeBehavior { +export interface ILimitSpeedOverLifeBehavior { type: "LimitSpeedOverLife"; maxSpeed?: Value; speed?: Value | { keys?: IGradientKey[] }; dampen?: Value; } -export interface ColorBySpeedBehavior { +export interface IColorBySpeedBehavior { type: "ColorBySpeed"; color?: { - keys: GradientKey[]; + keys: IGradientKey[]; }; minSpeed?: Value; maxSpeed?: Value; } -export interface SizeBySpeedBehavior { +export interface ISizeBySpeedBehavior { type: "SizeBySpeed"; size?: { - keys: GradientKey[]; + keys: IGradientKey[]; }; minSpeed?: Value; maxSpeed?: Value; } -export interface RotationBySpeedBehavior { +export interface IRotationBySpeedBehavior { type: "RotationBySpeed"; angularVelocity?: Value; minSpeed?: Value; maxSpeed?: Value; } -export interface OrbitOverLifeBehavior { +export interface IOrbitOverLifeBehavior { type: "OrbitOverLife"; center?: { x?: number; @@ -141,16 +141,16 @@ export interface OrbitOverLifeBehavior { } export type Behavior = - | ColorOverLifeBehavior - | SizeOverLifeBehavior - | RotationOverLifeBehavior - | ForceOverLifeBehavior - | GravityForceBehavior - | SpeedOverLifeBehavior - | FrameOverLifeBehavior - | LimitSpeedOverLifeBehavior - | ColorBySpeedBehavior - | SizeBySpeedBehavior - | RotationBySpeedBehavior - | OrbitOverLifeBehavior + | IColorOverLifeBehavior + | ISizeOverLifeBehavior + | IRotationOverLifeBehavior + | IForceOverLifeBehavior + | IGravityForceBehavior + | ISpeedOverLifeBehavior + | IFrameOverLifeBehavior + | ILimitSpeedOverLifeBehavior + | IColorBySpeedBehavior + | ISizeBySpeedBehavior + | IRotationBySpeedBehavior + | IOrbitOverLifeBehavior | { type: string; [key: string]: unknown }; // Fallback for unknown behaviors From d95b5892c4ee201991658f51015192c1fae6880b Mon Sep 17 00:00:00 2001 From: Mikalai Lazitski Date: Tue, 16 Dec 2025 15:04:24 +0300 Subject: [PATCH 40/62] refactor: update type definitions in effect behaviors and systems to use prefixed interfaces, improving code clarity and consistency --- tools/src/effect/behaviors/colorBySpeed.ts | 2 +- tools/src/effect/behaviors/colorOverLife.ts | 2 +- tools/src/effect/behaviors/forceOverLife.ts | 2 +- tools/src/effect/behaviors/limitSpeedOverLife.ts | 2 +- tools/src/effect/behaviors/orbitOverLife.ts | 2 +- tools/src/effect/behaviors/rotationBySpeed.ts | 2 +- tools/src/effect/behaviors/rotationOverLife.ts | 2 +- tools/src/effect/behaviors/sizeBySpeed.ts | 2 +- tools/src/effect/behaviors/sizeOverLife.ts | 2 +- tools/src/effect/behaviors/speedOverLife.ts | 2 +- tools/src/effect/effect.ts | 6 +++--- 11 files changed, 13 insertions(+), 13 deletions(-) diff --git a/tools/src/effect/behaviors/colorBySpeed.ts b/tools/src/effect/behaviors/colorBySpeed.ts index 73adf0133..885f57d3b 100644 --- a/tools/src/effect/behaviors/colorBySpeed.ts +++ b/tools/src/effect/behaviors/colorBySpeed.ts @@ -1,5 +1,5 @@ import { SolidParticle, Particle, Vector3 } from "babylonjs"; -import type { IIColorBySpeedBehavior } from "../types/behaviors"; +import type { IColorBySpeedBehavior } from "../types/behaviors"; import { interpolateColorKeys } from "./utils"; import { ValueUtils } from "../utils/valueParser"; diff --git a/tools/src/effect/behaviors/colorOverLife.ts b/tools/src/effect/behaviors/colorOverLife.ts index 18a6344a1..82113975f 100644 --- a/tools/src/effect/behaviors/colorOverLife.ts +++ b/tools/src/effect/behaviors/colorOverLife.ts @@ -1,5 +1,5 @@ import { Color4 } from "babylonjs"; -import type { IIColorOverLifeBehavior } from "../types/behaviors"; +import type { IColorOverLifeBehavior } from "../types/behaviors"; import { extractColorFromValue, extractAlphaFromValue } from "./utils"; import type { EffectSolidParticleSystem } from "../systems/effectSolidParticleSystem"; import type { EffectParticleSystem } from "../systems/effectParticleSystem"; diff --git a/tools/src/effect/behaviors/forceOverLife.ts b/tools/src/effect/behaviors/forceOverLife.ts index 52f4b0164..488e03bb4 100644 --- a/tools/src/effect/behaviors/forceOverLife.ts +++ b/tools/src/effect/behaviors/forceOverLife.ts @@ -1,5 +1,5 @@ import { Vector3 } from "babylonjs"; -import type { IIForceOverLifeBehavior, IIGravityForceBehavior } from "../types/behaviors"; +import type { IForceOverLifeBehavior, IGravityForceBehavior } from "../types/behaviors"; import { ValueUtils } from "../utils/valueParser"; import type { EffectParticleSystem } from "../systems/effectParticleSystem"; /** diff --git a/tools/src/effect/behaviors/limitSpeedOverLife.ts b/tools/src/effect/behaviors/limitSpeedOverLife.ts index b5efc92a1..2b3ae2f72 100644 --- a/tools/src/effect/behaviors/limitSpeedOverLife.ts +++ b/tools/src/effect/behaviors/limitSpeedOverLife.ts @@ -1,4 +1,4 @@ -import type { IILimitSpeedOverLifeBehavior } from "../types/behaviors"; +import type { ILimitSpeedOverLifeBehavior } from "../types/behaviors"; import { extractNumberFromValue } from "./utils"; import { ValueUtils } from "../utils/valueParser"; import type { EffectSolidParticleSystem } from "../systems/effectSolidParticleSystem"; diff --git a/tools/src/effect/behaviors/orbitOverLife.ts b/tools/src/effect/behaviors/orbitOverLife.ts index cded859c6..32321fd1f 100644 --- a/tools/src/effect/behaviors/orbitOverLife.ts +++ b/tools/src/effect/behaviors/orbitOverLife.ts @@ -1,5 +1,5 @@ import { Particle, SolidParticle } from "babylonjs"; -import type { IIOrbitOverLifeBehavior } from "../types/behaviors"; +import type { IOrbitOverLifeBehavior } from "../types/behaviors"; import { extractNumberFromValue, interpolateGradientKeys } from "./utils"; import { ValueUtils } from "../utils/valueParser"; import type { Value } from "../types/values"; diff --git a/tools/src/effect/behaviors/rotationBySpeed.ts b/tools/src/effect/behaviors/rotationBySpeed.ts index 0686753ea..b6fe763dd 100644 --- a/tools/src/effect/behaviors/rotationBySpeed.ts +++ b/tools/src/effect/behaviors/rotationBySpeed.ts @@ -1,5 +1,5 @@ import { Particle, SolidParticle, Vector3 } from "babylonjs"; -import type { IIRotationBySpeedBehavior } from "../types/behaviors"; +import type { IRotationBySpeedBehavior } from "../types/behaviors"; import { extractNumberFromValue, interpolateGradientKeys } from "./utils"; import { ValueUtils } from "../utils/valueParser"; import { ParticleWithSystem, SolidParticleWithSystem } from "../types/system"; diff --git a/tools/src/effect/behaviors/rotationOverLife.ts b/tools/src/effect/behaviors/rotationOverLife.ts index 6992207c7..2fc38e86e 100644 --- a/tools/src/effect/behaviors/rotationOverLife.ts +++ b/tools/src/effect/behaviors/rotationOverLife.ts @@ -1,4 +1,4 @@ -import type { IIRotationOverLifeBehavior } from "../types/behaviors"; +import type { IRotationOverLifeBehavior } from "../types/behaviors"; import { ValueUtils } from "../utils/valueParser"; import { extractNumberFromValue } from "./utils"; import type { EffectSolidParticleSystem } from "../systems/effectSolidParticleSystem"; diff --git a/tools/src/effect/behaviors/sizeBySpeed.ts b/tools/src/effect/behaviors/sizeBySpeed.ts index ea88b6396..5dffd1496 100644 --- a/tools/src/effect/behaviors/sizeBySpeed.ts +++ b/tools/src/effect/behaviors/sizeBySpeed.ts @@ -1,5 +1,5 @@ import { Particle, SolidParticle, Vector3 } from "babylonjs"; -import type { IISizeBySpeedBehavior } from "../types/behaviors"; +import type { ISizeBySpeedBehavior } from "../types/behaviors"; import { extractNumberFromValue, interpolateGradientKeys } from "./utils"; import { ValueUtils } from "../utils/valueParser"; diff --git a/tools/src/effect/behaviors/sizeOverLife.ts b/tools/src/effect/behaviors/sizeOverLife.ts index 20c151871..d43dba19c 100644 --- a/tools/src/effect/behaviors/sizeOverLife.ts +++ b/tools/src/effect/behaviors/sizeOverLife.ts @@ -1,4 +1,4 @@ -import type { IISizeOverLifeBehavior } from "../types/behaviors"; +import type { ISizeOverLifeBehavior } from "../types/behaviors"; import { extractNumberFromValue } from "./utils"; import type { EffectSolidParticleSystem } from "../systems/effectSolidParticleSystem"; import type { EffectParticleSystem } from "../systems/effectParticleSystem"; diff --git a/tools/src/effect/behaviors/speedOverLife.ts b/tools/src/effect/behaviors/speedOverLife.ts index 095744e5f..8eeb4c87e 100644 --- a/tools/src/effect/behaviors/speedOverLife.ts +++ b/tools/src/effect/behaviors/speedOverLife.ts @@ -1,4 +1,4 @@ -import type { IISpeedOverLifeBehavior } from "../types/behaviors"; +import type { ISpeedOverLifeBehavior } from "../types/behaviors"; import { extractNumberFromValue } from "./utils"; import { ValueUtils } from "../utils/valueParser"; import type { EffectSolidParticleSystem } from "../systems/effectSolidParticleSystem"; diff --git a/tools/src/effect/effect.ts b/tools/src/effect/effect.ts index 3bf542455..102e1af09 100644 --- a/tools/src/effect/effect.ts +++ b/tools/src/effect/effect.ts @@ -1,5 +1,5 @@ import { Scene, Tools, IDisposable, TransformNode, Vector3, CreatePlane, MeshBuilder, Texture } from "babylonjs"; -import type { QuarksJSON } from "./types/quarksTypes"; +import type { IQuarksJSON } from "./types/quarksTypes"; import type { ILoaderOptions } from "./types/loader"; import { Parser } from "./parsers/parser"; import { EffectParticleSystem } from "./systems/effectParticleSystem"; @@ -111,7 +111,7 @@ export class Effect implements IDisposable { * @param options Optional parsing options * @returns A Effect containing all particle systems */ - public static Parse(jsonData: QuarksJSON, scene: Scene, rootUrl: string = "", options?: ILoaderOptions): Effect { + public static Parse(jsonData: IQuarksJSON, scene: Scene, rootUrl: string = "", options?: ILoaderOptions): Effect { return new Effect(jsonData, scene, rootUrl, options); } @@ -122,7 +122,7 @@ export class Effect implements IDisposable { * @param rootUrl Root URL for loading textures * @param options Optional parsing options */ - constructor(jsonData?: QuarksJSON, scene?: Scene, rootUrl: string = "", options?: ILoaderOptions) { + constructor(jsonData?: IQuarksJSON, scene?: Scene, rootUrl: string = "", options?: ILoaderOptions) { this._scene = scene || null; if (jsonData && scene) { const parser = new Parser(scene, rootUrl, jsonData, options); From 8ca851352f57a523901599b8b5e8ce2b73fa4e51 Mon Sep 17 00:00:00 2001 From: Mikalai Lazitski Date: Tue, 16 Dec 2025 17:27:17 +0300 Subject: [PATCH 41/62] refactor: update type definitions across effect components to use prefixed interfaces, enhancing code clarity and consistency --- .../effect-editor/editors/rotation.tsx | 4 +- .../windows/effect-editor/editors/value.tsx | 4 +- .../editor/windows/effect-editor/graph.tsx | 52 +++++++++---------- .../editor/windows/effect-editor/preview.tsx | 10 ++-- .../effect-editor/properties/behaviors.tsx | 4 +- .../effect-editor/properties/emission.tsx | 12 ++--- .../properties/initialization.tsx | 4 +- .../effect-editor/properties/object.tsx | 4 +- .../effect-editor/properties/renderer.tsx | 4 +- .../windows/effect-editor/properties/tab.tsx | 4 +- 10 files changed, 51 insertions(+), 51 deletions(-) diff --git a/editor/src/editor/windows/effect-editor/editors/rotation.tsx b/editor/src/editor/windows/effect-editor/editors/rotation.tsx index 2997c9d13..267b7dd09 100644 --- a/editor/src/editor/windows/effect-editor/editors/rotation.tsx +++ b/editor/src/editor/windows/effect-editor/editors/rotation.tsx @@ -3,10 +3,10 @@ import { ReactNode } from "react"; import { EditorInspectorListField } from "../../../layout/inspector/fields/list"; import { EditorInspectorBlockField } from "../../../layout/inspector/fields/block"; -import { type Rotation, type EulerRotation, type AxisAngleRotation, type RandomQuatRotation, ValueUtils, type Value } from "babylonjs-editor-tools"; +import { type Rotation, type IEulerRotation, type IAxisAngleRotation, type IRandomQuatRotation, ValueUtils, type Value } from "babylonjs-editor-tools"; import { EffectValueEditor } from "./value"; -export type EffectRotationType = EulerRotation["type"] | AxisAngleRotation["type"] | RandomQuatRotation["type"]; +export type EffectRotationType = IEulerRotation["type"] | IAxisAngleRotation["type"] | IRandomQuatRotation["type"]; export interface IEffectRotationEditorProps { value: Rotation | undefined; diff --git a/editor/src/editor/windows/effect-editor/editors/value.tsx b/editor/src/editor/windows/effect-editor/editors/value.tsx index 9140053eb..c299850d5 100644 --- a/editor/src/editor/windows/effect-editor/editors/value.tsx +++ b/editor/src/editor/windows/effect-editor/editors/value.tsx @@ -4,13 +4,13 @@ import { EditorInspectorNumberField } from "../../../layout/inspector/fields/num import { EditorInspectorListField } from "../../../layout/inspector/fields/list"; import { EditorInspectorBlockField } from "../../../layout/inspector/fields/block"; -import { type Value, type ConstantValue, type IntervalValue, ValueUtils } from "babylonjs-editor-tools"; +import { type Value, type IConstantValue, type IIntervalValue, ValueUtils } from "babylonjs-editor-tools"; type PiecewiseBezier = Extract; import { BezierEditor } from "./bezier"; // Vec3Function is a custom editor extension, not part of the core Value type -export type EffectValueType = ConstantValue["type"] | IntervalValue["type"] | PiecewiseBezier["type"] | "Vec3Function"; +export type EffectValueType = IConstantValue["type"] | IIntervalValue["type"] | PiecewiseBezier["type"] | "Vec3Function"; export interface IVec3Function { type: "Vec3Function"; diff --git a/editor/src/editor/windows/effect-editor/graph.tsx b/editor/src/editor/windows/effect-editor/graph.tsx index 27691d0aa..ab51dbd76 100644 --- a/editor/src/editor/windows/effect-editor/graph.tsx +++ b/editor/src/editor/windows/effect-editor/graph.tsx @@ -19,7 +19,7 @@ import { IEffectEditor } from "."; import { saveSingleFileDialog } from "../../../tools/dialog"; import { writeJSON } from "fs-extra"; import { toast } from "sonner"; -import { Effect, type EffectNode } from "babylonjs-editor-tools"; +import { Effect, type IEffectNode } from "babylonjs-editor-tools"; export interface IEffectEditorGraphProps { filePath: string | null; @@ -28,7 +28,7 @@ export interface IEffectEditorGraphProps { } export interface IEffectEditorGraphState { - nodes: TreeNodeInfo[]; + nodes: TreeNodeInfo[]; selectedNodeId: string | number | null; } @@ -42,7 +42,7 @@ interface IEffectInfo { export class EffectEditorGraph extends Component { private _effects: Map = new Map(); /** Map of node instances to unique IDs for tree nodes */ - private _nodeIdMap: Map = new Map(); + private _nodeIdMap: Map = new Map(); public constructor(props: IEffectEditorGraphProps) { super(props); @@ -79,7 +79,7 @@ export class EffectEditorGraph extends Component[], nodeId: string | number): TreeNodeInfo | null { + private _findNodeById(nodes: TreeNodeInfo[], nodeId: string | number): TreeNodeInfo | null { for (const node of nodes) { if (node.id === nodeId) { return node; @@ -97,7 +97,7 @@ export class EffectEditorGraph extends Component[] = []; + const nodes: TreeNodeInfo[] = []; for (const [effectId, effectInfo] of this._effects.entries()) { if (effectInfo.effect.root) { @@ -181,7 +181,7 @@ export class EffectEditorGraph extends Component { + private _convertNodeToTreeNode(Node: IEffectNode, isEffectRoot: boolean = false): TreeNodeInfo { // Always use unique ID instead of uuid or name const nodeId = this._generateUniqueNodeId(Node); const childNodes = Node.children.length > 0 ? Node.children.map((child) => this._convertNodeToTreeNode(child, false)) : undefined; @@ -230,7 +230,7 @@ export class EffectEditorGraph extends Component[]): TreeNodeInfo[] { + private _updateAllNodeNames(nodes: TreeNodeInfo[]): TreeNodeInfo[] { return nodes.map((n) => { const nodeName = n.nodeData?.name || "Unknown"; const childNodes = n.childNodes ? this._updateAllNodeNames(n.childNodes) : undefined; @@ -295,19 +295,19 @@ export class EffectEditorGraph extends Component): void { + private _handleNodeExpanded(node: TreeNodeInfo): void { const nodeId = node.id; const nodes = this._updateNodeExpanded(this.state.nodes, nodeId as string | number, true); this.setState({ nodes }); } - private _handleNodeCollapsed(node: TreeNodeInfo): void { + private _handleNodeCollapsed(node: TreeNodeInfo): void { const nodeId = node.id; const nodes = this._updateNodeExpanded(this.state.nodes, nodeId as string | number, false); this.setState({ nodes }); } - private _updateNodeExpanded(nodes: TreeNodeInfo[], nodeId: string | number, isExpanded: boolean): TreeNodeInfo[] { + private _updateNodeExpanded(nodes: TreeNodeInfo[], nodeId: string | number, isExpanded: boolean): TreeNodeInfo[] { return nodes.map((n) => { const nodeName = n.nodeData?.name || "Unknown"; if (n.id === nodeId) { @@ -327,14 +327,14 @@ export class EffectEditorGraph extends Component): void { + private _handleNodeClicked(node: TreeNodeInfo): void { const selectedId = node.id as string | number; const nodes = this._updateNodeSelection(this.state.nodes, selectedId); this.setState({ nodes, selectedNodeId: selectedId }); this.props.onNodeSelected?.(selectedId); } - private _updateNodeSelection(nodes: TreeNodeInfo[], selectedId: string | number): TreeNodeInfo[] { + private _updateNodeSelection(nodes: TreeNodeInfo[], selectedId: string | number): TreeNodeInfo[] { return nodes.map((n) => { const nodeName = n.nodeData?.name || "Unknown"; const isSelected = n.id === selectedId; @@ -348,7 +348,7 @@ export class EffectEditorGraph extends Component, name: string): JSX.Element { + private _getNodeLabelComponent(node: TreeNodeInfo, name: string): JSX.Element { const label = (
): boolean { + private _isEffectRootNode(node: TreeNodeInfo): boolean { const nodeData = node.nodeData; if (!nodeData || !nodeData.uuid) { return false; @@ -444,7 +444,7 @@ export class EffectEditorGraph extends Component): Promise { + private async _handleExportEffect(node: TreeNodeInfo): Promise { const nodeData = node.nodeData; if (!nodeData || !nodeData.uuid) { return; @@ -508,7 +508,7 @@ export class EffectEditorGraph extends Component): Effect | null { + private _findEffectForNode(node: TreeNodeInfo): Effect | null { // Find the effect that contains this node by traversing up the tree const nodeData = node.nodeData; if (!nodeData) { @@ -528,7 +528,7 @@ export class EffectEditorGraph extends Component { + const findNodeInHierarchy = (current: IEffectNode): boolean => { // Use instance comparison and uuid for matching if (current === nodeData || (current.uuid && nodeData.uuid && current.uuid === nodeData.uuid)) { return true; @@ -550,7 +550,7 @@ export class EffectEditorGraph extends Component, systemType: "solid" | "base"): void { + private _handleAddParticleSystemToNode(node: TreeNodeInfo, systemType: "solid" | "base"): void { const effect = this._findEffectForNode(node); if (!effect) { console.error("No effect found for node"); @@ -570,7 +570,7 @@ export class EffectEditorGraph extends Component): void { + private _handleAddGroupToNode(node: TreeNodeInfo): void { const effect = this._findEffectForNode(node); if (!effect) { console.error("No effect found for node"); @@ -604,7 +604,7 @@ export class EffectEditorGraph extends Component, ev: React.DragEvent): void { + private _handleDropOnNode(node: TreeNodeInfo, ev: React.DragEvent): void { ev.preventDefault(); ev.stopPropagation(); @@ -626,7 +626,7 @@ export class EffectEditorGraph extends Component[], parentId: string | number, newNode: TreeNodeInfo): TreeNodeInfo[] { + private _addNodeToParent(nodes: TreeNodeInfo[], parentId: string | number, newNode: TreeNodeInfo): TreeNodeInfo[] { return nodes.map((n) => { if (n.id === parentId) { const childNodes = n.childNodes || []; @@ -647,7 +647,7 @@ export class EffectEditorGraph extends Component): void { + private _handleDeleteNode(node: TreeNodeInfo): void { const nodeData = node.nodeData; if (!nodeData) { return; @@ -671,7 +671,7 @@ export class EffectEditorGraph extends Component { + const removeNodeFromHierarchy = (current: IEffectNode): boolean => { // Remove from children const index = current.children.findIndex((child) => child === nodeData || child.uuid === nodeData.uuid || child.name === nodeData.name); if (index !== -1) { diff --git a/editor/src/editor/windows/effect-editor/preview.tsx b/editor/src/editor/windows/effect-editor/preview.tsx index 825b6c22f..25e8d3cb9 100644 --- a/editor/src/editor/windows/effect-editor/preview.tsx +++ b/editor/src/editor/windows/effect-editor/preview.tsx @@ -7,7 +7,7 @@ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "../../ import { IoPlay, IoStop, IoRefresh } from "react-icons/io5"; import type { IEffectEditor } from "."; -import { Effect, EffectNode } from "babylonjs-editor-tools"; +import { Effect, type IEffectNode } from "babylonjs-editor-tools"; export interface IEffectEditorPreviewProps { filePath: string | null; @@ -123,7 +123,7 @@ export class EffectEditorPreview extends Component { + const findNode = (current: IEffectNode): boolean => { if (current === node || current.uuid === node.uuid || current.name === node.name) { return true; } @@ -160,7 +160,7 @@ export class EffectEditorPreview extends Component void; } diff --git a/editor/src/editor/windows/effect-editor/properties/emission.tsx b/editor/src/editor/windows/effect-editor/properties/emission.tsx index f8084539f..2f33871db 100644 --- a/editor/src/editor/windows/effect-editor/properties/emission.tsx +++ b/editor/src/editor/windows/effect-editor/properties/emission.tsx @@ -9,18 +9,18 @@ import { EditorInspectorBlockField } from "../../../layout/inspector/fields/bloc import { EditorInspectorSectionField } from "../../../layout/inspector/fields/section"; import { - type EffectNode, + type IEffectNode, + type IEmissionBurst, EffectSolidParticleSystem, EffectParticleSystem, SolidSphereParticleEmitter, SolidConeParticleEmitter, - EmissionBurst, Value, } from "babylonjs-editor-tools"; import { EffectValueEditor } from "../editors/value"; export interface IEffectEditorEmissionPropertiesProps { - nodeData: EffectNode; + nodeData: IEffectNode; onChange: () => void; } @@ -252,7 +252,7 @@ function renderParticleSystemEmitter(system: EffectParticleSystem, onChange: () /** * Renders emitter shape properties */ -function renderEmitterShape(nodeData: EffectNode, onChange: () => void): ReactNode { +function renderEmitterShape(nodeData: IEffectNode, onChange: () => void): ReactNode { if (nodeData.type !== "particle" || !nodeData.system) { return null; } @@ -274,7 +274,7 @@ function renderEmitterShape(nodeData: EffectNode, onChange: () => void): ReactNo * Renders emission bursts */ function renderBursts(system: EffectParticleSystem | EffectSolidParticleSystem, onChange: () => void): ReactNode { - const bursts: (EmissionBurst & { cycle?: number; interval?: number; probability?: number })[] = Array.isArray((system as any).emissionBursts) + const bursts: (IEmissionBurst & { cycle?: number; interval?: number; probability?: number })[] = Array.isArray((system as any).emissionBursts) ? (system as any).emissionBursts : []; @@ -341,7 +341,7 @@ function renderBursts(system: EffectParticleSystem | EffectSolidParticleSystem, /** * Renders emission parameters (looping, duration, emit over time/distance, bursts) */ -function renderEmissionParameters(nodeData: EffectNode, onChange: () => void): ReactNode { +function renderEmissionParameters(nodeData: IEffectNode, onChange: () => void): ReactNode { if (nodeData.type !== "particle" || !nodeData.system) { return null; } diff --git a/editor/src/editor/windows/effect-editor/properties/initialization.tsx b/editor/src/editor/windows/effect-editor/properties/initialization.tsx index 0192f615c..4e938e758 100644 --- a/editor/src/editor/windows/effect-editor/properties/initialization.tsx +++ b/editor/src/editor/windows/effect-editor/properties/initialization.tsx @@ -2,13 +2,13 @@ import { ReactNode } from "react"; import { EditorInspectorBlockField } from "../../../layout/inspector/fields/block"; -import { type EffectNode, EffectSolidParticleSystem, EffectParticleSystem, ValueUtils, Value, Color, Rotation } from "babylonjs-editor-tools"; +import { type IEffectNode, EffectSolidParticleSystem, EffectParticleSystem, ValueUtils, Value, Color, Rotation } from "babylonjs-editor-tools"; import { EffectValueEditor, type IVec3Function } from "../editors/value"; import { EffectColorEditor } from "../editors/color"; import { EffectRotationEditor } from "../editors/rotation"; export interface IEffectEditorParticleInitializationPropertiesProps { - nodeData: EffectNode; + nodeData: IEffectNode; onChange?: () => void; } diff --git a/editor/src/editor/windows/effect-editor/properties/object.tsx b/editor/src/editor/windows/effect-editor/properties/object.tsx index 00b79ff3f..f48fbfd96 100644 --- a/editor/src/editor/windows/effect-editor/properties/object.tsx +++ b/editor/src/editor/windows/effect-editor/properties/object.tsx @@ -5,10 +5,10 @@ import { EditorInspectorStringField } from "../../../layout/inspector/fields/str import { EditorInspectorVectorField } from "../../../layout/inspector/fields/vector"; import { EditorInspectorSwitchField } from "../../../layout/inspector/fields/switch"; -import { type EffectNode, EffectSolidParticleSystem, EffectParticleSystem } from "babylonjs-editor-tools"; +import { type IEffectNode, EffectSolidParticleSystem, EffectParticleSystem } from "babylonjs-editor-tools"; export interface IEffectEditorObjectPropertiesProps { - nodeData: EffectNode; + nodeData: IEffectNode; onChange?: () => void; } diff --git a/editor/src/editor/windows/effect-editor/properties/renderer.tsx b/editor/src/editor/windows/effect-editor/properties/renderer.tsx index 4bf50a0e8..ed2b29cc1 100644 --- a/editor/src/editor/windows/effect-editor/properties/renderer.tsx +++ b/editor/src/editor/windows/effect-editor/properties/renderer.tsx @@ -23,13 +23,13 @@ import { EditorCellMaterialInspector } from "../../../layout/inspector/material/ import { EditorFireMaterialInspector } from "../../../layout/inspector/material/fire"; import { EditorGradientMaterialInspector } from "../../../layout/inspector/material/gradient"; -import { type EffectNode, EffectSolidParticleSystem, EffectParticleSystem } from "babylonjs-editor-tools"; +import { type IEffectNode, EffectSolidParticleSystem, EffectParticleSystem } from "babylonjs-editor-tools"; import { IEffectEditor } from ".."; import { EffectValueEditor } from "../editors/value"; import { CellMaterial, FireMaterial, GradientMaterial, GridMaterial, LavaMaterial, NormalMaterial, SkyMaterial, TriPlanarMaterial, WaterMaterial } from "babylonjs-materials"; export interface IEffectEditorParticleRendererPropertiesProps { - nodeData: EffectNode; + nodeData: IEffectNode; editor: IEffectEditor; onChange: () => void; } diff --git a/editor/src/editor/windows/effect-editor/properties/tab.tsx b/editor/src/editor/windows/effect-editor/properties/tab.tsx index a6e01c9d7..a2f4144b3 100644 --- a/editor/src/editor/windows/effect-editor/properties/tab.tsx +++ b/editor/src/editor/windows/effect-editor/properties/tab.tsx @@ -1,5 +1,5 @@ import { Component, ReactNode } from "react"; -import type { EffectNode } from "babylonjs-editor-tools"; +import type { IEffectNode } from "babylonjs-editor-tools"; import { IEffectEditor } from ".."; import { EffectEditorObjectProperties } from "./object"; import { EffectEditorParticleRendererProperties } from "./renderer"; @@ -13,7 +13,7 @@ export interface IEffectEditorPropertiesTabProps { editor: IEffectEditor; tabType: "object" | "emission" | "renderer" | "initialization" | "behaviors"; onNameChanged?: () => void; - getNodeData: (nodeId: string | number) => EffectNode | null; + getNodeData: (nodeId: string | number) => IEffectNode | null; } export class EffectEditorPropertiesTab extends Component { From 86799c35cefab88e1b0ebc70a369b2991379046f Mon Sep 17 00:00:00 2001 From: Mikalai Lazitski Date: Thu, 18 Dec 2025 09:40:27 +0300 Subject: [PATCH 42/62] refactor: transition emitter configuration to prefixed interfaces, enhancing type clarity and consistency across effect systems and parsers --- tools/src/effect/bjs/particle.ts | 352 +++ tools/src/effect/bjs/particleHelper.ts | 230 ++ .../effect/bjs/particleSystem.functions.ts | 95 + tools/src/effect/bjs/particleSystem.ts | 1269 +++++++++ tools/src/effect/bjs/subEmitter.ts | 134 + .../effect/bjs/thinParticleSystem.function.ts | 427 +++ tools/src/effect/bjs/thinParticleSystem.ts | 2422 +++++++++++++++++ tools/src/effect/effect.ts | 124 +- tools/src/effect/factories/emitterFactory.ts | 176 -- tools/src/effect/factories/index.ts | 1 - tools/src/effect/factories/systemFactory.ts | 338 ++- tools/src/effect/parsers/dataConverter.ts | 40 +- .../effect/systems/effectParticleSystem.ts | 382 +-- .../systems/effectSolidParticleSystem.ts | 751 ++--- tools/src/effect/types/emitter.ts | 79 +- tools/src/effect/types/hierarchy.ts | 4 +- 16 files changed, 5690 insertions(+), 1134 deletions(-) create mode 100644 tools/src/effect/bjs/particle.ts create mode 100644 tools/src/effect/bjs/particleHelper.ts create mode 100644 tools/src/effect/bjs/particleSystem.functions.ts create mode 100644 tools/src/effect/bjs/particleSystem.ts create mode 100644 tools/src/effect/bjs/subEmitter.ts create mode 100644 tools/src/effect/bjs/thinParticleSystem.function.ts create mode 100644 tools/src/effect/bjs/thinParticleSystem.ts delete mode 100644 tools/src/effect/factories/emitterFactory.ts diff --git a/tools/src/effect/bjs/particle.ts b/tools/src/effect/bjs/particle.ts new file mode 100644 index 000000000..fe8158791 --- /dev/null +++ b/tools/src/effect/bjs/particle.ts @@ -0,0 +1,352 @@ +import type { Nullable } from "../types"; +import { Vector2, Vector3, TmpVectors, Vector4 } from "../Maths/math.vector"; +import { Color4 } from "../Maths/math.color"; +import type { SubEmitter } from "./subEmitter"; +import type { ColorGradient, FactorGradient } from "../Misc/gradients"; + +import type { AbstractMesh } from "../Meshes/abstractMesh"; +import type { ThinParticleSystem } from "./thinParticleSystem"; +import { Clamp } from "../Maths/math.scalar.functions"; + +/** + * A particle represents one of the element emitted by a particle system. + * This is mainly define by its coordinates, direction, velocity and age. + */ +export class Particle { + private static _Count = 0; + /** + * Unique ID of the particle + */ + public id: number; + /** + * The world position of the particle in the scene. + */ + public position = Vector3.Zero(); + + /** + * The world direction of the particle in the scene. + */ + public direction = Vector3.Zero(); + + /** + * The color of the particle. + */ + public color = new Color4(0, 0, 0, 0); + + /** + * The color change of the particle per step. + */ + public colorStep = new Color4(0, 0, 0, 0); + + /** + * The creation color of the particle. + */ + public initialColor = new Color4(0, 0, 0, 0); + + /** + * The color used when the end of life of the particle. + */ + public colorDead = new Color4(0, 0, 0, 0); + + /** + * Defines how long will the life of the particle be. + */ + public lifeTime = 1.0; + + /** + * The current age of the particle. + */ + public age = 0; + + /** + * The current size of the particle. + */ + public size = 0; + + /** + * The current scale of the particle. + */ + public scale = new Vector2(1, 1); + + /** + * The current angle of the particle. + */ + public angle = 0; + + /** + * Defines how fast is the angle changing. + */ + public angularSpeed = 0; + + /** + * Defines the cell index used by the particle to be rendered from a sprite. + */ + public cellIndex: number = 0; + + /** + * The information required to support color remapping + */ + public remapData: Vector4; + + /** @internal */ + public _randomCellOffset?: number; + + /** @internal */ + public _initialDirection: Nullable; + + /** @internal */ + public _attachedSubEmitters: Nullable> = null; + + /** @internal */ + public _initialStartSpriteCellId: number; + /** @internal */ + public _initialEndSpriteCellId: number; + /** @internal */ + public _initialSpriteCellLoop: boolean; + + /** @internal */ + public _currentColorGradient: Nullable; + /** @internal */ + public _currentColor1 = new Color4(0, 0, 0, 0); + /** @internal */ + public _currentColor2 = new Color4(0, 0, 0, 0); + + /** @internal */ + public _currentSizeGradient: Nullable; + /** @internal */ + public _currentSize1 = 0; + /** @internal */ + public _currentSize2 = 0; + + /** @internal */ + public _currentAngularSpeedGradient: Nullable; + /** @internal */ + public _currentAngularSpeed1 = 0; + /** @internal */ + public _currentAngularSpeed2 = 0; + + /** @internal */ + public _currentVelocityGradient: Nullable; + /** @internal */ + public _currentVelocity1 = 0; + /** @internal */ + public _currentVelocity2 = 0; + + /** @internal */ + public _directionScale: number; + + /** @internal */ + public _scaledDirection = Vector3.Zero(); + + /** @internal */ + public _currentLimitVelocityGradient: Nullable; + /** @internal */ + public _currentLimitVelocity1 = 0; + /** @internal */ + public _currentLimitVelocity2 = 0; + + /** @internal */ + public _currentDragGradient: Nullable; + /** @internal */ + public _currentDrag1 = 0; + /** @internal */ + public _currentDrag2 = 0; + + /** @internal */ + public _randomNoiseCoordinates1: Nullable; + /** @internal */ + public _randomNoiseCoordinates2: Nullable; + + /** @internal */ + public _localPosition?: Vector3; + + /** + * Creates a new instance Particle + * @param particleSystem the particle system the particle belongs to + */ + constructor( + /** + * The particle system the particle belongs to. + */ + public particleSystem: ThinParticleSystem + ) { + this.id = Particle._Count++; + if (!this.particleSystem.isAnimationSheetEnabled) { + return; + } + + this._updateCellInfoFromSystem(); + } + + private _updateCellInfoFromSystem(): void { + this.cellIndex = this.particleSystem.startSpriteCellID; + } + + /** + * Defines how the sprite cell index is updated for the particle + */ + public updateCellIndex(): void { + let offsetAge = this.age; + let changeSpeed = this.particleSystem.spriteCellChangeSpeed; + + if (this.particleSystem.spriteRandomStartCell) { + if (this._randomCellOffset === undefined) { + this._randomCellOffset = Math.random() * this.lifeTime; + } + + if (changeSpeed === 0) { + // Special case when speed = 0 meaning we want to stay on initial cell + changeSpeed = 1; + offsetAge = this._randomCellOffset; + } else { + offsetAge += this._randomCellOffset; + } + } + + const dist = this._initialEndSpriteCellId - this._initialStartSpriteCellId + 1; + let ratio: number; + if (this._initialSpriteCellLoop) { + ratio = Clamp(((offsetAge * changeSpeed) % this.lifeTime) / this.lifeTime); + } else { + ratio = Clamp((offsetAge * changeSpeed) / this.lifeTime); + } + this.cellIndex = (this._initialStartSpriteCellId + ratio * dist) | 0; + } + + /** + * @internal + */ + public _inheritParticleInfoToSubEmitter(subEmitter: SubEmitter) { + if ((subEmitter.particleSystem.emitter).position) { + const emitterMesh = subEmitter.particleSystem.emitter; + emitterMesh.position.copyFrom(this.position); + if (subEmitter.inheritDirection) { + const temp = TmpVectors.Vector3[0]; + this.direction.normalizeToRef(temp); + emitterMesh.setDirection(temp, 0, Math.PI / 2); + } + } else { + const emitterPosition = subEmitter.particleSystem.emitter; + emitterPosition.copyFrom(this.position); + } + // Set inheritedVelocityOffset to be used when new particles are created + this.direction.scaleToRef(subEmitter.inheritedVelocityAmount / 2, TmpVectors.Vector3[0]); + subEmitter.particleSystem._inheritedVelocityOffset.copyFrom(TmpVectors.Vector3[0]); + } + + /** @internal */ + public _inheritParticleInfoToSubEmitters() { + if (this._attachedSubEmitters && this._attachedSubEmitters.length > 0) { + for (const subEmitter of this._attachedSubEmitters) { + this._inheritParticleInfoToSubEmitter(subEmitter); + } + } + } + + /** @internal */ + public _reset() { + this.age = 0; + this.id = Particle._Count++; + this._currentColorGradient = null; + this._currentSizeGradient = null; + this._currentAngularSpeedGradient = null; + this._currentVelocityGradient = null; + this._currentLimitVelocityGradient = null; + this._currentDragGradient = null; + this.cellIndex = this.particleSystem.startSpriteCellID; + this._randomCellOffset = undefined; + this._randomNoiseCoordinates1 = null; + this._randomNoiseCoordinates2 = null; + } + + /** + * Copy the properties of particle to another one. + * @param other the particle to copy the information to. + */ + public copyTo(other: Particle) { + other.position.copyFrom(this.position); + if (this._initialDirection) { + if (other._initialDirection) { + other._initialDirection.copyFrom(this._initialDirection); + } else { + other._initialDirection = this._initialDirection.clone(); + } + } else { + other._initialDirection = null; + } + other.direction.copyFrom(this.direction); + if (this._localPosition) { + if (other._localPosition) { + other._localPosition.copyFrom(this._localPosition); + } else { + other._localPosition = this._localPosition.clone(); + } + } + other.color.copyFrom(this.color); + other.colorStep.copyFrom(this.colorStep); + other.initialColor.copyFrom(this.initialColor); + other.colorDead.copyFrom(this.colorDead); + other.lifeTime = this.lifeTime; + other.age = this.age; + other._randomCellOffset = this._randomCellOffset; + other.size = this.size; + other.scale.copyFrom(this.scale); + other.angle = this.angle; + other.angularSpeed = this.angularSpeed; + other.particleSystem = this.particleSystem; + other.cellIndex = this.cellIndex; + other.id = this.id; + other._attachedSubEmitters = this._attachedSubEmitters; + if (this._currentColorGradient) { + other._currentColorGradient = this._currentColorGradient; + other._currentColor1.copyFrom(this._currentColor1); + other._currentColor2.copyFrom(this._currentColor2); + } + if (this._currentSizeGradient) { + other._currentSizeGradient = this._currentSizeGradient; + other._currentSize1 = this._currentSize1; + other._currentSize2 = this._currentSize2; + } + if (this._currentAngularSpeedGradient) { + other._currentAngularSpeedGradient = this._currentAngularSpeedGradient; + other._currentAngularSpeed1 = this._currentAngularSpeed1; + other._currentAngularSpeed2 = this._currentAngularSpeed2; + } + if (this._currentVelocityGradient) { + other._currentVelocityGradient = this._currentVelocityGradient; + other._currentVelocity1 = this._currentVelocity1; + other._currentVelocity2 = this._currentVelocity2; + } + if (this._currentLimitVelocityGradient) { + other._currentLimitVelocityGradient = this._currentLimitVelocityGradient; + other._currentLimitVelocity1 = this._currentLimitVelocity1; + other._currentLimitVelocity2 = this._currentLimitVelocity2; + } + if (this._currentDragGradient) { + other._currentDragGradient = this._currentDragGradient; + other._currentDrag1 = this._currentDrag1; + other._currentDrag2 = this._currentDrag2; + } + if (this.particleSystem.isAnimationSheetEnabled) { + other._initialStartSpriteCellId = this._initialStartSpriteCellId; + other._initialEndSpriteCellId = this._initialEndSpriteCellId; + other._initialSpriteCellLoop = this._initialSpriteCellLoop; + } + if (this.particleSystem.useRampGradients) { + if (other.remapData && this.remapData) { + other.remapData.copyFrom(this.remapData); + } else { + other.remapData = new Vector4(0, 0, 0, 0); + } + } + if (this._randomNoiseCoordinates1 && this._randomNoiseCoordinates2) { + if (other._randomNoiseCoordinates1 && other._randomNoiseCoordinates2) { + other._randomNoiseCoordinates1.copyFrom(this._randomNoiseCoordinates1); + other._randomNoiseCoordinates2.copyFrom(this._randomNoiseCoordinates2); + } else { + other._randomNoiseCoordinates1 = this._randomNoiseCoordinates1.clone(); + other._randomNoiseCoordinates2 = this._randomNoiseCoordinates2.clone(); + } + } + } +} diff --git a/tools/src/effect/bjs/particleHelper.ts b/tools/src/effect/bjs/particleHelper.ts new file mode 100644 index 000000000..1b66ddb45 --- /dev/null +++ b/tools/src/effect/bjs/particleHelper.ts @@ -0,0 +1,230 @@ +import type { Nullable } from "../types"; +import type { Scene } from "../scene"; +import { Tools } from "../Misc/tools"; +import type { Vector3 } from "../Maths/math.vector"; +import { Color4 } from "../Maths/math.color"; +import type { AbstractMesh } from "../Meshes/abstractMesh"; +import { Texture } from "../Materials/Textures/texture"; +import { EngineStore } from "../Engines/engineStore"; +import type { IParticleSystem } from "./IParticleSystem"; +import { GPUParticleSystem } from "./gpuParticleSystem"; +import { ParticleSystemSet } from "./particleSystemSet"; +import { ParticleSystem } from "./particleSystem"; +import { WebRequest } from "../Misc/webRequest"; +import { Constants } from "../Engines/constants"; +/** + * This class is made for on one-liner static method to help creating particle system set. + */ +export class ParticleHelper { + /** + * Gets or sets base Assets URL + */ + public static BaseAssetsUrl = ParticleSystemSet.BaseAssetsUrl; + + /** Define the Url to load snippets */ + public static SnippetUrl = Constants.SnippetUrl; + + /** + * Create a default particle system that you can tweak + * @param emitter defines the emitter to use + * @param capacity defines the system capacity (default is 500 particles) + * @param scene defines the hosting scene + * @param useGPU defines if a GPUParticleSystem must be created (default is false) + * @returns the new Particle system + */ + public static CreateDefault(emitter: Nullable, capacity = 500, scene?: Scene, useGPU = false): IParticleSystem { + let system: IParticleSystem; + + if (useGPU) { + system = new GPUParticleSystem("default system", { capacity: capacity }, scene!); + } else { + system = new ParticleSystem("default system", capacity, scene!); + } + + system.emitter = emitter; + const textureUrl = Tools.GetAssetUrl("https://assets.babylonjs.com/core/textures/flare.png"); + system.particleTexture = new Texture(textureUrl, system.getScene()); + system.createConeEmitter(0.1, Math.PI / 4); + + // Particle color + system.color1 = new Color4(1.0, 1.0, 1.0, 1.0); + system.color2 = new Color4(1.0, 1.0, 1.0, 1.0); + system.colorDead = new Color4(1.0, 1.0, 1.0, 0.0); + + // Particle Size + system.minSize = 0.1; + system.maxSize = 0.1; + + // Emission speed + system.minEmitPower = 2; + system.maxEmitPower = 2; + + // Update speed + system.updateSpeed = 1 / 60; + + system.emitRate = 30; + + return system; + } + + /** + * This is the main static method (one-liner) of this helper to create different particle systems + * @param type This string represents the type to the particle system to create + * @param scene The scene where the particle system should live + * @param gpu If the system will use gpu + * @param capacity defines the system capacity (if null or undefined the sotred capacity will be used) + * @returns the ParticleSystemSet created + */ + // eslint-disable-next-line @typescript-eslint/promise-function-async, no-restricted-syntax + public static CreateAsync(type: string, scene: Nullable, gpu: boolean = false, capacity?: number): Promise { + if (!scene) { + scene = EngineStore.LastCreatedScene; + } + + const token = {}; + + scene!.addPendingData(token); + + return new Promise((resolve, reject) => { + if (gpu && !GPUParticleSystem.IsSupported) { + scene!.removePendingData(token); + // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors + return reject("Particle system with GPU is not supported."); + } + + Tools.LoadFile( + `${ParticleHelper.BaseAssetsUrl}/systems/${type}.json`, + (data) => { + scene!.removePendingData(token); + const newData = JSON.parse(data.toString()); + return resolve(ParticleSystemSet.Parse(newData, scene!, gpu, capacity)); + }, + undefined, + undefined, + undefined, + () => { + scene!.removePendingData(token); + // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors + return reject(`An error occurred with the creation of your particle system. Check if your type '${type}' exists.`); + } + ); + }); + } + + /** + * Static function used to export a particle system to a ParticleSystemSet variable. + * Please note that the emitter shape is not exported + * @param systems defines the particle systems to export + * @returns the created particle system set + */ + public static ExportSet(systems: IParticleSystem[]): ParticleSystemSet { + const set = new ParticleSystemSet(); + + for (const system of systems) { + set.systems.push(system); + } + + return set; + } + + /** + * Creates a particle system from a snippet saved in a remote file + * @param name defines the name of the particle system to create (can be null or empty to use the one from the json data) + * @param url defines the url to load from + * @param scene defines the hosting scene + * @param gpu If the system will use gpu + * @param rootUrl defines the root URL to use to load textures and relative dependencies + * @param capacity defines the system capacity (if null or undefined the sotred capacity will be used) + * @returns a promise that will resolve to the new particle system + */ + // eslint-disable-next-line @typescript-eslint/promise-function-async, no-restricted-syntax + public static ParseFromFileAsync(name: Nullable, url: string, scene: Scene, gpu: boolean = false, rootUrl: string = "", capacity?: number): Promise { + return new Promise((resolve, reject) => { + const request = new WebRequest(); + request.addEventListener("readystatechange", () => { + if (request.readyState == 4) { + if (request.status == 200) { + const serializationObject = JSON.parse(request.responseText); + let output: IParticleSystem; + + if (gpu) { + output = GPUParticleSystem.Parse(serializationObject, scene, rootUrl, false, capacity); + } else { + output = ParticleSystem.Parse(serializationObject, scene, rootUrl, false, capacity); + } + + if (name) { + output.name = name; + } + + resolve(output); + } else { + // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors + reject("Unable to load the particle system"); + } + } + }); + + request.open("GET", url); + request.send(); + }); + } + + /** + * Creates a particle system from a snippet saved by the particle system editor + * @param snippetId defines the snippet to load (can be set to _BLANK to create a default one) + * @param scene defines the hosting scene + * @param gpu If the system will use gpu + * @param rootUrl defines the root URL to use to load textures and relative dependencies + * @param capacity defines the system capacity (if null or undefined the sotred capacity will be used) + * @returns a promise that will resolve to the new particle system + */ + // eslint-disable-next-line @typescript-eslint/promise-function-async, no-restricted-syntax + public static ParseFromSnippetAsync(snippetId: string, scene: Scene, gpu: boolean = false, rootUrl: string = "", capacity?: number): Promise { + if (snippetId === "_BLANK") { + const system = this.CreateDefault(null); + system.start(); + return Promise.resolve(system); + } + + return new Promise((resolve, reject) => { + const request = new WebRequest(); + request.addEventListener("readystatechange", () => { + if (request.readyState == 4) { + if (request.status == 200) { + const snippet = JSON.parse(JSON.parse(request.responseText).jsonPayload); + const serializationObject = JSON.parse(snippet.particleSystem); + let output: IParticleSystem; + + if (gpu) { + output = GPUParticleSystem.Parse(serializationObject, scene, rootUrl, false, capacity); + } else { + output = ParticleSystem.Parse(serializationObject, scene, rootUrl, false, capacity); + } + output.snippetId = snippetId; + + resolve(output); + } else { + // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors + reject("Unable to load the snippet " + snippetId); + } + } + }); + + request.open("GET", this.SnippetUrl + "/" + snippetId.replace(/#/g, "/")); + request.send(); + }); + } + + /** + * Creates a particle system from a snippet saved by the particle system editor + * @deprecated Please use ParseFromSnippetAsync instead + * @param snippetId defines the snippet to load (can be set to _BLANK to create a default one) + * @param scene defines the hosting scene + * @param gpu If the system will use gpu + * @param rootUrl defines the root URL to use to load textures and relative dependencies + * @param capacity defines the system capacity (if null or undefined the sotred capacity will be used) + * @returns a promise that will resolve to the new particle system + */ + public static CreateFromSnippetAsync = ParticleHelper.ParseFromSnippetAsync; +} diff --git a/tools/src/effect/bjs/particleSystem.functions.ts b/tools/src/effect/bjs/particleSystem.functions.ts new file mode 100644 index 000000000..ff572e629 --- /dev/null +++ b/tools/src/effect/bjs/particleSystem.functions.ts @@ -0,0 +1,95 @@ +import { Vector3 } from "core/Maths/math.vector"; +import { PointParticleEmitter } from "./EmitterTypes/pointParticleEmitter"; +import { HemisphericParticleEmitter } from "./EmitterTypes/hemisphericParticleEmitter"; +import { SphereDirectedParticleEmitter, SphereParticleEmitter } from "./EmitterTypes/sphereParticleEmitter"; +import { CylinderDirectedParticleEmitter, CylinderParticleEmitter } from "./EmitterTypes/cylinderParticleEmitter"; +import { ConeDirectedParticleEmitter, ConeParticleEmitter } from "./EmitterTypes/coneParticleEmitter"; + +/** + * Creates a Point Emitter for the particle system (emits directly from the emitter position) + * @param direction1 Particles are emitted between the direction1 and direction2 from within the box + * @param direction2 Particles are emitted between the direction1 and direction2 from within the box + * @returns the emitter + */ +export function CreatePointEmitter(direction1: Vector3, direction2: Vector3): PointParticleEmitter { + const particleEmitter = new PointParticleEmitter(); + particleEmitter.direction1 = direction1; + particleEmitter.direction2 = direction2; + return particleEmitter; +} + +/** + * Creates a Hemisphere Emitter for the particle system (emits along the hemisphere radius) + * @param radius The radius of the hemisphere to emit from + * @param radiusRange The range of the hemisphere to emit from [0-1] 0 Surface Only, 1 Entire Radius + * @returns the emitter + */ +export function CreateHemisphericEmitter(radius = 1, radiusRange = 1): HemisphericParticleEmitter { + return new HemisphericParticleEmitter(radius, radiusRange); +} + +/** + * Creates a Sphere Emitter for the particle system (emits along the sphere radius) + * @param radius The radius of the sphere to emit from + * @param radiusRange The range of the sphere to emit from [0-1] 0 Surface Only, 1 Entire Radius + * @returns the emitter + */ +export function CreateSphereEmitter(radius = 1, radiusRange = 1): SphereParticleEmitter { + return new SphereParticleEmitter(radius, radiusRange); +} + +/** + * Creates a Directed Sphere Emitter for the particle system (emits between direction1 and direction2) + * @param radius The radius of the sphere to emit from + * @param direction1 Particles are emitted between the direction1 and direction2 from within the sphere + * @param direction2 Particles are emitted between the direction1 and direction2 from within the sphere + * @returns the emitter + */ +export function CreateDirectedSphereEmitter(radius = 1, direction1 = new Vector3(0, 1.0, 0), direction2 = new Vector3(0, 1.0, 0)): SphereDirectedParticleEmitter { + return new SphereDirectedParticleEmitter(radius, direction1, direction2); +} + +/** + * Creates a Cylinder Emitter for the particle system (emits from the cylinder to the particle position) + * @param radius The radius of the emission cylinder + * @param height The height of the emission cylinder + * @param radiusRange The range of emission [0-1] 0 Surface only, 1 Entire Radius + * @param directionRandomizer How much to randomize the particle direction [0-1] + * @returns the emitter + */ +export function CreateCylinderEmitter(radius = 1, height = 1, radiusRange = 1, directionRandomizer = 0): CylinderParticleEmitter { + return new CylinderParticleEmitter(radius, height, radiusRange, directionRandomizer); +} + +/** + * Creates a Directed Cylinder Emitter for the particle system (emits between direction1 and direction2) + * @param radius The radius of the cylinder to emit from + * @param height The height of the emission cylinder + * @param radiusRange the range of the emission cylinder [0-1] 0 Surface only, 1 Entire Radius (1 by default) + * @param direction1 Particles are emitted between the direction1 and direction2 from within the cylinder + * @param direction2 Particles are emitted between the direction1 and direction2 from within the cylinder + * @returns the emitter + */ +export function CreateDirectedCylinderEmitter( + radius = 1, + height = 1, + radiusRange = 1, + direction1 = new Vector3(0, 1.0, 0), + direction2 = new Vector3(0, 1.0, 0) +): CylinderDirectedParticleEmitter { + return new CylinderDirectedParticleEmitter(radius, height, radiusRange, direction1, direction2); +} + +/** + * Creates a Cone Emitter for the particle system (emits from the cone to the particle position) + * @param radius The radius of the cone to emit from + * @param angle The base angle of the cone + * @returns the emitter + */ +export function CreateConeEmitter(radius = 1, angle = Math.PI / 4): ConeParticleEmitter { + return new ConeParticleEmitter(radius, angle); +} + +export function CreateDirectedConeEmitter(radius = 1, angle = Math.PI / 4, direction1 = new Vector3(0, 1.0, 0), direction2 = new Vector3(0, 1.0, 0)): ConeDirectedParticleEmitter { + return new ConeDirectedParticleEmitter(radius, angle, direction1, direction2); +} diff --git a/tools/src/effect/bjs/particleSystem.ts b/tools/src/effect/bjs/particleSystem.ts new file mode 100644 index 000000000..a2dc27657 --- /dev/null +++ b/tools/src/effect/bjs/particleSystem.ts @@ -0,0 +1,1269 @@ +import { ThinParticleSystem } from "./thinParticleSystem"; +import type { IParticleEmitterType } from "./EmitterTypes/IParticleEmitterType"; +import { SubEmitter, SubEmitterType } from "./subEmitter"; +import { Color3, Color4 } from "../Maths/math.color"; +import { Vector3 } from "../Maths/math.vector"; +import type { IParticleSystem } from "./IParticleSystem"; +import type { AbstractMesh } from "../Meshes/abstractMesh"; +import type { Nullable } from "../types"; +import type { Scene } from "../scene"; +import { AbstractEngine } from "../Engines/abstractEngine"; +import { GetClass } from "../Misc/typeStore"; +import type { BaseTexture } from "../Materials/Textures/baseTexture"; +import type { Effect } from "../Materials/effect"; +import type { Particle } from "./particle"; +import { Constants } from "../Engines/constants"; +import { SerializationHelper } from "../Misc/decorators.serialization"; +import { MeshParticleEmitter } from "./EmitterTypes/meshParticleEmitter"; +import { CustomParticleEmitter } from "./EmitterTypes/customParticleEmitter"; +import { BoxParticleEmitter } from "./EmitterTypes/boxParticleEmitter"; +import { PointParticleEmitter } from "./EmitterTypes/pointParticleEmitter"; +import { HemisphericParticleEmitter } from "./EmitterTypes/hemisphericParticleEmitter"; +import { SphereDirectedParticleEmitter, SphereParticleEmitter } from "./EmitterTypes/sphereParticleEmitter"; +import { CylinderDirectedParticleEmitter, CylinderParticleEmitter } from "./EmitterTypes/cylinderParticleEmitter"; +import { ConeDirectedParticleEmitter, ConeParticleEmitter } from "./EmitterTypes/coneParticleEmitter"; +import { + CreateConeEmitter, + CreateCylinderEmitter, + CreateDirectedCylinderEmitter, + CreateDirectedSphereEmitter, + CreateDirectedConeEmitter, + CreateHemisphericEmitter, + CreatePointEmitter, + CreateSphereEmitter, +} from "./particleSystem.functions"; +import { Attractor } from "./attractor"; +import type { _IExecutionQueueItem } from "./Queue/executionQueue"; +import { _ConnectAfter, _RemoveFromQueue } from "./Queue/executionQueue"; +import type { FlowMap } from "./flowMap"; +import type { NodeParticleSystemSet } from "./Node/nodeParticleSystemSet"; + +/** + * This represents a particle system in Babylon. + * Particles are often small sprites used to simulate hard-to-reproduce phenomena like fire, smoke, water, or abstract visual effects like magic glitter and faery dust. + * Particles can take different shapes while emitted like box, sphere, cone or you can write your custom function. + * @example https://doc.babylonjs.com/features/featuresDeepDive/particles/particle_system/particle_system_intro + */ +export class ParticleSystem extends ThinParticleSystem { + /** + * Billboard mode will only apply to Y axis + */ + public static readonly BILLBOARDMODE_Y = Constants.PARTICLES_BILLBOARDMODE_Y; + /** + * Billboard mode will apply to all axes + */ + public static readonly BILLBOARDMODE_ALL = Constants.PARTICLES_BILLBOARDMODE_ALL; + /** + * Special billboard mode where the particle will be biilboard to the camera but rotated to align with direction + */ + public static readonly BILLBOARDMODE_STRETCHED = Constants.PARTICLES_BILLBOARDMODE_STRETCHED; + /** + * Special billboard mode where the particle will be billboard to the camera but only around the axis of the direction of particle emission + */ + public static readonly BILLBOARDMODE_STRETCHED_LOCAL = Constants.PARTICLES_BILLBOARDMODE_STRETCHED_LOCAL; + + // Sub-emitters + private _rootParticleSystem: Nullable; + + /** + * The Sub-emitters templates that will be used to generate the sub particle system to be associated with the system, this property is used by the root particle system only. + * When a particle is spawned, an array will be chosen at random and all the emitters in that array will be attached to the particle. (Default: []) + */ + public subEmitters: Array>; + // the subEmitters field above converted to a constant type + private _subEmitters: Array>; + + /** + * @internal + * If the particle systems emitter should be disposed when the particle system is disposed + */ + public _disposeEmitterOnDispose = false; + /** + * The current active Sub-systems, this property is used by the root particle system only. + */ + public activeSubSystems: Array; + + /** + * Specifies if the particle system should be serialized + */ + public doNotSerialize = false; + + /** + * Creates a Point Emitter for the particle system (emits directly from the emitter position) + * @param direction1 Particles are emitted between the direction1 and direction2 from within the box + * @param direction2 Particles are emitted between the direction1 and direction2 from within the box + * @returns the emitter + */ + public override createPointEmitter(direction1: Vector3, direction2: Vector3): PointParticleEmitter { + const particleEmitter = CreatePointEmitter(direction1, direction2); + this.particleEmitterType = particleEmitter; + return particleEmitter; + } + + /** + * Gets or sets a function indicating if the particle system can start. + * @returns true if the particle system can start, false otherwise. + */ + public canStart = () => { + return true; + }; + + /** Flow map */ + private _flowMap: Nullable = null; + private _flowMapUpdate: Nullable<_IExecutionQueueItem> = null; + + /** @internal */ + public _source: Nullable = null; + + /** @internal */ + public _blockReference: number = 0; + + /** + * Gets the NodeParticleSystemSet that this particle system belongs to. + */ + public get source(): Nullable { + return this._source; + } + + /** + * Returns true if the particle system was generated by a node particle system set + */ + public override get isNodeGenerated(): boolean { + return this._source !== null; + } + /** + * The strength of the flow map + */ + public flowMapStrength = 1.0; + + /** Gets or sets the current flow map */ + public get flowMap(): Nullable { + return this._flowMap; + } + + public set flowMap(value: Nullable) { + if (this._flowMap === value) { + return; + } + + this._flowMap = value; + + if (this._flowMapUpdate) { + _RemoveFromQueue(this._flowMapUpdate); + this._flowMapUpdate = null; + } + if (value) { + this._flowMapUpdate = { + process: (particle: Particle) => { + const matrix = this.getScene()?.getTransformMatrix(); + this._flowMap!._processParticle(particle, this.flowMapStrength * this._tempScaledUpdateSpeed, matrix); + }, + previousItem: null, + nextItem: null, + }; + _ConnectAfter(this._flowMapUpdate, this._directionProcessing!); + } + } + + /** Attractors */ + private _attractors: Attractor[] = []; + private _attractorUpdate: Nullable<_IExecutionQueueItem> = null; + + /** + * The list of attractors used to change the direction of the particles in the system. + * Please note that this is a copy of the internal array. If you want to modify it, please use the addAttractor and removeAttractor methods. + */ + public get attractors(): Attractor[] { + return this._attractors.slice(0); + } + + /** + * Gets or sets an object used to store user defined information for the particle system + */ + public metadata: any = null; + + /** + * Add an attractor to the particle system. Attractors are used to change the direction of the particles in the system. + * @param attractor The attractor to add to the particle system + */ + public addAttractor(attractor: Attractor): void { + this._attractors.push(attractor); + + if (this._attractors.length === 1) { + this._attractorUpdate = { + process: (particle: Particle) => { + for (const attractor of this._attractors) { + attractor._processParticle(particle, this); + } + }, + previousItem: null, + nextItem: null, + }; + _ConnectAfter(this._attractorUpdate, this._directionProcessing!); + } + } + + /** + * Removes an attractor from the particle system. Attractors are used to change the direction of the particles in the system. + * @param attractor The attractor to remove from the particle system + */ + public removeAttractor(attractor: Attractor): void { + const index = this._attractors.indexOf(attractor); + if (index !== -1) { + this._attractors.splice(index, 1); + } + + if (this._attractors.length === 0) { + _RemoveFromQueue(this._attractorUpdate!); + } + } + + public override start(delay = this.startDelay): void { + if (!this.canStart()) { + return; + } + super.start(delay); + } + + /** + * Creates a Hemisphere Emitter for the particle system (emits along the hemisphere radius) + * @param radius The radius of the hemisphere to emit from + * @param radiusRange The range of the hemisphere to emit from [0-1] 0 Surface Only, 1 Entire Radius + * @returns the emitter + */ + public override createHemisphericEmitter(radius = 1, radiusRange = 1): HemisphericParticleEmitter { + const particleEmitter = CreateHemisphericEmitter(radius, radiusRange); + this.particleEmitterType = particleEmitter; + return particleEmitter; + } + + /** + * Creates a Sphere Emitter for the particle system (emits along the sphere radius) + * @param radius The radius of the sphere to emit from + * @param radiusRange The range of the sphere to emit from [0-1] 0 Surface Only, 1 Entire Radius + * @returns the emitter + */ + public override createSphereEmitter(radius = 1, radiusRange = 1): SphereParticleEmitter { + const particleEmitter = CreateSphereEmitter(radius, radiusRange); + this.particleEmitterType = particleEmitter; + return particleEmitter; + } + + /** + * Creates a Directed Sphere Emitter for the particle system (emits between direction1 and direction2) + * @param radius The radius of the sphere to emit from + * @param direction1 Particles are emitted between the direction1 and direction2 from within the sphere + * @param direction2 Particles are emitted between the direction1 and direction2 from within the sphere + * @returns the emitter + */ + public override createDirectedSphereEmitter(radius = 1, direction1 = new Vector3(0, 1.0, 0), direction2 = new Vector3(0, 1.0, 0)): SphereDirectedParticleEmitter { + const particleEmitter = CreateDirectedSphereEmitter(radius, direction1, direction2); + this.particleEmitterType = particleEmitter; + return particleEmitter; + } + + /** + * Creates a Cylinder Emitter for the particle system (emits from the cylinder to the particle position) + * @param radius The radius of the emission cylinder + * @param height The height of the emission cylinder + * @param radiusRange The range of emission [0-1] 0 Surface only, 1 Entire Radius + * @param directionRandomizer How much to randomize the particle direction [0-1] + * @returns the emitter + */ + public override createCylinderEmitter(radius = 1, height = 1, radiusRange = 1, directionRandomizer = 0): CylinderParticleEmitter { + const particleEmitter = CreateCylinderEmitter(radius, height, radiusRange, directionRandomizer); + this.particleEmitterType = particleEmitter; + return particleEmitter; + } + + /** + * Creates a Directed Cylinder Emitter for the particle system (emits between direction1 and direction2) + * @param radius The radius of the cylinder to emit from + * @param height The height of the emission cylinder + * @param radiusRange the range of the emission cylinder [0-1] 0 Surface only, 1 Entire Radius (1 by default) + * @param direction1 Particles are emitted between the direction1 and direction2 from within the cylinder + * @param direction2 Particles are emitted between the direction1 and direction2 from within the cylinder + * @returns the emitter + */ + public override createDirectedCylinderEmitter( + radius = 1, + height = 1, + radiusRange = 1, + direction1 = new Vector3(0, 1.0, 0), + direction2 = new Vector3(0, 1.0, 0) + ): CylinderDirectedParticleEmitter { + const particleEmitter = CreateDirectedCylinderEmitter(radius, height, radiusRange, direction1, direction2); + this.particleEmitterType = particleEmitter; + return particleEmitter; + } + + /** + * Creates a Cone Emitter for the particle system (emits from the cone to the particle position) + * @param radius The radius of the cone to emit from + * @param angle The base angle of the cone + * @returns the emitter + */ + public override createConeEmitter(radius = 1, angle = Math.PI / 4): ConeParticleEmitter { + const particleEmitter = CreateConeEmitter(radius, angle); + this.particleEmitterType = particleEmitter; + return particleEmitter; + } + + public override createDirectedConeEmitter( + radius = 1, + angle = Math.PI / 4, + direction1 = new Vector3(0, 1.0, 0), + direction2 = new Vector3(0, 1.0, 0) + ): ConeDirectedParticleEmitter { + const particleEmitter = CreateDirectedConeEmitter(radius, angle, direction1, direction2); + this.particleEmitterType = particleEmitter; + return particleEmitter; + } + + /** + * Creates a Box Emitter for the particle system. (emits between direction1 and direction2 from withing the box defined by minEmitBox and maxEmitBox) + * @param direction1 Particles are emitted between the direction1 and direction2 from within the box + * @param direction2 Particles are emitted between the direction1 and direction2 from within the box + * @param minEmitBox Particles are emitted from the box between minEmitBox and maxEmitBox + * @param maxEmitBox Particles are emitted from the box between minEmitBox and maxEmitBox + * @returns the emitter + */ + public override createBoxEmitter(direction1: Vector3, direction2: Vector3, minEmitBox: Vector3, maxEmitBox: Vector3): BoxParticleEmitter { + const particleEmitter = new BoxParticleEmitter(); + this.particleEmitterType = particleEmitter; + this.direction1 = direction1; + this.direction2 = direction2; + this.minEmitBox = minEmitBox; + this.maxEmitBox = maxEmitBox; + return particleEmitter; + } + + private _prepareSubEmitterInternalArray() { + this._subEmitters = new Array>(); + if (this.subEmitters) { + for (const subEmitter of this.subEmitters) { + if (subEmitter instanceof ParticleSystem) { + this._subEmitters.push([new SubEmitter(subEmitter)]); + } else if (subEmitter instanceof SubEmitter) { + this._subEmitters.push([subEmitter]); + } else if (subEmitter instanceof Array) { + this._subEmitters.push(subEmitter); + } + } + } + } + + private _stopSubEmitters(): void { + if (!this.activeSubSystems) { + return; + } + for (const subSystem of this.activeSubSystems) { + subSystem.stop(true); + } + this.activeSubSystems = [] as ParticleSystem[]; + } + + private _removeFromRoot(): void { + if (!this._rootParticleSystem) { + return; + } + + const index = this._rootParticleSystem.activeSubSystems.indexOf(this); + if (index !== -1) { + this._rootParticleSystem.activeSubSystems.splice(index, 1); + } + + this._rootParticleSystem = null; + } + + /** @internal */ + public override _emitFromParticle: (particle: Particle) => void = (particle) => { + if (!this._subEmitters || this._subEmitters.length === 0) { + return; + } + const templateIndex = Math.floor(Math.random() * this._subEmitters.length); + + for (const subEmitter of this._subEmitters[templateIndex]) { + if (subEmitter.type === SubEmitterType.END) { + const subSystem = subEmitter.clone(); + particle._inheritParticleInfoToSubEmitter(subSystem); + subSystem.particleSystem._rootParticleSystem = this; + this.activeSubSystems.push(subSystem.particleSystem); + subSystem.particleSystem.start(); + } + } + }; + + /** @internal */ + public override _preStart() { + // Convert the subEmitters field to the constant type field _subEmitters + this._prepareSubEmitterInternalArray(); + + if (this._subEmitters && this._subEmitters.length != 0) { + this.activeSubSystems = [] as ParticleSystem[]; + } + } + + /** @internal */ + public override _postStop(stopSubEmitters: boolean) { + if (stopSubEmitters) { + this._stopSubEmitters(); + } + } + + /** @internal */ + public override _prepareParticle(particle: Particle): void { + // Attach emitters + if (this._subEmitters && this._subEmitters.length > 0) { + const subEmitters = this._subEmitters[Math.floor(Math.random() * this._subEmitters.length)]; + particle._attachedSubEmitters = []; + for (const subEmitter of subEmitters) { + if (subEmitter.type === SubEmitterType.ATTACHED) { + const newEmitter = subEmitter.clone(); + particle._attachedSubEmitters.push(newEmitter); + newEmitter.particleSystem.start(); + } + } + } + } + + /** @internal */ + public override _onDispose(disposeAttachedSubEmitters = false, disposeEndSubEmitters = false) { + this._removeFromRoot(); + + if (this.subEmitters && !this._subEmitters) { + this._prepareSubEmitterInternalArray(); + } + + if (disposeAttachedSubEmitters) { + if (this.particles) { + for (const particle of this.particles) { + if (particle._attachedSubEmitters) { + for (let i = particle._attachedSubEmitters.length - 1; i >= 0; i -= 1) { + particle._attachedSubEmitters[i].dispose(); + } + } + } + } + } + + if (disposeEndSubEmitters) { + if (this.activeSubSystems) { + for (let i = this.activeSubSystems.length - 1; i >= 0; i -= 1) { + this.activeSubSystems[i].dispose(); + } + } + } + + if (this._subEmitters && this._subEmitters.length) { + for (let index = 0; index < this._subEmitters.length; index++) { + for (const subEmitter of this._subEmitters[index]) { + subEmitter.dispose(); + } + } + + this._subEmitters = []; + this.subEmitters = []; + } + + if (this._disposeEmitterOnDispose && this.emitter && (this.emitter as AbstractMesh).dispose) { + (this.emitter).dispose(true); + } + } + + /** + * @internal + */ + public static _Parse(parsedParticleSystem: any, particleSystem: IParticleSystem, sceneOrEngine: Scene | AbstractEngine, rootUrl: string) { + let scene: Nullable; + + if (sceneOrEngine instanceof AbstractEngine) { + scene = null; + } else { + scene = sceneOrEngine; + } + + const internalClass = GetClass("BABYLON.Texture"); + if (internalClass && scene) { + // Texture + if (parsedParticleSystem.texture) { + particleSystem.particleTexture = internalClass.Parse(parsedParticleSystem.texture, scene, rootUrl) as BaseTexture; + } else if (parsedParticleSystem.textureName) { + particleSystem.particleTexture = new internalClass( + rootUrl + parsedParticleSystem.textureName, + scene, + false, + parsedParticleSystem.invertY !== undefined ? parsedParticleSystem.invertY : true + ); + particleSystem.particleTexture!.name = parsedParticleSystem.textureName; + } + } + + // Emitter + if (!parsedParticleSystem.emitterId && parsedParticleSystem.emitterId !== 0 && parsedParticleSystem.emitter === undefined) { + particleSystem.emitter = Vector3.Zero(); + } else if (parsedParticleSystem.emitterId && scene) { + particleSystem.emitter = scene.getLastMeshById(parsedParticleSystem.emitterId); + } else { + particleSystem.emitter = Vector3.FromArray(parsedParticleSystem.emitter); + } + + particleSystem.isLocal = !!parsedParticleSystem.isLocal; + + // Misc. + if (parsedParticleSystem.renderingGroupId !== undefined) { + particleSystem.renderingGroupId = parsedParticleSystem.renderingGroupId; + } + + if (parsedParticleSystem.isBillboardBased !== undefined) { + particleSystem.isBillboardBased = parsedParticleSystem.isBillboardBased; + } + + if (parsedParticleSystem.billboardMode !== undefined) { + particleSystem.billboardMode = parsedParticleSystem.billboardMode; + } + + if (parsedParticleSystem.useLogarithmicDepth !== undefined) { + particleSystem.useLogarithmicDepth = parsedParticleSystem.useLogarithmicDepth; + } + + // Animations + if (parsedParticleSystem.animations) { + for (let animationIndex = 0; animationIndex < parsedParticleSystem.animations.length; animationIndex++) { + const parsedAnimation = parsedParticleSystem.animations[animationIndex]; + const internalClass = GetClass("BABYLON.Animation"); + if (internalClass) { + particleSystem.animations.push(internalClass.Parse(parsedAnimation)); + } + } + particleSystem.beginAnimationOnStart = parsedParticleSystem.beginAnimationOnStart; + particleSystem.beginAnimationFrom = parsedParticleSystem.beginAnimationFrom; + particleSystem.beginAnimationTo = parsedParticleSystem.beginAnimationTo; + particleSystem.beginAnimationLoop = parsedParticleSystem.beginAnimationLoop; + } + + if (parsedParticleSystem.autoAnimate && scene) { + scene.beginAnimation( + particleSystem, + parsedParticleSystem.autoAnimateFrom, + parsedParticleSystem.autoAnimateTo, + parsedParticleSystem.autoAnimateLoop, + parsedParticleSystem.autoAnimateSpeed || 1.0 + ); + } + + // Particle system + particleSystem.startDelay = parsedParticleSystem.startDelay | 0; + particleSystem.minAngularSpeed = parsedParticleSystem.minAngularSpeed; + particleSystem.maxAngularSpeed = parsedParticleSystem.maxAngularSpeed; + particleSystem.minSize = parsedParticleSystem.minSize; + particleSystem.maxSize = parsedParticleSystem.maxSize; + + if (parsedParticleSystem.minScaleX) { + particleSystem.minScaleX = parsedParticleSystem.minScaleX; + particleSystem.maxScaleX = parsedParticleSystem.maxScaleX; + particleSystem.minScaleY = parsedParticleSystem.minScaleY; + particleSystem.maxScaleY = parsedParticleSystem.maxScaleY; + } + + if (parsedParticleSystem.preWarmCycles !== undefined) { + particleSystem.preWarmCycles = parsedParticleSystem.preWarmCycles; + particleSystem.preWarmStepOffset = parsedParticleSystem.preWarmStepOffset; + } + + if (parsedParticleSystem.minInitialRotation !== undefined) { + particleSystem.minInitialRotation = parsedParticleSystem.minInitialRotation; + particleSystem.maxInitialRotation = parsedParticleSystem.maxInitialRotation; + } + + particleSystem.minLifeTime = parsedParticleSystem.minLifeTime; + particleSystem.maxLifeTime = parsedParticleSystem.maxLifeTime; + particleSystem.minEmitPower = parsedParticleSystem.minEmitPower; + particleSystem.maxEmitPower = parsedParticleSystem.maxEmitPower; + particleSystem.emitRate = parsedParticleSystem.emitRate; + particleSystem.gravity = Vector3.FromArray(parsedParticleSystem.gravity); + if (parsedParticleSystem.noiseStrength) { + particleSystem.noiseStrength = Vector3.FromArray(parsedParticleSystem.noiseStrength); + } + particleSystem.color1 = Color4.FromArray(parsedParticleSystem.color1); + particleSystem.color2 = Color4.FromArray(parsedParticleSystem.color2); + particleSystem.colorDead = Color4.FromArray(parsedParticleSystem.colorDead); + particleSystem.updateSpeed = parsedParticleSystem.updateSpeed; + particleSystem.targetStopDuration = parsedParticleSystem.targetStopDuration; + particleSystem.blendMode = parsedParticleSystem.blendMode; + + if (parsedParticleSystem.colorGradients) { + for (const colorGradient of parsedParticleSystem.colorGradients) { + particleSystem.addColorGradient( + colorGradient.gradient, + Color4.FromArray(colorGradient.color1), + colorGradient.color2 ? Color4.FromArray(colorGradient.color2) : undefined + ); + } + } + + if (parsedParticleSystem.rampGradients) { + for (const rampGradient of parsedParticleSystem.rampGradients) { + particleSystem.addRampGradient(rampGradient.gradient, Color3.FromArray(rampGradient.color)); + } + particleSystem.useRampGradients = parsedParticleSystem.useRampGradients; + } + + if (parsedParticleSystem.colorRemapGradients) { + for (const colorRemapGradient of parsedParticleSystem.colorRemapGradients) { + particleSystem.addColorRemapGradient( + colorRemapGradient.gradient, + colorRemapGradient.factor1 !== undefined ? colorRemapGradient.factor1 : colorRemapGradient.factor, + colorRemapGradient.factor2 + ); + } + } + + if (parsedParticleSystem.alphaRemapGradients) { + for (const alphaRemapGradient of parsedParticleSystem.alphaRemapGradients) { + particleSystem.addAlphaRemapGradient( + alphaRemapGradient.gradient, + alphaRemapGradient.factor1 !== undefined ? alphaRemapGradient.factor1 : alphaRemapGradient.factor, + alphaRemapGradient.factor2 + ); + } + } + + if (parsedParticleSystem.sizeGradients) { + for (const sizeGradient of parsedParticleSystem.sizeGradients) { + particleSystem.addSizeGradient(sizeGradient.gradient, sizeGradient.factor1 !== undefined ? sizeGradient.factor1 : sizeGradient.factor, sizeGradient.factor2); + } + } + + if (parsedParticleSystem.angularSpeedGradients) { + for (const angularSpeedGradient of parsedParticleSystem.angularSpeedGradients) { + particleSystem.addAngularSpeedGradient( + angularSpeedGradient.gradient, + angularSpeedGradient.factor1 !== undefined ? angularSpeedGradient.factor1 : angularSpeedGradient.factor, + angularSpeedGradient.factor2 + ); + } + } + + if (parsedParticleSystem.velocityGradients) { + for (const velocityGradient of parsedParticleSystem.velocityGradients) { + particleSystem.addVelocityGradient( + velocityGradient.gradient, + velocityGradient.factor1 !== undefined ? velocityGradient.factor1 : velocityGradient.factor, + velocityGradient.factor2 + ); + } + } + + if (parsedParticleSystem.dragGradients) { + for (const dragGradient of parsedParticleSystem.dragGradients) { + particleSystem.addDragGradient(dragGradient.gradient, dragGradient.factor1 !== undefined ? dragGradient.factor1 : dragGradient.factor, dragGradient.factor2); + } + } + + if (parsedParticleSystem.emitRateGradients) { + for (const emitRateGradient of parsedParticleSystem.emitRateGradients) { + particleSystem.addEmitRateGradient( + emitRateGradient.gradient, + emitRateGradient.factor1 !== undefined ? emitRateGradient.factor1 : emitRateGradient.factor, + emitRateGradient.factor2 + ); + } + } + + if (parsedParticleSystem.startSizeGradients) { + for (const startSizeGradient of parsedParticleSystem.startSizeGradients) { + particleSystem.addStartSizeGradient( + startSizeGradient.gradient, + startSizeGradient.factor1 !== undefined ? startSizeGradient.factor1 : startSizeGradient.factor, + startSizeGradient.factor2 + ); + } + } + + if (parsedParticleSystem.lifeTimeGradients) { + for (const lifeTimeGradient of parsedParticleSystem.lifeTimeGradients) { + particleSystem.addLifeTimeGradient( + lifeTimeGradient.gradient, + lifeTimeGradient.factor1 !== undefined ? lifeTimeGradient.factor1 : lifeTimeGradient.factor, + lifeTimeGradient.factor2 + ); + } + } + + if (parsedParticleSystem.limitVelocityGradients) { + for (const limitVelocityGradient of parsedParticleSystem.limitVelocityGradients) { + particleSystem.addLimitVelocityGradient( + limitVelocityGradient.gradient, + limitVelocityGradient.factor1 !== undefined ? limitVelocityGradient.factor1 : limitVelocityGradient.factor, + limitVelocityGradient.factor2 + ); + } + particleSystem.limitVelocityDamping = parsedParticleSystem.limitVelocityDamping; + } + + if (parsedParticleSystem.noiseTexture && scene) { + const internalClass = GetClass("BABYLON.ProceduralTexture"); + particleSystem.noiseTexture = internalClass.Parse(parsedParticleSystem.noiseTexture, scene, rootUrl); + } + + // Emitter + let emitterType: IParticleEmitterType; + if (parsedParticleSystem.particleEmitterType) { + switch (parsedParticleSystem.particleEmitterType.type) { + case "SphereParticleEmitter": + emitterType = new SphereParticleEmitter(); + break; + case "SphereDirectedParticleEmitter": + emitterType = new SphereDirectedParticleEmitter(); + break; + case "ConeEmitter": + case "ConeParticleEmitter": + emitterType = new ConeParticleEmitter(); + break; + case "ConeDirectedParticleEmitter": + emitterType = new ConeDirectedParticleEmitter(); + break; + case "CylinderParticleEmitter": + emitterType = new CylinderParticleEmitter(); + break; + case "CylinderDirectedParticleEmitter": + emitterType = new CylinderDirectedParticleEmitter(); + break; + case "HemisphericParticleEmitter": + emitterType = new HemisphericParticleEmitter(); + break; + case "PointParticleEmitter": + emitterType = new PointParticleEmitter(); + break; + case "MeshParticleEmitter": + emitterType = new MeshParticleEmitter(); + break; + case "CustomParticleEmitter": + emitterType = new CustomParticleEmitter(); + break; + case "BoxEmitter": + case "BoxParticleEmitter": + default: + emitterType = new BoxParticleEmitter(); + break; + } + + emitterType.parse(parsedParticleSystem.particleEmitterType, scene); + } else { + emitterType = new BoxParticleEmitter(); + emitterType.parse(parsedParticleSystem, scene); + } + particleSystem.particleEmitterType = emitterType; + + // Animation sheet + particleSystem.startSpriteCellID = parsedParticleSystem.startSpriteCellID; + particleSystem.endSpriteCellID = parsedParticleSystem.endSpriteCellID; + particleSystem.spriteCellLoop = parsedParticleSystem.spriteCellLoop ?? true; + particleSystem.spriteCellWidth = parsedParticleSystem.spriteCellWidth; + particleSystem.spriteCellHeight = parsedParticleSystem.spriteCellHeight; + particleSystem.spriteCellChangeSpeed = parsedParticleSystem.spriteCellChangeSpeed; + particleSystem.spriteRandomStartCell = parsedParticleSystem.spriteRandomStartCell; + + particleSystem.disposeOnStop = parsedParticleSystem.disposeOnStop ?? false; + particleSystem.manualEmitCount = parsedParticleSystem.manualEmitCount ?? -1; + } + + /** + * Parses a JSON object to create a particle system. + * @param parsedParticleSystem The JSON object to parse + * @param sceneOrEngine The scene or the engine to create the particle system in + * @param rootUrl The root url to use to load external dependencies like texture + * @param doNotStart Ignore the preventAutoStart attribute and does not start + * @param capacity defines the system capacity (if null or undefined the sotred capacity will be used) + * @returns the Parsed particle system + */ + public static Parse(parsedParticleSystem: any, sceneOrEngine: Scene | AbstractEngine, rootUrl: string, doNotStart = false, capacity?: number): ParticleSystem { + const name = parsedParticleSystem.name; + let custom: Nullable = null; + let program: any = null; + let engine: AbstractEngine; + let scene: Nullable; + + if (sceneOrEngine instanceof AbstractEngine) { + engine = sceneOrEngine; + } else { + scene = sceneOrEngine; + engine = scene.getEngine(); + } + + if (parsedParticleSystem.customShader && (engine as any).createEffectForParticles) { + program = parsedParticleSystem.customShader; + const defines: string = program.shaderOptions.defines.length > 0 ? program.shaderOptions.defines.join("\n") : ""; + custom = (engine as any).createEffectForParticles(program.shaderPath.fragmentElement, program.shaderOptions.uniforms, program.shaderOptions.samplers, defines); + } + const particleSystem = new ParticleSystem(name, capacity || parsedParticleSystem.capacity, sceneOrEngine, custom, parsedParticleSystem.isAnimationSheetEnabled); + particleSystem.customShader = program; + particleSystem._rootUrl = rootUrl; + + if (parsedParticleSystem.id) { + particleSystem.id = parsedParticleSystem.id; + } + + // SubEmitters + if (parsedParticleSystem.subEmitters) { + particleSystem.subEmitters = []; + for (const cell of parsedParticleSystem.subEmitters) { + const cellArray = []; + for (const sub of cell) { + cellArray.push(SubEmitter.Parse(sub, sceneOrEngine, rootUrl)); + } + + particleSystem.subEmitters.push(cellArray); + } + } + + // Attractors + if (parsedParticleSystem.attractors) { + for (const attractor of parsedParticleSystem.attractors) { + const newAttractor = new Attractor(); + newAttractor.position = Vector3.FromArray(attractor.position); + newAttractor.strength = attractor.strength; + particleSystem.addAttractor(newAttractor); + } + } + + ParticleSystem._Parse(parsedParticleSystem, particleSystem, sceneOrEngine, rootUrl); + + if (parsedParticleSystem.textureMask) { + particleSystem.textureMask = Color4.FromArray(parsedParticleSystem.textureMask); + } + + if (parsedParticleSystem.worldOffset) { + particleSystem.worldOffset = Vector3.FromArray(parsedParticleSystem.worldOffset); + } + + // Auto start + if (parsedParticleSystem.preventAutoStart) { + particleSystem.preventAutoStart = parsedParticleSystem.preventAutoStart; + } + + if (parsedParticleSystem.metadata) { + particleSystem.metadata = parsedParticleSystem.metadata; + } + + if (!doNotStart && !particleSystem.preventAutoStart) { + particleSystem.start(); + } + + return particleSystem; + } + + /** + * Serializes the particle system to a JSON object + * @param serializeTexture defines if the texture must be serialized as well + * @returns the JSON object + */ + public override serialize(serializeTexture = false): any { + const serializationObject: any = {}; + + ParticleSystem._Serialize(serializationObject, this, serializeTexture); + + serializationObject.textureMask = this.textureMask.asArray(); + serializationObject.customShader = this.customShader; + serializationObject.preventAutoStart = this.preventAutoStart; + serializationObject.worldOffset = this.worldOffset.asArray(); + + if (this.metadata) { + serializationObject.metadata = this.metadata; + } + + // SubEmitters + if (this.subEmitters) { + serializationObject.subEmitters = []; + + if (!this._subEmitters) { + this._prepareSubEmitterInternalArray(); + } + + for (const subs of this._subEmitters) { + const cell = []; + for (const sub of subs) { + if (!sub.particleSystem.doNotSerialize) { + cell.push(sub.serialize(serializeTexture)); + } + } + + serializationObject.subEmitters.push(cell); + } + } + + // Attractors + if (this._attractors && this._attractors.length) { + serializationObject.attractors = []; + for (const attractor of this._attractors) { + serializationObject.attractors.push(attractor.serialize()); + } + } + + return serializationObject; + } + + /** + * @internal + */ + public static _Serialize(serializationObject: any, particleSystem: IParticleSystem, serializeTexture: boolean) { + serializationObject.name = particleSystem.name; + serializationObject.id = particleSystem.id; + + serializationObject.capacity = particleSystem.getCapacity(); + + serializationObject.disposeOnStop = particleSystem.disposeOnStop; + serializationObject.manualEmitCount = particleSystem.manualEmitCount; + + // Emitter + if ((particleSystem.emitter).position) { + const emitterMesh = particleSystem.emitter; + serializationObject.emitterId = emitterMesh.id; + } else { + const emitterPosition = particleSystem.emitter; + serializationObject.emitter = emitterPosition.asArray(); + } + + // Emitter + if (particleSystem.particleEmitterType) { + serializationObject.particleEmitterType = particleSystem.particleEmitterType.serialize(); + } + + if (particleSystem.particleTexture) { + if (serializeTexture) { + serializationObject.texture = particleSystem.particleTexture.serialize(); + } else { + serializationObject.textureName = particleSystem.particleTexture.name; + serializationObject.invertY = !!(particleSystem.particleTexture as any)._invertY; + } + } + + serializationObject.isLocal = particleSystem.isLocal; + + // Animations + SerializationHelper.AppendSerializedAnimations(particleSystem, serializationObject); + serializationObject.beginAnimationOnStart = particleSystem.beginAnimationOnStart; + serializationObject.beginAnimationFrom = particleSystem.beginAnimationFrom; + serializationObject.beginAnimationTo = particleSystem.beginAnimationTo; + serializationObject.beginAnimationLoop = particleSystem.beginAnimationLoop; + + // Particle system + serializationObject.startDelay = particleSystem.startDelay; + serializationObject.renderingGroupId = particleSystem.renderingGroupId; + serializationObject.isBillboardBased = particleSystem.isBillboardBased; + serializationObject.billboardMode = particleSystem.billboardMode; + serializationObject.minAngularSpeed = particleSystem.minAngularSpeed; + serializationObject.maxAngularSpeed = particleSystem.maxAngularSpeed; + serializationObject.minSize = particleSystem.minSize; + serializationObject.maxSize = particleSystem.maxSize; + serializationObject.minScaleX = particleSystem.minScaleX; + serializationObject.maxScaleX = particleSystem.maxScaleX; + serializationObject.minScaleY = particleSystem.minScaleY; + serializationObject.maxScaleY = particleSystem.maxScaleY; + serializationObject.minEmitPower = particleSystem.minEmitPower; + serializationObject.maxEmitPower = particleSystem.maxEmitPower; + serializationObject.minLifeTime = particleSystem.minLifeTime; + serializationObject.maxLifeTime = particleSystem.maxLifeTime; + serializationObject.emitRate = particleSystem.emitRate; + serializationObject.gravity = particleSystem.gravity.asArray(); + serializationObject.noiseStrength = particleSystem.noiseStrength.asArray(); + serializationObject.color1 = particleSystem.color1.asArray(); + serializationObject.color2 = particleSystem.color2.asArray(); + serializationObject.colorDead = particleSystem.colorDead.asArray(); + serializationObject.updateSpeed = particleSystem.updateSpeed; + serializationObject.targetStopDuration = particleSystem.targetStopDuration; + serializationObject.blendMode = particleSystem.blendMode; + serializationObject.preWarmCycles = particleSystem.preWarmCycles; + serializationObject.preWarmStepOffset = particleSystem.preWarmStepOffset; + serializationObject.minInitialRotation = particleSystem.minInitialRotation; + serializationObject.maxInitialRotation = particleSystem.maxInitialRotation; + serializationObject.startSpriteCellID = particleSystem.startSpriteCellID; + serializationObject.spriteCellLoop = particleSystem.spriteCellLoop; + serializationObject.endSpriteCellID = particleSystem.endSpriteCellID; + serializationObject.spriteCellChangeSpeed = particleSystem.spriteCellChangeSpeed; + serializationObject.spriteCellWidth = particleSystem.spriteCellWidth; + serializationObject.spriteCellHeight = particleSystem.spriteCellHeight; + serializationObject.spriteRandomStartCell = particleSystem.spriteRandomStartCell; + serializationObject.isAnimationSheetEnabled = particleSystem.isAnimationSheetEnabled; + serializationObject.useLogarithmicDepth = particleSystem.useLogarithmicDepth; + + const colorGradients = particleSystem.getColorGradients(); + if (colorGradients) { + serializationObject.colorGradients = []; + for (const colorGradient of colorGradients) { + const serializedGradient: any = { + gradient: colorGradient.gradient, + color1: colorGradient.color1.asArray(), + }; + + if (colorGradient.color2) { + serializedGradient.color2 = colorGradient.color2.asArray(); + } else { + serializedGradient.color2 = colorGradient.color1.asArray(); + } + + serializationObject.colorGradients.push(serializedGradient); + } + } + + const rampGradients = particleSystem.getRampGradients(); + if (rampGradients) { + serializationObject.rampGradients = []; + for (const rampGradient of rampGradients) { + const serializedGradient: any = { + gradient: rampGradient.gradient, + color: rampGradient.color.asArray(), + }; + + serializationObject.rampGradients.push(serializedGradient); + } + serializationObject.useRampGradients = particleSystem.useRampGradients; + } + + const colorRemapGradients = particleSystem.getColorRemapGradients(); + if (colorRemapGradients) { + serializationObject.colorRemapGradients = []; + for (const colorRemapGradient of colorRemapGradients) { + const serializedGradient: any = { + gradient: colorRemapGradient.gradient, + factor1: colorRemapGradient.factor1, + }; + + if (colorRemapGradient.factor2 !== undefined) { + serializedGradient.factor2 = colorRemapGradient.factor2; + } else { + serializedGradient.factor2 = colorRemapGradient.factor1; + } + + serializationObject.colorRemapGradients.push(serializedGradient); + } + } + + const alphaRemapGradients = particleSystem.getAlphaRemapGradients(); + if (alphaRemapGradients) { + serializationObject.alphaRemapGradients = []; + for (const alphaRemapGradient of alphaRemapGradients) { + const serializedGradient: any = { + gradient: alphaRemapGradient.gradient, + factor1: alphaRemapGradient.factor1, + }; + + if (alphaRemapGradient.factor2 !== undefined) { + serializedGradient.factor2 = alphaRemapGradient.factor2; + } else { + serializedGradient.factor2 = alphaRemapGradient.factor1; + } + + serializationObject.alphaRemapGradients.push(serializedGradient); + } + } + + const sizeGradients = particleSystem.getSizeGradients(); + if (sizeGradients) { + serializationObject.sizeGradients = []; + for (const sizeGradient of sizeGradients) { + const serializedGradient: any = { + gradient: sizeGradient.gradient, + factor1: sizeGradient.factor1, + }; + + if (sizeGradient.factor2 !== undefined) { + serializedGradient.factor2 = sizeGradient.factor2; + } else { + serializedGradient.factor2 = sizeGradient.factor1; + } + + serializationObject.sizeGradients.push(serializedGradient); + } + } + + const angularSpeedGradients = particleSystem.getAngularSpeedGradients(); + if (angularSpeedGradients) { + serializationObject.angularSpeedGradients = []; + for (const angularSpeedGradient of angularSpeedGradients) { + const serializedGradient: any = { + gradient: angularSpeedGradient.gradient, + factor1: angularSpeedGradient.factor1, + }; + + if (angularSpeedGradient.factor2 !== undefined) { + serializedGradient.factor2 = angularSpeedGradient.factor2; + } else { + serializedGradient.factor2 = angularSpeedGradient.factor1; + } + + serializationObject.angularSpeedGradients.push(serializedGradient); + } + } + + const velocityGradients = particleSystem.getVelocityGradients(); + if (velocityGradients) { + serializationObject.velocityGradients = []; + for (const velocityGradient of velocityGradients) { + const serializedGradient: any = { + gradient: velocityGradient.gradient, + factor1: velocityGradient.factor1, + }; + + if (velocityGradient.factor2 !== undefined) { + serializedGradient.factor2 = velocityGradient.factor2; + } else { + serializedGradient.factor2 = velocityGradient.factor1; + } + + serializationObject.velocityGradients.push(serializedGradient); + } + } + + const dragGradients = particleSystem.getDragGradients(); + if (dragGradients) { + serializationObject.dragGradients = []; + for (const dragGradient of dragGradients) { + const serializedGradient: any = { + gradient: dragGradient.gradient, + factor1: dragGradient.factor1, + }; + + if (dragGradient.factor2 !== undefined) { + serializedGradient.factor2 = dragGradient.factor2; + } else { + serializedGradient.factor2 = dragGradient.factor1; + } + + serializationObject.dragGradients.push(serializedGradient); + } + } + + const emitRateGradients = particleSystem.getEmitRateGradients(); + if (emitRateGradients) { + serializationObject.emitRateGradients = []; + for (const emitRateGradient of emitRateGradients) { + const serializedGradient: any = { + gradient: emitRateGradient.gradient, + factor1: emitRateGradient.factor1, + }; + + if (emitRateGradient.factor2 !== undefined) { + serializedGradient.factor2 = emitRateGradient.factor2; + } else { + serializedGradient.factor2 = emitRateGradient.factor1; + } + + serializationObject.emitRateGradients.push(serializedGradient); + } + } + + const startSizeGradients = particleSystem.getStartSizeGradients(); + if (startSizeGradients) { + serializationObject.startSizeGradients = []; + for (const startSizeGradient of startSizeGradients) { + const serializedGradient: any = { + gradient: startSizeGradient.gradient, + factor1: startSizeGradient.factor1, + }; + + if (startSizeGradient.factor2 !== undefined) { + serializedGradient.factor2 = startSizeGradient.factor2; + } else { + serializedGradient.factor2 = startSizeGradient.factor1; + } + + serializationObject.startSizeGradients.push(serializedGradient); + } + } + + const lifeTimeGradients = particleSystem.getLifeTimeGradients(); + if (lifeTimeGradients) { + serializationObject.lifeTimeGradients = []; + for (const lifeTimeGradient of lifeTimeGradients) { + const serializedGradient: any = { + gradient: lifeTimeGradient.gradient, + factor1: lifeTimeGradient.factor1, + }; + + if (lifeTimeGradient.factor2 !== undefined) { + serializedGradient.factor2 = lifeTimeGradient.factor2; + } else { + serializedGradient.factor2 = lifeTimeGradient.factor1; + } + + serializationObject.lifeTimeGradients.push(serializedGradient); + } + } + + const limitVelocityGradients = particleSystem.getLimitVelocityGradients(); + if (limitVelocityGradients) { + serializationObject.limitVelocityGradients = []; + for (const limitVelocityGradient of limitVelocityGradients) { + const serializedGradient: any = { + gradient: limitVelocityGradient.gradient, + factor1: limitVelocityGradient.factor1, + }; + + if (limitVelocityGradient.factor2 !== undefined) { + serializedGradient.factor2 = limitVelocityGradient.factor2; + } else { + serializedGradient.factor2 = limitVelocityGradient.factor1; + } + + serializationObject.limitVelocityGradients.push(serializedGradient); + } + + serializationObject.limitVelocityDamping = particleSystem.limitVelocityDamping; + } + + if (particleSystem.noiseTexture) { + serializationObject.noiseTexture = particleSystem.noiseTexture.serialize(); + } + } + + // Clone + /** + * Clones the particle system. + * @param name The name of the cloned object + * @param newEmitter The new emitter to use + * @param cloneTexture Also clone the textures if true + * @returns the cloned particle system + */ + public override clone(name: string, newEmitter: any, cloneTexture = false): ParticleSystem { + const custom = { ...this._customWrappers }; + let program: any = null; + const engine = this._engine; + if (engine.createEffectForParticles) { + if (this.customShader != null) { + program = this.customShader; + const defines: string = program.shaderOptions.defines.length > 0 ? program.shaderOptions.defines.join("\n") : ""; + const effect = engine.createEffectForParticles(program.shaderPath.fragmentElement, program.shaderOptions.uniforms, program.shaderOptions.samplers, defines); + if (!custom[0]) { + this.setCustomEffect(effect, 0); + } else { + custom[0].effect = effect; + } + } + } + + const serialization = this.serialize(cloneTexture); + const result = ParticleSystem.Parse(serialization, this._scene || this._engine, this._rootUrl); + result.name = name; + result.customShader = program; + result._customWrappers = custom; + + if (newEmitter === undefined) { + newEmitter = this.emitter; + } + + if (this.noiseTexture) { + result.noiseTexture = this.noiseTexture.clone(); + } + + result.emitter = newEmitter; + if (!this.preventAutoStart) { + result.start(); + } + + return result; + } +} + +SubEmitter._ParseParticleSystem = ParticleSystem.Parse; diff --git a/tools/src/effect/bjs/subEmitter.ts b/tools/src/effect/bjs/subEmitter.ts new file mode 100644 index 000000000..047bde1e1 --- /dev/null +++ b/tools/src/effect/bjs/subEmitter.ts @@ -0,0 +1,134 @@ +import { Vector3 } from "../Maths/math.vector"; +import { _WarnImport } from "../Misc/devTools"; +import type { AbstractEngine } from "../Engines/abstractEngine"; +import { GetClass } from "../Misc/typeStore"; + +import type { Scene } from "../scene"; +import type { AbstractMesh } from "../Meshes/abstractMesh"; +import type { ParticleSystem } from "../Particles/particleSystem"; + +/** + * Type of sub emitter + */ +export const enum SubEmitterType { + /** + * Attached to the particle over it's lifetime + */ + ATTACHED, + /** + * Created when the particle dies + */ + END, +} + +/** + * Sub emitter class used to emit particles from an existing particle + */ +export class SubEmitter { + /** + * Type of the submitter (Default: END) + */ + public type = SubEmitterType.END; + /** + * If the particle should inherit the direction from the particle it's attached to. (+Y will face the direction the particle is moving) (Default: false) + * Note: This only is supported when using an emitter of type Mesh + */ + public inheritDirection = false; + /** + * How much of the attached particles speed should be added to the sub emitted particle (default: 0) + */ + public inheritedVelocityAmount = 0; + + /** + * Creates a sub emitter + * @param particleSystem the particle system to be used by the sub emitter + */ + constructor( + /** + * the particle system to be used by the sub emitter + */ + public particleSystem: ParticleSystem + ) { + // Create mesh as emitter to support rotation + if (!particleSystem.emitter || !(particleSystem.emitter).dispose) { + const internalClass = GetClass("BABYLON.AbstractMesh"); + particleSystem.emitter = new internalClass("SubemitterSystemEmitter", particleSystem.getScene()); + particleSystem._disposeEmitterOnDispose = true; + } + } + /** + * Clones the sub emitter + * @returns the cloned sub emitter + */ + public clone(): SubEmitter { + // Clone particle system + let emitter = this.particleSystem.emitter; + if (!emitter) { + emitter = new Vector3(); + } else if (emitter instanceof Vector3) { + emitter = emitter.clone(); + } else if (emitter.getClassName().indexOf("Mesh") !== -1) { + const internalClass = GetClass("BABYLON.Mesh"); + emitter = new internalClass("", emitter.getScene()); + (emitter! as any).isVisible = false; + } + const clone = new SubEmitter(this.particleSystem.clone(this.particleSystem.name, emitter)); + + // Clone properties + clone.particleSystem.name += "Clone"; + clone.type = this.type; + clone.inheritDirection = this.inheritDirection; + clone.inheritedVelocityAmount = this.inheritedVelocityAmount; + + clone.particleSystem._disposeEmitterOnDispose = true; + clone.particleSystem.disposeOnStop = true; + return clone; + } + + /** + * Serialize current object to a JSON object + * @param serializeTexture defines if the texture must be serialized as well + * @returns the serialized object + */ + public serialize(serializeTexture: boolean = false): any { + const serializationObject: any = {}; + + serializationObject.type = this.type; + serializationObject.inheritDirection = this.inheritDirection; + serializationObject.inheritedVelocityAmount = this.inheritedVelocityAmount; + serializationObject.particleSystem = this.particleSystem.serialize(serializeTexture); + + return serializationObject; + } + + /** + * @internal + */ + // eslint-disable-next-line @typescript-eslint/no-unused-vars + public static _ParseParticleSystem(system: any, sceneOrEngine: Scene | AbstractEngine, rootUrl: string, doNotStart = false): ParticleSystem { + throw _WarnImport("ParseParticle"); + } + + /** + * Creates a new SubEmitter from a serialized JSON version + * @param serializationObject defines the JSON object to read from + * @param sceneOrEngine defines the hosting scene or the hosting engine + * @param rootUrl defines the rootUrl for data loading + * @returns a new SubEmitter + */ + public static Parse(serializationObject: any, sceneOrEngine: Scene | AbstractEngine, rootUrl: string): SubEmitter { + const system = serializationObject.particleSystem; + const subEmitter = new SubEmitter(SubEmitter._ParseParticleSystem(system, sceneOrEngine, rootUrl, true)); + subEmitter.type = serializationObject.type; + subEmitter.inheritDirection = serializationObject.inheritDirection; + subEmitter.inheritedVelocityAmount = serializationObject.inheritedVelocityAmount; + subEmitter.particleSystem._isSubEmitter = true; + + return subEmitter; + } + + /** Release associated resources */ + public dispose() { + this.particleSystem.dispose(); + } +} diff --git a/tools/src/effect/bjs/thinParticleSystem.function.ts b/tools/src/effect/bjs/thinParticleSystem.function.ts new file mode 100644 index 000000000..ac4d38630 --- /dev/null +++ b/tools/src/effect/bjs/thinParticleSystem.function.ts @@ -0,0 +1,427 @@ +import { Color4 } from "core/Maths/math.color"; +import type { ColorGradient, FactorGradient } from "core/Misc/gradients"; +import { GradientHelper } from "core/Misc/gradients"; +import type { Particle } from "./particle"; +import type { ThinParticleSystem } from "./thinParticleSystem"; +import { Clamp, Lerp, RandomRange } from "core/Maths/math.scalar.functions"; +import { TmpVectors, Vector3, Vector4 } from "core/Maths/math.vector"; + +/** Color */ + +/** @internal */ +export function _CreateColorData(particle: Particle, system: ThinParticleSystem) { + const step = RandomRange(0, 1.0); + + Color4.LerpToRef(system.color1, system.color2, step, particle.color); +} + +/** @internal */ +export function _CreateColorDeadData(particle: Particle, system: ThinParticleSystem) { + system.colorDead.subtractToRef(particle.color, system._colorDiff); + system._colorDiff.scaleToRef(1.0 / particle.lifeTime, particle.colorStep); +} + +/** @internal */ +export function _CreateColorGradientsData(particle: Particle, system: ThinParticleSystem) { + particle._currentColorGradient = system._colorGradients![0]; + particle._currentColorGradient.getColorToRef(particle.color); + particle._currentColor1.copyFrom(particle.color); + + if (system._colorGradients!.length > 1) { + system._colorGradients![1].getColorToRef(particle._currentColor2); + } else { + particle._currentColor2.copyFrom(particle.color); + } +} + +/** @internal */ +export function _ProcessColorGradients(particle: Particle, system: ThinParticleSystem) { + const colorGradients = system._colorGradients; + GradientHelper.GetCurrentGradient(system._ratio, colorGradients!, (currentGradient, nextGradient, scale) => { + if (currentGradient !== particle._currentColorGradient) { + particle._currentColor1.copyFrom(particle._currentColor2); + (nextGradient).getColorToRef(particle._currentColor2); + particle._currentColorGradient = currentGradient; + } + Color4.LerpToRef(particle._currentColor1, particle._currentColor2, scale, particle.color); + + if (particle.color.a < 0) { + particle.color.a = 0; + } + }); +} + +/** @internal */ +export function _ProcessColor(particle: Particle, system: ThinParticleSystem) { + particle.colorStep.scaleToRef(system._scaledUpdateSpeed, system._scaledColorStep); + particle.color.addInPlace(system._scaledColorStep); + + if (particle.color.a < 0) { + particle.color.a = 0; + } +} + +/** Angular speed */ + +/** @internal */ +export function _ProcessAngularSpeedGradients(particle: Particle, system: ThinParticleSystem) { + GradientHelper.GetCurrentGradient(system._ratio, system._angularSpeedGradients!, (currentGradient, nextGradient, scale) => { + if (currentGradient !== particle._currentAngularSpeedGradient) { + particle._currentAngularSpeed1 = particle._currentAngularSpeed2; + particle._currentAngularSpeed2 = (nextGradient).getFactor(); + particle._currentAngularSpeedGradient = currentGradient; + } + particle.angularSpeed = Lerp(particle._currentAngularSpeed1, particle._currentAngularSpeed2, scale); + }); +} + +/** @internal */ +export function _ProcessAngularSpeed(particle: Particle, system: ThinParticleSystem) { + particle.angle += particle.angularSpeed * system._scaledUpdateSpeed; +} + +/** Velocity & direction */ + +/** @internal */ +export function _CreateDirectionData(particle: Particle, system: ThinParticleSystem) { + system.particleEmitterType.startDirectionFunction(system._emitterWorldMatrix, particle.direction, particle, system.isLocal, system._emitterInverseWorldMatrix); +} + +/** @internal */ +export function _CreateCustomDirectionData(particle: Particle, system: ThinParticleSystem) { + system.startDirectionFunction!(system._emitterWorldMatrix, particle.direction, particle, system.isLocal); +} + +/** @internal */ +export function _CreateVelocityGradients(particle: Particle, system: ThinParticleSystem) { + particle._currentVelocityGradient = system._velocityGradients![0]; + particle._currentVelocity1 = particle._currentVelocityGradient.getFactor(); + + if (system._velocityGradients!.length > 1) { + particle._currentVelocity2 = system._velocityGradients![1].getFactor(); + } else { + particle._currentVelocity2 = particle._currentVelocity1; + } +} + +/** @internal */ +export function _CreateLimitVelocityGradients(particle: Particle, system: ThinParticleSystem) { + particle._currentLimitVelocityGradient = system._limitVelocityGradients![0]; + particle._currentLimitVelocity1 = particle._currentLimitVelocityGradient.getFactor(); + + if (system._limitVelocityGradients!.length > 1) { + particle._currentLimitVelocity2 = system._limitVelocityGradients![1].getFactor(); + } else { + particle._currentLimitVelocity2 = particle._currentLimitVelocity1; + } +} + +/** @internal */ +export function _ProcessVelocityGradients(particle: Particle, system: ThinParticleSystem) { + GradientHelper.GetCurrentGradient(system._ratio, system._velocityGradients!, (currentGradient, nextGradient, scale) => { + if (currentGradient !== particle._currentVelocityGradient) { + particle._currentVelocity1 = particle._currentVelocity2; + particle._currentVelocity2 = (nextGradient).getFactor(); + particle._currentVelocityGradient = currentGradient; + } + particle._directionScale *= Lerp(particle._currentVelocity1, particle._currentVelocity2, scale); + }); +} + +/** @internal */ +export function _ProcessLimitVelocityGradients(particle: Particle, system: ThinParticleSystem) { + GradientHelper.GetCurrentGradient(system._ratio, system._limitVelocityGradients!, (currentGradient, nextGradient, scale) => { + if (currentGradient !== particle._currentLimitVelocityGradient) { + particle._currentLimitVelocity1 = particle._currentLimitVelocity2; + particle._currentLimitVelocity2 = (nextGradient).getFactor(); + particle._currentLimitVelocityGradient = currentGradient; + } + + const limitVelocity = Lerp(particle._currentLimitVelocity1, particle._currentLimitVelocity2, scale); + const currentVelocity = particle.direction.length(); + + if (currentVelocity > limitVelocity) { + particle.direction.scaleInPlace(system.limitVelocityDamping); + } + }); +} + +/** @internal */ +export function _ProcessDirection(particle: Particle) { + particle.direction.scaleToRef(particle._directionScale, particle._scaledDirection); +} + +/** Position */ + +/** @internal */ +export function _CreatePositionData(particle: Particle, system: ThinParticleSystem) { + system.particleEmitterType.startPositionFunction(system._emitterWorldMatrix, particle.position, particle, system.isLocal); +} + +/** @internal */ +export function _CreateCustomPositionData(particle: Particle, system: ThinParticleSystem) { + system.startPositionFunction!(system._emitterWorldMatrix, particle.position, particle, system.isLocal); +} + +/** @internal */ +export function _CreateIsLocalData(particle: Particle, system: ThinParticleSystem) { + if (!particle._localPosition) { + particle._localPosition = particle.position.clone(); + } else { + particle._localPosition.copyFrom(particle.position); + } + Vector3.TransformCoordinatesToRef(particle._localPosition, system._emitterWorldMatrix, particle.position); +} + +/** @internal */ +export function _ProcessPosition(particle: Particle, system: ThinParticleSystem) { + if (system.isLocal && particle._localPosition) { + particle._localPosition!.addInPlace(particle._scaledDirection); + Vector3.TransformCoordinatesToRef(particle._localPosition!, system._emitterWorldMatrix, particle.position); + } else { + particle.position.addInPlace(particle._scaledDirection); + } +} + +/** Drag */ + +/** @internal */ +export function _CreateDragData(particle: Particle, system: ThinParticleSystem) { + particle._currentDragGradient = system._dragGradients![0]; + particle._currentDrag1 = particle._currentDragGradient.getFactor(); + + if (system._dragGradients!.length > 1) { + particle._currentDrag2 = system._dragGradients![1].getFactor(); + } else { + particle._currentDrag2 = particle._currentDrag1; + } +} + +/** @internal */ +export function _ProcessDragGradients(particle: Particle, system: ThinParticleSystem) { + GradientHelper.GetCurrentGradient(system._ratio, system._dragGradients!, (currentGradient, nextGradient, scale) => { + if (currentGradient !== particle._currentDragGradient) { + particle._currentDrag1 = particle._currentDrag2; + particle._currentDrag2 = (nextGradient).getFactor(); + particle._currentDragGradient = currentGradient; + } + + const drag = Lerp(particle._currentDrag1, particle._currentDrag2, scale); + + particle._scaledDirection.scaleInPlace(1.0 - drag); + }); +} + +/** Noise */ + +/** @internal */ +export function _CreateNoiseData(particle: Particle, _system: ThinParticleSystem) { + if (particle._randomNoiseCoordinates1 && particle._randomNoiseCoordinates2) { + particle._randomNoiseCoordinates1.copyFromFloats(Math.random(), Math.random(), Math.random()); + particle._randomNoiseCoordinates2.copyFromFloats(Math.random(), Math.random(), Math.random()); + } else { + particle._randomNoiseCoordinates1 = new Vector3(Math.random(), Math.random(), Math.random()); + particle._randomNoiseCoordinates2 = new Vector3(Math.random(), Math.random(), Math.random()); + } +} + +/** @internal */ +export function _ProcessNoise(particle: Particle, system: ThinParticleSystem) { + const noiseTextureData = system._noiseTextureData; + const noiseTextureSize = system._noiseTextureSize; + + if (noiseTextureData && noiseTextureSize && particle._randomNoiseCoordinates1 && particle._randomNoiseCoordinates2) { + const fetchedColorR = system._fetchR( + particle._randomNoiseCoordinates1.x, + particle._randomNoiseCoordinates1.y, + noiseTextureSize.width, + noiseTextureSize.height, + noiseTextureData + ); + const fetchedColorG = system._fetchR( + particle._randomNoiseCoordinates1.z, + particle._randomNoiseCoordinates2.x, + noiseTextureSize.width, + noiseTextureSize.height, + noiseTextureData + ); + const fetchedColorB = system._fetchR( + particle._randomNoiseCoordinates2.y, + particle._randomNoiseCoordinates2.z, + noiseTextureSize.width, + noiseTextureSize.height, + noiseTextureData + ); + + const force = TmpVectors.Vector3[0]; + const scaledForce = TmpVectors.Vector3[1]; + + force.copyFromFloats((2 * fetchedColorR - 1) * system.noiseStrength.x, (2 * fetchedColorG - 1) * system.noiseStrength.y, (2 * fetchedColorB - 1) * system.noiseStrength.z); + + force.scaleToRef(system._tempScaledUpdateSpeed, scaledForce); + particle.direction.addInPlace(scaledForce); + } +} + +/** Gravity */ + +/** @internal */ +export function _ProcessGravity(particle: Particle, system: ThinParticleSystem) { + system.gravity.scaleToRef(system._tempScaledUpdateSpeed, system._scaledGravity); + particle.direction.addInPlace(system._scaledGravity); +} + +/** Size */ + +/** @internal */ +export function _CreateSizeData(particle: Particle, system: ThinParticleSystem) { + particle.size = RandomRange(system.minSize, system.maxSize); + particle.scale.copyFromFloats(RandomRange(system.minScaleX, system.maxScaleX), RandomRange(system.minScaleY, system.maxScaleY)); +} + +/** @internal */ +export function _CreateSizeGradientsData(particle: Particle, system: ThinParticleSystem) { + particle._currentSizeGradient = system._sizeGradients![0]; + particle._currentSize1 = particle._currentSizeGradient.getFactor(); + particle.size = particle._currentSize1; + + if (system._sizeGradients!.length > 1) { + particle._currentSize2 = system._sizeGradients![1].getFactor(); + } else { + particle._currentSize2 = particle._currentSize1; + } + + particle.scale.copyFromFloats(RandomRange(system.minScaleX, system.maxScaleX), RandomRange(system.minScaleY, system.maxScaleY)); +} + +/** @internal */ +export function _CreateStartSizeGradientsData(particle: Particle, system: ThinParticleSystem) { + const ratio = system._actualFrame / system.targetStopDuration; + GradientHelper.GetCurrentGradient(ratio, system._startSizeGradients!, (currentGradient, nextGradient, scale) => { + if (currentGradient !== system._currentStartSizeGradient) { + system._currentStartSize1 = system._currentStartSize2; + system._currentStartSize2 = (nextGradient).getFactor(); + system._currentStartSizeGradient = currentGradient; + } + + const value = Lerp(system._currentStartSize1, system._currentStartSize2, scale); + particle.scale.scaleInPlace(value); + }); +} + +/** @internal */ +export function _ProcessSizeGradients(particle: Particle, system: ThinParticleSystem) { + GradientHelper.GetCurrentGradient(system._ratio, system._sizeGradients!, (currentGradient, nextGradient, scale) => { + if (currentGradient !== particle._currentSizeGradient) { + particle._currentSize1 = particle._currentSize2; + particle._currentSize2 = (nextGradient).getFactor(); + particle._currentSizeGradient = currentGradient; + } + particle.size = Lerp(particle._currentSize1, particle._currentSize2, scale); + }); +} + +/** Ramp */ + +/** @internal */ +export function _CreateRampData(particle: Particle, _system: ThinParticleSystem) { + particle.remapData = new Vector4(0, 1, 0, 1); +} + +/** Remap */ + +/** @internal */ +export function _ProcessRemapGradients(particle: Particle, system: ThinParticleSystem) { + if (system._colorRemapGradients && system._colorRemapGradients.length > 0) { + GradientHelper.GetCurrentGradient(system._ratio, system._colorRemapGradients, (currentGradient, nextGradient, scale) => { + const min = Lerp((currentGradient).factor1, (nextGradient).factor1, scale); + const max = Lerp((currentGradient).factor2!, (nextGradient).factor2!, scale); + + particle.remapData.x = min; + particle.remapData.y = max - min; + }); + } + + if (system._alphaRemapGradients && system._alphaRemapGradients.length > 0) { + GradientHelper.GetCurrentGradient(system._ratio, system._alphaRemapGradients, (currentGradient, nextGradient, scale) => { + const min = Lerp((currentGradient).factor1, (nextGradient).factor1, scale); + const max = Lerp((currentGradient).factor2!, (nextGradient).factor2!, scale); + + particle.remapData.z = min; + particle.remapData.w = max - min; + }); + } +} + +/** Life */ + +/** @internal */ +export function _CreateLifeGradientsData(particle: Particle, system: ThinParticleSystem) { + const ratio = Clamp(system._actualFrame / system.targetStopDuration); + GradientHelper.GetCurrentGradient(ratio, system._lifeTimeGradients!, (currentGradient, nextGradient) => { + const factorGradient1 = currentGradient; + const factorGradient2 = nextGradient; + const lifeTime1 = factorGradient1.getFactor(); + const lifeTime2 = factorGradient2.getFactor(); + const gradient = (ratio - factorGradient1.gradient) / (factorGradient2.gradient - factorGradient1.gradient); + particle.lifeTime = Lerp(lifeTime1, lifeTime2, gradient); + }); + system._emitPower = RandomRange(system.minEmitPower, system.maxEmitPower); +} + +/** @internal */ +export function _CreateLifetimeData(particle: Particle, system: ThinParticleSystem) { + particle.lifeTime = RandomRange(system.minLifeTime, system.maxLifeTime); + system._emitPower = RandomRange(system.minEmitPower, system.maxEmitPower); +} + +/** Emit power */ + +/** @internal */ +export function _CreateEmitPowerData(particle: Particle, system: ThinParticleSystem) { + if (system._emitPower === 0) { + if (!particle._initialDirection) { + particle._initialDirection = particle.direction.clone(); + } else { + particle._initialDirection.copyFrom(particle.direction); + } + particle.direction.set(0, 0, 0); + } else { + particle._initialDirection = null; + particle.direction.scaleInPlace(system._emitPower); + } + + // Inherited Velocity + particle.direction.addInPlace(system._inheritedVelocityOffset); +} + +/** Angle */ + +/** @internal */ +export function _CreateAngleData(particle: Particle, system: ThinParticleSystem) { + particle.angularSpeed = RandomRange(system.minAngularSpeed, system.maxAngularSpeed); + particle.angle = RandomRange(system.minInitialRotation, system.maxInitialRotation); +} + +/** @internal */ +export function _CreateAngleGradientsData(particle: Particle, system: ThinParticleSystem) { + particle._currentAngularSpeedGradient = system._angularSpeedGradients![0]; + particle.angularSpeed = particle._currentAngularSpeedGradient.getFactor(); + particle._currentAngularSpeed1 = particle.angularSpeed; + + if (system._angularSpeedGradients!.length > 1) { + particle._currentAngularSpeed2 = system._angularSpeedGradients![1].getFactor(); + } else { + particle._currentAngularSpeed2 = particle._currentAngularSpeed1; + } + particle.angle = RandomRange(system.minInitialRotation, system.maxInitialRotation); +} + +/** Sheet */ + +/** @internal */ +export function _CreateSheetData(particle: Particle, system: ThinParticleSystem) { + particle._initialStartSpriteCellId = system.startSpriteCellID; + particle._initialEndSpriteCellId = system.endSpriteCellID; + particle._initialSpriteCellLoop = system.spriteCellLoop; +} diff --git a/tools/src/effect/bjs/thinParticleSystem.ts b/tools/src/effect/bjs/thinParticleSystem.ts new file mode 100644 index 000000000..c3fdb6f5e --- /dev/null +++ b/tools/src/effect/bjs/thinParticleSystem.ts @@ -0,0 +1,2422 @@ +import type { Immutable, Nullable } from "../types"; +import { FactorGradient, ColorGradient, Color3Gradient, GradientHelper } from "../Misc/gradients"; +import type { Observer } from "../Misc/observable"; +import { Observable } from "../Misc/observable"; +import { Vector3, Matrix, TmpVectors } from "../Maths/math.vector"; +import { VertexBuffer, Buffer } from "../Buffers/buffer"; + +import type { Effect } from "../Materials/effect"; +import { RawTexture } from "../Materials/Textures/rawTexture"; +import { EngineStore } from "../Engines/engineStore"; +import type { IDisposable, Scene } from "../scene"; + +import type { IParticleSystem } from "./IParticleSystem"; +import { BaseParticleSystem } from "./baseParticleSystem"; +import { Particle } from "./particle"; +import { Constants } from "../Engines/constants"; +import type { IAnimatable } from "../Animations/animatable.interface"; +import { DrawWrapper } from "../Materials/drawWrapper"; + +import type { DataBuffer } from "../Buffers/dataBuffer"; +import { Color4, Color3, TmpColors } from "../Maths/math.color"; +import type { ISize } from "../Maths/math.size"; +import type { AbstractEngine } from "../Engines/abstractEngine"; + +import "../Engines/Extensions/engine.alpha"; +import { AddClipPlaneUniforms, PrepareStringDefinesForClipPlanes, BindClipPlane } from "../Materials/clipPlaneMaterialHelper"; + +import type { AbstractMesh } from "../Meshes/abstractMesh"; +import type { ProceduralTexture } from "../Materials/Textures/Procedurals/proceduralTexture"; +import { BindFogParameters, BindLogDepth } from "../Materials/materialHelper.functions"; +import { BoxParticleEmitter } from "./EmitterTypes/boxParticleEmitter"; +import { Lerp } from "../Maths/math.scalar.functions"; +import { PrepareSamplersForImageProcessing, PrepareUniformsForImageProcessing } from "../Materials/imageProcessingConfiguration.functions"; +import type { ThinEngine } from "../Engines/thinEngine"; +import { ShaderLanguage } from "core/Materials/shaderLanguage"; +import { + _CreateAngleData, + _CreateAngleGradientsData, + _CreateColorData, + _CreateColorDeadData, + _CreateColorGradientsData, + _CreateCustomDirectionData, + _CreateCustomPositionData, + _CreateDirectionData, + _CreateDragData, + _CreateEmitPowerData, + _CreateIsLocalData, + _CreateLifeGradientsData, + _CreateLifetimeData, + _CreateLimitVelocityGradients, + _CreateNoiseData, + _CreatePositionData, + _CreateRampData, + _CreateSheetData, + _CreateSizeData, + _CreateSizeGradientsData, + _CreateStartSizeGradientsData, + _CreateVelocityGradients, + _ProcessAngularSpeed, + _ProcessAngularSpeedGradients, + _ProcessColor, + _ProcessColorGradients, + _ProcessDirection, + _ProcessDragGradients, + _ProcessGravity, + _ProcessLimitVelocityGradients, + _ProcessNoise, + _ProcessPosition, + _ProcessRemapGradients, + _ProcessSizeGradients, + _ProcessVelocityGradients, +} from "./thinParticleSystem.function"; +import type { _IExecutionQueueItem } from "./Queue/executionQueue"; +import { _ConnectAfter, _ConnectBefore, _RemoveFromQueue } from "./Queue/executionQueue"; + +/** + * This represents a thin particle system in Babylon. + * Particles are often small sprites used to simulate hard-to-reproduce phenomena like fire, smoke, water, or abstract visual effects like magic glitter and faery dust. + * Particles can take different shapes while emitted like box, sphere, cone or you can write your custom function. + * This thin version contains a limited subset of the total features in order to provide users with a way to get particles but with a smaller footprint + * @example https://doc.babylonjs.com/features/featuresDeepDive/particles/particle_system/particle_system_intro + */ +export class ThinParticleSystem extends BaseParticleSystem implements IDisposable, IAnimatable, IParticleSystem { + /** + * Force all the particle systems to compile to glsl even on WebGPU engines. + * False by default. This is mostly meant for backward compatibility. + */ + public static ForceGLSL = false; + + /** + * This function can be defined to provide custom update for active particles. + * This function will be called instead of regular update (age, position, color, etc.). + * Do not forget that this function will be called on every frame so try to keep it simple and fast :) + */ + public updateFunction: (particles: Particle[]) => void; + + /** @internal */ + public _emitterWorldMatrix: Matrix; + /** @internal */ + public _emitterInverseWorldMatrix: Matrix = Matrix.Identity(); + + private _startDirectionFunction: Nullable<(worldMatrix: Matrix, directionToUpdate: Vector3, particle: Particle, isLocal: boolean) => void> = null; + + /** + * This function can be defined to specify initial direction for every new particle. + * It by default use the emitterType defined function + */ + public get startDirectionFunction(): Nullable<(worldMatrix: Matrix, directionToUpdate: Vector3, particle: Particle, isLocal: boolean) => void> { + return this._startDirectionFunction; + } + + public set startDirectionFunction(value: Nullable<(worldMatrix: Matrix, directionToUpdate: Vector3, particle: Particle, isLocal: boolean) => void>) { + if (this._startDirectionFunction === value) { + return; + } + this._startDirectionFunction = value; + + if (value) { + this._directionProcessing.process = _CreateCustomDirectionData; + } else { + this._directionProcessing.process = _CreateDirectionData; + } + } + + private _startPositionFunction: Nullable<(worldMatrix: Matrix, positionToUpdate: Vector3, particle: Particle, isLocal: boolean) => void> = null; + + /** + * This function can be defined to specify initial position for every new particle. + * It by default use the emitterType defined function + */ + public get startPositionFunction(): Nullable<(worldMatrix: Matrix, positionToUpdate: Vector3, particle: Particle, isLocal: boolean) => void> { + return this._startPositionFunction; + } + + public set startPositionFunction(value: Nullable<(worldMatrix: Matrix, positionToUpdate: Vector3, particle: Particle, isLocal: boolean) => void>) { + if (this._startPositionFunction === value) { + return; + } + this._startPositionFunction = value; + + if (value) { + this._positionCreation.process = _CreateCustomPositionData; + } else { + this._positionCreation.process = _CreatePositionData; + } + } + + /** + * @internal + */ + public _inheritedVelocityOffset = new Vector3(); + /** + * An event triggered when the system is disposed + */ + public onDisposeObservable = new Observable(); + /** + * An event triggered when the system is stopped + */ + public onStoppedObservable = new Observable(); + /** + * An event triggered when the system is started + */ + public onStartedObservable = new Observable(); + + private _onDisposeObserver: Nullable>; + /** + * Sets a callback that will be triggered when the system is disposed + */ + public set onDispose(callback: () => void) { + if (this._onDisposeObserver) { + this.onDisposeObservable.remove(this._onDisposeObserver); + } + this._onDisposeObserver = this.onDisposeObservable.add(callback); + } + + /** @internal */ + public _noiseTextureSize: Nullable = null; + /** @internal */ + public _noiseTextureData: Nullable = null; + private _particles = new Array(); + private _epsilon: number; + private _capacity: number; + private _stockParticles = new Array(); + private _newPartsExcess = 0; + private _vertexData: Float32Array; + private _vertexBuffer: Nullable; + private _vertexBuffers: { [key: string]: VertexBuffer } = {}; + private _spriteBuffer: Nullable; + private _indexBuffer: Nullable; + private _linesIndexBuffer: Nullable; + private _linesIndexBufferUseInstancing: Nullable; + private _drawWrappers: DrawWrapper[][]; // first index is render pass id, second index is blend mode + /** @internal */ + public _customWrappers: { [blendMode: number]: Nullable }; + /** @internal */ + public _scaledColorStep = new Color4(0, 0, 0, 0); + /** @internal */ + public _colorDiff = new Color4(0, 0, 0, 0); + /** @internal */ + public _scaledGravity = Vector3.Zero(); + private _currentRenderId = -1; + private _alive: boolean; + private _useInstancing = false; + private _vertexArrayObject: Nullable; + + private _isDisposed = false; + + /** + * Gets a boolean indicating that the particle system was disposed + */ + public get isDisposed(): boolean { + return this._isDisposed; + } + + private _started = false; + private _stopped = false; + /** @internal */ + public _actualFrame = 0; + /** @internal */ + public _scaledUpdateSpeed: number; + private _vertexBufferSize: number; + + /** @internal */ + public _currentEmitRateGradient: Nullable; + /** @internal */ + public _currentEmitRate1 = 0; + /** @internal */ + public _currentEmitRate2 = 0; + + /** @internal */ + public _currentStartSizeGradient: Nullable; + /** @internal */ + public _currentStartSize1 = 0; + /** @internal */ + public _currentStartSize2 = 0; + + /** Indicates that the update of particles is done in the animate function */ + public readonly updateInAnimate = true; + + private readonly _rawTextureWidth = 256; + private _rampGradientsTexture: Nullable; + private _useRampGradients = false; + + /** @internal */ + public _updateQueueStart: Nullable<_IExecutionQueueItem> = null; + protected _colorProcessing: _IExecutionQueueItem; + protected _angularSpeedGradientProcessing: _IExecutionQueueItem; + protected _angularSpeedProcessing: _IExecutionQueueItem; + protected _velocityGradientProcessing: _IExecutionQueueItem; + protected _directionProcessing: _IExecutionQueueItem; + protected _limitVelocityGradientProcessing: _IExecutionQueueItem; + protected _positionProcessing: _IExecutionQueueItem; + protected _dragGradientProcessing: _IExecutionQueueItem; + protected _noiseProcessing: _IExecutionQueueItem; + protected _gravityProcessing: _IExecutionQueueItem; + protected _sizeGradientProcessing: _IExecutionQueueItem; + protected _remapGradientProcessing: _IExecutionQueueItem; + + /** @internal */ + public _lifeTimeCreation: _IExecutionQueueItem; + /** @internal */ + public _positionCreation: _IExecutionQueueItem; + private _isLocalCreation: _IExecutionQueueItem; + /** @internal */ + public _directionCreation: _IExecutionQueueItem; + private _emitPowerCreation: _IExecutionQueueItem; + /** @internal */ + public _sizeCreation: _IExecutionQueueItem; + private _startSizeCreation: Nullable<_IExecutionQueueItem> = null; + /** @internal */ + public _angleCreation: _IExecutionQueueItem; + private _velocityCreation: _IExecutionQueueItem; + private _limitVelocityCreation: _IExecutionQueueItem; + private _dragCreation: _IExecutionQueueItem; + /** @internal */ + public _colorCreation: _IExecutionQueueItem; + /** @internal */ + public _colorDeadCreation: _IExecutionQueueItem; + private _sheetCreation: _IExecutionQueueItem; + private _rampCreation: _IExecutionQueueItem; + private _noiseCreation: _IExecutionQueueItem; + private _createQueueStart: Nullable<_IExecutionQueueItem> = null; + + /** @internal */ + public _tempScaledUpdateSpeed: number; + /** @internal */ + public _ratio: number; + /** @internal */ + public _emitPower: number; + + /** Gets or sets a matrix to use to compute projection */ + public defaultProjectionMatrix: Matrix; + + /** Gets or sets a matrix to use to compute view */ + public defaultViewMatrix: Matrix; + + /** Gets or sets a boolean indicating that ramp gradients must be used + * @see https://doc.babylonjs.com/features/featuresDeepDive/particles/particle_system/particle_system_intro#ramp-gradients + */ + public get useRampGradients(): boolean { + return this._useRampGradients; + } + + public set useRampGradients(value: boolean) { + if (this._useRampGradients === value) { + return; + } + + this._useRampGradients = value; + + this._resetEffect(); + + if (value) { + this._rampCreation = { + process: _CreateRampData, + previousItem: null, + nextItem: null, + }; + _ConnectAfter(this._rampCreation, this._colorDeadCreation); + this._remapGradientProcessing = { + process: _ProcessRemapGradients, + previousItem: null, + nextItem: null, + }; + _ConnectAfter(this._remapGradientProcessing, this._gravityProcessing); + } else { + _RemoveFromQueue(this._rampCreation); + _RemoveFromQueue(this._remapGradientProcessing); + } + } + + private _isLocal = false; + + /** + * Specifies if the particles are updated in emitter local space or world space + */ + public get isLocal() { + return this._isLocal; + } + + public set isLocal(value: boolean) { + if (this._isLocal === value) { + return; + } + + this._isLocal = value; + + if (value) { + this._isLocalCreation = { + process: _CreateIsLocalData, + previousItem: null, + nextItem: null, + }; + + _ConnectAfter(this._isLocalCreation, this._positionCreation); + } else { + _RemoveFromQueue(this._isLocalCreation); + } + } + + /** Indicates that the particle system is CPU based */ + public readonly isGPU = false; + + /** + * Gets the current list of active particles + */ + public get particles(): Particle[] { + return this._particles; + } + + /** Shader language used by the material */ + protected _shaderLanguage = ShaderLanguage.GLSL; + + /** + * Gets the shader language used in this material. + */ + public get shaderLanguage(): ShaderLanguage { + return this._shaderLanguage; + } + + /** @internal */ + public override get _isAnimationSheetEnabled() { + return this._animationSheetEnabled; + } + + public override set _isAnimationSheetEnabled(value: boolean) { + if (this._animationSheetEnabled === value) { + return; + } + + this._animationSheetEnabled = value; + + if (value) { + this._sheetCreation = { + process: _CreateSheetData, + previousItem: null, + nextItem: null, + }; + + _ConnectAfter(this._sheetCreation, this._colorDeadCreation); + } else { + _RemoveFromQueue(this._sheetCreation); + } + + this._reset(); + } + + /** + * Gets the number of particles active at the same time. + * @returns The number of active particles. + */ + public getActiveCount() { + return this._particles.length; + } + + /** + * Returns the string "ParticleSystem" + * @returns a string containing the class name + */ + public getClassName(): string { + return "ParticleSystem"; + } + + /** + * Gets a boolean indicating that the system is stopping + * @returns true if the system is currently stopping + */ + public isStopping() { + return this._stopped && this.isAlive(); + } + + /** + * Gets the custom effect used to render the particles + * @param blendMode Blend mode for which the effect should be retrieved + * @returns The effect + */ + public getCustomEffect(blendMode: number = 0): Nullable { + return this._customWrappers[blendMode]?.effect ?? this._customWrappers[0]!.effect; + } + + private _getCustomDrawWrapper(blendMode: number = 0): Nullable { + return this._customWrappers[blendMode] ?? this._customWrappers[0]; + } + + /** + * Sets the custom effect used to render the particles + * @param effect The effect to set + * @param blendMode Blend mode for which the effect should be set + */ + public setCustomEffect(effect: Nullable, blendMode: number = 0) { + this._customWrappers[blendMode] = new DrawWrapper(this._engine); + this._customWrappers[blendMode].effect = effect; + if (this._customWrappers[blendMode].drawContext) { + this._customWrappers[blendMode].drawContext.useInstancing = this._useInstancing; + } + } + + /** @internal */ + private _onBeforeDrawParticlesObservable: Nullable>> = null; + + /** + * Observable that will be called just before the particles are drawn + */ + public get onBeforeDrawParticlesObservable(): Observable> { + if (!this._onBeforeDrawParticlesObservable) { + this._onBeforeDrawParticlesObservable = new Observable>(); + } + + return this._onBeforeDrawParticlesObservable; + } + + /** + * Gets the name of the particle vertex shader + */ + public get vertexShaderName(): string { + return "particles"; + } + + /** + * Gets the vertex buffers used by the particle system + */ + public get vertexBuffers(): Immutable<{ [key: string]: VertexBuffer }> { + return this._vertexBuffers; + } + + /** + * Gets the index buffer used by the particle system (or null if no index buffer is used (if _useInstancing=true)) + */ + public get indexBuffer(): Nullable { + return this._indexBuffer; + } + + public override get noiseTexture() { + return this._noiseTexture; + } + + public override set noiseTexture(value: Nullable) { + if (this.noiseTexture === value) { + return; + } + + this._noiseTexture = value; + + if (!value) { + _RemoveFromQueue(this._noiseCreation); + _RemoveFromQueue(this._noiseProcessing); + return; + } + + this._noiseCreation = { + process: _CreateNoiseData, + previousItem: null, + nextItem: null, + }; + _ConnectAfter(this._noiseCreation, this._colorDeadCreation); + + this._noiseProcessing = { + process: _ProcessNoise, + previousItem: null, + nextItem: null, + }; + _ConnectAfter(this._noiseProcessing, this._positionProcessing); + } + + /** + * Instantiates a particle system. + * Particles are often small sprites used to simulate hard-to-reproduce phenomena like fire, smoke, water, or abstract visual effects like magic glitter and faery dust. + * @param name The name of the particle system + * @param capacity The max number of particles alive at the same time + * @param sceneOrEngine The scene the particle system belongs to or the engine to use if no scene + * @param customEffect a custom effect used to change the way particles are rendered by default + * @param isAnimationSheetEnabled Must be true if using a spritesheet to animate the particles texture + * @param epsilon Offset used to render the particles + * @param noUpdateQueue If true, the particle system will start with an empty update queue + */ + constructor( + name: string, + capacity: number, + sceneOrEngine: Scene | AbstractEngine, + customEffect: Nullable = null, + isAnimationSheetEnabled: boolean = false, + epsilon: number = 0.01, + noUpdateQueue: boolean = false + ) { + super(name); + + this._capacity = capacity; + + this._epsilon = epsilon; + + if (!sceneOrEngine || sceneOrEngine.getClassName() === "Scene") { + this._scene = (sceneOrEngine as Scene) || EngineStore.LastCreatedScene; + this._engine = this._scene.getEngine(); + this.uniqueId = this._scene.getUniqueId(); + this._scene.particleSystems.push(this); + } else { + this._engine = sceneOrEngine as AbstractEngine; + this.defaultProjectionMatrix = Matrix.PerspectiveFovLH(0.8, 1, 0.1, 100, this._engine.isNDCHalfZRange); + } + + if (this._engine.getCaps().vertexArrayObject) { + this._vertexArrayObject = null; + } + + // eslint-disable-next-line @typescript-eslint/no-floating-promises + this._initShaderSourceAsync(); + + // Creation queue + this._lifeTimeCreation = { + process: _CreateLifetimeData, + previousItem: null, + nextItem: null, + }; + + this._positionCreation = { + process: _CreatePositionData, + previousItem: null, + nextItem: null, + }; + _ConnectAfter(this._positionCreation, this._lifeTimeCreation); + + this._directionCreation = { + process: _CreateDirectionData, + previousItem: null, + nextItem: null, + }; + _ConnectAfter(this._directionCreation, this._positionCreation); + + this._emitPowerCreation = { + process: _CreateEmitPowerData, + previousItem: null, + nextItem: null, + }; + _ConnectAfter(this._emitPowerCreation, this._directionCreation); + + this._sizeCreation = { + process: _CreateSizeData, + previousItem: null, + nextItem: null, + }; + _ConnectAfter(this._sizeCreation, this._emitPowerCreation); + + this._angleCreation = { + process: _CreateAngleData, + previousItem: null, + nextItem: null, + }; + _ConnectAfter(this._angleCreation, this._sizeCreation); + + this._colorCreation = { + process: _CreateColorData, + previousItem: null, + nextItem: null, + }; + _ConnectAfter(this._colorCreation, this._angleCreation); + + this._colorDeadCreation = { + process: _CreateColorDeadData, + previousItem: null, + nextItem: null, + }; + _ConnectAfter(this._colorDeadCreation, this._colorCreation); + + this._createQueueStart = this._lifeTimeCreation; + + // Processing queue + if (!noUpdateQueue) { + this._colorProcessing = { + process: _ProcessColor, + previousItem: null, + nextItem: null, + }; + + this._angularSpeedProcessing = { + process: _ProcessAngularSpeed, + previousItem: null, + nextItem: null, + }; + _ConnectAfter(this._angularSpeedProcessing, this._colorProcessing); + + this._directionProcessing = { + process: _ProcessDirection, + previousItem: null, + nextItem: null, + }; + _ConnectAfter(this._directionProcessing, this._angularSpeedProcessing); + + this._positionProcessing = { + process: _ProcessPosition, + previousItem: null, + nextItem: null, + }; + _ConnectAfter(this._positionProcessing, this._directionProcessing); + + this._gravityProcessing = { + process: _ProcessGravity, + previousItem: null, + nextItem: null, + }; + + _ConnectAfter(this._gravityProcessing, this._positionProcessing); + + this._updateQueueStart = this._colorProcessing; + } + + this._isAnimationSheetEnabled = isAnimationSheetEnabled; + + // Setup the default processing configuration to the scene. + this._attachImageProcessingConfiguration(null); + + // eslint-disable-next-line @typescript-eslint/naming-convention + this._customWrappers = { 0: new DrawWrapper(this._engine) }; + this._customWrappers[0]!.effect = customEffect; + + this._drawWrappers = []; + this._useInstancing = this._engine.getCaps().instancedArrays; + + this._createIndexBuffer(); + this._createVertexBuffers(); + + // Default emitter type + this.particleEmitterType = new BoxParticleEmitter(); + + // Update + this.updateFunction = (particles: Particle[]): void => { + if (this.noiseTexture) { + // We need to get texture data back to CPU + this._noiseTextureSize = this.noiseTexture.getSize(); + // eslint-disable-next-line @typescript-eslint/no-floating-promises, github/no-then + this.noiseTexture.getContent()?.then((data) => { + this._noiseTextureData = data as Uint8Array; + }); + } + + const sameParticleArray = particles === this._particles; + + for (let index = 0; index < particles.length; index++) { + const particle = particles[index]; + + this._tempScaledUpdateSpeed = this._scaledUpdateSpeed; + const previousAge = particle.age; + particle.age += this._tempScaledUpdateSpeed; + + // Evaluate step to death + if (particle.age > particle.lifeTime) { + const diff = particle.age - previousAge; + const oldDiff = particle.lifeTime - previousAge; + + this._tempScaledUpdateSpeed = (oldDiff * this._tempScaledUpdateSpeed) / diff; + + particle.age = particle.lifeTime; + } + + this._ratio = particle.age / particle.lifeTime; + particle._directionScale = this._tempScaledUpdateSpeed; + + // Processing queue + let currentQueueItem = this._updateQueueStart; + + while (currentQueueItem) { + currentQueueItem.process(particle, this); + currentQueueItem = currentQueueItem.nextItem; + } + + if (this._isAnimationSheetEnabled && !noUpdateQueue) { + particle.updateCellIndex(); + } + + // Update the position of the attached sub-emitters to match their attached particle + particle._inheritParticleInfoToSubEmitters(); + + if (particle.age >= particle.lifeTime) { + // Recycle by swapping with last particle + this._emitFromParticle(particle); + if (particle._attachedSubEmitters) { + for (const subEmitter of particle._attachedSubEmitters) { + subEmitter.particleSystem.disposeOnStop = true; + subEmitter.particleSystem.stop(); + } + particle._attachedSubEmitters = null; + } + this.recycleParticle(particle); + if (sameParticleArray) { + index--; + } + continue; + } + } + }; + } + + /** @internal */ + public _emitFromParticle: (particle: Particle) => void = (_particle) => { + // Do nothing + }; + + serialize(_serializeTexture: boolean) { + throw new Error("Method not implemented."); + } + + /** + * Clones the particle system. + * @param name The name of the cloned object + * @param newEmitter The new emitter to use + * @param _cloneTexture Also clone the textures if true + */ + public clone(name: string, newEmitter: any, _cloneTexture = false): ThinParticleSystem { + throw new Error("Method not implemented."); + } + + private _addFactorGradient(factorGradients: FactorGradient[], gradient: number, factor: number, factor2?: number) { + const newGradient = new FactorGradient(gradient, factor, factor2); + factorGradients.push(newGradient); + + factorGradients.sort((a, b) => { + if (a.gradient < b.gradient) { + return -1; + } else if (a.gradient > b.gradient) { + return 1; + } + + return 0; + }); + } + + private _removeFactorGradient(factorGradients: Nullable, gradient: number) { + if (!factorGradients) { + return; + } + + let index = 0; + for (const factorGradient of factorGradients) { + if (factorGradient.gradient === gradient) { + factorGradients.splice(index, 1); + break; + } + index++; + } + } + + private _syncLifeTimeCreation() { + if (this.targetStopDuration && this._lifeTimeGradients && this._lifeTimeGradients.length > 0) { + this._lifeTimeCreation.process = _CreateLifeGradientsData; + return; + } + + this._lifeTimeCreation.process = _CreateLifetimeData; + } + + private _syncStartSizeCreation() { + if (this._startSizeGradients && this._startSizeGradients[0] && this.targetStopDuration) { + if (!this._startSizeCreation) { + this._startSizeCreation = { + process: _CreateStartSizeGradientsData, + previousItem: null, + nextItem: null, + }; + _ConnectAfter(this._startSizeCreation, this._sizeCreation); + } + return; + } + + if (this._startSizeCreation) { + _RemoveFromQueue(this._startSizeCreation); + this._startSizeCreation = null; + } + } + + public override get targetStopDuration(): number { + return this._targetStopDuration; + } + + public override set targetStopDuration(value: number) { + if (this.targetStopDuration === value) { + return; + } + + this._targetStopDuration = value; + + this._syncLifeTimeCreation(); + this._syncStartSizeCreation(); + } + + /** + * Adds a new life time gradient + * @param gradient defines the gradient to use (between 0 and 1) + * @param factor defines the life time factor to affect to the specified gradient + * @param factor2 defines an additional factor used to define a range ([factor, factor2]) with main value to pick the final value from + * @returns the current particle system + */ + public addLifeTimeGradient(gradient: number, factor: number, factor2?: number): IParticleSystem { + if (!this._lifeTimeGradients) { + this._lifeTimeGradients = []; + } + + this._addFactorGradient(this._lifeTimeGradients, gradient, factor, factor2); + + this._syncLifeTimeCreation(); + + return this; + } + + /** + * Remove a specific life time gradient + * @param gradient defines the gradient to remove + * @returns the current particle system + */ + public removeLifeTimeGradient(gradient: number): IParticleSystem { + this._removeFactorGradient(this._lifeTimeGradients, gradient); + + this._syncLifeTimeCreation(); + + return this; + } + + /** + * Adds a new size gradient + * @param gradient defines the gradient to use (between 0 and 1) + * @param factor defines the size factor to affect to the specified gradient + * @param factor2 defines an additional factor used to define a range ([factor, factor2]) with main value to pick the final value from + * @returns the current particle system + */ + public addSizeGradient(gradient: number, factor: number, factor2?: number): IParticleSystem { + if (!this._sizeGradients) { + this._sizeGradients = []; + } + + if (this._sizeGradients.length === 0) { + this._sizeCreation.process = _CreateSizeGradientsData; + + this._sizeGradientProcessing = { + process: _ProcessSizeGradients, + previousItem: null, + nextItem: null, + }; + _ConnectBefore(this._sizeGradientProcessing, this._gravityProcessing); + } + + this._addFactorGradient(this._sizeGradients, gradient, factor, factor2); + + return this; + } + + /** + * Remove a specific size gradient + * @param gradient defines the gradient to remove + * @returns the current particle system + */ + public removeSizeGradient(gradient: number): IParticleSystem { + this._removeFactorGradient(this._sizeGradients, gradient); + + if (this._sizeGradients?.length === 0) { + _RemoveFromQueue(this._sizeGradientProcessing); + this._sizeCreation.process = _CreateSizeData; + } + + return this; + } + + /** + * Adds a new color remap gradient + * @param gradient defines the gradient to use (between 0 and 1) + * @param min defines the color remap minimal range + * @param max defines the color remap maximal range + * @returns the current particle system + */ + public addColorRemapGradient(gradient: number, min: number, max: number): IParticleSystem { + if (!this._colorRemapGradients) { + this._colorRemapGradients = []; + } + + this._addFactorGradient(this._colorRemapGradients, gradient, min, max); + + return this; + } + + /** + * Remove a specific color remap gradient + * @param gradient defines the gradient to remove + * @returns the current particle system + */ + public removeColorRemapGradient(gradient: number): IParticleSystem { + this._removeFactorGradient(this._colorRemapGradients, gradient); + + return this; + } + + /** + * Adds a new alpha remap gradient + * @param gradient defines the gradient to use (between 0 and 1) + * @param min defines the alpha remap minimal range + * @param max defines the alpha remap maximal range + * @returns the current particle system + */ + public addAlphaRemapGradient(gradient: number, min: number, max: number): IParticleSystem { + if (!this._alphaRemapGradients) { + this._alphaRemapGradients = []; + } + + this._addFactorGradient(this._alphaRemapGradients, gradient, min, max); + + return this; + } + + /** + * Remove a specific alpha remap gradient + * @param gradient defines the gradient to remove + * @returns the current particle system + */ + public removeAlphaRemapGradient(gradient: number): IParticleSystem { + this._removeFactorGradient(this._alphaRemapGradients, gradient); + + return this; + } + + /** + * Adds a new angular speed gradient + * @param gradient defines the gradient to use (between 0 and 1) + * @param factor defines the angular speed to affect to the specified gradient + * @param factor2 defines an additional factor used to define a range ([factor, factor2]) with main value to pick the final value from + * @returns the current particle system + */ + public addAngularSpeedGradient(gradient: number, factor: number, factor2?: number): IParticleSystem { + if (!this._angularSpeedGradients) { + this._angularSpeedGradients = []; + } + + if (this._angularSpeedGradients.length === 0) { + this._angleCreation.process = _CreateAngleGradientsData; + + this._angularSpeedGradientProcessing = { + process: _ProcessAngularSpeedGradients, + previousItem: null, + nextItem: null, + }; + + _ConnectBefore(this._angularSpeedGradientProcessing, this._angularSpeedProcessing); + } + + this._addFactorGradient(this._angularSpeedGradients, gradient, factor, factor2); + + return this; + } + + /** + * Remove a specific angular speed gradient + * @param gradient defines the gradient to remove + * @returns the current particle system + */ + public removeAngularSpeedGradient(gradient: number): IParticleSystem { + this._removeFactorGradient(this._angularSpeedGradients, gradient); + + if (this._angularSpeedGradients?.length === 0) { + this._angleCreation.process = _CreateAngleData; + _RemoveFromQueue(this._angularSpeedGradientProcessing); + } + + return this; + } + + /** + * Adds a new velocity gradient + * @param gradient defines the gradient to use (between 0 and 1) + * @param factor defines the velocity to affect to the specified gradient + * @param factor2 defines an additional factor used to define a range ([factor, factor2]) with main value to pick the final value from + * @returns the current particle system + */ + public addVelocityGradient(gradient: number, factor: number, factor2?: number): IParticleSystem { + if (!this._velocityGradients) { + this._velocityGradients = []; + } + + if (this._velocityGradients.length === 0) { + this._velocityCreation = { + process: _CreateVelocityGradients, + previousItem: null, + nextItem: null, + }; + _ConnectAfter(this._velocityCreation, this._angleCreation); + + this._velocityGradientProcessing = { + process: _ProcessVelocityGradients, + previousItem: null, + nextItem: null, + }; + _ConnectBefore(this._velocityGradientProcessing, this._directionProcessing); + } + + this._addFactorGradient(this._velocityGradients, gradient, factor, factor2); + + return this; + } + + /** + * Remove a specific velocity gradient + * @param gradient defines the gradient to remove + * @returns the current particle system + */ + public removeVelocityGradient(gradient: number): IParticleSystem { + this._removeFactorGradient(this._velocityGradients, gradient); + + if (this._velocityGradients?.length === 0) { + _RemoveFromQueue(this._velocityCreation); + _RemoveFromQueue(this._velocityGradientProcessing); + } + + return this; + } + + /** + * Adds a new limit velocity gradient + * @param gradient defines the gradient to use (between 0 and 1) + * @param factor defines the limit velocity value to affect to the specified gradient + * @param factor2 defines an additional factor used to define a range ([factor, factor2]) with main value to pick the final value from + * @returns the current particle system + */ + public addLimitVelocityGradient(gradient: number, factor: number, factor2?: number): IParticleSystem { + if (!this._limitVelocityGradients) { + this._limitVelocityGradients = []; + } + + if (this._limitVelocityGradients.length === 0) { + this._limitVelocityCreation = { + process: _CreateLimitVelocityGradients, + previousItem: null, + nextItem: null, + }; + _ConnectAfter(this._limitVelocityCreation, this._angleCreation); + + this._limitVelocityGradientProcessing = { + process: _ProcessLimitVelocityGradients, + previousItem: null, + nextItem: null, + }; + _ConnectAfter(this._limitVelocityGradientProcessing, this._directionProcessing); + } + + this._addFactorGradient(this._limitVelocityGradients, gradient, factor, factor2); + + return this; + } + + /** + * Remove a specific limit velocity gradient + * @param gradient defines the gradient to remove + * @returns the current particle system + */ + public removeLimitVelocityGradient(gradient: number): IParticleSystem { + this._removeFactorGradient(this._limitVelocityGradients, gradient); + + if (this._limitVelocityGradients?.length === 0) { + _RemoveFromQueue(this._limitVelocityCreation); + _RemoveFromQueue(this._limitVelocityGradientProcessing); + } + + return this; + } + + /** + * Adds a new drag gradient + * @param gradient defines the gradient to use (between 0 and 1) + * @param factor defines the drag value to affect to the specified gradient + * @param factor2 defines an additional factor used to define a range ([factor, factor2]) with main value to pick the final value from + * @returns the current particle system + */ + public addDragGradient(gradient: number, factor: number, factor2?: number): IParticleSystem { + if (!this._dragGradients) { + this._dragGradients = []; + } + + if (this._dragGradients.length === 0) { + this._dragCreation = { + process: _CreateDragData, + previousItem: null, + nextItem: null, + }; + _ConnectBefore(this._dragCreation, this._colorDeadCreation); + + this._dragGradientProcessing = { + process: _ProcessDragGradients, + previousItem: null, + nextItem: null, + }; + _ConnectBefore(this._dragGradientProcessing, this._positionProcessing); + } + + this._addFactorGradient(this._dragGradients, gradient, factor, factor2); + + return this; + } + + /** + * Remove a specific drag gradient + * @param gradient defines the gradient to remove + * @returns the current particle system + */ + public removeDragGradient(gradient: number): IParticleSystem { + this._removeFactorGradient(this._dragGradients, gradient); + + if (this._dragGradients?.length === 0) { + _RemoveFromQueue(this._dragCreation); + _RemoveFromQueue(this._dragGradientProcessing); + } + + return this; + } + + /** + * Adds a new emit rate gradient (please note that this will only work if you set the targetStopDuration property) + * @param gradient defines the gradient to use (between 0 and 1) + * @param factor defines the emit rate value to affect to the specified gradient + * @param factor2 defines an additional factor used to define a range ([factor, factor2]) with main value to pick the final value from + * @returns the current particle system + */ + public addEmitRateGradient(gradient: number, factor: number, factor2?: number): IParticleSystem { + if (!this._emitRateGradients) { + this._emitRateGradients = []; + } + + this._addFactorGradient(this._emitRateGradients, gradient, factor, factor2); + return this; + } + + /** + * Remove a specific emit rate gradient + * @param gradient defines the gradient to remove + * @returns the current particle system + */ + public removeEmitRateGradient(gradient: number): IParticleSystem { + this._removeFactorGradient(this._emitRateGradients, gradient); + + return this; + } + + /** + * Adds a new start size gradient (please note that this will only work if you set the targetStopDuration property) + * @param gradient defines the gradient to use (between 0 and 1) + * @param factor defines the start size value to affect to the specified gradient + * @param factor2 defines an additional factor used to define a range ([factor, factor2]) with main value to pick the final value from + * @returns the current particle system + */ + public addStartSizeGradient(gradient: number, factor: number, factor2?: number): IParticleSystem { + if (!this._startSizeGradients) { + this._startSizeGradients = []; + } + + this._addFactorGradient(this._startSizeGradients, gradient, factor, factor2); + + this._syncStartSizeCreation(); + + return this; + } + + /** + * Remove a specific start size gradient + * @param gradient defines the gradient to remove + * @returns the current particle system + */ + public removeStartSizeGradient(gradient: number): IParticleSystem { + this._removeFactorGradient(this._startSizeGradients, gradient); + + this._syncStartSizeCreation(); + + return this; + } + + private _createRampGradientTexture() { + if (!this._rampGradients || !this._rampGradients.length || this._rampGradientsTexture || !this._scene) { + return; + } + + const data = new Uint8Array(this._rawTextureWidth * 4); + const tmpColor = TmpColors.Color3[0]; + + for (let x = 0; x < this._rawTextureWidth; x++) { + const ratio = x / this._rawTextureWidth; + + GradientHelper.GetCurrentGradient(ratio, this._rampGradients, (currentGradient, nextGradient, scale) => { + Color3.LerpToRef((currentGradient).color, (nextGradient).color, scale, tmpColor); + data[x * 4] = tmpColor.r * 255; + data[x * 4 + 1] = tmpColor.g * 255; + data[x * 4 + 2] = tmpColor.b * 255; + data[x * 4 + 3] = 255; + }); + } + + this._rampGradientsTexture = RawTexture.CreateRGBATexture(data, this._rawTextureWidth, 1, this._scene, false, false, Constants.TEXTURE_NEAREST_SAMPLINGMODE); + } + + /** + * Gets the current list of ramp gradients. + * You must use addRampGradient and removeRampGradient to update this list + * @returns the list of ramp gradients + */ + public getRampGradients(): Nullable> { + return this._rampGradients; + } + + /** Force the system to rebuild all gradients that need to be resync */ + public forceRefreshGradients() { + this._syncRampGradientTexture(); + } + + private _syncRampGradientTexture() { + if (!this._rampGradients) { + return; + } + + this._rampGradients.sort((a, b) => { + if (a.gradient < b.gradient) { + return -1; + } else if (a.gradient > b.gradient) { + return 1; + } + + return 0; + }); + + if (this._rampGradientsTexture) { + this._rampGradientsTexture.dispose(); + this._rampGradientsTexture = null; + } + + this._createRampGradientTexture(); + } + + /** + * Adds a new ramp gradient used to remap particle colors + * @param gradient defines the gradient to use (between 0 and 1) + * @param color defines the color to affect to the specified gradient + * @returns the current particle system + */ + public addRampGradient(gradient: number, color: Color3): ThinParticleSystem { + if (!this._rampGradients) { + this._rampGradients = []; + } + + const rampGradient = new Color3Gradient(gradient, color); + this._rampGradients.push(rampGradient); + + this._syncRampGradientTexture(); + + return this; + } + + /** + * Remove a specific ramp gradient + * @param gradient defines the gradient to remove + * @returns the current particle system + */ + public removeRampGradient(gradient: number): ThinParticleSystem { + this._removeGradientAndTexture(gradient, this._rampGradients, this._rampGradientsTexture); + this._rampGradientsTexture = null; + + if (this._rampGradients && this._rampGradients.length > 0) { + this._createRampGradientTexture(); + } + + return this; + } + + /** + * Adds a new color gradient + * @param gradient defines the gradient to use (between 0 and 1) + * @param color1 defines the color to affect to the specified gradient + * @param color2 defines an additional color used to define a range ([color, color2]) with main color to pick the final color from + * @returns this particle system + */ + public addColorGradient(gradient: number, color1: Color4, color2?: Color4): IParticleSystem { + if (!this._colorGradients) { + this._colorGradients = []; + } + + if (this._colorGradients.length === 0) { + this._colorCreation.process = _CreateColorGradientsData; + this._colorProcessing.process = _ProcessColorGradients; + } + + const colorGradient = new ColorGradient(gradient, color1, color2); + this._colorGradients.push(colorGradient); + + this._colorGradients.sort((a, b) => { + if (a.gradient < b.gradient) { + return -1; + } else if (a.gradient > b.gradient) { + return 1; + } + + return 0; + }); + + return this; + } + + /** + * Remove a specific color gradient + * @param gradient defines the gradient to remove + * @returns this particle system + */ + public removeColorGradient(gradient: number): IParticleSystem { + if (!this._colorGradients) { + return this; + } + + let index = 0; + for (const colorGradient of this._colorGradients) { + if (colorGradient.gradient === gradient) { + this._colorGradients.splice(index, 1); + break; + } + index++; + } + + if (this._colorGradients.length === 0) { + this._colorCreation.process = _CreateColorData; + this._colorProcessing.process = _ProcessColor; + } + + return this; + } + + /** + * Resets the draw wrappers cache + */ + public resetDrawCache(): void { + if (!this._drawWrappers) { + return; + } + for (const drawWrappers of this._drawWrappers) { + if (drawWrappers) { + for (const drawWrapper of drawWrappers) { + drawWrapper?.dispose(); + } + } + } + + this._drawWrappers = []; + } + + /** @internal */ + public _fetchR(u: number, v: number, width: number, height: number, pixels: Uint8Array | Uint8ClampedArray): number { + u = Math.abs(u) * 0.5 + 0.5; + v = Math.abs(v) * 0.5 + 0.5; + + const wrappedU = (u * width) % width | 0; + const wrappedV = (v * height) % height | 0; + + const position = (wrappedU + wrappedV * width) * 4; + return pixels[position] / 255; + } + + protected override _reset() { + this._resetEffect(); + } + + private _resetEffect() { + if (this._vertexBuffer) { + this._vertexBuffer.dispose(); + this._vertexBuffer = null; + } + + if (this._spriteBuffer) { + this._spriteBuffer.dispose(); + this._spriteBuffer = null; + } + + if (this._vertexArrayObject) { + (this._engine as ThinEngine).releaseVertexArrayObject(this._vertexArrayObject); + this._vertexArrayObject = null; + } + + this._createVertexBuffers(); + } + + private _createVertexBuffers() { + this._vertexBufferSize = this._useInstancing ? 10 : 12; + if (this._isAnimationSheetEnabled) { + this._vertexBufferSize += 1; + } + + if ( + !this._isBillboardBased || + this.billboardMode === Constants.PARTICLES_BILLBOARDMODE_STRETCHED || + this.billboardMode === Constants.PARTICLES_BILLBOARDMODE_STRETCHED_LOCAL + ) { + this._vertexBufferSize += 3; + } + + if (this._useRampGradients) { + this._vertexBufferSize += 4; + } + + const engine = this._engine; + const vertexSize = this._vertexBufferSize * (this._useInstancing ? 1 : 4); + this._vertexData = new Float32Array(this._capacity * vertexSize); + this._vertexBuffer = new Buffer(engine, this._vertexData, true, vertexSize); + + let dataOffset = 0; + const positions = this._vertexBuffer.createVertexBuffer(VertexBuffer.PositionKind, dataOffset, 3, this._vertexBufferSize, this._useInstancing); + this._vertexBuffers[VertexBuffer.PositionKind] = positions; + dataOffset += 3; + + const colors = this._vertexBuffer.createVertexBuffer(VertexBuffer.ColorKind, dataOffset, 4, this._vertexBufferSize, this._useInstancing); + this._vertexBuffers[VertexBuffer.ColorKind] = colors; + dataOffset += 4; + + const options = this._vertexBuffer.createVertexBuffer("angle", dataOffset, 1, this._vertexBufferSize, this._useInstancing); + this._vertexBuffers["angle"] = options; + dataOffset += 1; + + const size = this._vertexBuffer.createVertexBuffer("size", dataOffset, 2, this._vertexBufferSize, this._useInstancing); + this._vertexBuffers["size"] = size; + dataOffset += 2; + + if (this._isAnimationSheetEnabled) { + const cellIndexBuffer = this._vertexBuffer.createVertexBuffer("cellIndex", dataOffset, 1, this._vertexBufferSize, this._useInstancing); + this._vertexBuffers["cellIndex"] = cellIndexBuffer; + dataOffset += 1; + } + + if ( + !this._isBillboardBased || + this.billboardMode === Constants.PARTICLES_BILLBOARDMODE_STRETCHED || + this.billboardMode === Constants.PARTICLES_BILLBOARDMODE_STRETCHED_LOCAL + ) { + const directionBuffer = this._vertexBuffer.createVertexBuffer("direction", dataOffset, 3, this._vertexBufferSize, this._useInstancing); + this._vertexBuffers["direction"] = directionBuffer; + dataOffset += 3; + } + + if (this._useRampGradients) { + const rampDataBuffer = this._vertexBuffer.createVertexBuffer("remapData", dataOffset, 4, this._vertexBufferSize, this._useInstancing); + this._vertexBuffers["remapData"] = rampDataBuffer; + dataOffset += 4; + } + + let offsets: VertexBuffer; + if (this._useInstancing) { + const spriteData = new Float32Array([0, 0, 1, 0, 0, 1, 1, 1]); + this._spriteBuffer = new Buffer(engine, spriteData, false, 2); + offsets = this._spriteBuffer.createVertexBuffer("offset", 0, 2); + } else { + offsets = this._vertexBuffer.createVertexBuffer("offset", dataOffset, 2, this._vertexBufferSize, this._useInstancing); + dataOffset += 2; + } + this._vertexBuffers["offset"] = offsets; + + this.resetDrawCache(); + } + + private _createIndexBuffer() { + if (this._useInstancing) { + this._linesIndexBufferUseInstancing = this._engine.createIndexBuffer(new Uint32Array([0, 1, 1, 3, 3, 2, 2, 0, 0, 3])); + return; + } + const indices = []; + const indicesWireframe = []; + let index = 0; + for (let count = 0; count < this._capacity; count++) { + indices.push(index); + indices.push(index + 1); + indices.push(index + 2); + indices.push(index); + indices.push(index + 2); + indices.push(index + 3); + indicesWireframe.push(index, index + 1, index + 1, index + 2, index + 2, index + 3, index + 3, index, index, index + 3); + index += 4; + } + + this._indexBuffer = this._engine.createIndexBuffer(indices); + this._linesIndexBuffer = this._engine.createIndexBuffer(indicesWireframe); + } + + /** + * Gets the maximum number of particles active at the same time. + * @returns The max number of active particles. + */ + public getCapacity(): number { + return this._capacity; + } + + /** + * Gets whether there are still active particles in the system. + * @returns True if it is alive, otherwise false. + */ + public isAlive(): boolean { + return this._alive; + } + + /** + * Gets if the system has been started. (Note: this will still be true after stop is called) + * @returns True if it has been started, otherwise false. + */ + public isStarted(): boolean { + return this._started; + } + + /** @internal */ + public _preStart() { + // Do nothing + } + + /** + * Starts the particle system and begins to emit + * @param delay defines the delay in milliseconds before starting the system (this.startDelay by default) + */ + public start(delay = this.startDelay): void { + if (!this.targetStopDuration && this._hasTargetStopDurationDependantGradient()) { + // eslint-disable-next-line no-throw-literal + throw "Particle system started with a targetStopDuration dependant gradient (eg. startSizeGradients) but no targetStopDuration set"; + } + if (delay) { + this.startDelay = delay; + setTimeout(() => { + this.start(0); + }, delay); + return; + } + this._started = true; + this._stopped = false; + this._actualFrame = 0; + + this._preStart(); + + // Reset emit gradient so it acts the same on every start + if (this._emitRateGradients) { + if (this._emitRateGradients.length > 0) { + this._currentEmitRateGradient = this._emitRateGradients[0]; + this._currentEmitRate1 = this._currentEmitRateGradient.getFactor(); + this._currentEmitRate2 = this._currentEmitRate1; + } + if (this._emitRateGradients.length > 1) { + this._currentEmitRate2 = this._emitRateGradients[1].getFactor(); + } + } + // Reset start size gradient so it acts the same on every start + if (this._startSizeGradients) { + if (this._startSizeGradients.length > 0) { + this._currentStartSizeGradient = this._startSizeGradients[0]; + this._currentStartSize1 = this._currentStartSizeGradient.getFactor(); + this._currentStartSize2 = this._currentStartSize1; + } + if (this._startSizeGradients.length > 1) { + this._currentStartSize2 = this._startSizeGradients[1].getFactor(); + } + } + + if (this.preWarmCycles) { + if (this.emitter?.getClassName().indexOf("Mesh") !== -1) { + (this.emitter as any).computeWorldMatrix(true); + } + + const noiseTextureAsProcedural = this.noiseTexture as ProceduralTexture; + + if (noiseTextureAsProcedural && noiseTextureAsProcedural.onGeneratedObservable) { + noiseTextureAsProcedural.onGeneratedObservable.addOnce(() => { + setTimeout(() => { + for (let index = 0; index < this.preWarmCycles; index++) { + this.animate(true); + noiseTextureAsProcedural.render(); + } + }); + }); + } else { + for (let index = 0; index < this.preWarmCycles; index++) { + this.animate(true); + } + } + } + + // Animations + if (this.beginAnimationOnStart && this.animations && this.animations.length > 0 && this._scene) { + this._scene.beginAnimation(this, this.beginAnimationFrom, this.beginAnimationTo, this.beginAnimationLoop); + } + + this.onStartedObservable.notifyObservers(this); + } + + /** + * Stops the particle system. + * @param stopSubEmitters if true it will stop the current system and all created sub-Systems if false it will stop the current root system only, this param is used by the root particle system only. The default value is true. + */ + public stop(stopSubEmitters = true): void { + if (this._stopped) { + return; + } + + this.onStoppedObservable.notifyObservers(this); + + this._stopped = true; + + this._postStop(stopSubEmitters); + } + + /** @internal */ + public _postStop(_stopSubEmitters: boolean) { + // Do nothing + } + + // Animation sheet + + /** + * Remove all active particles + */ + public reset(): void { + this._stockParticles.length = 0; + this._particles.length = 0; + } + + /** + * @internal (for internal use only) + */ + public _appendParticleVertex(index: number, particle: Particle, offsetX: number, offsetY: number): void { + let offset = index * this._vertexBufferSize; + + const floatingOriginOffset = TmpVectors.Vector3[0].copyFrom(this._scene?.floatingOriginOffset || Vector3.ZeroReadOnly); + this._vertexData[offset++] = particle.position.x + this.worldOffset.x - floatingOriginOffset.x; + this._vertexData[offset++] = particle.position.y + this.worldOffset.y - floatingOriginOffset.y; + this._vertexData[offset++] = particle.position.z + this.worldOffset.z - floatingOriginOffset.z; + this._vertexData[offset++] = particle.color.r; + this._vertexData[offset++] = particle.color.g; + this._vertexData[offset++] = particle.color.b; + this._vertexData[offset++] = particle.color.a; + this._vertexData[offset++] = particle.angle; + + this._vertexData[offset++] = particle.scale.x * particle.size; + this._vertexData[offset++] = particle.scale.y * particle.size; + + if (this._isAnimationSheetEnabled) { + this._vertexData[offset++] = particle.cellIndex; + } + + if (!this._isBillboardBased) { + if (particle._initialDirection) { + let initialDirection = particle._initialDirection; + if (this.isLocal) { + Vector3.TransformNormalToRef(initialDirection, this._emitterWorldMatrix, TmpVectors.Vector3[0]); + initialDirection = TmpVectors.Vector3[0]; + } + if (initialDirection.x === 0 && initialDirection.z === 0) { + initialDirection.x = 0.001; + } + + this._vertexData[offset++] = initialDirection.x; + this._vertexData[offset++] = initialDirection.y; + this._vertexData[offset++] = initialDirection.z; + } else { + let direction = particle.direction; + if (this.isLocal) { + Vector3.TransformNormalToRef(direction, this._emitterWorldMatrix, TmpVectors.Vector3[0]); + direction = TmpVectors.Vector3[0]; + } + + if (direction.x === 0 && direction.z === 0) { + direction.x = 0.001; + } + this._vertexData[offset++] = direction.x; + this._vertexData[offset++] = direction.y; + this._vertexData[offset++] = direction.z; + } + } else if (this.billboardMode === Constants.PARTICLES_BILLBOARDMODE_STRETCHED || this.billboardMode === Constants.PARTICLES_BILLBOARDMODE_STRETCHED_LOCAL) { + this._vertexData[offset++] = particle.direction.x; + this._vertexData[offset++] = particle.direction.y; + this._vertexData[offset++] = particle.direction.z; + } + + if (this._useRampGradients && particle.remapData) { + this._vertexData[offset++] = particle.remapData.x; + this._vertexData[offset++] = particle.remapData.y; + this._vertexData[offset++] = particle.remapData.z; + this._vertexData[offset++] = particle.remapData.w; + } + + if (!this._useInstancing) { + if (this._isAnimationSheetEnabled) { + if (offsetX === 0) { + offsetX = this._epsilon; + } else if (offsetX === 1) { + offsetX = 1 - this._epsilon; + } + + if (offsetY === 0) { + offsetY = this._epsilon; + } else if (offsetY === 1) { + offsetY = 1 - this._epsilon; + } + } + + this._vertexData[offset++] = offsetX; + this._vertexData[offset++] = offsetY; + } + } + + // start of sub system methods + + /** + * "Recycles" one of the particle by copying it back to the "stock" of particles and removing it from the active list. + * Its lifetime will start back at 0. + * @param particle + */ + public recycleParticle: (particle: Particle) => void = (particle) => { + // move particle from activeParticle list to stock particles + const lastParticle = this._particles.pop(); + if (lastParticle !== particle) { + lastParticle.copyTo(particle); + } + this._stockParticles.push(lastParticle); + }; + + private _createParticle: () => Particle = () => { + let particle: Particle; + if (this._stockParticles.length !== 0) { + particle = this._stockParticles.pop(); + particle._reset(); + } else { + particle = new Particle(this); + } + + this._prepareParticle(particle); + return particle; + }; + + /** @internal */ + public _prepareParticle(_particle: Particle) { + //Do nothing + } + + private _createNewOnes(newParticles: number) { + // Add new ones + let particle: Particle; + for (let index = 0; index < newParticles; index++) { + if (this._particles.length === this._capacity) { + break; + } + + particle = this._createParticle(); + + this._particles.push(particle); + + // Creation queue + let currentQueueItem = this._createQueueStart; + + while (currentQueueItem) { + currentQueueItem.process(particle, this); + currentQueueItem = currentQueueItem.nextItem; + } + + // Update the position of the attached sub-emitters to match their attached particle + particle._inheritParticleInfoToSubEmitters(); + } + } + + private _update(newParticles: number): void { + // Update current + this._alive = this._particles.length > 0; + + if ((this.emitter).position) { + const emitterMesh = this.emitter; + this._emitterWorldMatrix = emitterMesh.getWorldMatrix(); + } else { + const emitterPosition = this.emitter; + this._emitterWorldMatrix = Matrix.Translation(emitterPosition.x, emitterPosition.y, emitterPosition.z); + } + + this._emitterWorldMatrix.invertToRef(this._emitterInverseWorldMatrix); + this.updateFunction(this._particles); + + this._createNewOnes(newParticles); + } + + /** + * @internal + */ + public static _GetAttributeNamesOrOptions(isAnimationSheetEnabled = false, isBillboardBased = false, useRampGradients = false): string[] { + const attributeNamesOrOptions = [VertexBuffer.PositionKind, VertexBuffer.ColorKind, "angle", "offset", "size"]; + + if (isAnimationSheetEnabled) { + attributeNamesOrOptions.push("cellIndex"); + } + + if (!isBillboardBased) { + attributeNamesOrOptions.push("direction"); + } + + if (useRampGradients) { + attributeNamesOrOptions.push("remapData"); + } + + return attributeNamesOrOptions; + } + + /** + * @internal + */ + public static _GetEffectCreationOptions(isAnimationSheetEnabled = false, useLogarithmicDepth = false, applyFog = false): string[] { + const effectCreationOption = ["invView", "view", "projection", "textureMask", "translationPivot", "eyePosition"]; + + AddClipPlaneUniforms(effectCreationOption); + + if (isAnimationSheetEnabled) { + effectCreationOption.push("particlesInfos"); + } + if (useLogarithmicDepth) { + effectCreationOption.push("logarithmicDepthConstant"); + } + + if (applyFog) { + effectCreationOption.push("vFogInfos"); + effectCreationOption.push("vFogColor"); + } + + return effectCreationOption; + } + + /** + * Fill the defines array according to the current settings of the particle system + * @param defines Array to be updated + * @param blendMode blend mode to take into account when updating the array + * @param fillImageProcessing fills the image processing defines + */ + public fillDefines(defines: Array, blendMode: number, fillImageProcessing: boolean = true): void { + if (this._scene) { + PrepareStringDefinesForClipPlanes(this, this._scene, defines); + if (this.applyFog && this._scene.fogEnabled && this._scene.fogMode !== Constants.FOGMODE_NONE) { + defines.push("#define FOG"); + } + } + + if (this._isAnimationSheetEnabled) { + defines.push("#define ANIMATESHEET"); + } + + if (this.useLogarithmicDepth) { + defines.push("#define LOGARITHMICDEPTH"); + } + + if (blendMode === BaseParticleSystem.BLENDMODE_MULTIPLY) { + defines.push("#define BLENDMULTIPLYMODE"); + } + + if (this._useRampGradients) { + defines.push("#define RAMPGRADIENT"); + } + + if (this._isBillboardBased) { + defines.push("#define BILLBOARD"); + + switch (this.billboardMode) { + case Constants.PARTICLES_BILLBOARDMODE_Y: + defines.push("#define BILLBOARDY"); + break; + case Constants.PARTICLES_BILLBOARDMODE_STRETCHED: + case Constants.PARTICLES_BILLBOARDMODE_STRETCHED_LOCAL: + defines.push("#define BILLBOARDSTRETCHED"); + if (this.billboardMode === Constants.PARTICLES_BILLBOARDMODE_STRETCHED_LOCAL) { + defines.push("#define BILLBOARDSTRETCHED_LOCAL"); + } + break; + case Constants.PARTICLES_BILLBOARDMODE_ALL: + defines.push("#define BILLBOARDMODE_ALL"); + break; + default: + break; + } + } + + if (fillImageProcessing && this._imageProcessingConfiguration) { + this._imageProcessingConfiguration.prepareDefines(this._imageProcessingConfigurationDefines); + defines.push(this._imageProcessingConfigurationDefines.toString()); + } + } + + /** + * Fill the uniforms, attributes and samplers arrays according to the current settings of the particle system + * @param uniforms Uniforms array to fill + * @param attributes Attributes array to fill + * @param samplers Samplers array to fill + */ + public fillUniformsAttributesAndSamplerNames(uniforms: Array, attributes: Array, samplers: Array) { + attributes.push( + ...ThinParticleSystem._GetAttributeNamesOrOptions( + this._isAnimationSheetEnabled, + this._isBillboardBased && + this.billboardMode !== Constants.PARTICLES_BILLBOARDMODE_STRETCHED && + this.billboardMode !== Constants.PARTICLES_BILLBOARDMODE_STRETCHED_LOCAL, + this._useRampGradients + ) + ); + + uniforms.push(...ThinParticleSystem._GetEffectCreationOptions(this._isAnimationSheetEnabled, this.useLogarithmicDepth, this.applyFog)); + + samplers.push("diffuseSampler", "rampSampler"); + + if (this._imageProcessingConfiguration) { + PrepareUniformsForImageProcessing(uniforms, this._imageProcessingConfigurationDefines); + PrepareSamplersForImageProcessing(samplers, this._imageProcessingConfigurationDefines); + } + } + + /** + * @internal + */ + private _getWrapper(blendMode: number): DrawWrapper { + const customWrapper = this._getCustomDrawWrapper(blendMode); + + if (customWrapper?.effect) { + return customWrapper; + } + + const defines: Array = []; + + this.fillDefines(defines, blendMode); + + // Effect + const currentRenderPassId = this._engine._features.supportRenderPasses ? this._engine.currentRenderPassId : Constants.RENDERPASS_MAIN; + let drawWrappers = this._drawWrappers[currentRenderPassId]; + if (!drawWrappers) { + drawWrappers = this._drawWrappers[currentRenderPassId] = []; + } + let drawWrapper = drawWrappers[blendMode]; + if (!drawWrapper) { + drawWrapper = new DrawWrapper(this._engine); + if (drawWrapper.drawContext) { + drawWrapper.drawContext.useInstancing = this._useInstancing; + } + drawWrappers[blendMode] = drawWrapper; + } + + const join = defines.join("\n"); + if (drawWrapper.defines !== join) { + const attributesNamesOrOptions: Array = []; + const effectCreationOption: Array = []; + const samplers: Array = []; + + this.fillUniformsAttributesAndSamplerNames(effectCreationOption, attributesNamesOrOptions, samplers); + + drawWrapper.setEffect( + this._engine.createEffect( + "particles", + attributesNamesOrOptions, + effectCreationOption, + samplers, + join, + undefined, + undefined, + undefined, + undefined, + this._shaderLanguage + ), + join + ); + } + + return drawWrapper; + } + + /** + * Gets or sets a boolean indicating that the particle system is paused (no animation will be done). + */ + public paused = false; + + /** + * Animates the particle system for the current frame by emitting new particles and or animating the living ones. + * @param preWarmOnly will prevent the system from updating the vertex buffer (default is false) + */ + public animate(preWarmOnly = false): void { + if (!this._started || this.paused) { + return; + } + + if (!preWarmOnly && this._scene) { + // Check + if (!this.isReady()) { + return; + } + + if (this._currentRenderId === this._scene.getFrameId()) { + return; + } + this._currentRenderId = this._scene.getFrameId(); + } + + this._scaledUpdateSpeed = this.updateSpeed * (preWarmOnly ? this.preWarmStepOffset : this._scene?.getAnimationRatio() || 1); + + // Determine the number of particles we need to create + let newParticles; + + if (this.manualEmitCount > -1) { + newParticles = this.manualEmitCount; + this._newPartsExcess = 0; + this.manualEmitCount = 0; + } else { + const rate = this._calculateEmitRate(); + newParticles = (rate * this._scaledUpdateSpeed) >> 0; + this._newPartsExcess += rate * this._scaledUpdateSpeed - newParticles; + } + + if (this._newPartsExcess > 1.0) { + newParticles += this._newPartsExcess >> 0; + this._newPartsExcess -= this._newPartsExcess >> 0; + } + + this._alive = false; + + if (!this._stopped) { + this._actualFrame += this._scaledUpdateSpeed; + + if (this.targetStopDuration && this._actualFrame >= this.targetStopDuration) { + this.stop(); + } + } else { + newParticles = 0; + } + this._update(newParticles); + + // Stopped? + if (this._stopped) { + if (!this._alive) { + this._started = false; + if (this.onAnimationEnd) { + this.onAnimationEnd(); + } + if (this.disposeOnStop && this._scene) { + this._scene._toBeDisposed.push(this); + } + } + } + + if (!preWarmOnly) { + // Update VBO + let offset = 0; + for (let index = 0; index < this._particles.length; index++) { + const particle = this._particles[index]; + this._appendParticleVertices(offset, particle); + offset += this._useInstancing ? 1 : 4; + } + + if (this._vertexBuffer) { + this._vertexBuffer.updateDirectly(this._vertexData, 0, this._particles.length); + } + } + + if (this.manualEmitCount === 0 && this.disposeOnStop) { + this.stop(); + } + } + + /** + * Internal only. Calculates the current emit rate based on the gradients if any. + * @returns The emit rate + * @internal + */ + public _calculateEmitRate(): number { + let rate = this.emitRate; + + if (this._emitRateGradients && this._emitRateGradients.length > 0 && this.targetStopDuration) { + const ratio = this._actualFrame / this.targetStopDuration; + GradientHelper.GetCurrentGradient(ratio, this._emitRateGradients, (currentGradient, nextGradient, scale) => { + if (currentGradient !== this._currentEmitRateGradient) { + this._currentEmitRate1 = this._currentEmitRate2; + this._currentEmitRate2 = (nextGradient).getFactor(); + this._currentEmitRateGradient = currentGradient; + } + + rate = Lerp(this._currentEmitRate1, this._currentEmitRate2, scale); + }); + } + + return rate; + } + + private _appendParticleVertices(offset: number, particle: Particle) { + this._appendParticleVertex(offset++, particle, 0, 0); + if (!this._useInstancing) { + this._appendParticleVertex(offset++, particle, 1, 0); + this._appendParticleVertex(offset++, particle, 1, 1); + this._appendParticleVertex(offset++, particle, 0, 1); + } + } + + /** + * Rebuilds the particle system. + */ + public rebuild(): void { + if (this._engine.getCaps().vertexArrayObject) { + this._vertexArrayObject = null; + } + + this._createIndexBuffer(); + + this._spriteBuffer?._rebuild(); + + this._createVertexBuffers(); + + this.resetDrawCache(); + } + + private _shadersLoaded = false; + private async _initShaderSourceAsync() { + const engine = this._engine; + + if (engine.isWebGPU && !ThinParticleSystem.ForceGLSL) { + this._shaderLanguage = ShaderLanguage.WGSL; + + await Promise.all([import("../ShadersWGSL/particles.vertex"), import("../ShadersWGSL/particles.fragment")]); + } else { + await Promise.all([import("../Shaders/particles.vertex"), import("../Shaders/particles.fragment")]); + } + + this._shadersLoaded = true; + } + + /** + * Is this system ready to be used/rendered + * @returns true if the system is ready + */ + public isReady(): boolean { + if (!this._shadersLoaded) { + return false; + } + if (!this.emitter || (this._imageProcessingConfiguration && !this._imageProcessingConfiguration.isReady()) || !this.particleTexture || !this.particleTexture.isReady()) { + return false; + } + + if (this.blendMode !== BaseParticleSystem.BLENDMODE_MULTIPLYADD) { + if (!this._getWrapper(this.blendMode).effect!.isReady()) { + return false; + } + } else { + if (!this._getWrapper(BaseParticleSystem.BLENDMODE_MULTIPLY).effect!.isReady()) { + return false; + } + if (!this._getWrapper(BaseParticleSystem.BLENDMODE_ADD).effect!.isReady()) { + return false; + } + } + + return true; + } + + private _render(blendMode: number) { + const drawWrapper = this._getWrapper(blendMode); + const effect = drawWrapper.effect!; + + const engine = this._engine; + + // Render + engine.enableEffect(drawWrapper); + + const viewMatrix = this.defaultViewMatrix ?? this._scene!.getViewMatrix(); + effect.setTexture("diffuseSampler", this.particleTexture); + effect.setMatrix("view", viewMatrix); + effect.setMatrix("projection", this.defaultProjectionMatrix ?? this._scene!.getProjectionMatrix()); + + if (this._isAnimationSheetEnabled && this.particleTexture) { + const baseSize = this.particleTexture.getBaseSize(); + effect.setFloat3("particlesInfos", this.spriteCellWidth / baseSize.width, this.spriteCellHeight / baseSize.height, this.spriteCellWidth / baseSize.width); + } + + effect.setVector2("translationPivot", this.translationPivot); + effect.setFloat4("textureMask", this.textureMask.r, this.textureMask.g, this.textureMask.b, this.textureMask.a); + + if (this._isBillboardBased && this._scene) { + const camera = this._scene.activeCamera!; + effect.setVector3("eyePosition", camera.globalPosition); + } + + if (this._rampGradientsTexture) { + if (!this._rampGradients || !this._rampGradients.length) { + this._rampGradientsTexture.dispose(); + this._rampGradientsTexture = null; + } + effect.setTexture("rampSampler", this._rampGradientsTexture); + } + + const defines = effect.defines; + + if (this._scene) { + BindClipPlane(effect, this, this._scene); + + if (this.applyFog) { + BindFogParameters(this._scene, undefined, effect); + } + } + + if (defines.indexOf("#define BILLBOARDMODE_ALL") >= 0) { + viewMatrix.invertToRef(TmpVectors.Matrix[0]); + effect.setMatrix("invView", TmpVectors.Matrix[0]); + } + + if (this._vertexArrayObject !== undefined) { + if (this._scene?.forceWireframe) { + engine.bindBuffers(this._vertexBuffers, this._linesIndexBufferUseInstancing, effect); + } else { + if (!this._vertexArrayObject) { + this._vertexArrayObject = (this._engine as ThinEngine).recordVertexArrayObject(this._vertexBuffers, this._indexBuffer, effect); + } + + (this._engine as ThinEngine).bindVertexArrayObject(this._vertexArrayObject, this._indexBuffer); + } + } else { + if (!this._indexBuffer) { + // Use instancing mode + engine.bindBuffers(this._vertexBuffers, this._scene?.forceWireframe ? this._linesIndexBufferUseInstancing : null, effect); + } else { + engine.bindBuffers(this._vertexBuffers, this._scene?.forceWireframe ? this._linesIndexBuffer : this._indexBuffer, effect); + } + } + + // Log. depth + if (this.useLogarithmicDepth && this._scene) { + BindLogDepth(defines, effect, this._scene); + } + + // image processing + if (this._imageProcessingConfiguration && !this._imageProcessingConfiguration.applyByPostProcess) { + this._imageProcessingConfiguration.bind(effect); + } + + // Draw order + this._setEngineBasedOnBlendMode(blendMode); + + if (this._onBeforeDrawParticlesObservable) { + this._onBeforeDrawParticlesObservable.notifyObservers(effect); + } + + if (this._useInstancing) { + if (this._scene?.forceWireframe) { + engine.drawElementsType(Constants.MATERIAL_LineStripDrawMode, 0, 10, this._particles.length); + } else { + engine.drawArraysType(Constants.MATERIAL_TriangleStripDrawMode, 0, 4, this._particles.length); + } + } else { + if (this._scene?.forceWireframe) { + engine.drawElementsType(Constants.MATERIAL_WireFrameFillMode, 0, this._particles.length * 10); + } else { + engine.drawElementsType(Constants.MATERIAL_TriangleFillMode, 0, this._particles.length * 6); + } + } + + return this._particles.length; + } + + /** + * Renders the particle system in its current state. + * @returns the current number of particles + */ + public render(): number { + // Check + if (!this.isReady() || !this._particles.length) { + return 0; + } + + const engine = this._engine as any; + if (engine.setState) { + engine.setState(false); + + if (this.forceDepthWrite) { + engine.setDepthWrite(true); + } + } + + let outparticles = 0; + + if (this.blendMode === BaseParticleSystem.BLENDMODE_MULTIPLYADD) { + outparticles = this._render(BaseParticleSystem.BLENDMODE_MULTIPLY) + this._render(BaseParticleSystem.BLENDMODE_ADD); + } else { + outparticles = this._render(this.blendMode); + } + + this._engine.unbindInstanceAttributes(); + this._engine.setAlphaMode(Constants.ALPHA_DISABLE); + + return outparticles; + } + + /** @internal */ + public _onDispose(_disposeAttachedSubEmitters = false, _disposeEndSubEmitters = false) { + // Do Nothing + } + + /** + * Disposes the particle system and free the associated resources + * @param disposeTexture defines if the particle texture must be disposed as well (true by default) + * @param disposeAttachedSubEmitters defines if the attached sub-emitters must be disposed as well (false by default) + * @param disposeEndSubEmitters defines if the end type sub-emitters must be disposed as well (false by default) + */ + public dispose(disposeTexture = true, disposeAttachedSubEmitters = false, disposeEndSubEmitters = false): void { + this.resetDrawCache(); + + if (this._vertexBuffer) { + this._vertexBuffer.dispose(); + this._vertexBuffer = null; + } + + if (this._spriteBuffer) { + this._spriteBuffer.dispose(); + this._spriteBuffer = null; + } + + if (this._indexBuffer) { + this._engine._releaseBuffer(this._indexBuffer); + this._indexBuffer = null; + } + + if (this._linesIndexBuffer) { + this._engine._releaseBuffer(this._linesIndexBuffer); + this._linesIndexBuffer = null; + } + + if (this._linesIndexBufferUseInstancing) { + this._engine._releaseBuffer(this._linesIndexBufferUseInstancing); + this._linesIndexBufferUseInstancing = null; + } + + if (this._vertexArrayObject) { + (this._engine as ThinEngine).releaseVertexArrayObject(this._vertexArrayObject); + this._vertexArrayObject = null; + } + + if (disposeTexture && this.particleTexture) { + this.particleTexture.dispose(); + this.particleTexture = null; + } + + if (disposeTexture && this.noiseTexture) { + this.noiseTexture.dispose(); + this.noiseTexture = null; + } + + if (this._rampGradientsTexture) { + this._rampGradientsTexture.dispose(); + this._rampGradientsTexture = null; + } + + this._onDispose(disposeAttachedSubEmitters, disposeEndSubEmitters); + + if (this._onBeforeDrawParticlesObservable) { + this._onBeforeDrawParticlesObservable.clear(); + } + + // Remove from scene + if (this._scene) { + const index = this._scene.particleSystems.indexOf(this); + if (index > -1) { + this._scene.particleSystems.splice(index, 1); + } + + this._scene._activeParticleSystems.dispose(); + } + + // Callback + this.onDisposeObservable.notifyObservers(this); + this.onDisposeObservable.clear(); + this.onStoppedObservable.clear(); + this.onStartedObservable.clear(); + + this.reset(); + + this._isDisposed = true; + } +} diff --git a/tools/src/effect/effect.ts b/tools/src/effect/effect.ts index 102e1af09..66e35697f 100644 --- a/tools/src/effect/effect.ts +++ b/tools/src/effect/effect.ts @@ -1,13 +1,13 @@ -import { Scene, Tools, IDisposable, TransformNode, Vector3, CreatePlane, MeshBuilder, Texture } from "babylonjs"; +import { Scene, Tools, IDisposable, TransformNode, MeshBuilder, Texture, AbstractMesh } from "babylonjs"; import type { IQuarksJSON } from "./types/quarksTypes"; import type { ILoaderOptions } from "./types/loader"; import { Parser } from "./parsers/parser"; import { EffectParticleSystem } from "./systems/effectParticleSystem"; import { EffectSolidParticleSystem } from "./systems/effectSolidParticleSystem"; import type { IGroup, IEmitter, IData } from "./types/hierarchy"; -import type { IEmitterConfig } from "./types/emitter"; +import type { IParticleSystemConfig } from "./types/emitter"; +import { Color4 } from "babylonjs"; import { isSystem } from "./types/system"; -import { EmitterFactory } from "./factories/emitterFactory"; /** * Effect Node - represents either a particle system or a group @@ -485,13 +485,10 @@ export class Effect implements IDisposable { */ public applyPrewarm(): void { for (const system of this._systems) { - if (system instanceof EffectParticleSystem && system.prewarm) { - // For ParticleSystem, use Babylon.js built-in prewarm - const duration = system.targetStopDuration || 5; - const cycles = Math.ceil(duration * 60); // Simulate 60 FPS for duration - (system as any).preWarmCycles = cycles; - (system as any).preWarmStepOffset = 1; // Use normal time step - } else if (system instanceof EffectSolidParticleSystem && system.prewarm) { + if (system instanceof EffectParticleSystem && system.preWarmCycles > 0) { + // ParticleSystem uses native preWarmCycles/preWarmStepOffset + // Already configured via config.preWarmCycles, nothing more needed + } else if (system instanceof EffectSolidParticleSystem && system.preWarmCycles > 0) { // For SolidParticleSystem, we need to manually simulate prewarm // Start the system and let it run for duration // Note: SPS doesn't have built-in prewarm, so we'll start it normally @@ -632,59 +629,86 @@ export class Effect implements IDisposable { const systemUuid = Tools.RandomId(); // Create default config - const config: IEmitterConfig = { + const config: IParticleSystemConfig = { systemType, - looping: true, - duration: 5, - prewarm: false, - emissionOverTime: 10, - startLife: 1, - startSpeed: 1, - startSize: 1, - startColor: { type: "ConstantColor", value: [1, 1, 1, 1] }, + targetStopDuration: 0, // looping + manualEmitCount: -1, + emitRate: 10, + minLifeTime: 1, + maxLifeTime: 1, + minEmitPower: 1, + maxEmitPower: 1, + minSize: 1, + maxSize: 1, + color1: new Color4(1, 1, 1, 1), + color2: new Color4(1, 1, 1, 1), + colorDead: new Color4(1, 1, 1, 0), behaviors: [], }; let system: EffectParticleSystem | EffectSolidParticleSystem; + // Create system instance based on type if (systemType === "solid") { - // Create default plane mesh for SPS - const planeMesh = CreatePlane("particleMesh", { size: 1 }, this._scene); - planeMesh.setEnabled(false); // Hide the source mesh - - system = new EffectSolidParticleSystem(uniqueName, this._scene, config, { - particleMesh: planeMesh, - parentGroup: parent.group || undefined, + system = new EffectSolidParticleSystem(uniqueName, this._scene, { + updatable: true, + isPickable: false, + enableDepthSort: false, + particleIntersection: false, + useModelMaterial: true, }); - - // Store reference to source mesh for geometry field - (system as any)._sourceMesh = planeMesh.clone(`${uniqueName}_sourceMesh`); - - // Create default point emitter - system.createPointEmitter(); + const particleMesh = MeshBuilder.CreatePlane("particleMesh", { size: 1 }, this._scene); + system.particleMesh = particleMesh; } else { - // Create base particle system with default flare texture - const flareTexture = new Texture(Tools.GetAssetUrl("https://assets.babylonjs.com/core/textures/flare.png"), this._scene); - system = new EffectParticleSystem(uniqueName, this._scene, config, { - texture: flareTexture, - }); - - // Create default point emitter - const emitterFactory = new EmitterFactory(); - emitterFactory.createParticleSystemEmitter(system, undefined, Vector3.One(), null); - - // Create emitter mesh (Mesh for ParticleSystem) - const emitterMesh = MeshBuilder.CreateBox(`${uniqueName}_Emitter`, { size: 0.1 }, this._scene); - emitterMesh.id = Tools.RandomId(); - emitterMesh.setEnabled(false); // Hide the emitter mesh - if (parent.group) { - emitterMesh.setParent(parent.group, false, true); - } - system.emitter = emitterMesh; + const capacity = 500; + system = new EffectParticleSystem(uniqueName, capacity, this._scene); + system.particleTexture = new Texture("https://assets.babylonjs.com/core/textures/flare.png", this._scene); } // Set system name system.name = uniqueName; + system.emitter = parent.group as AbstractMesh; + // === Assign native properties (shared by both systems) === + if (config.minSize !== undefined) system.minSize = config.minSize; + if (config.maxSize !== undefined) system.maxSize = config.maxSize; + if (config.minLifeTime !== undefined) system.minLifeTime = config.minLifeTime; + if (config.maxLifeTime !== undefined) system.maxLifeTime = config.maxLifeTime; + if (config.minEmitPower !== undefined) system.minEmitPower = config.minEmitPower; + if (config.maxEmitPower !== undefined) system.maxEmitPower = config.maxEmitPower; + if (config.emitRate !== undefined) system.emitRate = config.emitRate; + if (config.targetStopDuration !== undefined) system.targetStopDuration = config.targetStopDuration; + if (config.manualEmitCount !== undefined) system.manualEmitCount = config.manualEmitCount; + if (config.preWarmCycles !== undefined) system.preWarmCycles = config.preWarmCycles; + if (config.preWarmStepOffset !== undefined) system.preWarmStepOffset = config.preWarmStepOffset; + if (config.color1 !== undefined) system.color1 = config.color1; + if (config.color2 !== undefined) system.color2 = config.color2; + if (config.colorDead !== undefined) system.colorDead = config.colorDead; + if (config.minInitialRotation !== undefined) system.minInitialRotation = config.minInitialRotation; + if (config.maxInitialRotation !== undefined) system.maxInitialRotation = config.maxInitialRotation; + if (config.isLocal !== undefined) system.isLocal = config.isLocal; + if (config.disposeOnStop !== undefined) system.disposeOnStop = config.disposeOnStop; + + // === Apply gradients (shared by both systems) === + if (config.startSizeGradients) { + for (const grad of config.startSizeGradients) { + system.addStartSizeGradient(grad.gradient, grad.factor, grad.factor2); + } + } + if (config.lifeTimeGradients) { + for (const grad of config.lifeTimeGradients) { + system.addLifeTimeGradient(grad.gradient, grad.factor, grad.factor2); + } + } + if (config.emitRateGradients) { + for (const grad of config.emitRateGradients) { + system.addEmitRateGradient(grad.gradient, grad.factor, grad.factor2); + } + } + + // === Apply behaviors (shared by both systems) === + if (config.behaviors !== undefined) { + system.setBehaviors(config.behaviors); + } const newNode: IEffectNode = { name: uniqueName, diff --git a/tools/src/effect/factories/emitterFactory.ts b/tools/src/effect/factories/emitterFactory.ts deleted file mode 100644 index bc10cb64e..000000000 --- a/tools/src/effect/factories/emitterFactory.ts +++ /dev/null @@ -1,176 +0,0 @@ -import { Vector3, Matrix } from "babylonjs"; -import type { IShape } from "../types/shapes"; -import type { EffectSolidParticleSystem } from "../systems/effectSolidParticleSystem"; -import type { EffectParticleSystem } from "../systems/effectParticleSystem"; - -/** - * Factory for creating emitters for particle systems - * Handles both ParticleSystem and SolidParticleSystem emitter creation - */ -export class EmitterFactory { - /** - * Create emitter for ParticleSystem - * Applies emitter shape to the particle system - */ - public createParticleSystemEmitter(particleSystem: EffectParticleSystem, shape: IShape | undefined, cumulativeScale: Vector3, rotationMatrix: Matrix | null): void { - if (!shape || !shape.type) { - this._createPointEmitter(particleSystem, Vector3.Zero(), Vector3.Zero()); - return; - } - - const shapeType = shape.type.toLowerCase(); - const shapeHandlers: Record void> = { - cone: this._createConeEmitter.bind(this, particleSystem), - sphere: this._createSphereEmitter.bind(this, particleSystem), - point: this._createPointEmitter.bind(this, particleSystem), - box: this._createBoxEmitter.bind(this, particleSystem), - hemisphere: this._createHemisphereEmitter.bind(this, particleSystem), - cylinder: this._createCylinderEmitter.bind(this, particleSystem), - }; - - const handler = shapeHandlers[shapeType]; - if (handler) { - handler(shape, cumulativeScale, rotationMatrix); - } else { - this._createDefaultPointEmitter(particleSystem, rotationMatrix); - } - } - - /** - * Create emitter for SolidParticleSystem - * Creates emitter using system's create*Emitter methods (similar to ParticleSystem) - */ - public createSolidParticleSystemEmitter(sps: EffectSolidParticleSystem, shape: IShape | undefined): void { - if (!shape || !shape.type) { - sps.createPointEmitter(); - return; - } - - const shapeType = shape.type.toLowerCase(); - const radius = shape.radius ?? 1; - const arc = shape.arc ?? Math.PI * 2; - const thickness = shape.thickness ?? 1; - const angle = shape.angle ?? Math.PI / 6; - - switch (shapeType) { - case "sphere": - sps.createSphereEmitter(radius, arc, thickness); - break; - case "cone": - sps.createConeEmitter(radius, arc, thickness, angle); - break; - case "point": - sps.createPointEmitter(); - break; - default: - sps.createPointEmitter(); - break; - } - } - - /** - * Applies rotation to default direction vector - */ - private _applyRotationToDirection(defaultDir: Vector3, rotationMatrix: Matrix | null): Vector3 { - if (!rotationMatrix) { - return defaultDir; - } - - const rotatedDir = Vector3.Zero(); - Vector3.TransformNormalToRef(defaultDir, rotationMatrix, rotatedDir); - return rotatedDir; - } - - /** - * Creates cone emitter for ParticleSystem - */ - private _createConeEmitter(particleSystem: EffectParticleSystem, shape: IShape, scale: Vector3, rotationMatrix: Matrix | null): void { - const radius = ((shape as any).radius || 1) * ((scale.x + scale.z) / 2); - const angle = (shape as any).angle !== undefined ? (shape as any).angle : Math.PI / 4; - const defaultDir = new Vector3(0, 1, 0); - const rotatedDir = this._applyRotationToDirection(defaultDir, rotationMatrix); - - if (rotationMatrix) { - particleSystem.createDirectedConeEmitter(radius, angle, rotatedDir, rotatedDir); - } else { - particleSystem.createConeEmitter(radius, angle); - } - } - - /** - * Creates sphere emitter for ParticleSystem - */ - private _createSphereEmitter(particleSystem: EffectParticleSystem, shape: IShape, scale: Vector3, rotationMatrix: Matrix | null): void { - const radius = ((shape as any).radius || 1) * ((scale.x + scale.y + scale.z) / 3); - const defaultDir = new Vector3(0, 1, 0); - const rotatedDir = this._applyRotationToDirection(defaultDir, rotationMatrix); - - if (rotationMatrix) { - particleSystem.createDirectedSphereEmitter(radius, rotatedDir, rotatedDir); - } else { - particleSystem.createSphereEmitter(radius); - } - } - - /** - * Creates point emitter for ParticleSystem - */ - private _createPointEmitter(particleSystem: EffectParticleSystem, direction: Vector3, minDirection: Vector3): void { - particleSystem.createPointEmitter(direction, minDirection); - } - - /** - * Creates box emitter for ParticleSystem - */ - private _createBoxEmitter(particleSystem: EffectParticleSystem, shape: IShape, scale: Vector3, rotationMatrix: Matrix | null): void { - const boxSize = ((shape as any).size || [1, 1, 1]).map((s: number, i: number) => s * [scale.x, scale.y, scale.z][i]); - const minBox = new Vector3(-boxSize[0] / 2, -boxSize[1] / 2, -boxSize[2] / 2); - const maxBox = new Vector3(boxSize[0] / 2, boxSize[1] / 2, boxSize[2] / 2); - const defaultDir = new Vector3(0, 1, 0); - const rotatedDir = this._applyRotationToDirection(defaultDir, rotationMatrix); - - if (rotationMatrix) { - particleSystem.createBoxEmitter(rotatedDir, rotatedDir, minBox, maxBox); - } else { - particleSystem.createBoxEmitter(Vector3.Zero(), Vector3.Zero(), minBox, maxBox); - } - } - - /** - * Creates hemisphere emitter for ParticleSystem - */ - private _createHemisphereEmitter(particleSystem: EffectParticleSystem, shape: IShape, scale: Vector3, _rotationMatrix: Matrix | null): void { - const radius = ((shape as any).radius || 1) * ((scale.x + scale.y + scale.z) / 3); - particleSystem.createHemisphericEmitter(radius); - } - - /** - * Creates cylinder emitter for ParticleSystem - */ - private _createCylinderEmitter(particleSystem: EffectParticleSystem, shape: IShape, scale: Vector3, rotationMatrix: Matrix | null): void { - const radius = ((shape as any).radius || 1) * ((scale.x + scale.z) / 2); - const height = ((shape as any).height || 1) * scale.y; - const defaultDir = new Vector3(0, 1, 0); - const rotatedDir = this._applyRotationToDirection(defaultDir, rotationMatrix); - - if (rotationMatrix) { - particleSystem.createDirectedCylinderEmitter(radius, height, 1, rotatedDir, rotatedDir); - } else { - particleSystem.createCylinderEmitter(radius, height); - } - } - - /** - * Creates default point emitter for ParticleSystem - */ - private _createDefaultPointEmitter(particleSystem: EffectParticleSystem, rotationMatrix: Matrix | null): void { - const defaultDir = new Vector3(0, 1, 0); - const rotatedDir = this._applyRotationToDirection(defaultDir, rotationMatrix); - - if (rotationMatrix) { - this._createPointEmitter(particleSystem, rotatedDir, rotatedDir); - } else { - this._createPointEmitter(particleSystem, Vector3.Zero(), Vector3.Zero()); - } - } -} diff --git a/tools/src/effect/factories/index.ts b/tools/src/effect/factories/index.ts index 01b5e1fc2..91f5e73be 100644 --- a/tools/src/effect/factories/index.ts +++ b/tools/src/effect/factories/index.ts @@ -1,4 +1,3 @@ -export { EmitterFactory } from "./emitterFactory"; export { MaterialFactory } from "./materialFactory"; export { GeometryFactory } from "./geometryFactory"; export { SystemFactory } from "./systemFactory"; diff --git a/tools/src/effect/factories/systemFactory.ts b/tools/src/effect/factories/systemFactory.ts index dd54c5410..56a8893b0 100644 --- a/tools/src/effect/factories/systemFactory.ts +++ b/tools/src/effect/factories/systemFactory.ts @@ -1,15 +1,17 @@ -import { Nullable, Vector3, TransformNode, Texture, Scene } from "babylonjs"; +import { Nullable, Vector3, TransformNode, Scene, AbstractMesh } from "babylonjs"; import { EffectParticleSystem } from "../systems/effectParticleSystem"; import { EffectSolidParticleSystem } from "../systems/effectSolidParticleSystem"; import type { IData, IGroup, IEmitter, ITransform } from "../types/hierarchy"; +import type { IParticleSystemConfig } from "../types/emitter"; import { Logger } from "../loggers/logger"; import { MatrixUtils } from "../utils/matrixUtils"; -import { EmitterFactory } from "./emitterFactory"; import type { IMaterialFactory, IGeometryFactory } from "../types/factories"; import type { ILoaderOptions } from "../types/loader"; +import { CapacityCalculator } from "../utils/capacityCalculator"; +import { ValueUtils } from "../utils/valueParser"; /** - * Factory for creating particle systems from data + * Factory for creating particle systems from data * Creates all nodes, sets parents, and applies transformations in a single pass */ export class SystemFactory { @@ -18,7 +20,6 @@ export class SystemFactory { private _groupNodesMap: Map; private _materialFactory: IMaterialFactory; private _geometryFactory: IGeometryFactory; - private _emitterFactory: EmitterFactory; constructor(scene: Scene, options: ILoaderOptions, groupNodesMap: Map, materialFactory: IMaterialFactory, geometryFactory: IGeometryFactory) { this._scene = scene; @@ -26,22 +27,21 @@ export class SystemFactory { this._logger = new Logger("[SystemFactory]", options); this._materialFactory = materialFactory; this._geometryFactory = geometryFactory; - this._emitterFactory = new EmitterFactory(); } /** * Create particle systems from data * Creates all nodes, sets parents, and applies transformations in one pass */ - public createSystems(Data: IData): (EffectParticleSystem | EffectSolidParticleSystem)[] { - if (!Data.root) { + public createSystems(data: IData): (EffectParticleSystem | EffectSolidParticleSystem)[] { + if (!data.root) { this._logger.warn("No root object found in data"); return []; } this._logger.log("Processing hierarchy: creating nodes, setting parents, and applying transformations"); const particleSystems: (EffectParticleSystem | EffectSolidParticleSystem)[] = []; - this._processObject(Data.root, null, 0, particleSystems, Data); + this._processObject(data.root, null, 0, particleSystems, data); return particleSystems; } @@ -50,18 +50,18 @@ export class SystemFactory { * Creates nodes, sets parents, and applies transformations in one pass */ private _processObject( - Obj: IGroup | IEmitter, + obj: IGroup | IEmitter, parentGroup: Nullable, depth: number, particleSystems: (EffectParticleSystem | EffectSolidParticleSystem)[], - Data: IData + data: IData ): void { - this._logger.log(`${" ".repeat(depth)}Processing object: ${Obj.name}`); + this._logger.log(`${" ".repeat(depth)}Processing object: ${obj.name}`); - if (this._isGroup(Obj)) { - this._processGroup(Obj, parentGroup, depth, particleSystems, Data); + if (this._isGroup(obj)) { + this._processGroup(obj, parentGroup, depth, particleSystems, data); } else { - this._processEmitter(Obj, parentGroup, depth, particleSystems); + this._processEmitter(obj, parentGroup, depth, particleSystems); } } @@ -69,21 +69,21 @@ export class SystemFactory { * Process a Group object */ private _processGroup( - Group: IGroup, + group: IGroup, parentGroup: Nullable, depth: number, particleSystems: (EffectParticleSystem | EffectSolidParticleSystem)[], - Data: IData + data: IData ): void { - const groupNode = this._createGroupNode(Group, parentGroup, depth); - this._processChildren(Group.children, groupNode, depth, particleSystems, Data); + const groupNode = this._createGroupNode(group, parentGroup, depth); + this._processChildren(group.children, groupNode, depth, particleSystems, data); } /** * Process a Emitter object */ - private _processEmitter(Emitter: IEmitter, parentGroup: Nullable, depth: number, particleSystems: (EffectParticleSystem | EffectSolidParticleSystem)[]): void { - const particleSystem = this._createParticleSystem(Emitter, parentGroup, depth); + private _processEmitter(emitter: IEmitter, parentGroup: Nullable, depth: number, particleSystems: (EffectParticleSystem | EffectSolidParticleSystem)[]): void { + const particleSystem = this._createParticleSystem(emitter, parentGroup, depth); if (particleSystem) { particleSystems.push(particleSystem); } @@ -97,7 +97,7 @@ export class SystemFactory { parentGroup: TransformNode, depth: number, particleSystems: (EffectParticleSystem | EffectSolidParticleSystem)[], - Data: IData + data: IData ): void { if (!children || children.length === 0) { return; @@ -105,66 +105,67 @@ export class SystemFactory { this._logger.log(`${" ".repeat(depth)}Processing ${children.length} children`); children.forEach((child) => { - this._processObject(child, parentGroup, depth + 1, particleSystems, Data); + this._processObject(child, parentGroup, depth + 1, particleSystems, data); }); } /** * Create a TransformNode for a Group */ - private _createGroupNode(Group: IGroup, parentGroup: Nullable, depth: number): TransformNode { - const groupNode = new TransformNode(Group.name, this._scene); - groupNode.id = Group.uuid; + private _createGroupNode(group: IGroup, parentGroup: Nullable, depth: number): TransformNode { + const groupNode = new TransformNode(group.name, this._scene); + groupNode.id = group.uuid; - this._applyTransform(groupNode, Group.transform, depth); + this._applyTransform(groupNode, group.transform, depth); this._setParent(groupNode, parentGroup, depth); // Store in map for potential future reference - this._groupNodesMap.set(Group.uuid, groupNode); + this._groupNodesMap.set(group.uuid, groupNode); - this._logger.log(`${" ".repeat(depth)}Created group node: ${Group.name}`); + this._logger.log(`${" ".repeat(depth)}Created group node: ${group.name}`); return groupNode; } /** * Create a particle system from a Emitter */ - private _createParticleSystem(Emitter: IEmitter, parentGroup: Nullable, depth: number): Nullable { + private _createParticleSystem(emitter: IEmitter, parentGroup: Nullable, depth: number): Nullable { const indent = " ".repeat(depth); const parentName = parentGroup ? parentGroup.name : "none"; - this._logger.log(`${indent}Processing emitter: ${Emitter.name} (parent: ${parentName})`); + this._logger.log(`${indent}Processing emitter: ${emitter.name} (parent: ${parentName})`); try { - const config = Emitter.config; + const config = emitter.config; if (!config) { - this._logger.warn(`${indent}Emitter ${Emitter.name} has no config, skipping`); + this._logger.warn(`${indent}Emitter ${emitter.name} has no config, skipping`); return null; } - this._logger.log(`${indent} Config: duration=${config.duration}, looping=${config.looping}, systemType=${Emitter.systemType}`); + const isLooping = config.targetStopDuration === 0; + this._logger.log(`${indent} Config: targetStopDuration=${config.targetStopDuration}, looping=${isLooping}, systemType=${emitter.systemType}`); const cumulativeScale = this._calculateCumulativeScale(parentGroup); this._logger.log(`${indent}Cumulative scale: (${cumulativeScale.x.toFixed(2)}, ${cumulativeScale.y.toFixed(2)}, ${cumulativeScale.z.toFixed(2)})`); // Use systemType from emitter (determined during conversion) - const systemType = Emitter.systemType || "base"; + const systemType = emitter.systemType || "base"; this._logger.log(`Using ${systemType === "solid" ? "SolidParticleSystem" : "ParticleSystem"}`); let particleSystem: EffectParticleSystem | EffectSolidParticleSystem | null = null; try { if (systemType === "solid") { - particleSystem = this._createSolidParticleSystem(Emitter, parentGroup); + particleSystem = this._createSolidParticleSystem(emitter, parentGroup); } else { - particleSystem = this._createParticleSystemInstance(Emitter, parentGroup, cumulativeScale, depth); + particleSystem = this._createParticleSystemInstance(emitter, parentGroup, cumulativeScale, depth); } } catch (error) { - this._logger.error(`${indent}Failed to create ${systemType} system for emitter ${Emitter.name}: ${error instanceof Error ? error.message : String(error)}`); + this._logger.error(`${indent}Failed to create ${systemType} system for emitter ${emitter.name}: ${error instanceof Error ? error.message : String(error)}`); return null; } if (!particleSystem) { - this._logger.warn(`${indent}Failed to create particle system for emitter: ${Emitter.name}`); + this._logger.warn(`${indent}Failed to create particle system for emitter: ${emitter.name}`); return null; } @@ -173,53 +174,191 @@ export class SystemFactory { if (particleSystem instanceof EffectSolidParticleSystem) { // For SPS, transform is applied to the mesh if (particleSystem.mesh) { - this._applyTransform(particleSystem.mesh, Emitter.transform, depth); + this._applyTransform(particleSystem.mesh, emitter.transform, depth); this._setParent(particleSystem.mesh, parentGroup, depth); } } else if (particleSystem instanceof EffectParticleSystem) { // For PS, transform is applied to the emitter mesh - const emitter = particleSystem.getParentNode(); - if (emitter) { - this._applyTransform(emitter, Emitter.transform, depth); - this._setParent(emitter, parentGroup, depth); + const emitterNode = particleSystem.getParentNode(); + if (emitterNode) { + this._applyTransform(emitterNode, emitter.transform, depth); + this._setParent(emitterNode, parentGroup, depth); } } } catch (error) { - this._logger.warn(`${indent}Failed to apply transform to system ${Emitter.name}: ${error instanceof Error ? error.message : String(error)}`); + this._logger.warn(`${indent}Failed to apply transform to system ${emitter.name}: ${error instanceof Error ? error.message : String(error)}`); // Continue - system is created, just transform failed } - this._logger.log(`${indent}Created particle system: ${Emitter.name}`); + this._logger.log(`${indent}Created particle system: ${emitter.name}`); return particleSystem; } catch (error) { - this._logger.error(`${indent}Unexpected error creating particle system ${Emitter.name}: ${error instanceof Error ? error.message : String(error)}`); + this._logger.error(`${indent}Unexpected error creating particle system ${emitter.name}: ${error instanceof Error ? error.message : String(error)}`); return null; } } + /** + * Apply common native properties to both ParticleSystem and SolidParticleSystem + */ + private _applyCommonProperties(system: EffectParticleSystem | EffectSolidParticleSystem, config: IParticleSystemConfig): void { + if (config.minSize !== undefined) system.minSize = config.minSize; + if (config.maxSize !== undefined) system.maxSize = config.maxSize; + if (config.minLifeTime !== undefined) system.minLifeTime = config.minLifeTime; + if (config.maxLifeTime !== undefined) system.maxLifeTime = config.maxLifeTime; + if (config.minEmitPower !== undefined) system.minEmitPower = config.minEmitPower; + if (config.maxEmitPower !== undefined) system.maxEmitPower = config.maxEmitPower; + if (config.emitRate !== undefined) system.emitRate = config.emitRate; + if (config.targetStopDuration !== undefined) system.targetStopDuration = config.targetStopDuration; + if (config.manualEmitCount !== undefined) system.manualEmitCount = config.manualEmitCount; + if (config.preWarmCycles !== undefined) system.preWarmCycles = config.preWarmCycles; + if (config.preWarmStepOffset !== undefined) system.preWarmStepOffset = config.preWarmStepOffset; + if (config.color1 !== undefined) system.color1 = config.color1; + if (config.color2 !== undefined) system.color2 = config.color2; + if (config.colorDead !== undefined) system.colorDead = config.colorDead; + if (config.minInitialRotation !== undefined) system.minInitialRotation = config.minInitialRotation; + if (config.maxInitialRotation !== undefined) system.maxInitialRotation = config.maxInitialRotation; + if (config.isLocal !== undefined) system.isLocal = config.isLocal; + if (config.disposeOnStop !== undefined) system.disposeOnStop = config.disposeOnStop; + if (config.gravity !== undefined) system.gravity = config.gravity; + if (config.noiseStrength !== undefined) system.noiseStrength = config.noiseStrength; + if (config.updateSpeed !== undefined) system.updateSpeed = config.updateSpeed; + if (config.minAngularSpeed !== undefined) system.minAngularSpeed = config.minAngularSpeed; + if (config.maxAngularSpeed !== undefined) system.maxAngularSpeed = config.maxAngularSpeed; + if (config.minScaleX !== undefined) system.minScaleX = config.minScaleX; + if (config.maxScaleX !== undefined) system.maxScaleX = config.maxScaleX; + if (config.minScaleY !== undefined) system.minScaleY = config.minScaleY; + if (config.maxScaleY !== undefined) system.maxScaleY = config.maxScaleY; + } + + /** + * Apply gradients (PiecewiseBezier) to both ParticleSystem and SolidParticleSystem + */ + private _applyGradients(system: EffectParticleSystem | EffectSolidParticleSystem, config: IParticleSystemConfig): void { + if (config.startSizeGradients) { + for (const grad of config.startSizeGradients) { + system.addStartSizeGradient(grad.gradient, grad.factor, grad.factor2); + } + } + if (config.lifeTimeGradients) { + for (const grad of config.lifeTimeGradients) { + system.addLifeTimeGradient(grad.gradient, grad.factor, grad.factor2); + } + } + if (config.emitRateGradients) { + for (const grad of config.emitRateGradients) { + system.addEmitRateGradient(grad.gradient, grad.factor, grad.factor2); + } + } + } + + /** + * Apply common rendering and behavior options + */ + private _applyCommonOptions(system: EffectParticleSystem | EffectSolidParticleSystem, config: IParticleSystemConfig): void { + // Rendering + if (config.renderOrder !== undefined) { + if (system instanceof EffectParticleSystem) { + system.renderingGroupId = config.renderOrder; + } else { + system.renderOrder = config.renderOrder; + } + } + if (config.layers !== undefined) { + if (system instanceof EffectParticleSystem) { + system.layerMask = config.layers; + } else { + system.layers = config.layers; + } + } + + // Billboard + if (config.isBillboardBased !== undefined) { + system.isBillboardBased = config.isBillboardBased; + } + + // Behaviors + if (config.behaviors) { + system.setBehaviors(config.behaviors); + } + } + /** * Create a ParticleSystem instance */ - private _createParticleSystemInstance(Emitter: IEmitter, _parentGroup: Nullable, cumulativeScale: Vector3, _depth: number): Nullable { - const { name, config } = Emitter; + private _createParticleSystemInstance(emitter: IEmitter, _parentGroup: Nullable, cumulativeScale: Vector3, _depth: number): Nullable { + const { name, config } = emitter; this._logger.log(`Creating ParticleSystem: ${name}`); - // Get texture and blend mode - const texture: Texture | undefined = Emitter.materialId ? this._materialFactory.createTexture(Emitter.materialId) || undefined : undefined; - const blendMode = Emitter.materialId ? this._materialFactory.getBlendMode(Emitter.materialId) : undefined; + // Calculate capacity + const duration = config.targetStopDuration !== undefined && config.targetStopDuration > 0 ? config.targetStopDuration : 5; + const emitRate = config.emitRate || 10; + const capacity = CapacityCalculator.calculateForParticleSystem(emitRate, duration); - // Extract rotation matrix from emitter matrix if available - const rotationMatrix = Emitter.matrix ? MatrixUtils.extractRotationMatrix(Emitter.matrix) : null; + // Create instance (simple constructor) + const particleSystem = new EffectParticleSystem(name, capacity, this._scene); - // Create instance - all configuration happens in constructor - const particleSystem = new EffectParticleSystem(name, this._scene, config, { - texture, - blendMode, - }); + // Apply common properties and gradients + this._applyCommonProperties(particleSystem, config); + this._applyGradients(particleSystem, config); - // Create emitter using factory - this._emitterFactory.createParticleSystemEmitter(particleSystem, config.shape, cumulativeScale, rotationMatrix); + // === Настройка текстуры и blend mode === + if (emitter.materialId) { + const texture = this._materialFactory.createTexture(emitter.materialId); + if (texture) { + particleSystem.particleTexture = texture; + } + const blendMode = this._materialFactory.getBlendMode(emitter.materialId); + if (blendMode !== undefined) { + particleSystem.blendMode = blendMode; + } + } + + // === Настройка sprite tiles === + if (config.uTileCount !== undefined && config.vTileCount !== undefined) { + if (config.uTileCount > 1 || config.vTileCount > 1) { + particleSystem.isAnimationSheetEnabled = true; + particleSystem.spriteCellWidth = config.uTileCount; + particleSystem.spriteCellHeight = config.vTileCount; + if (config.startTileIndex !== undefined) { + const startTile = ValueUtils.parseConstantValue(config.startTileIndex); + particleSystem.startSpriteCellID = Math.floor(startTile); + particleSystem.endSpriteCellID = Math.floor(startTile); + } + } + } + + // Apply common rendering and behavior options + this._applyCommonOptions(particleSystem, config); + + // ParticleSystem-specific: billboard mode + if (config.billboardMode !== undefined) { + particleSystem.billboardMode = config.billboardMode; + } + + // === Настройка emission bursts === + if (config.emissionBursts && config.emissionBursts.length > 0) { + const baseEmitRate = config.emitRate || 10; + for (const burst of config.emissionBursts) { + if (burst.time !== undefined && burst.count !== undefined) { + const burstTime = ValueUtils.parseConstantValue(burst.time); + const burstCount = ValueUtils.parseConstantValue(burst.count); + const timeRatio = Math.min(Math.max(burstTime / duration, 0), 1); + const windowSize = 0.02; + const burstEmitRate = burstCount / windowSize; + const beforeTime = Math.max(0, timeRatio - windowSize); + const afterTime = Math.min(1, timeRatio + windowSize); + particleSystem.addEmitRateGradient(beforeTime, baseEmitRate); + particleSystem.addEmitRateGradient(timeRatio, burstEmitRate); + particleSystem.addEmitRateGradient(afterTime, baseEmitRate); + } + } + } + + // === Создание emitter === + const rotationMatrix = emitter.matrix ? MatrixUtils.extractRotationMatrix(emitter.matrix) : null; + particleSystem.configureEmitterFromShape(config.shape, cumulativeScale, rotationMatrix); this._logger.log(`ParticleSystem created: ${name}`); return particleSystem; @@ -228,13 +367,13 @@ export class SystemFactory { /** * Create a SolidParticleSystem instance */ - private _createSolidParticleSystem(Emitter: IEmitter, parentGroup: Nullable): Nullable { - const { name, config } = Emitter; + private _createSolidParticleSystem(emitter: IEmitter, parentGroup: Nullable): Nullable { + const { name, config } = emitter; this._logger.log(`Creating SolidParticleSystem: ${name}`); - // Get transform - const transform = Emitter.transform || null; + // Get transform + const transform = emitter.transform || null; // Create or load particle mesh const particleMesh = this._geometryFactory.createParticleMesh(config, name, this._scene); @@ -243,27 +382,82 @@ export class SystemFactory { } // Apply material if provided - if (Emitter.materialId) { - const material = this._materialFactory.createMaterial(Emitter.materialId, name); + if (emitter.materialId) { + const material = this._materialFactory.createMaterial(emitter.materialId, name); if (material) { particleMesh.material = material; } } - // Create SPS instance - mesh initialization and capacity calculation happen in constructor - const sps = new EffectSolidParticleSystem(name, this._scene, config, { + // Create SPS instance (simple constructor) + const sps = new EffectSolidParticleSystem(name, this._scene, { updatable: true, isPickable: false, enableDepthSort: false, particleIntersection: false, useModelMaterial: true, - parentGroup, transform, - particleMesh, }); - // Create emitter using factory (similar to ParticleSystem) - this._emitterFactory.createSolidParticleSystemEmitter(sps, config.shape); + // Set particle mesh and emitter (like ParticleSystem interface) + sps.particleMesh = particleMesh; + if (parentGroup) { + sps.emitter = parentGroup as AbstractMesh; + } + + // Apply common properties and gradients + this._applyCommonProperties(sps, config); + this._applyGradients(sps, config); + + // Apply common rendering and behavior options + this._applyCommonOptions(sps, config); + + // === SolidParticleSystem-specific properties === + if (config.shape !== undefined) { + sps.shape = config.shape; + } + if (config.emissionOverDistance !== undefined) { + sps.emissionOverDistance = config.emissionOverDistance; + } + if (config.emissionBursts !== undefined) { + sps.emissionBursts = config.emissionBursts; + } + if (config.onlyUsedByOther !== undefined) { + sps.onlyUsedByOther = config.onlyUsedByOther; + } + if (config.instancingGeometry !== undefined) { + sps.instancingGeometry = config.instancingGeometry; + } + if (config.rendererEmitterSettings !== undefined) { + sps.rendererEmitterSettings = config.rendererEmitterSettings; + } + if (config.material !== undefined) { + sps.material = config.material; + } + if (config.startTileIndex !== undefined) { + sps.startTileIndex = config.startTileIndex; + } + if (config.uTileCount !== undefined) { + sps.uTileCount = config.uTileCount; + } + if (config.vTileCount !== undefined) { + sps.vTileCount = config.vTileCount; + } + if (config.blendTiles !== undefined) { + sps.blendTiles = config.blendTiles; + } + if (config.softParticles !== undefined) { + sps.softParticles = config.softParticles; + } + if (config.softFarFade !== undefined) { + sps.softFarFade = config.softFarFade; + } + if (config.softNearFade !== undefined) { + sps.softNearFade = config.softNearFade; + } + + // === Создание emitter === + sps.configureEmitterFromShape(config.shape); this._logger.log(`SolidParticleSystem created: ${name}`); return sps; diff --git a/tools/src/effect/parsers/dataConverter.ts b/tools/src/effect/parsers/dataConverter.ts index 1662cbe1c..7ff76468e 100644 --- a/tools/src/effect/parsers/dataConverter.ts +++ b/tools/src/effect/parsers/dataConverter.ts @@ -29,7 +29,7 @@ import type { } from "../types/quarksTypes"; import type { ITransform, IGroup, IEmitter, IData } from "../types/hierarchy"; import type { IMaterial, ITexture, IImage, IGeometry, IGeometryData } from "../types/resources"; -import type { IEmitterConfig } from "../types/emitter"; +import type { IParticleSystemConfig } from "../types/emitter"; import type { Behavior, IColorOverLifeBehavior, @@ -246,20 +246,43 @@ export class DataConverter { /** * Convert emitter config from IQuarks to format */ - private _convertEmitterConfig(IQuarksConfig: IQuarksParticleEmitterConfig): IEmitterConfig { + private _convertEmitterConfig(IQuarksConfig: IQuarksParticleEmitterConfig): IParticleSystemConfig { // Determine system type based on renderMode: 2 = solid, otherwise base const systemType: "solid" | "base" = IQuarksConfig.renderMode === 2 ? "solid" : "base"; - const Config: IEmitterConfig = { + // Convert duration/looping to native targetStopDuration + // In Babylon.js: targetStopDuration = 0 means infinite loop + const duration = IQuarksConfig.duration ?? 5; + const targetStopDuration = IQuarksConfig.looping ? 0 : duration; + + // Convert prewarm to native preWarmCycles + // In Babylon.js: preWarmCycles > 0 means prewarm enabled + let preWarmCycles = 0; + let preWarmStepOffset = 0.016; + if (IQuarksConfig.prewarm) { + preWarmCycles = Math.ceil(duration * 60); // Simulate ~60fps for duration + preWarmStepOffset = 1 / 60; + } + + // Convert worldSpace to native isLocal (inverse) + const isLocal = IQuarksConfig.worldSpace === undefined ? false : !IQuarksConfig.worldSpace; + + // Convert autoDestroy to native disposeOnStop + const disposeOnStop = IQuarksConfig.autoDestroy ?? false; + + const Config: IParticleSystemConfig = { version: IQuarksConfig.version, - autoDestroy: IQuarksConfig.autoDestroy, - looping: IQuarksConfig.looping, - prewarm: IQuarksConfig.prewarm, - duration: IQuarksConfig.duration, + systemType, + // Native properties + targetStopDuration, + preWarmCycles, + preWarmStepOffset, + isLocal, + disposeOnStop, + // Other properties onlyUsedByOther: IQuarksConfig.onlyUsedByOther, instancingGeometry: IQuarksConfig.instancingGeometry, renderOrder: IQuarksConfig.renderOrder, - systemType, rendererEmitterSettings: IQuarksConfig.rendererEmitterSettings, material: IQuarksConfig.material, layers: IQuarksConfig.layers, @@ -269,7 +292,6 @@ export class DataConverter { softParticles: IQuarksConfig.softParticles, softFarFade: IQuarksConfig.softFarFade, softNearFade: IQuarksConfig.softNearFade, - worldSpace: IQuarksConfig.worldSpace, }; // Convert values diff --git a/tools/src/effect/systems/effectParticleSystem.ts b/tools/src/effect/systems/effectParticleSystem.ts index d381573a9..e4b5a2b99 100644 --- a/tools/src/effect/systems/effectParticleSystem.ts +++ b/tools/src/effect/systems/effectParticleSystem.ts @@ -1,4 +1,4 @@ -import { Color4, ParticleSystem, Scene, Vector3, Matrix, Texture, AbstractMesh, TransformNode, Particle } from "babylonjs"; +import { ParticleSystem, Scene, AbstractMesh, TransformNode, Particle, Vector3 } from "babylonjs"; import type { Behavior, IColorOverLifeBehavior, @@ -16,12 +16,7 @@ import type { PerParticleBehaviorFunction, ISystem, ParticleWithSystem, - IShape, - IEmitterConfig, - IEmissionBurst, } from "../types"; -import { ValueUtils } from "../utils/valueParser"; -import { CapacityCalculator } from "../utils/capacityCalculator"; import { applyColorOverLifePS, applySizeOverLifePS, @@ -38,39 +33,55 @@ import { } from "../behaviors"; /** - * Extended ParticleSystem with behaviors support - * Fully self-contained, no dependencies on parsers or factories + * Extended ParticleSystem with behaviors support + * Integrates per-particle behaviors (ColorBySpeed, OrbitOverLife, etc.) + * into the native Babylon.js particle update loop */ export class EffectParticleSystem extends ParticleSystem implements ISystem { - public startSize: number; - public startSpeed: number; - public startColor: Color4; - public prewarm: boolean; - private _behaviors: PerParticleBehaviorFunction[]; - public readonly behaviorConfigs: Behavior[]; + private _perParticleBehaviors: PerParticleBehaviorFunction[]; + private _behaviorConfigs: Behavior[]; - constructor( - name: string, - scene: Scene, - config: IEmitterConfig, - options?: { - texture?: Texture; - blendMode?: number; - } - ) { - // Calculate capacity - const duration = config.duration || 5; - const capacity = CapacityCalculator.calculateForParticleSystem(config.emissionOverTime, duration); + /** Store reference to default updateFunction */ + private _defaultUpdateFunction: (particles: Particle[]) => void; + constructor(name: string, capacity: number, scene: Scene) { super(name, capacity, scene); - this._behaviors = []; - this.prewarm = config.prewarm || false; + this._perParticleBehaviors = []; + this._behaviorConfigs = []; + + // Store reference to the default updateFunction created by ParticleSystem + this._defaultUpdateFunction = this.updateFunction; + + // Override updateFunction to integrate per-particle behaviors + this._setupCustomUpdateFunction(); + } + + /** + * Setup custom updateFunction that extends default behavior + * with per-particle behavior execution + */ + private _setupCustomUpdateFunction(): void { + this.updateFunction = (particles: Particle[]): void => { + // First, run the default Babylon.js update logic + // This handles: age, gradients (color, size, angular speed, velocity), position, gravity, etc. + this._defaultUpdateFunction(particles); + + // Then apply per-particle behaviors if any exist + if (this._perParticleBehaviors.length === 0) { + return; + } - // Create proxy array that updates functions when modified - this.behaviorConfigs = this._createBehaviorConfigsProxy(config.behaviors || []); + // Apply per-particle behaviors to each active particle + for (const particle of particles) { + // Attach system reference for behaviors that need it + (particle as ParticleWithSystem).particleSystem = this; - // Configure from config - this._configureFromConfig(config, options); + // Execute all per-particle behavior functions + for (const behaviorFn of this._perParticleBehaviors) { + behaviorFn(particle); + } + } + }; } /** @@ -78,144 +89,68 @@ export class EffectParticleSystem extends ParticleSystem implements ISystem { * Required by ISystem interface */ public getParentNode(): AbstractMesh | TransformNode | null { - // ParticleSystem.emitter can be AbstractMesh, Vector3, or null - // Return emitter if it's an AbstractMesh, otherwise null return this.emitter instanceof AbstractMesh ? this.emitter : null; } /** - * Get behavior functions (internal use) + * Get current behavior configurations */ - public get behaviors(): PerParticleBehaviorFunction[] { - return this._behaviors; + public get behaviorConfigs(): Behavior[] { + return this._behaviorConfigs; } /** - * Create a proxy array that automatically updates behavior functions when configs change + * Set behaviors and apply them to the particle system + * System-level behaviors configure gradients, per-particle behaviors run each frame */ - private _createBehaviorConfigsProxy(configs: Behavior[]): Behavior[] { - const self = this; - - // Wrap each behavior object in a proxy to detect property changes - const wrapBehavior = (behavior: Behavior): Behavior => { - return new Proxy(behavior, { - set(target, prop, value) { - const result = Reflect.set(target, prop, value); - // When a behavior property changes, update functions - self._updateBehaviorFunctions(); - return result; - }, - }); - }; - - // Wrap all initial behaviors - const wrappedConfigs = configs.map(wrapBehavior); - - return new Proxy(wrappedConfigs, { - set(target, property, value) { - const result = Reflect.set(target, property, value); - - // Update functions when array is modified - if (property === "length" || typeof property === "number") { - // If setting an element, wrap it in proxy - if (typeof property === "number" && value && typeof value === "object") { - Reflect.set(target, property, wrapBehavior(value as Behavior)); - } - self._updateBehaviorFunctions(); - } - - return result; - }, + public setBehaviors(behaviors: Behavior[]): void { + this._behaviorConfigs = behaviors; - get(target, property) { - const value = Reflect.get(target, property); - - // Intercept array methods that modify the array - if ( - typeof value === "function" && - (property === "push" || - property === "pop" || - property === "splice" || - property === "shift" || - property === "unshift" || - property === "sort" || - property === "reverse") - ) { - return function (...args: any[]) { - const result = value.apply(target, args); - // Wrap any new behaviors added via push/unshift - if (property === "push" || property === "unshift") { - for (let i = 0; i < args.length; i++) { - if (args[i] && typeof args[i] === "object") { - const index = property === "push" ? target.length - args.length + i : i; - Reflect.set(target, index, wrapBehavior(args[i] as Behavior)); - } - } - } - self._updateBehaviorFunctions(); - return result; - }; - } + // Apply system-level behaviors (gradients) to ParticleSystem + this._applySystemLevelBehaviors(behaviors); - return value; - }, - }); + // Build per-particle behavior functions for update loop + this._perParticleBehaviors = this._buildPerParticleBehaviors(behaviors); } /** - * Update behavior functions from configs - * Internal method, called automatically when configs change - * Applies both system-level behaviors (gradients) and per-particle behaviors + * Add a single behavior */ - private _updateBehaviorFunctions(): void { - // Apply system-level behaviors (gradients, etc.) - these configure the ParticleSystem once - this._applySystemLevelBehaviors(); - - // Create per-particle behavior functions (BySpeed, OrbitOverLife, etc.) - this._behaviors = this._createPerParticleBehaviorFunctions(this.behaviorConfigs); + public addBehavior(behavior: Behavior): void { + this._behaviorConfigs.push(behavior); + this.setBehaviors(this._behaviorConfigs); } /** - * Create per-particle behavior functions from configurations - * Only creates functions for behaviors that depend on particle properties (speed, orbit) + * Build per-particle behavior functions from configurations + * Per-particle behaviors run each frame for each particle (ColorBySpeed, OrbitOverLife, etc.) */ - private _createPerParticleBehaviorFunctions(behaviors: Behavior[]): PerParticleBehaviorFunction[] { + private _buildPerParticleBehaviors(behaviors: Behavior[]): PerParticleBehaviorFunction[] { const functions: PerParticleBehaviorFunction[] = []; for (const behavior of behaviors) { switch (behavior.type) { case "ColorBySpeed": { const b = behavior as IColorBySpeedBehavior; - functions.push((particle: Particle) => { - applyColorBySpeedPS(particle, b); - }); + functions.push((particle: Particle) => applyColorBySpeedPS(particle, b)); break; } case "SizeBySpeed": { const b = behavior as ISizeBySpeedBehavior; - functions.push((particle: Particle) => { - applySizeBySpeedPS(particle, b); - }); + functions.push((particle: Particle) => applySizeBySpeedPS(particle, b)); break; } case "RotationBySpeed": { const b = behavior as IRotationBySpeedBehavior; - functions.push((particle: Particle) => { - // Store reference to system in particle for behaviors that need it - const particleWithSystem = particle as ParticleWithSystem; - particleWithSystem.particleSystem = this; - applyRotationBySpeedPS(particle, b); - }); + functions.push((particle: Particle) => applyRotationBySpeedPS(particle, b)); break; } case "OrbitOverLife": { const b = behavior as IOrbitOverLifeBehavior; - functions.push((particle: Particle) => { - applyOrbitOverLifePS(particle, b); - }); + functions.push((particle: Particle) => applyOrbitOverLifePS(particle, b)); break; } } @@ -225,11 +160,11 @@ export class EffectParticleSystem extends ParticleSystem implements ISystem { } /** - * Apply system-level behaviors (gradients, etc.) to ParticleSystem - * These are applied once when behaviors change, not per-particle + * Apply system-level behaviors (gradients) to ParticleSystem + * These configure native Babylon.js gradients once, not per-particle */ - private _applySystemLevelBehaviors(): void { - for (const behavior of this.behaviorConfigs) { + private _applySystemLevelBehaviors(behaviors: Behavior[]): void { + for (const behavior of behaviors) { if (!behavior.type) { continue; } @@ -266,150 +201,47 @@ export class EffectParticleSystem extends ParticleSystem implements ISystem { } /** - * Configure particle system from config (internal use) - * This method applies all configuration from ParticleEmitterConfig + * Configure emitter from shape config + * This replaces the need for EmitterFactory */ - private _configureFromConfig( - config: IEmitterConfig, - options?: { - texture?: Texture; - blendMode?: number; - emitterShape?: { shape: IShape | undefined; cumulativeScale: Vector3; rotationMatrix: Matrix | null }; - } - ): void { - // Parse values - const emissionRate = config.emissionOverTime !== undefined ? ValueUtils.parseConstantValue(config.emissionOverTime) : 10; - const duration = config.duration || 5; - const lifeTime = config.startLife !== undefined ? ValueUtils.parseIntervalValue(config.startLife) : { min: 1, max: 1 }; - const speed = config.startSpeed !== undefined ? ValueUtils.parseIntervalValue(config.startSpeed) : { min: 1, max: 1 }; - const size = config.startSize !== undefined ? ValueUtils.parseIntervalValue(config.startSize) : { min: 1, max: 1 }; - const startColor = config.startColor !== undefined ? ValueUtils.parseConstantColor(config.startColor) : new Color4(1, 1, 1, 1); - - // Configure basic properties - this.targetStopDuration = duration; - this.emitRate = emissionRate; - this.manualEmitCount = -1; - this.minLifeTime = lifeTime.min; - this.maxLifeTime = lifeTime.max; - this.minEmitPower = speed.min; - this.maxEmitPower = speed.max; - this.minSize = size.min; - this.maxSize = size.max; - this.color1 = startColor; - this.color2 = startColor; - this.colorDead = new Color4(startColor.r, startColor.g, startColor.b, 0); - - // Configure rotation - if (config.startRotation) { - if (this._isEulerRotation(config.startRotation)) { - if (config.startRotation.angleZ !== undefined) { - const angleZ = ValueUtils.parseIntervalValue(config.startRotation.angleZ); - this.minInitialRotation = angleZ.min; - this.maxInitialRotation = angleZ.max; - } - } else { - const rotation = ValueUtils.parseIntervalValue(config.startRotation as any); - this.minInitialRotation = rotation.min; - this.maxInitialRotation = rotation.max; - } - } - - // Configure sprite tiles - if (config.uTileCount !== undefined && config.vTileCount !== undefined) { - if (config.uTileCount > 1 || config.vTileCount > 1) { - this.isAnimationSheetEnabled = true; - this.spriteCellWidth = config.uTileCount; - this.spriteCellHeight = config.vTileCount; - - if (config.startTileIndex !== undefined) { - const startTile = ValueUtils.parseConstantValue(config.startTileIndex); - this.startSpriteCellID = Math.floor(startTile); - this.endSpriteCellID = Math.floor(startTile); - } + public configureEmitterFromShape(shape: any, cumulativeScale: any, _rotationMatrix: any): void { + if (!shape || !shape.type) { + this.createPointEmitter(new Vector3(0, 1, 0), new Vector3(0, 1, 0)); + return; + } + + const shapeType = shape.type.toLowerCase(); + const radius = (shape.radius ?? 1) * ((cumulativeScale.x + cumulativeScale.y + cumulativeScale.z) / 3); + const angle = shape.angle ?? Math.PI / 4; + + switch (shapeType) { + case "cone": + this.createConeEmitter(radius, angle); + break; + case "sphere": + this.createSphereEmitter(radius); + break; + case "point": + this.createPointEmitter(new Vector3(0, 1, 0), new Vector3(0, 1, 0)); + break; + case "box": { + const boxSize = (shape.size || [1, 1, 1]).map((s: number, i: number) => s * [cumulativeScale.x, cumulativeScale.y, cumulativeScale.z][i]); + const minBox = new Vector3(-boxSize[0] / 2, -boxSize[1] / 2, -boxSize[2] / 2); + const maxBox = new Vector3(boxSize[0] / 2, boxSize[1] / 2, boxSize[2] / 2); + this.createBoxEmitter(new Vector3(0, 1, 0), new Vector3(0, 1, 0), minBox, maxBox); + break; } - } - - // Configure rendering - if (config.renderOrder !== undefined) { - this.renderingGroupId = config.renderOrder; - } - if (config.layers !== undefined) { - this.layerMask = config.layers; - } - - // Apply texture and blend mode - if (options?.texture) { - this.particleTexture = options.texture; - } - if (options?.blendMode !== undefined) { - this.blendMode = options.blendMode; - } - - // Apply emission bursts - if (config.emissionBursts && Array.isArray(config.emissionBursts) && config.emissionBursts.length > 0) { - this._applyEmissionBursts(config.emissionBursts, emissionRate, duration); - } - - // Apply behaviors - if (config.behaviors && Array.isArray(config.behaviors) && config.behaviors.length > 0) { - this.behaviorConfigs.length = 0; - this.behaviorConfigs.push(...config.behaviors); - } - - // Configure world space - if (config.worldSpace !== undefined) { - this.isLocal = !config.worldSpace; - } - - // Configure looping - if (config.looping !== undefined) { - this.targetStopDuration = config.looping ? 0 : duration; - } - - // Configure billboard mode (converted from renderMode in DataConverter) - if (config.isBillboardBased !== undefined) { - this.isBillboardBased = config.isBillboardBased; - } - if (config.billboardMode !== undefined) { - this.billboardMode = config.billboardMode; - } - - // Configure auto destroy - if (config.autoDestroy !== undefined) { - this.disposeOnStop = config.autoDestroy; - } - - // Emitter shape is created in SystemFactory after system creation - } - - /** - * Check if rotation is Euler type - */ - private _isEulerRotation(rotation: any): rotation is { type: "Euler"; angleZ?: any } { - return typeof rotation === "object" && rotation !== null && "type" in rotation && rotation.type === "Euler"; - } - - /** - * Apply emission bursts via emit rate gradients - */ - private _applyEmissionBursts(bursts: IEmissionBurst[], baseEmitRate: number, duration: number): void { - for (const burst of bursts) { - if (burst.time === undefined || burst.count === undefined) { - continue; + case "hemisphere": + this.createHemisphericEmitter(radius); + break; + case "cylinder": { + const height = (shape.height ?? 1) * cumulativeScale.y; + this.createCylinderEmitter(radius, height); + break; } - - const burstTime = ValueUtils.parseConstantValue(burst.time); - const burstCount = ValueUtils.parseConstantValue(burst.count); - const timeRatio = Math.min(Math.max(burstTime / duration, 0), 1); - const windowSize = 0.02; - const burstEmitRate = burstCount / windowSize; - - const beforeTime = Math.max(0, timeRatio - windowSize); - const afterTime = Math.min(1, timeRatio + windowSize); - - this.addEmitRateGradient(beforeTime, baseEmitRate); - this.addEmitRateGradient(timeRatio, burstEmitRate); - this.addEmitRateGradient(afterTime, baseEmitRate); + default: + this.createPointEmitter(new Vector3(0, 1, 0), new Vector3(0, 1, 0)); + break; } } } diff --git a/tools/src/effect/systems/effectSolidParticleSystem.ts b/tools/src/effect/systems/effectSolidParticleSystem.ts index 9aa305a1e..e14080d10 100644 --- a/tools/src/effect/systems/effectSolidParticleSystem.ts +++ b/tools/src/effect/systems/effectSolidParticleSystem.ts @@ -6,16 +6,13 @@ import type { ISizeBySpeedBehavior, IRotationBySpeedBehavior, IOrbitOverLifeBehavior, - IEmitterConfig, IEmissionBurst, ISolidParticleEmitterType, PerSolidParticleBehaviorFunction, ISystem, SolidParticleWithSystem, IShape, - Color, Value, - Rotation, } from "../types"; import { SolidPointParticleEmitter, SolidSphereParticleEmitter, SolidConeParticleEmitter } from "../emitters"; import { ValueUtils, CapacityCalculator, ColorGradientSystem, NumberGradientSystem } from "../utils"; @@ -54,9 +51,9 @@ export class EffectSolidParticleSystem extends SolidParticleSystem implements IS private _emissionState: IEmissionState; private _behaviors: PerSolidParticleBehaviorFunction[]; public particleEmitterType: ISolidParticleEmitterType | null; - private _parent: TransformNode | null; private _transform: { position: Vector3; rotation: Quaternion; scale: Vector3 } | null; private _emitEnded: boolean; + private _emitter: AbstractMesh | null; // Gradient systems for "OverLife" behaviors (similar to ParticleSystem native gradients) private _colorGradients: ColorGradientSystem; @@ -66,18 +63,43 @@ export class EffectSolidParticleSystem extends SolidParticleSystem implements IS private _limitVelocityGradients: NumberGradientSystem; private _limitVelocityDamping: number; - // Properties moved from config - public isLooping: boolean; - public duration: number; - public prewarm: boolean; + // === Native Babylon.js properties (like ParticleSystem) === + public minSize: number = 1; + public maxSize: number = 1; + public minLifeTime: number = 1; + public maxLifeTime: number = 1; + public minEmitPower: number = 1; + public maxEmitPower: number = 1; + public emitRate: number = 10; + public targetStopDuration: number = 5; + public manualEmitCount: number = -1; + public preWarmCycles: number = 0; + public preWarmStepOffset: number = 0.016; + public color1: Color4 = new Color4(1, 1, 1, 1); + public color2: Color4 = new Color4(1, 1, 1, 1); + public colorDead: Color4 = new Color4(1, 1, 1, 0); + public minInitialRotation: number = 0; + public maxInitialRotation: number = 0; + public isLocal: boolean = false; + public disposeOnStop: boolean = false; + public gravity?: Vector3; + public noiseStrength?: Vector3; + public updateSpeed: number = 1; + public minAngularSpeed: number = 0; + public maxAngularSpeed: number = 0; + public minScaleX: number = 1; + public maxScaleX: number = 1; + public minScaleY: number = 1; + public maxScaleY: number = 1; + + // Gradients for PiecewiseBezier (like ParticleSystem) + private _startSizeGradients: NumberGradientSystem; + private _lifeTimeGradients: NumberGradientSystem; + private _emitRateGradients: NumberGradientSystem; + + // === Other properties === public shape?: IShape; - public startLife?: Value; - public startSpeed?: Value; - public startRotation?: Rotation; - public startSize?: Value; - public startColor?: Color; - public emissionOverTime?: Value; - public emissionOverDistance?: Value; + public emissionOverDistance?: Value; // For distance-based emission public emissionBursts?: IEmissionBurst[]; public onlyUsedByOther: boolean; public instancingGeometry?: string; @@ -93,313 +115,114 @@ export class EffectSolidParticleSystem extends SolidParticleSystem implements IS public softParticles: boolean; public softFarFade?: number; public softNearFade?: number; - public worldSpace: boolean; - public readonly behaviorConfigs: Behavior[]; + private _behaviorConfigs: Behavior[]; /** - * Get/set parent transform node + * Get current behavior configurations */ - public get parent(): TransformNode | null { - return this._parent; - } - public set parent(value: TransformNode | null) { - this._parent = value; - if (this.mesh) { - this.mesh.setParent(value, false, true); - } + public get behaviorConfigs(): Behavior[] { + return this._behaviorConfigs; } /** - * Get/set minSize (compatible with ParticleSystem API) - * Works with startSize Value under the hood + * Set behaviors and apply them to the system */ - public get minSize(): number { - if (!this.startSize) { - return 1; - } - return ValueUtils.parseIntervalValue(this.startSize).min; - } - public set minSize(value: number) { - if (!this.startSize) { - this.startSize = { type: "IntervalValue", min: value, max: value }; - return; - } - if (typeof this.startSize === "number") { - this.startSize = { type: "IntervalValue", min: value, max: this.startSize }; - return; - } - if (this.startSize.type === "ConstantValue") { - this.startSize = { type: "IntervalValue", min: value, max: this.startSize.value }; - return; - } - if (this.startSize.type === "IntervalValue") { - this.startSize.min = value; - return; - } - // For PiecewiseBezier, convert to IntervalValue - this.startSize = { type: "IntervalValue", min: value, max: value }; + public setBehaviors(behaviors: Behavior[]): void { + this._behaviorConfigs = behaviors; + this._applyBehaviors(); } /** - * Get/set maxSize (compatible with ParticleSystem API) - * Works with startSize Value under the hood + * Add a single behavior */ - public get maxSize(): number { - if (!this.startSize) { - return 1; - } - return ValueUtils.parseIntervalValue(this.startSize).max; - } - public set maxSize(value: number) { - if (!this.startSize) { - this.startSize = { type: "IntervalValue", min: value, max: value }; - return; - } - if (typeof this.startSize === "number") { - this.startSize = { type: "IntervalValue", min: this.startSize, max: value }; - return; - } - if (this.startSize.type === "ConstantValue") { - this.startSize = { type: "IntervalValue", min: this.startSize.value, max: value }; - return; - } - if (this.startSize.type === "IntervalValue") { - this.startSize.max = value; - return; - } - // For PiecewiseBezier, convert to IntervalValue - this.startSize = { type: "IntervalValue", min: value, max: value }; + public addBehavior(behavior: Behavior): void { + this._behaviorConfigs.push(behavior); + this._applyBehaviors(); } /** - * Get/set minLifeTime (compatible with ParticleSystem API) - * Works with startLife Value under the hood + * Apply behaviors - system-level (gradients) and per-particle */ - public get minLifeTime(): number { - if (!this.startLife) { - return 1; - } - return ValueUtils.parseIntervalValue(this.startLife).min; - } - public set minLifeTime(value: number) { - if (!this.startLife) { - this.startLife = { type: "IntervalValue", min: value, max: value }; - return; - } - if (typeof this.startLife === "number") { - this.startLife = { type: "IntervalValue", min: value, max: this.startLife }; - return; - } - if (this.startLife.type === "ConstantValue") { - this.startLife = { type: "IntervalValue", min: value, max: this.startLife.value }; - return; - } - if (this.startLife.type === "IntervalValue") { - this.startLife.min = value; - return; - } - // For PiecewiseBezier, convert to IntervalValue - this.startLife = { type: "IntervalValue", min: value, max: value }; - } + private _applyBehaviors(): void { + // Clear existing gradients + this._colorGradients.clear(); + this._sizeGradients.clear(); + this._velocityGradients.clear(); + this._angularSpeedGradients.clear(); + this._limitVelocityGradients.clear(); - /** - * Get/set maxLifeTime (compatible with ParticleSystem API) - * Works with startLife Value under the hood - */ - public get maxLifeTime(): number { - if (!this.startLife) { - return 1; - } - return ValueUtils.parseIntervalValue(this.startLife).max; - } - public set maxLifeTime(value: number) { - if (!this.startLife) { - this.startLife = { type: "IntervalValue", min: value, max: value }; - return; - } - if (typeof this.startLife === "number") { - this.startLife = { type: "IntervalValue", min: this.startLife, max: value }; - return; - } - if (this.startLife.type === "ConstantValue") { - this.startLife = { type: "IntervalValue", min: this.startLife.value, max: value }; - return; - } - if (this.startLife.type === "IntervalValue") { - this.startLife.max = value; - return; - } - // For PiecewiseBezier, convert to IntervalValue - this.startLife = { type: "IntervalValue", min: value, max: value }; + // Apply system-level behaviors (gradients) + this._applySystemLevelBehaviors(); + + // Build per-particle behavior functions + this._behaviors = this._buildPerParticleBehaviors(this._behaviorConfigs); } /** - * Get/set minEmitPower (compatible with ParticleSystem API) - * Works with startSpeed Value under the hood + * Add start size gradient (like ParticleSystem) */ - public get minEmitPower(): number { - if (!this.startSpeed) { - return 1; - } - return ValueUtils.parseIntervalValue(this.startSpeed).min; - } - public set minEmitPower(value: number) { - if (!this.startSpeed) { - this.startSpeed = { type: "IntervalValue", min: value, max: value }; - return; - } - if (typeof this.startSpeed === "number") { - this.startSpeed = { type: "IntervalValue", min: value, max: this.startSpeed }; - return; - } - if (this.startSpeed.type === "ConstantValue") { - this.startSpeed = { type: "IntervalValue", min: value, max: this.startSpeed.value }; - return; - } - if (this.startSpeed.type === "IntervalValue") { - this.startSpeed.min = value; - return; + public addStartSizeGradient(gradient: number, factor: number, factor2?: number): void { + if (factor2 !== undefined) { + this._startSizeGradients.addGradient(gradient, factor); + this._startSizeGradients.addGradient(gradient, factor2); + } else { + this._startSizeGradients.addGradient(gradient, factor); } - // For PiecewiseBezier, convert to IntervalValue - this.startSpeed = { type: "IntervalValue", min: value, max: value }; } /** - * Get/set maxEmitPower (compatible with ParticleSystem API) - * Works with startSpeed Value under the hood + * Add life time gradient (like ParticleSystem) */ - public get maxEmitPower(): number { - if (!this.startSpeed) { - return 1; - } - return ValueUtils.parseIntervalValue(this.startSpeed).max; - } - public set maxEmitPower(value: number) { - if (!this.startSpeed) { - this.startSpeed = { type: "IntervalValue", min: value, max: value }; - return; - } - if (typeof this.startSpeed === "number") { - this.startSpeed = { type: "IntervalValue", min: this.startSpeed, max: value }; - return; - } - if (this.startSpeed.type === "ConstantValue") { - this.startSpeed = { type: "IntervalValue", min: this.startSpeed.value, max: value }; - return; - } - if (this.startSpeed.type === "IntervalValue") { - this.startSpeed.max = value; - return; + public addLifeTimeGradient(gradient: number, factor: number, factor2?: number): void { + if (factor2 !== undefined) { + this._lifeTimeGradients.addGradient(gradient, factor); + this._lifeTimeGradients.addGradient(gradient, factor2); + } else { + this._lifeTimeGradients.addGradient(gradient, factor); } - // For PiecewiseBezier, convert to IntervalValue - this.startSpeed = { type: "IntervalValue", min: value, max: value }; } /** - * Get/set color1 (compatible with ParticleSystem API) - * Works with startColor Color under the hood + * Add emit rate gradient (like ParticleSystem) */ - public get color1(): Color4 { - if (!this.startColor) { - return new Color4(1, 1, 1, 1); + public addEmitRateGradient(gradient: number, factor: number, factor2?: number): void { + if (factor2 !== undefined) { + this._emitRateGradients.addGradient(gradient, factor); + this._emitRateGradients.addGradient(gradient, factor2); + } else { + this._emitRateGradients.addGradient(gradient, factor); } - return ValueUtils.parseConstantColor(this.startColor); - } - public set color1(value: Color4) { - this.startColor = { - type: "ConstantColor", - value: [value.r, value.g, value.b, value.a], - }; } /** - * Get/set minInitialRotation (compatible with ParticleSystem API) - * Works with startRotation Rotation under the hood (uses angleZ) + * Get the parent node (mesh) for hierarchy operations + * Implements ISystem interface */ - public get minInitialRotation(): number { - if (!this.startRotation) { - return 0; - } - // Handle Euler rotation with angleZ - if (typeof this.startRotation === "object" && "type" in this.startRotation && this.startRotation.type === "Euler") { - if (this.startRotation.angleZ) { - return ValueUtils.parseIntervalValue(this.startRotation.angleZ).min; - } - return 0; - } - // Handle simple Value rotation - if (typeof this.startRotation === "object" && "type" in this.startRotation) { - return ValueUtils.parseIntervalValue(this.startRotation as any).min; - } - return typeof this.startRotation === "number" ? this.startRotation : 0; - } - public set minInitialRotation(value: number) { - if (!this.startRotation) { - this.startRotation = { type: "Euler", angleZ: { type: "IntervalValue", min: value, max: value } }; - return; - } - // Handle Euler rotation - if (typeof this.startRotation === "object" && "type" in this.startRotation && this.startRotation.type === "Euler") { - if (!this.startRotation.angleZ) { - this.startRotation.angleZ = { type: "IntervalValue", min: value, max: value }; - } else { - const currentMax = ValueUtils.parseIntervalValue(this.startRotation.angleZ).max; - this.startRotation.angleZ = { type: "IntervalValue", min: value, max: currentMax }; - } - return; - } - // Convert to Euler rotation - const currentMax = this.maxInitialRotation; - this.startRotation = { type: "Euler", angleZ: { type: "IntervalValue", min: value, max: currentMax } }; + public getParentNode(): AbstractMesh | TransformNode | null { + return this.mesh || null; } /** - * Get/set maxInitialRotation (compatible with ParticleSystem API) - * Works with startRotation Rotation under the hood (uses angleZ) + * Emitter property (like ParticleSystem) + * Sets the parent for the mesh - the point from which particles emit */ - public get maxInitialRotation(): number { - if (!this.startRotation) { - return 0; - } - // Handle Euler rotation with angleZ - if (typeof this.startRotation === "object" && "type" in this.startRotation && this.startRotation.type === "Euler") { - if (this.startRotation.angleZ) { - return ValueUtils.parseIntervalValue(this.startRotation.angleZ).max; - } - return 0; - } - // Handle simple Value rotation - if (typeof this.startRotation === "object" && "type" in this.startRotation) { - return ValueUtils.parseIntervalValue(this.startRotation as any).max; - } - return typeof this.startRotation === "number" ? this.startRotation : 0; + public get emitter(): AbstractMesh | null { + return this._emitter; } - public set maxInitialRotation(value: number) { - if (!this.startRotation) { - this.startRotation = { type: "Euler", angleZ: { type: "IntervalValue", min: value, max: value } }; - return; - } - // Handle Euler rotation - if (typeof this.startRotation === "object" && "type" in this.startRotation && this.startRotation.type === "Euler") { - if (!this.startRotation.angleZ) { - this.startRotation.angleZ = { type: "IntervalValue", min: value, max: value }; - } else { - const currentMin = ValueUtils.parseIntervalValue(this.startRotation.angleZ).min; - this.startRotation.angleZ = { type: "IntervalValue", min: currentMin, max: value }; - } - return; + public set emitter(value: AbstractMesh | null) { + this._emitter = value; + // If mesh is already created, set its parent + if (this.mesh && value) { + this.mesh.setParent(value, false, true); } - // Convert to Euler rotation - const currentMin = this.minInitialRotation; - this.startRotation = { type: "Euler", angleZ: { type: "IntervalValue", min: currentMin, max: value } }; } /** - * Get the parent node (mesh) for hierarchy operations - * Implements ISystem interface + * Set particle mesh to use for rendering + * Initializes the SPS with this mesh */ - public getParentNode(): AbstractMesh | TransformNode | null { - return this.mesh || null; + public set particleMesh(mesh: Mesh) { + this._initializeMesh(mesh); } /** @@ -534,7 +357,8 @@ export class EffectSolidParticleSystem extends SolidParticleSystem implements IS return; } - const capacity = CapacityCalculator.calculateForSolidParticleSystem(this.emissionOverTime, this.duration, this.isLooping); + const isLooping = this.targetStopDuration === 0; + const capacity = CapacityCalculator.calculateForSolidParticleSystem(this.emitRate, this.targetStopDuration, isLooping); this.addShape(particleMesh, capacity); if (this.isBillboardBased !== undefined) { @@ -548,28 +372,6 @@ export class EffectSolidParticleSystem extends SolidParticleSystem implements IS particleMesh.dispose(); } - /** - * Get emit rate (constant value from emissionOverTime) - */ - public get emitRate(): number { - if (!this.emissionOverTime) { - return 10; - } - return ValueUtils.parseConstantValue(this.emissionOverTime); - } - public set emitRate(value: number) { - this.emissionOverTime = { type: "ConstantValue", value }; - } - - /** - * Get target stop duration (alias for duration) - */ - public get targetStopDuration(): number { - return this.duration; - } - public set targetStopDuration(value: number) { - this.duration = value; - } private _normalMatrix: Matrix; private _tempVec: Vector3; private _tempVec2: Vector3; @@ -578,16 +380,13 @@ export class EffectSolidParticleSystem extends SolidParticleSystem implements IS constructor( name: string, scene: any, - initialConfig: IEmitterConfig, options?: { updatable?: boolean; isPickable?: boolean; enableDepthSort?: boolean; particleIntersection?: boolean; useModelMaterial?: boolean; - parentGroup?: TransformNode | null; transform?: { position: Vector3; rotation: Quaternion; scale: Vector3 } | null; - particleMesh?: Mesh | null; } ) { super(name, scene, options); @@ -595,34 +394,11 @@ export class EffectSolidParticleSystem extends SolidParticleSystem implements IS this.name = name; this._behaviors = []; this.particleEmitterType = null; - this.isLooping = initialConfig.looping !== false; - this.duration = initialConfig.duration || 5; - this.prewarm = initialConfig.prewarm || false; - this.shape = initialConfig.shape; - this.startLife = initialConfig.startLife; - this.startSpeed = initialConfig.startSpeed; - this.startRotation = initialConfig.startRotation; - this.startSize = initialConfig.startSize; - this.startColor = initialConfig.startColor; - this.emissionOverTime = initialConfig.emissionOverTime; - this.emissionOverDistance = initialConfig.emissionOverDistance; - this.emissionBursts = initialConfig.emissionBursts; - this.onlyUsedByOther = initialConfig.onlyUsedByOther || false; - this.instancingGeometry = initialConfig.instancingGeometry; - this.renderOrder = initialConfig.renderOrder; - this.rendererEmitterSettings = initialConfig.rendererEmitterSettings; - this.material = initialConfig.material; - this.layers = initialConfig.layers; - this.isBillboardBased = initialConfig.isBillboardBased; - this.startTileIndex = initialConfig.startTileIndex; - this.uTileCount = initialConfig.uTileCount; - this.vTileCount = initialConfig.vTileCount; - this.blendTiles = initialConfig.blendTiles; - this.softParticles = initialConfig.softParticles || false; - this.softFarFade = initialConfig.softFarFade; - this.softNearFade = initialConfig.softNearFade; - this.worldSpace = initialConfig.worldSpace || false; + this.onlyUsedByOther = false; + this.softParticles = false; + this._emitter = null; + // Gradient systems for "OverLife" behaviors this._colorGradients = new ColorGradientSystem(); this._sizeGradients = new NumberGradientSystem(); this._velocityGradients = new NumberGradientSystem(); @@ -630,11 +406,14 @@ export class EffectSolidParticleSystem extends SolidParticleSystem implements IS this._limitVelocityGradients = new NumberGradientSystem(); this._limitVelocityDamping = 0.1; - this.behaviorConfigs = this._createBehaviorConfigsProxy(initialConfig.behaviors || []); + // Gradients for PiecewiseBezier (like ParticleSystem) + this._startSizeGradients = new NumberGradientSystem(); + this._lifeTimeGradients = new NumberGradientSystem(); + this._emitRateGradients = new NumberGradientSystem(); - this._updateBehaviorFunctions(); + this._behaviorConfigs = []; + this._behaviors = []; - this._parent = options?.parentGroup ?? null; this._transform = options?.transform ?? null; this._emitEnded = false; this._normalMatrix = new Matrix(); @@ -654,10 +433,6 @@ export class EffectSolidParticleSystem extends SolidParticleSystem implements IS burstParticleCount: 0, isBursting: false, }; - - if (options?.particleMesh) { - this._initializeMesh(options.particleMesh); - } } /** @@ -702,22 +477,11 @@ export class EffectSolidParticleSystem extends SolidParticleSystem implements IS */ private _initializeParticleColor(particle: SolidParticle): void { const props = particle.props!; - - if (this.startColor !== undefined) { - const startColor = ValueUtils.parseConstantColor(this.startColor); - props.startColor = startColor.clone(); - if (particle.color) { - particle.color.copyFrom(startColor); - } else { - particle.color = startColor.clone(); - } + props.startColor = this.color1.clone(); + if (particle.color) { + particle.color.copyFrom(this.color1); } else { - if (!particle.color) { - particle.color = new Color4(1, 1, 1, 1); - } else { - particle.color.set(1, 1, 1, 1); - } - props.startColor = particle.color.clone(); + particle.color = this.color1.clone(); } } @@ -726,22 +490,38 @@ export class EffectSolidParticleSystem extends SolidParticleSystem implements IS */ private _initializeParticleSpeed(particle: SolidParticle, normalizedTime: number): void { const props = particle.props!; - if (this.startSpeed !== undefined) { - props.startSpeed = ValueUtils.parseValue(this.startSpeed, normalizedTime); + // Use min/max or gradient + let speedValue: number; + const emitRateGradients = this._emitRateGradients.getGradients(); + if (emitRateGradients.length > 0 && this.targetStopDuration > 0) { + const ratio = Math.max(0, Math.min(1, normalizedTime)); + const gradientValue = this._emitRateGradients.getValue(ratio); + if (gradientValue !== null) { + speedValue = gradientValue; + } else { + speedValue = this._randomRange(this.minEmitPower, this.maxEmitPower); + } } else { - props.startSpeed = 0; + speedValue = this._randomRange(this.minEmitPower, this.maxEmitPower); } + props.startSpeed = speedValue; } /** * Initialize particle lifetime */ private _initializeParticleLife(particle: SolidParticle, normalizedTime: number): void { - if (this.startLife !== undefined) { - particle.lifeTime = ValueUtils.parseValue(this.startLife, normalizedTime); - } else { - particle.lifeTime = 1; + // Use min/max or gradient + const lifeTimeGradients = this._lifeTimeGradients.getGradients(); + if (lifeTimeGradients.length > 0 && this.targetStopDuration > 0) { + const ratio = Math.max(0, Math.min(1, normalizedTime)); + const gradientValue = this._lifeTimeGradients.getValue(ratio); + if (gradientValue !== null) { + particle.lifeTime = gradientValue; + return; + } } + particle.lifeTime = this._randomRange(this.minLifeTime, this.maxLifeTime); } /** @@ -749,89 +529,38 @@ export class EffectSolidParticleSystem extends SolidParticleSystem implements IS */ private _initializeParticleSize(particle: SolidParticle, normalizedTime: number): void { const props = particle.props!; - if (this.startSize !== undefined) { - const sizeValue = ValueUtils.parseValue(this.startSize, normalizedTime); - props.startSize = sizeValue; - particle.scaling.setAll(sizeValue); + // Use min/max or gradient + let sizeValue: number; + const startSizeGradients = this._startSizeGradients.getGradients(); + if (startSizeGradients.length > 0 && this.targetStopDuration > 0) { + const ratio = Math.max(0, Math.min(1, normalizedTime)); + const gradientValue = this._startSizeGradients.getValue(ratio); + if (gradientValue !== null) { + sizeValue = gradientValue; + } else { + sizeValue = this._randomRange(this.minSize, this.maxSize); + } } else { - props.startSize = 1; - particle.scaling.setAll(1); + sizeValue = this._randomRange(this.minSize, this.maxSize); } + props.startSize = sizeValue; + particle.scaling.setAll(sizeValue); } /** - * Initialize particle rotation - * Supports Euler, AxisAngle, and RandomQuat rotation types + * Random range helper */ - private _initializeParticleRotation(particle: SolidParticle, normalizedTime: number): void { - if (!this.startRotation) { - particle.rotation.setAll(0); - return; - } - - if ( - typeof this.startRotation === "number" || - (typeof this.startRotation === "object" && - "type" in this.startRotation && - (this.startRotation.type === "ConstantValue" || this.startRotation.type === "IntervalValue" || this.startRotation.type === "PiecewiseBezier")) - ) { - const angleZ = ValueUtils.parseValue(this.startRotation as Value, normalizedTime); - particle.rotation.set(0, 0, angleZ); - return; - } - - if (this.startRotation.type === "Euler") { - const angleX = this.startRotation.angleX ? ValueUtils.parseValue(this.startRotation.angleX, normalizedTime) : 0; - const angleY = this.startRotation.angleY ? ValueUtils.parseValue(this.startRotation.angleY, normalizedTime) : 0; - const angleZ = this.startRotation.angleZ ? ValueUtils.parseValue(this.startRotation.angleZ, normalizedTime) : 0; - const order = this.startRotation.order || "xyz"; - - let quat: Quaternion; - if (order === "xyz") { - quat = Quaternion.RotationYawPitchRoll(angleY, angleX, angleZ); - } else { - const quatZ = Quaternion.RotationAxis(Vector3.Forward(), angleZ); - const quatY = Quaternion.RotationAxis(Vector3.Up(), angleY); - const quatX = Quaternion.RotationAxis(Vector3.Right(), angleX); - quat = quatZ.multiply(quatY).multiply(quatX); - } - const euler = quat.toEulerAngles(); - particle.rotation.set(euler.x, euler.y, euler.z); - return; - } - - if (this.startRotation.type === "AxisAngle") { - const axisX = this.startRotation.x ? ValueUtils.parseValue(this.startRotation.x, normalizedTime) : 0; - const axisY = this.startRotation.y ? ValueUtils.parseValue(this.startRotation.y, normalizedTime) : 0; - const axisZ = this.startRotation.z ? ValueUtils.parseValue(this.startRotation.z, normalizedTime) : 1; - const angle = this.startRotation.angle ? ValueUtils.parseValue(this.startRotation.angle, normalizedTime) : 0; - - const axis = new Vector3(axisX, axisY, axisZ); - axis.normalize(); - const quat = Quaternion.RotationAxis(axis, angle); - const euler = quat.toEulerAngles(); - particle.rotation.set(euler.x, euler.y, euler.z); - return; - } - - if (this.startRotation.type === "RandomQuat") { - const u1 = Math.random(); - const u2 = Math.random(); - const u3 = Math.random(); - const sqrt1MinusU1 = Math.sqrt(1 - u1); - const sqrtU1 = Math.sqrt(u1); - const quat = new Quaternion( - sqrt1MinusU1 * Math.sin(2 * Math.PI * u2), - sqrt1MinusU1 * Math.cos(2 * Math.PI * u2), - sqrtU1 * Math.sin(2 * Math.PI * u3), - sqrtU1 * Math.cos(2 * Math.PI * u3) - ); - const euler = quat.toEulerAngles(); - particle.rotation.set(euler.x, euler.y, euler.z); - return; - } + private _randomRange(min: number, max: number): number { + return min + Math.random() * (max - min); + } - particle.rotation.setAll(0); + /** + * Initialize particle rotation + * Uses minInitialRotation/maxInitialRotation (like ParticleSystem) + */ + private _initializeParticleRotation(particle: SolidParticle, _normalizedTime: number): void { + const angleZ = this._randomRange(this.minInitialRotation, this.maxInitialRotation); + particle.rotation.set(0, 0, angleZ); } /** @@ -851,7 +580,7 @@ export class EffectSolidParticleSystem extends SolidParticleSystem implements IS emitterMatrix.decompose(scale, quaternion, translation); emitterMatrix.toNormalMatrix(this._normalMatrix); - const normalizedTime = this.duration > 0 ? this._emissionState.time / this.duration : 0; + const normalizedTime = this.targetStopDuration > 0 ? this._emissionState.time / this.targetStopDuration : 0; for (let i = 0; i < count; i++) { emissionState.burstParticleIndex = i; @@ -912,6 +641,38 @@ export class EffectSolidParticleSystem extends SolidParticleSystem implements IS return emitter; } + /** + * Configure emitter from shape config + * This replaces the need for EmitterFactory + */ + public configureEmitterFromShape(shape: any): void { + if (!shape || !shape.type) { + this.createPointEmitter(); + return; + } + + const shapeType = shape.type.toLowerCase(); + const radius = shape.radius ?? 1; + const arc = shape.arc ?? Math.PI * 2; + const thickness = shape.thickness ?? 1; + const angle = shape.angle ?? Math.PI / 6; + + switch (shapeType) { + case "sphere": + this.createSphereEmitter(radius, arc, thickness); + break; + case "cone": + this.createConeEmitter(radius, arc, thickness, angle); + break; + case "point": + this.createPointEmitter(); + break; + default: + this.createPointEmitter(); + break; + } + } + private _getEmitterMatrix(): Matrix { const matrix = Matrix.Identity(); if (this.mesh) { @@ -923,12 +684,16 @@ export class EffectSolidParticleSystem extends SolidParticleSystem implements IS private _handleEmissionLooping(): void { const emissionState = this._emissionState; + const isLooping = this.targetStopDuration === 0; + const duration = isLooping ? 5 : this.targetStopDuration; // Use default 5s for looping - if (emissionState.time > this.duration) { - if (this.isLooping) { - emissionState.time -= this.duration; + if (emissionState.time > duration) { + if (isLooping) { + // Loop: reset time and burst index + emissionState.time -= duration; emissionState.burstIndex = 0; } else if (!this._emitEnded) { + // Not looping: end emission this._emitEnded = true; } } @@ -968,7 +733,17 @@ export class EffectSolidParticleSystem extends SolidParticleSystem implements IS return; } - const emissionRate = this.emissionOverTime !== undefined ? ValueUtils.parseConstantValue(this.emissionOverTime) : 10; + // Get emit rate (use gradient if available) + let emissionRate = this.emitRate; + const emitRateGradients = this._emitRateGradients.getGradients(); + if (emitRateGradients.length > 0 && this.targetStopDuration > 0) { + const normalizedTime = this.targetStopDuration > 0 ? this._emissionState.time / this.targetStopDuration : 0; + const ratio = Math.max(0, Math.min(1, normalizedTime)); + const gradientValue = this._emitRateGradients.getValue(ratio); + if (gradientValue !== null) { + emissionRate = gradientValue; + } + } emissionState.waitEmiting += delta * emissionRate; if (this.emissionOverDistance !== undefined && this.mesh && this.mesh.position) { @@ -1030,8 +805,9 @@ export class EffectSolidParticleSystem extends SolidParticleSystem implements IS this.mesh.layerMask = this.layers; } - if (this._parent) { - this.mesh.setParent(this._parent, false, true); + // Emitter is the point from which particles emit (like ParticleSystem.emitter) + if (this._emitter) { + this.mesh.setParent(this._emitter, false, true); } if (this._transform) { @@ -1082,96 +858,11 @@ export class EffectSolidParticleSystem extends SolidParticleSystem implements IS } /** - * Create a proxy array that automatically updates behavior functions when configs change - */ - private _createBehaviorConfigsProxy(configs: Behavior[]): Behavior[] { - const self = this; - - const wrapBehavior = (behavior: Behavior): Behavior => { - return new Proxy(behavior, { - set(target, prop, value) { - const result = Reflect.set(target, prop, value); - self._updateBehaviorFunctions(); - return result; - }, - }); - }; - - const wrappedConfigs = configs.map(wrapBehavior); - - return new Proxy(wrappedConfigs, { - set(target, property, value) { - const result = Reflect.set(target, property, value); - - if (property === "length" || typeof property === "number") { - if (typeof property === "number" && value && typeof value === "object") { - Reflect.set(target, property, wrapBehavior(value as Behavior)); - } - self._updateBehaviorFunctions(); - } - - return result; - }, - - get(target, property) { - const value = Reflect.get(target, property); - - if ( - typeof value === "function" && - (property === "push" || - property === "pop" || - property === "splice" || - property === "shift" || - property === "unshift" || - property === "sort" || - property === "reverse") - ) { - return function (...args: any[]) { - const result = value.apply(target, args); - if (property === "push" || property === "unshift") { - for (let i = 0; i < args.length; i++) { - if (args[i] && typeof args[i] === "object") { - const index = property === "push" ? target.length - args.length + i : i; - Reflect.set(target, index, wrapBehavior(args[i] as Behavior)); - } - } - } - self._updateBehaviorFunctions(); - return result; - }; - } - - return value; - }, - }); - } - - /** - * Update behavior functions from configs - * Internal method, called automatically when configs change - */ - /** - * Update behavior functions from configs - * Applies both system-level behaviors (gradients) and per-particle behaviors - */ - private _updateBehaviorFunctions(): void { - this._colorGradients.clear(); - this._sizeGradients.clear(); - this._velocityGradients.clear(); - this._angularSpeedGradients.clear(); - this._limitVelocityGradients.clear(); - - this._applySystemLevelBehaviors(); - - this._behaviors = this._createPerParticleBehaviorFunctions(this.behaviorConfigs); - } - - /** - * Create per-particle behavior functions from configurations - * Only creates functions for behaviors that depend on particle properties (speed, orbit, force) + * Build per-particle behavior functions from configurations + * Per-particle behaviors run each frame for each particle * "OverLife" behaviors are handled by gradients (system-level) */ - private _createPerParticleBehaviorFunctions(behaviors: Behavior[]): PerSolidParticleBehaviorFunction[] { + private _buildPerParticleBehaviors(behaviors: Behavior[]): PerSolidParticleBehaviorFunction[] { const functions: PerSolidParticleBehaviorFunction[] = []; for (const behavior of behaviors) { diff --git a/tools/src/effect/types/emitter.ts b/tools/src/effect/types/emitter.ts index f93cd279d..0584c7e84 100644 --- a/tools/src/effect/types/emitter.ts +++ b/tools/src/effect/types/emitter.ts @@ -1,8 +1,6 @@ import { Nullable, SolidParticle, TransformNode, Vector3 } from "babylonjs"; import type { IEmitter } from "./hierarchy"; import type { Value } from "./values"; -import type { Color } from "./colors"; -import type { Rotation } from "./rotations"; import type { IShape } from "./shapes"; import type { Behavior } from "./behaviors"; @@ -15,27 +13,71 @@ export interface IEmissionBurst { } /** - * particle emitter configuration (converted from Quarks) + * Particle system configuration (converted from Quarks to native Babylon.js properties) */ -export interface IEmitterConfig { +export interface IParticleSystemConfig { version?: string; - autoDestroy?: boolean; - looping?: boolean; - prewarm?: boolean; - duration?: number; + systemType: "solid" | "base"; + + // === Native Babylon.js properties (converted from Quarks Value) === + + // Life & Size + minLifeTime?: number; + maxLifeTime?: number; + minSize?: number; + maxSize?: number; + minScaleX?: number; + maxScaleX?: number; + minScaleY?: number; + maxScaleY?: number; + + // Speed & Power + minEmitPower?: number; + maxEmitPower?: number; + emitRate?: number; + + // Rotation + minInitialRotation?: number; + maxInitialRotation?: number; + minAngularSpeed?: number; + maxAngularSpeed?: number; + + // Color + color1?: import("babylonjs").Color4; + color2?: import("babylonjs").Color4; + colorDead?: import("babylonjs").Color4; + + // Duration & Looping + targetStopDuration?: number; // 0 = infinite (looping), >0 = duration + manualEmitCount?: number; // -1 = automatic, otherwise specific count + + // Prewarm + preWarmCycles?: number; + preWarmStepOffset?: number; + + // Physics + gravity?: import("babylonjs").Vector3; + noiseStrength?: import("babylonjs").Vector3; + updateSpeed?: number; + + // World space + isLocal?: boolean; + + // Auto destroy + disposeOnStop?: boolean; + + // Gradients for PiecewiseBezier + startSizeGradients?: Array<{ gradient: number; factor: number; factor2?: number }>; + lifeTimeGradients?: Array<{ gradient: number; factor: number; factor2?: number }>; + emitRateGradients?: Array<{ gradient: number; factor: number; factor2?: number }>; + + // === Other properties === shape?: IShape; - startLife?: Value; - startSpeed?: Value; - startRotation?: Rotation; - startSize?: Value; - startColor?: Color; - emissionOverTime?: Value; - emissionOverDistance?: Value; emissionBursts?: IEmissionBurst[]; + emissionOverDistance?: Value; // For solid system only onlyUsedByOther?: boolean; instancingGeometry?: string; renderOrder?: number; - systemType: "solid" | "base"; rendererEmitterSettings?: Record; material?: string; layers?: number; @@ -49,7 +91,6 @@ export interface IEmitterConfig { softFarFade?: number; softNearFade?: number; behaviors?: Behavior[]; - worldSpace?: boolean; } /** @@ -57,13 +98,13 @@ export interface IEmitterConfig { */ export interface IEmitterData { name: string; - config: IEmitterConfig; + config: IParticleSystemConfig; materialId?: string; matrix?: number[]; position?: number[]; parentGroup: Nullable; cumulativeScale: Vector3; - Emitter?: IEmitter; + emitter?: IEmitter; } /** diff --git a/tools/src/effect/types/hierarchy.ts b/tools/src/effect/types/hierarchy.ts index a7a2d128e..cace99240 100644 --- a/tools/src/effect/types/hierarchy.ts +++ b/tools/src/effect/types/hierarchy.ts @@ -1,5 +1,5 @@ import { Vector3, Quaternion } from "babylonjs"; -import type { IEmitterConfig } from "./emitter"; +import type { IParticleSystemConfig } from "./emitter"; import type { IMaterial, ITexture, IImage, IGeometry } from "./resources"; /** @@ -28,7 +28,7 @@ export interface IEmitter { uuid: string; name: string; transform: ITransform; - config: IEmitterConfig; + config: IParticleSystemConfig; materialId?: string; parentUuid?: string; systemType: "solid" | "base"; // Determined from renderMode: 2 = solid, otherwise base From 75075eddb50774af4fc20f70b5101f862c8854f1 Mon Sep 17 00:00:00 2001 From: Mikalai Lazitski Date: Thu, 18 Dec 2025 09:40:54 +0300 Subject: [PATCH 43/62] refactor: remove deprecated particle system files to streamline the effect system and enhance maintainability --- tools/src/effect/bjs/particle.ts | 352 --- tools/src/effect/bjs/particleHelper.ts | 230 -- .../effect/bjs/particleSystem.functions.ts | 95 - tools/src/effect/bjs/particleSystem.ts | 1269 --------- tools/src/effect/bjs/subEmitter.ts | 134 - .../effect/bjs/thinParticleSystem.function.ts | 427 --- tools/src/effect/bjs/thinParticleSystem.ts | 2422 ----------------- 7 files changed, 4929 deletions(-) delete mode 100644 tools/src/effect/bjs/particle.ts delete mode 100644 tools/src/effect/bjs/particleHelper.ts delete mode 100644 tools/src/effect/bjs/particleSystem.functions.ts delete mode 100644 tools/src/effect/bjs/particleSystem.ts delete mode 100644 tools/src/effect/bjs/subEmitter.ts delete mode 100644 tools/src/effect/bjs/thinParticleSystem.function.ts delete mode 100644 tools/src/effect/bjs/thinParticleSystem.ts diff --git a/tools/src/effect/bjs/particle.ts b/tools/src/effect/bjs/particle.ts deleted file mode 100644 index fe8158791..000000000 --- a/tools/src/effect/bjs/particle.ts +++ /dev/null @@ -1,352 +0,0 @@ -import type { Nullable } from "../types"; -import { Vector2, Vector3, TmpVectors, Vector4 } from "../Maths/math.vector"; -import { Color4 } from "../Maths/math.color"; -import type { SubEmitter } from "./subEmitter"; -import type { ColorGradient, FactorGradient } from "../Misc/gradients"; - -import type { AbstractMesh } from "../Meshes/abstractMesh"; -import type { ThinParticleSystem } from "./thinParticleSystem"; -import { Clamp } from "../Maths/math.scalar.functions"; - -/** - * A particle represents one of the element emitted by a particle system. - * This is mainly define by its coordinates, direction, velocity and age. - */ -export class Particle { - private static _Count = 0; - /** - * Unique ID of the particle - */ - public id: number; - /** - * The world position of the particle in the scene. - */ - public position = Vector3.Zero(); - - /** - * The world direction of the particle in the scene. - */ - public direction = Vector3.Zero(); - - /** - * The color of the particle. - */ - public color = new Color4(0, 0, 0, 0); - - /** - * The color change of the particle per step. - */ - public colorStep = new Color4(0, 0, 0, 0); - - /** - * The creation color of the particle. - */ - public initialColor = new Color4(0, 0, 0, 0); - - /** - * The color used when the end of life of the particle. - */ - public colorDead = new Color4(0, 0, 0, 0); - - /** - * Defines how long will the life of the particle be. - */ - public lifeTime = 1.0; - - /** - * The current age of the particle. - */ - public age = 0; - - /** - * The current size of the particle. - */ - public size = 0; - - /** - * The current scale of the particle. - */ - public scale = new Vector2(1, 1); - - /** - * The current angle of the particle. - */ - public angle = 0; - - /** - * Defines how fast is the angle changing. - */ - public angularSpeed = 0; - - /** - * Defines the cell index used by the particle to be rendered from a sprite. - */ - public cellIndex: number = 0; - - /** - * The information required to support color remapping - */ - public remapData: Vector4; - - /** @internal */ - public _randomCellOffset?: number; - - /** @internal */ - public _initialDirection: Nullable; - - /** @internal */ - public _attachedSubEmitters: Nullable> = null; - - /** @internal */ - public _initialStartSpriteCellId: number; - /** @internal */ - public _initialEndSpriteCellId: number; - /** @internal */ - public _initialSpriteCellLoop: boolean; - - /** @internal */ - public _currentColorGradient: Nullable; - /** @internal */ - public _currentColor1 = new Color4(0, 0, 0, 0); - /** @internal */ - public _currentColor2 = new Color4(0, 0, 0, 0); - - /** @internal */ - public _currentSizeGradient: Nullable; - /** @internal */ - public _currentSize1 = 0; - /** @internal */ - public _currentSize2 = 0; - - /** @internal */ - public _currentAngularSpeedGradient: Nullable; - /** @internal */ - public _currentAngularSpeed1 = 0; - /** @internal */ - public _currentAngularSpeed2 = 0; - - /** @internal */ - public _currentVelocityGradient: Nullable; - /** @internal */ - public _currentVelocity1 = 0; - /** @internal */ - public _currentVelocity2 = 0; - - /** @internal */ - public _directionScale: number; - - /** @internal */ - public _scaledDirection = Vector3.Zero(); - - /** @internal */ - public _currentLimitVelocityGradient: Nullable; - /** @internal */ - public _currentLimitVelocity1 = 0; - /** @internal */ - public _currentLimitVelocity2 = 0; - - /** @internal */ - public _currentDragGradient: Nullable; - /** @internal */ - public _currentDrag1 = 0; - /** @internal */ - public _currentDrag2 = 0; - - /** @internal */ - public _randomNoiseCoordinates1: Nullable; - /** @internal */ - public _randomNoiseCoordinates2: Nullable; - - /** @internal */ - public _localPosition?: Vector3; - - /** - * Creates a new instance Particle - * @param particleSystem the particle system the particle belongs to - */ - constructor( - /** - * The particle system the particle belongs to. - */ - public particleSystem: ThinParticleSystem - ) { - this.id = Particle._Count++; - if (!this.particleSystem.isAnimationSheetEnabled) { - return; - } - - this._updateCellInfoFromSystem(); - } - - private _updateCellInfoFromSystem(): void { - this.cellIndex = this.particleSystem.startSpriteCellID; - } - - /** - * Defines how the sprite cell index is updated for the particle - */ - public updateCellIndex(): void { - let offsetAge = this.age; - let changeSpeed = this.particleSystem.spriteCellChangeSpeed; - - if (this.particleSystem.spriteRandomStartCell) { - if (this._randomCellOffset === undefined) { - this._randomCellOffset = Math.random() * this.lifeTime; - } - - if (changeSpeed === 0) { - // Special case when speed = 0 meaning we want to stay on initial cell - changeSpeed = 1; - offsetAge = this._randomCellOffset; - } else { - offsetAge += this._randomCellOffset; - } - } - - const dist = this._initialEndSpriteCellId - this._initialStartSpriteCellId + 1; - let ratio: number; - if (this._initialSpriteCellLoop) { - ratio = Clamp(((offsetAge * changeSpeed) % this.lifeTime) / this.lifeTime); - } else { - ratio = Clamp((offsetAge * changeSpeed) / this.lifeTime); - } - this.cellIndex = (this._initialStartSpriteCellId + ratio * dist) | 0; - } - - /** - * @internal - */ - public _inheritParticleInfoToSubEmitter(subEmitter: SubEmitter) { - if ((subEmitter.particleSystem.emitter).position) { - const emitterMesh = subEmitter.particleSystem.emitter; - emitterMesh.position.copyFrom(this.position); - if (subEmitter.inheritDirection) { - const temp = TmpVectors.Vector3[0]; - this.direction.normalizeToRef(temp); - emitterMesh.setDirection(temp, 0, Math.PI / 2); - } - } else { - const emitterPosition = subEmitter.particleSystem.emitter; - emitterPosition.copyFrom(this.position); - } - // Set inheritedVelocityOffset to be used when new particles are created - this.direction.scaleToRef(subEmitter.inheritedVelocityAmount / 2, TmpVectors.Vector3[0]); - subEmitter.particleSystem._inheritedVelocityOffset.copyFrom(TmpVectors.Vector3[0]); - } - - /** @internal */ - public _inheritParticleInfoToSubEmitters() { - if (this._attachedSubEmitters && this._attachedSubEmitters.length > 0) { - for (const subEmitter of this._attachedSubEmitters) { - this._inheritParticleInfoToSubEmitter(subEmitter); - } - } - } - - /** @internal */ - public _reset() { - this.age = 0; - this.id = Particle._Count++; - this._currentColorGradient = null; - this._currentSizeGradient = null; - this._currentAngularSpeedGradient = null; - this._currentVelocityGradient = null; - this._currentLimitVelocityGradient = null; - this._currentDragGradient = null; - this.cellIndex = this.particleSystem.startSpriteCellID; - this._randomCellOffset = undefined; - this._randomNoiseCoordinates1 = null; - this._randomNoiseCoordinates2 = null; - } - - /** - * Copy the properties of particle to another one. - * @param other the particle to copy the information to. - */ - public copyTo(other: Particle) { - other.position.copyFrom(this.position); - if (this._initialDirection) { - if (other._initialDirection) { - other._initialDirection.copyFrom(this._initialDirection); - } else { - other._initialDirection = this._initialDirection.clone(); - } - } else { - other._initialDirection = null; - } - other.direction.copyFrom(this.direction); - if (this._localPosition) { - if (other._localPosition) { - other._localPosition.copyFrom(this._localPosition); - } else { - other._localPosition = this._localPosition.clone(); - } - } - other.color.copyFrom(this.color); - other.colorStep.copyFrom(this.colorStep); - other.initialColor.copyFrom(this.initialColor); - other.colorDead.copyFrom(this.colorDead); - other.lifeTime = this.lifeTime; - other.age = this.age; - other._randomCellOffset = this._randomCellOffset; - other.size = this.size; - other.scale.copyFrom(this.scale); - other.angle = this.angle; - other.angularSpeed = this.angularSpeed; - other.particleSystem = this.particleSystem; - other.cellIndex = this.cellIndex; - other.id = this.id; - other._attachedSubEmitters = this._attachedSubEmitters; - if (this._currentColorGradient) { - other._currentColorGradient = this._currentColorGradient; - other._currentColor1.copyFrom(this._currentColor1); - other._currentColor2.copyFrom(this._currentColor2); - } - if (this._currentSizeGradient) { - other._currentSizeGradient = this._currentSizeGradient; - other._currentSize1 = this._currentSize1; - other._currentSize2 = this._currentSize2; - } - if (this._currentAngularSpeedGradient) { - other._currentAngularSpeedGradient = this._currentAngularSpeedGradient; - other._currentAngularSpeed1 = this._currentAngularSpeed1; - other._currentAngularSpeed2 = this._currentAngularSpeed2; - } - if (this._currentVelocityGradient) { - other._currentVelocityGradient = this._currentVelocityGradient; - other._currentVelocity1 = this._currentVelocity1; - other._currentVelocity2 = this._currentVelocity2; - } - if (this._currentLimitVelocityGradient) { - other._currentLimitVelocityGradient = this._currentLimitVelocityGradient; - other._currentLimitVelocity1 = this._currentLimitVelocity1; - other._currentLimitVelocity2 = this._currentLimitVelocity2; - } - if (this._currentDragGradient) { - other._currentDragGradient = this._currentDragGradient; - other._currentDrag1 = this._currentDrag1; - other._currentDrag2 = this._currentDrag2; - } - if (this.particleSystem.isAnimationSheetEnabled) { - other._initialStartSpriteCellId = this._initialStartSpriteCellId; - other._initialEndSpriteCellId = this._initialEndSpriteCellId; - other._initialSpriteCellLoop = this._initialSpriteCellLoop; - } - if (this.particleSystem.useRampGradients) { - if (other.remapData && this.remapData) { - other.remapData.copyFrom(this.remapData); - } else { - other.remapData = new Vector4(0, 0, 0, 0); - } - } - if (this._randomNoiseCoordinates1 && this._randomNoiseCoordinates2) { - if (other._randomNoiseCoordinates1 && other._randomNoiseCoordinates2) { - other._randomNoiseCoordinates1.copyFrom(this._randomNoiseCoordinates1); - other._randomNoiseCoordinates2.copyFrom(this._randomNoiseCoordinates2); - } else { - other._randomNoiseCoordinates1 = this._randomNoiseCoordinates1.clone(); - other._randomNoiseCoordinates2 = this._randomNoiseCoordinates2.clone(); - } - } - } -} diff --git a/tools/src/effect/bjs/particleHelper.ts b/tools/src/effect/bjs/particleHelper.ts deleted file mode 100644 index 1b66ddb45..000000000 --- a/tools/src/effect/bjs/particleHelper.ts +++ /dev/null @@ -1,230 +0,0 @@ -import type { Nullable } from "../types"; -import type { Scene } from "../scene"; -import { Tools } from "../Misc/tools"; -import type { Vector3 } from "../Maths/math.vector"; -import { Color4 } from "../Maths/math.color"; -import type { AbstractMesh } from "../Meshes/abstractMesh"; -import { Texture } from "../Materials/Textures/texture"; -import { EngineStore } from "../Engines/engineStore"; -import type { IParticleSystem } from "./IParticleSystem"; -import { GPUParticleSystem } from "./gpuParticleSystem"; -import { ParticleSystemSet } from "./particleSystemSet"; -import { ParticleSystem } from "./particleSystem"; -import { WebRequest } from "../Misc/webRequest"; -import { Constants } from "../Engines/constants"; -/** - * This class is made for on one-liner static method to help creating particle system set. - */ -export class ParticleHelper { - /** - * Gets or sets base Assets URL - */ - public static BaseAssetsUrl = ParticleSystemSet.BaseAssetsUrl; - - /** Define the Url to load snippets */ - public static SnippetUrl = Constants.SnippetUrl; - - /** - * Create a default particle system that you can tweak - * @param emitter defines the emitter to use - * @param capacity defines the system capacity (default is 500 particles) - * @param scene defines the hosting scene - * @param useGPU defines if a GPUParticleSystem must be created (default is false) - * @returns the new Particle system - */ - public static CreateDefault(emitter: Nullable, capacity = 500, scene?: Scene, useGPU = false): IParticleSystem { - let system: IParticleSystem; - - if (useGPU) { - system = new GPUParticleSystem("default system", { capacity: capacity }, scene!); - } else { - system = new ParticleSystem("default system", capacity, scene!); - } - - system.emitter = emitter; - const textureUrl = Tools.GetAssetUrl("https://assets.babylonjs.com/core/textures/flare.png"); - system.particleTexture = new Texture(textureUrl, system.getScene()); - system.createConeEmitter(0.1, Math.PI / 4); - - // Particle color - system.color1 = new Color4(1.0, 1.0, 1.0, 1.0); - system.color2 = new Color4(1.0, 1.0, 1.0, 1.0); - system.colorDead = new Color4(1.0, 1.0, 1.0, 0.0); - - // Particle Size - system.minSize = 0.1; - system.maxSize = 0.1; - - // Emission speed - system.minEmitPower = 2; - system.maxEmitPower = 2; - - // Update speed - system.updateSpeed = 1 / 60; - - system.emitRate = 30; - - return system; - } - - /** - * This is the main static method (one-liner) of this helper to create different particle systems - * @param type This string represents the type to the particle system to create - * @param scene The scene where the particle system should live - * @param gpu If the system will use gpu - * @param capacity defines the system capacity (if null or undefined the sotred capacity will be used) - * @returns the ParticleSystemSet created - */ - // eslint-disable-next-line @typescript-eslint/promise-function-async, no-restricted-syntax - public static CreateAsync(type: string, scene: Nullable, gpu: boolean = false, capacity?: number): Promise { - if (!scene) { - scene = EngineStore.LastCreatedScene; - } - - const token = {}; - - scene!.addPendingData(token); - - return new Promise((resolve, reject) => { - if (gpu && !GPUParticleSystem.IsSupported) { - scene!.removePendingData(token); - // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors - return reject("Particle system with GPU is not supported."); - } - - Tools.LoadFile( - `${ParticleHelper.BaseAssetsUrl}/systems/${type}.json`, - (data) => { - scene!.removePendingData(token); - const newData = JSON.parse(data.toString()); - return resolve(ParticleSystemSet.Parse(newData, scene!, gpu, capacity)); - }, - undefined, - undefined, - undefined, - () => { - scene!.removePendingData(token); - // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors - return reject(`An error occurred with the creation of your particle system. Check if your type '${type}' exists.`); - } - ); - }); - } - - /** - * Static function used to export a particle system to a ParticleSystemSet variable. - * Please note that the emitter shape is not exported - * @param systems defines the particle systems to export - * @returns the created particle system set - */ - public static ExportSet(systems: IParticleSystem[]): ParticleSystemSet { - const set = new ParticleSystemSet(); - - for (const system of systems) { - set.systems.push(system); - } - - return set; - } - - /** - * Creates a particle system from a snippet saved in a remote file - * @param name defines the name of the particle system to create (can be null or empty to use the one from the json data) - * @param url defines the url to load from - * @param scene defines the hosting scene - * @param gpu If the system will use gpu - * @param rootUrl defines the root URL to use to load textures and relative dependencies - * @param capacity defines the system capacity (if null or undefined the sotred capacity will be used) - * @returns a promise that will resolve to the new particle system - */ - // eslint-disable-next-line @typescript-eslint/promise-function-async, no-restricted-syntax - public static ParseFromFileAsync(name: Nullable, url: string, scene: Scene, gpu: boolean = false, rootUrl: string = "", capacity?: number): Promise { - return new Promise((resolve, reject) => { - const request = new WebRequest(); - request.addEventListener("readystatechange", () => { - if (request.readyState == 4) { - if (request.status == 200) { - const serializationObject = JSON.parse(request.responseText); - let output: IParticleSystem; - - if (gpu) { - output = GPUParticleSystem.Parse(serializationObject, scene, rootUrl, false, capacity); - } else { - output = ParticleSystem.Parse(serializationObject, scene, rootUrl, false, capacity); - } - - if (name) { - output.name = name; - } - - resolve(output); - } else { - // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors - reject("Unable to load the particle system"); - } - } - }); - - request.open("GET", url); - request.send(); - }); - } - - /** - * Creates a particle system from a snippet saved by the particle system editor - * @param snippetId defines the snippet to load (can be set to _BLANK to create a default one) - * @param scene defines the hosting scene - * @param gpu If the system will use gpu - * @param rootUrl defines the root URL to use to load textures and relative dependencies - * @param capacity defines the system capacity (if null or undefined the sotred capacity will be used) - * @returns a promise that will resolve to the new particle system - */ - // eslint-disable-next-line @typescript-eslint/promise-function-async, no-restricted-syntax - public static ParseFromSnippetAsync(snippetId: string, scene: Scene, gpu: boolean = false, rootUrl: string = "", capacity?: number): Promise { - if (snippetId === "_BLANK") { - const system = this.CreateDefault(null); - system.start(); - return Promise.resolve(system); - } - - return new Promise((resolve, reject) => { - const request = new WebRequest(); - request.addEventListener("readystatechange", () => { - if (request.readyState == 4) { - if (request.status == 200) { - const snippet = JSON.parse(JSON.parse(request.responseText).jsonPayload); - const serializationObject = JSON.parse(snippet.particleSystem); - let output: IParticleSystem; - - if (gpu) { - output = GPUParticleSystem.Parse(serializationObject, scene, rootUrl, false, capacity); - } else { - output = ParticleSystem.Parse(serializationObject, scene, rootUrl, false, capacity); - } - output.snippetId = snippetId; - - resolve(output); - } else { - // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors - reject("Unable to load the snippet " + snippetId); - } - } - }); - - request.open("GET", this.SnippetUrl + "/" + snippetId.replace(/#/g, "/")); - request.send(); - }); - } - - /** - * Creates a particle system from a snippet saved by the particle system editor - * @deprecated Please use ParseFromSnippetAsync instead - * @param snippetId defines the snippet to load (can be set to _BLANK to create a default one) - * @param scene defines the hosting scene - * @param gpu If the system will use gpu - * @param rootUrl defines the root URL to use to load textures and relative dependencies - * @param capacity defines the system capacity (if null or undefined the sotred capacity will be used) - * @returns a promise that will resolve to the new particle system - */ - public static CreateFromSnippetAsync = ParticleHelper.ParseFromSnippetAsync; -} diff --git a/tools/src/effect/bjs/particleSystem.functions.ts b/tools/src/effect/bjs/particleSystem.functions.ts deleted file mode 100644 index ff572e629..000000000 --- a/tools/src/effect/bjs/particleSystem.functions.ts +++ /dev/null @@ -1,95 +0,0 @@ -import { Vector3 } from "core/Maths/math.vector"; -import { PointParticleEmitter } from "./EmitterTypes/pointParticleEmitter"; -import { HemisphericParticleEmitter } from "./EmitterTypes/hemisphericParticleEmitter"; -import { SphereDirectedParticleEmitter, SphereParticleEmitter } from "./EmitterTypes/sphereParticleEmitter"; -import { CylinderDirectedParticleEmitter, CylinderParticleEmitter } from "./EmitterTypes/cylinderParticleEmitter"; -import { ConeDirectedParticleEmitter, ConeParticleEmitter } from "./EmitterTypes/coneParticleEmitter"; - -/** - * Creates a Point Emitter for the particle system (emits directly from the emitter position) - * @param direction1 Particles are emitted between the direction1 and direction2 from within the box - * @param direction2 Particles are emitted between the direction1 and direction2 from within the box - * @returns the emitter - */ -export function CreatePointEmitter(direction1: Vector3, direction2: Vector3): PointParticleEmitter { - const particleEmitter = new PointParticleEmitter(); - particleEmitter.direction1 = direction1; - particleEmitter.direction2 = direction2; - return particleEmitter; -} - -/** - * Creates a Hemisphere Emitter for the particle system (emits along the hemisphere radius) - * @param radius The radius of the hemisphere to emit from - * @param radiusRange The range of the hemisphere to emit from [0-1] 0 Surface Only, 1 Entire Radius - * @returns the emitter - */ -export function CreateHemisphericEmitter(radius = 1, radiusRange = 1): HemisphericParticleEmitter { - return new HemisphericParticleEmitter(radius, radiusRange); -} - -/** - * Creates a Sphere Emitter for the particle system (emits along the sphere radius) - * @param radius The radius of the sphere to emit from - * @param radiusRange The range of the sphere to emit from [0-1] 0 Surface Only, 1 Entire Radius - * @returns the emitter - */ -export function CreateSphereEmitter(radius = 1, radiusRange = 1): SphereParticleEmitter { - return new SphereParticleEmitter(radius, radiusRange); -} - -/** - * Creates a Directed Sphere Emitter for the particle system (emits between direction1 and direction2) - * @param radius The radius of the sphere to emit from - * @param direction1 Particles are emitted between the direction1 and direction2 from within the sphere - * @param direction2 Particles are emitted between the direction1 and direction2 from within the sphere - * @returns the emitter - */ -export function CreateDirectedSphereEmitter(radius = 1, direction1 = new Vector3(0, 1.0, 0), direction2 = new Vector3(0, 1.0, 0)): SphereDirectedParticleEmitter { - return new SphereDirectedParticleEmitter(radius, direction1, direction2); -} - -/** - * Creates a Cylinder Emitter for the particle system (emits from the cylinder to the particle position) - * @param radius The radius of the emission cylinder - * @param height The height of the emission cylinder - * @param radiusRange The range of emission [0-1] 0 Surface only, 1 Entire Radius - * @param directionRandomizer How much to randomize the particle direction [0-1] - * @returns the emitter - */ -export function CreateCylinderEmitter(radius = 1, height = 1, radiusRange = 1, directionRandomizer = 0): CylinderParticleEmitter { - return new CylinderParticleEmitter(radius, height, radiusRange, directionRandomizer); -} - -/** - * Creates a Directed Cylinder Emitter for the particle system (emits between direction1 and direction2) - * @param radius The radius of the cylinder to emit from - * @param height The height of the emission cylinder - * @param radiusRange the range of the emission cylinder [0-1] 0 Surface only, 1 Entire Radius (1 by default) - * @param direction1 Particles are emitted between the direction1 and direction2 from within the cylinder - * @param direction2 Particles are emitted between the direction1 and direction2 from within the cylinder - * @returns the emitter - */ -export function CreateDirectedCylinderEmitter( - radius = 1, - height = 1, - radiusRange = 1, - direction1 = new Vector3(0, 1.0, 0), - direction2 = new Vector3(0, 1.0, 0) -): CylinderDirectedParticleEmitter { - return new CylinderDirectedParticleEmitter(radius, height, radiusRange, direction1, direction2); -} - -/** - * Creates a Cone Emitter for the particle system (emits from the cone to the particle position) - * @param radius The radius of the cone to emit from - * @param angle The base angle of the cone - * @returns the emitter - */ -export function CreateConeEmitter(radius = 1, angle = Math.PI / 4): ConeParticleEmitter { - return new ConeParticleEmitter(radius, angle); -} - -export function CreateDirectedConeEmitter(radius = 1, angle = Math.PI / 4, direction1 = new Vector3(0, 1.0, 0), direction2 = new Vector3(0, 1.0, 0)): ConeDirectedParticleEmitter { - return new ConeDirectedParticleEmitter(radius, angle, direction1, direction2); -} diff --git a/tools/src/effect/bjs/particleSystem.ts b/tools/src/effect/bjs/particleSystem.ts deleted file mode 100644 index a2dc27657..000000000 --- a/tools/src/effect/bjs/particleSystem.ts +++ /dev/null @@ -1,1269 +0,0 @@ -import { ThinParticleSystem } from "./thinParticleSystem"; -import type { IParticleEmitterType } from "./EmitterTypes/IParticleEmitterType"; -import { SubEmitter, SubEmitterType } from "./subEmitter"; -import { Color3, Color4 } from "../Maths/math.color"; -import { Vector3 } from "../Maths/math.vector"; -import type { IParticleSystem } from "./IParticleSystem"; -import type { AbstractMesh } from "../Meshes/abstractMesh"; -import type { Nullable } from "../types"; -import type { Scene } from "../scene"; -import { AbstractEngine } from "../Engines/abstractEngine"; -import { GetClass } from "../Misc/typeStore"; -import type { BaseTexture } from "../Materials/Textures/baseTexture"; -import type { Effect } from "../Materials/effect"; -import type { Particle } from "./particle"; -import { Constants } from "../Engines/constants"; -import { SerializationHelper } from "../Misc/decorators.serialization"; -import { MeshParticleEmitter } from "./EmitterTypes/meshParticleEmitter"; -import { CustomParticleEmitter } from "./EmitterTypes/customParticleEmitter"; -import { BoxParticleEmitter } from "./EmitterTypes/boxParticleEmitter"; -import { PointParticleEmitter } from "./EmitterTypes/pointParticleEmitter"; -import { HemisphericParticleEmitter } from "./EmitterTypes/hemisphericParticleEmitter"; -import { SphereDirectedParticleEmitter, SphereParticleEmitter } from "./EmitterTypes/sphereParticleEmitter"; -import { CylinderDirectedParticleEmitter, CylinderParticleEmitter } from "./EmitterTypes/cylinderParticleEmitter"; -import { ConeDirectedParticleEmitter, ConeParticleEmitter } from "./EmitterTypes/coneParticleEmitter"; -import { - CreateConeEmitter, - CreateCylinderEmitter, - CreateDirectedCylinderEmitter, - CreateDirectedSphereEmitter, - CreateDirectedConeEmitter, - CreateHemisphericEmitter, - CreatePointEmitter, - CreateSphereEmitter, -} from "./particleSystem.functions"; -import { Attractor } from "./attractor"; -import type { _IExecutionQueueItem } from "./Queue/executionQueue"; -import { _ConnectAfter, _RemoveFromQueue } from "./Queue/executionQueue"; -import type { FlowMap } from "./flowMap"; -import type { NodeParticleSystemSet } from "./Node/nodeParticleSystemSet"; - -/** - * This represents a particle system in Babylon. - * Particles are often small sprites used to simulate hard-to-reproduce phenomena like fire, smoke, water, or abstract visual effects like magic glitter and faery dust. - * Particles can take different shapes while emitted like box, sphere, cone or you can write your custom function. - * @example https://doc.babylonjs.com/features/featuresDeepDive/particles/particle_system/particle_system_intro - */ -export class ParticleSystem extends ThinParticleSystem { - /** - * Billboard mode will only apply to Y axis - */ - public static readonly BILLBOARDMODE_Y = Constants.PARTICLES_BILLBOARDMODE_Y; - /** - * Billboard mode will apply to all axes - */ - public static readonly BILLBOARDMODE_ALL = Constants.PARTICLES_BILLBOARDMODE_ALL; - /** - * Special billboard mode where the particle will be biilboard to the camera but rotated to align with direction - */ - public static readonly BILLBOARDMODE_STRETCHED = Constants.PARTICLES_BILLBOARDMODE_STRETCHED; - /** - * Special billboard mode where the particle will be billboard to the camera but only around the axis of the direction of particle emission - */ - public static readonly BILLBOARDMODE_STRETCHED_LOCAL = Constants.PARTICLES_BILLBOARDMODE_STRETCHED_LOCAL; - - // Sub-emitters - private _rootParticleSystem: Nullable; - - /** - * The Sub-emitters templates that will be used to generate the sub particle system to be associated with the system, this property is used by the root particle system only. - * When a particle is spawned, an array will be chosen at random and all the emitters in that array will be attached to the particle. (Default: []) - */ - public subEmitters: Array>; - // the subEmitters field above converted to a constant type - private _subEmitters: Array>; - - /** - * @internal - * If the particle systems emitter should be disposed when the particle system is disposed - */ - public _disposeEmitterOnDispose = false; - /** - * The current active Sub-systems, this property is used by the root particle system only. - */ - public activeSubSystems: Array; - - /** - * Specifies if the particle system should be serialized - */ - public doNotSerialize = false; - - /** - * Creates a Point Emitter for the particle system (emits directly from the emitter position) - * @param direction1 Particles are emitted between the direction1 and direction2 from within the box - * @param direction2 Particles are emitted between the direction1 and direction2 from within the box - * @returns the emitter - */ - public override createPointEmitter(direction1: Vector3, direction2: Vector3): PointParticleEmitter { - const particleEmitter = CreatePointEmitter(direction1, direction2); - this.particleEmitterType = particleEmitter; - return particleEmitter; - } - - /** - * Gets or sets a function indicating if the particle system can start. - * @returns true if the particle system can start, false otherwise. - */ - public canStart = () => { - return true; - }; - - /** Flow map */ - private _flowMap: Nullable = null; - private _flowMapUpdate: Nullable<_IExecutionQueueItem> = null; - - /** @internal */ - public _source: Nullable = null; - - /** @internal */ - public _blockReference: number = 0; - - /** - * Gets the NodeParticleSystemSet that this particle system belongs to. - */ - public get source(): Nullable { - return this._source; - } - - /** - * Returns true if the particle system was generated by a node particle system set - */ - public override get isNodeGenerated(): boolean { - return this._source !== null; - } - /** - * The strength of the flow map - */ - public flowMapStrength = 1.0; - - /** Gets or sets the current flow map */ - public get flowMap(): Nullable { - return this._flowMap; - } - - public set flowMap(value: Nullable) { - if (this._flowMap === value) { - return; - } - - this._flowMap = value; - - if (this._flowMapUpdate) { - _RemoveFromQueue(this._flowMapUpdate); - this._flowMapUpdate = null; - } - if (value) { - this._flowMapUpdate = { - process: (particle: Particle) => { - const matrix = this.getScene()?.getTransformMatrix(); - this._flowMap!._processParticle(particle, this.flowMapStrength * this._tempScaledUpdateSpeed, matrix); - }, - previousItem: null, - nextItem: null, - }; - _ConnectAfter(this._flowMapUpdate, this._directionProcessing!); - } - } - - /** Attractors */ - private _attractors: Attractor[] = []; - private _attractorUpdate: Nullable<_IExecutionQueueItem> = null; - - /** - * The list of attractors used to change the direction of the particles in the system. - * Please note that this is a copy of the internal array. If you want to modify it, please use the addAttractor and removeAttractor methods. - */ - public get attractors(): Attractor[] { - return this._attractors.slice(0); - } - - /** - * Gets or sets an object used to store user defined information for the particle system - */ - public metadata: any = null; - - /** - * Add an attractor to the particle system. Attractors are used to change the direction of the particles in the system. - * @param attractor The attractor to add to the particle system - */ - public addAttractor(attractor: Attractor): void { - this._attractors.push(attractor); - - if (this._attractors.length === 1) { - this._attractorUpdate = { - process: (particle: Particle) => { - for (const attractor of this._attractors) { - attractor._processParticle(particle, this); - } - }, - previousItem: null, - nextItem: null, - }; - _ConnectAfter(this._attractorUpdate, this._directionProcessing!); - } - } - - /** - * Removes an attractor from the particle system. Attractors are used to change the direction of the particles in the system. - * @param attractor The attractor to remove from the particle system - */ - public removeAttractor(attractor: Attractor): void { - const index = this._attractors.indexOf(attractor); - if (index !== -1) { - this._attractors.splice(index, 1); - } - - if (this._attractors.length === 0) { - _RemoveFromQueue(this._attractorUpdate!); - } - } - - public override start(delay = this.startDelay): void { - if (!this.canStart()) { - return; - } - super.start(delay); - } - - /** - * Creates a Hemisphere Emitter for the particle system (emits along the hemisphere radius) - * @param radius The radius of the hemisphere to emit from - * @param radiusRange The range of the hemisphere to emit from [0-1] 0 Surface Only, 1 Entire Radius - * @returns the emitter - */ - public override createHemisphericEmitter(radius = 1, radiusRange = 1): HemisphericParticleEmitter { - const particleEmitter = CreateHemisphericEmitter(radius, radiusRange); - this.particleEmitterType = particleEmitter; - return particleEmitter; - } - - /** - * Creates a Sphere Emitter for the particle system (emits along the sphere radius) - * @param radius The radius of the sphere to emit from - * @param radiusRange The range of the sphere to emit from [0-1] 0 Surface Only, 1 Entire Radius - * @returns the emitter - */ - public override createSphereEmitter(radius = 1, radiusRange = 1): SphereParticleEmitter { - const particleEmitter = CreateSphereEmitter(radius, radiusRange); - this.particleEmitterType = particleEmitter; - return particleEmitter; - } - - /** - * Creates a Directed Sphere Emitter for the particle system (emits between direction1 and direction2) - * @param radius The radius of the sphere to emit from - * @param direction1 Particles are emitted between the direction1 and direction2 from within the sphere - * @param direction2 Particles are emitted between the direction1 and direction2 from within the sphere - * @returns the emitter - */ - public override createDirectedSphereEmitter(radius = 1, direction1 = new Vector3(0, 1.0, 0), direction2 = new Vector3(0, 1.0, 0)): SphereDirectedParticleEmitter { - const particleEmitter = CreateDirectedSphereEmitter(radius, direction1, direction2); - this.particleEmitterType = particleEmitter; - return particleEmitter; - } - - /** - * Creates a Cylinder Emitter for the particle system (emits from the cylinder to the particle position) - * @param radius The radius of the emission cylinder - * @param height The height of the emission cylinder - * @param radiusRange The range of emission [0-1] 0 Surface only, 1 Entire Radius - * @param directionRandomizer How much to randomize the particle direction [0-1] - * @returns the emitter - */ - public override createCylinderEmitter(radius = 1, height = 1, radiusRange = 1, directionRandomizer = 0): CylinderParticleEmitter { - const particleEmitter = CreateCylinderEmitter(radius, height, radiusRange, directionRandomizer); - this.particleEmitterType = particleEmitter; - return particleEmitter; - } - - /** - * Creates a Directed Cylinder Emitter for the particle system (emits between direction1 and direction2) - * @param radius The radius of the cylinder to emit from - * @param height The height of the emission cylinder - * @param radiusRange the range of the emission cylinder [0-1] 0 Surface only, 1 Entire Radius (1 by default) - * @param direction1 Particles are emitted between the direction1 and direction2 from within the cylinder - * @param direction2 Particles are emitted between the direction1 and direction2 from within the cylinder - * @returns the emitter - */ - public override createDirectedCylinderEmitter( - radius = 1, - height = 1, - radiusRange = 1, - direction1 = new Vector3(0, 1.0, 0), - direction2 = new Vector3(0, 1.0, 0) - ): CylinderDirectedParticleEmitter { - const particleEmitter = CreateDirectedCylinderEmitter(radius, height, radiusRange, direction1, direction2); - this.particleEmitterType = particleEmitter; - return particleEmitter; - } - - /** - * Creates a Cone Emitter for the particle system (emits from the cone to the particle position) - * @param radius The radius of the cone to emit from - * @param angle The base angle of the cone - * @returns the emitter - */ - public override createConeEmitter(radius = 1, angle = Math.PI / 4): ConeParticleEmitter { - const particleEmitter = CreateConeEmitter(radius, angle); - this.particleEmitterType = particleEmitter; - return particleEmitter; - } - - public override createDirectedConeEmitter( - radius = 1, - angle = Math.PI / 4, - direction1 = new Vector3(0, 1.0, 0), - direction2 = new Vector3(0, 1.0, 0) - ): ConeDirectedParticleEmitter { - const particleEmitter = CreateDirectedConeEmitter(radius, angle, direction1, direction2); - this.particleEmitterType = particleEmitter; - return particleEmitter; - } - - /** - * Creates a Box Emitter for the particle system. (emits between direction1 and direction2 from withing the box defined by minEmitBox and maxEmitBox) - * @param direction1 Particles are emitted between the direction1 and direction2 from within the box - * @param direction2 Particles are emitted between the direction1 and direction2 from within the box - * @param minEmitBox Particles are emitted from the box between minEmitBox and maxEmitBox - * @param maxEmitBox Particles are emitted from the box between minEmitBox and maxEmitBox - * @returns the emitter - */ - public override createBoxEmitter(direction1: Vector3, direction2: Vector3, minEmitBox: Vector3, maxEmitBox: Vector3): BoxParticleEmitter { - const particleEmitter = new BoxParticleEmitter(); - this.particleEmitterType = particleEmitter; - this.direction1 = direction1; - this.direction2 = direction2; - this.minEmitBox = minEmitBox; - this.maxEmitBox = maxEmitBox; - return particleEmitter; - } - - private _prepareSubEmitterInternalArray() { - this._subEmitters = new Array>(); - if (this.subEmitters) { - for (const subEmitter of this.subEmitters) { - if (subEmitter instanceof ParticleSystem) { - this._subEmitters.push([new SubEmitter(subEmitter)]); - } else if (subEmitter instanceof SubEmitter) { - this._subEmitters.push([subEmitter]); - } else if (subEmitter instanceof Array) { - this._subEmitters.push(subEmitter); - } - } - } - } - - private _stopSubEmitters(): void { - if (!this.activeSubSystems) { - return; - } - for (const subSystem of this.activeSubSystems) { - subSystem.stop(true); - } - this.activeSubSystems = [] as ParticleSystem[]; - } - - private _removeFromRoot(): void { - if (!this._rootParticleSystem) { - return; - } - - const index = this._rootParticleSystem.activeSubSystems.indexOf(this); - if (index !== -1) { - this._rootParticleSystem.activeSubSystems.splice(index, 1); - } - - this._rootParticleSystem = null; - } - - /** @internal */ - public override _emitFromParticle: (particle: Particle) => void = (particle) => { - if (!this._subEmitters || this._subEmitters.length === 0) { - return; - } - const templateIndex = Math.floor(Math.random() * this._subEmitters.length); - - for (const subEmitter of this._subEmitters[templateIndex]) { - if (subEmitter.type === SubEmitterType.END) { - const subSystem = subEmitter.clone(); - particle._inheritParticleInfoToSubEmitter(subSystem); - subSystem.particleSystem._rootParticleSystem = this; - this.activeSubSystems.push(subSystem.particleSystem); - subSystem.particleSystem.start(); - } - } - }; - - /** @internal */ - public override _preStart() { - // Convert the subEmitters field to the constant type field _subEmitters - this._prepareSubEmitterInternalArray(); - - if (this._subEmitters && this._subEmitters.length != 0) { - this.activeSubSystems = [] as ParticleSystem[]; - } - } - - /** @internal */ - public override _postStop(stopSubEmitters: boolean) { - if (stopSubEmitters) { - this._stopSubEmitters(); - } - } - - /** @internal */ - public override _prepareParticle(particle: Particle): void { - // Attach emitters - if (this._subEmitters && this._subEmitters.length > 0) { - const subEmitters = this._subEmitters[Math.floor(Math.random() * this._subEmitters.length)]; - particle._attachedSubEmitters = []; - for (const subEmitter of subEmitters) { - if (subEmitter.type === SubEmitterType.ATTACHED) { - const newEmitter = subEmitter.clone(); - particle._attachedSubEmitters.push(newEmitter); - newEmitter.particleSystem.start(); - } - } - } - } - - /** @internal */ - public override _onDispose(disposeAttachedSubEmitters = false, disposeEndSubEmitters = false) { - this._removeFromRoot(); - - if (this.subEmitters && !this._subEmitters) { - this._prepareSubEmitterInternalArray(); - } - - if (disposeAttachedSubEmitters) { - if (this.particles) { - for (const particle of this.particles) { - if (particle._attachedSubEmitters) { - for (let i = particle._attachedSubEmitters.length - 1; i >= 0; i -= 1) { - particle._attachedSubEmitters[i].dispose(); - } - } - } - } - } - - if (disposeEndSubEmitters) { - if (this.activeSubSystems) { - for (let i = this.activeSubSystems.length - 1; i >= 0; i -= 1) { - this.activeSubSystems[i].dispose(); - } - } - } - - if (this._subEmitters && this._subEmitters.length) { - for (let index = 0; index < this._subEmitters.length; index++) { - for (const subEmitter of this._subEmitters[index]) { - subEmitter.dispose(); - } - } - - this._subEmitters = []; - this.subEmitters = []; - } - - if (this._disposeEmitterOnDispose && this.emitter && (this.emitter as AbstractMesh).dispose) { - (this.emitter).dispose(true); - } - } - - /** - * @internal - */ - public static _Parse(parsedParticleSystem: any, particleSystem: IParticleSystem, sceneOrEngine: Scene | AbstractEngine, rootUrl: string) { - let scene: Nullable; - - if (sceneOrEngine instanceof AbstractEngine) { - scene = null; - } else { - scene = sceneOrEngine; - } - - const internalClass = GetClass("BABYLON.Texture"); - if (internalClass && scene) { - // Texture - if (parsedParticleSystem.texture) { - particleSystem.particleTexture = internalClass.Parse(parsedParticleSystem.texture, scene, rootUrl) as BaseTexture; - } else if (parsedParticleSystem.textureName) { - particleSystem.particleTexture = new internalClass( - rootUrl + parsedParticleSystem.textureName, - scene, - false, - parsedParticleSystem.invertY !== undefined ? parsedParticleSystem.invertY : true - ); - particleSystem.particleTexture!.name = parsedParticleSystem.textureName; - } - } - - // Emitter - if (!parsedParticleSystem.emitterId && parsedParticleSystem.emitterId !== 0 && parsedParticleSystem.emitter === undefined) { - particleSystem.emitter = Vector3.Zero(); - } else if (parsedParticleSystem.emitterId && scene) { - particleSystem.emitter = scene.getLastMeshById(parsedParticleSystem.emitterId); - } else { - particleSystem.emitter = Vector3.FromArray(parsedParticleSystem.emitter); - } - - particleSystem.isLocal = !!parsedParticleSystem.isLocal; - - // Misc. - if (parsedParticleSystem.renderingGroupId !== undefined) { - particleSystem.renderingGroupId = parsedParticleSystem.renderingGroupId; - } - - if (parsedParticleSystem.isBillboardBased !== undefined) { - particleSystem.isBillboardBased = parsedParticleSystem.isBillboardBased; - } - - if (parsedParticleSystem.billboardMode !== undefined) { - particleSystem.billboardMode = parsedParticleSystem.billboardMode; - } - - if (parsedParticleSystem.useLogarithmicDepth !== undefined) { - particleSystem.useLogarithmicDepth = parsedParticleSystem.useLogarithmicDepth; - } - - // Animations - if (parsedParticleSystem.animations) { - for (let animationIndex = 0; animationIndex < parsedParticleSystem.animations.length; animationIndex++) { - const parsedAnimation = parsedParticleSystem.animations[animationIndex]; - const internalClass = GetClass("BABYLON.Animation"); - if (internalClass) { - particleSystem.animations.push(internalClass.Parse(parsedAnimation)); - } - } - particleSystem.beginAnimationOnStart = parsedParticleSystem.beginAnimationOnStart; - particleSystem.beginAnimationFrom = parsedParticleSystem.beginAnimationFrom; - particleSystem.beginAnimationTo = parsedParticleSystem.beginAnimationTo; - particleSystem.beginAnimationLoop = parsedParticleSystem.beginAnimationLoop; - } - - if (parsedParticleSystem.autoAnimate && scene) { - scene.beginAnimation( - particleSystem, - parsedParticleSystem.autoAnimateFrom, - parsedParticleSystem.autoAnimateTo, - parsedParticleSystem.autoAnimateLoop, - parsedParticleSystem.autoAnimateSpeed || 1.0 - ); - } - - // Particle system - particleSystem.startDelay = parsedParticleSystem.startDelay | 0; - particleSystem.minAngularSpeed = parsedParticleSystem.minAngularSpeed; - particleSystem.maxAngularSpeed = parsedParticleSystem.maxAngularSpeed; - particleSystem.minSize = parsedParticleSystem.minSize; - particleSystem.maxSize = parsedParticleSystem.maxSize; - - if (parsedParticleSystem.minScaleX) { - particleSystem.minScaleX = parsedParticleSystem.minScaleX; - particleSystem.maxScaleX = parsedParticleSystem.maxScaleX; - particleSystem.minScaleY = parsedParticleSystem.minScaleY; - particleSystem.maxScaleY = parsedParticleSystem.maxScaleY; - } - - if (parsedParticleSystem.preWarmCycles !== undefined) { - particleSystem.preWarmCycles = parsedParticleSystem.preWarmCycles; - particleSystem.preWarmStepOffset = parsedParticleSystem.preWarmStepOffset; - } - - if (parsedParticleSystem.minInitialRotation !== undefined) { - particleSystem.minInitialRotation = parsedParticleSystem.minInitialRotation; - particleSystem.maxInitialRotation = parsedParticleSystem.maxInitialRotation; - } - - particleSystem.minLifeTime = parsedParticleSystem.minLifeTime; - particleSystem.maxLifeTime = parsedParticleSystem.maxLifeTime; - particleSystem.minEmitPower = parsedParticleSystem.minEmitPower; - particleSystem.maxEmitPower = parsedParticleSystem.maxEmitPower; - particleSystem.emitRate = parsedParticleSystem.emitRate; - particleSystem.gravity = Vector3.FromArray(parsedParticleSystem.gravity); - if (parsedParticleSystem.noiseStrength) { - particleSystem.noiseStrength = Vector3.FromArray(parsedParticleSystem.noiseStrength); - } - particleSystem.color1 = Color4.FromArray(parsedParticleSystem.color1); - particleSystem.color2 = Color4.FromArray(parsedParticleSystem.color2); - particleSystem.colorDead = Color4.FromArray(parsedParticleSystem.colorDead); - particleSystem.updateSpeed = parsedParticleSystem.updateSpeed; - particleSystem.targetStopDuration = parsedParticleSystem.targetStopDuration; - particleSystem.blendMode = parsedParticleSystem.blendMode; - - if (parsedParticleSystem.colorGradients) { - for (const colorGradient of parsedParticleSystem.colorGradients) { - particleSystem.addColorGradient( - colorGradient.gradient, - Color4.FromArray(colorGradient.color1), - colorGradient.color2 ? Color4.FromArray(colorGradient.color2) : undefined - ); - } - } - - if (parsedParticleSystem.rampGradients) { - for (const rampGradient of parsedParticleSystem.rampGradients) { - particleSystem.addRampGradient(rampGradient.gradient, Color3.FromArray(rampGradient.color)); - } - particleSystem.useRampGradients = parsedParticleSystem.useRampGradients; - } - - if (parsedParticleSystem.colorRemapGradients) { - for (const colorRemapGradient of parsedParticleSystem.colorRemapGradients) { - particleSystem.addColorRemapGradient( - colorRemapGradient.gradient, - colorRemapGradient.factor1 !== undefined ? colorRemapGradient.factor1 : colorRemapGradient.factor, - colorRemapGradient.factor2 - ); - } - } - - if (parsedParticleSystem.alphaRemapGradients) { - for (const alphaRemapGradient of parsedParticleSystem.alphaRemapGradients) { - particleSystem.addAlphaRemapGradient( - alphaRemapGradient.gradient, - alphaRemapGradient.factor1 !== undefined ? alphaRemapGradient.factor1 : alphaRemapGradient.factor, - alphaRemapGradient.factor2 - ); - } - } - - if (parsedParticleSystem.sizeGradients) { - for (const sizeGradient of parsedParticleSystem.sizeGradients) { - particleSystem.addSizeGradient(sizeGradient.gradient, sizeGradient.factor1 !== undefined ? sizeGradient.factor1 : sizeGradient.factor, sizeGradient.factor2); - } - } - - if (parsedParticleSystem.angularSpeedGradients) { - for (const angularSpeedGradient of parsedParticleSystem.angularSpeedGradients) { - particleSystem.addAngularSpeedGradient( - angularSpeedGradient.gradient, - angularSpeedGradient.factor1 !== undefined ? angularSpeedGradient.factor1 : angularSpeedGradient.factor, - angularSpeedGradient.factor2 - ); - } - } - - if (parsedParticleSystem.velocityGradients) { - for (const velocityGradient of parsedParticleSystem.velocityGradients) { - particleSystem.addVelocityGradient( - velocityGradient.gradient, - velocityGradient.factor1 !== undefined ? velocityGradient.factor1 : velocityGradient.factor, - velocityGradient.factor2 - ); - } - } - - if (parsedParticleSystem.dragGradients) { - for (const dragGradient of parsedParticleSystem.dragGradients) { - particleSystem.addDragGradient(dragGradient.gradient, dragGradient.factor1 !== undefined ? dragGradient.factor1 : dragGradient.factor, dragGradient.factor2); - } - } - - if (parsedParticleSystem.emitRateGradients) { - for (const emitRateGradient of parsedParticleSystem.emitRateGradients) { - particleSystem.addEmitRateGradient( - emitRateGradient.gradient, - emitRateGradient.factor1 !== undefined ? emitRateGradient.factor1 : emitRateGradient.factor, - emitRateGradient.factor2 - ); - } - } - - if (parsedParticleSystem.startSizeGradients) { - for (const startSizeGradient of parsedParticleSystem.startSizeGradients) { - particleSystem.addStartSizeGradient( - startSizeGradient.gradient, - startSizeGradient.factor1 !== undefined ? startSizeGradient.factor1 : startSizeGradient.factor, - startSizeGradient.factor2 - ); - } - } - - if (parsedParticleSystem.lifeTimeGradients) { - for (const lifeTimeGradient of parsedParticleSystem.lifeTimeGradients) { - particleSystem.addLifeTimeGradient( - lifeTimeGradient.gradient, - lifeTimeGradient.factor1 !== undefined ? lifeTimeGradient.factor1 : lifeTimeGradient.factor, - lifeTimeGradient.factor2 - ); - } - } - - if (parsedParticleSystem.limitVelocityGradients) { - for (const limitVelocityGradient of parsedParticleSystem.limitVelocityGradients) { - particleSystem.addLimitVelocityGradient( - limitVelocityGradient.gradient, - limitVelocityGradient.factor1 !== undefined ? limitVelocityGradient.factor1 : limitVelocityGradient.factor, - limitVelocityGradient.factor2 - ); - } - particleSystem.limitVelocityDamping = parsedParticleSystem.limitVelocityDamping; - } - - if (parsedParticleSystem.noiseTexture && scene) { - const internalClass = GetClass("BABYLON.ProceduralTexture"); - particleSystem.noiseTexture = internalClass.Parse(parsedParticleSystem.noiseTexture, scene, rootUrl); - } - - // Emitter - let emitterType: IParticleEmitterType; - if (parsedParticleSystem.particleEmitterType) { - switch (parsedParticleSystem.particleEmitterType.type) { - case "SphereParticleEmitter": - emitterType = new SphereParticleEmitter(); - break; - case "SphereDirectedParticleEmitter": - emitterType = new SphereDirectedParticleEmitter(); - break; - case "ConeEmitter": - case "ConeParticleEmitter": - emitterType = new ConeParticleEmitter(); - break; - case "ConeDirectedParticleEmitter": - emitterType = new ConeDirectedParticleEmitter(); - break; - case "CylinderParticleEmitter": - emitterType = new CylinderParticleEmitter(); - break; - case "CylinderDirectedParticleEmitter": - emitterType = new CylinderDirectedParticleEmitter(); - break; - case "HemisphericParticleEmitter": - emitterType = new HemisphericParticleEmitter(); - break; - case "PointParticleEmitter": - emitterType = new PointParticleEmitter(); - break; - case "MeshParticleEmitter": - emitterType = new MeshParticleEmitter(); - break; - case "CustomParticleEmitter": - emitterType = new CustomParticleEmitter(); - break; - case "BoxEmitter": - case "BoxParticleEmitter": - default: - emitterType = new BoxParticleEmitter(); - break; - } - - emitterType.parse(parsedParticleSystem.particleEmitterType, scene); - } else { - emitterType = new BoxParticleEmitter(); - emitterType.parse(parsedParticleSystem, scene); - } - particleSystem.particleEmitterType = emitterType; - - // Animation sheet - particleSystem.startSpriteCellID = parsedParticleSystem.startSpriteCellID; - particleSystem.endSpriteCellID = parsedParticleSystem.endSpriteCellID; - particleSystem.spriteCellLoop = parsedParticleSystem.spriteCellLoop ?? true; - particleSystem.spriteCellWidth = parsedParticleSystem.spriteCellWidth; - particleSystem.spriteCellHeight = parsedParticleSystem.spriteCellHeight; - particleSystem.spriteCellChangeSpeed = parsedParticleSystem.spriteCellChangeSpeed; - particleSystem.spriteRandomStartCell = parsedParticleSystem.spriteRandomStartCell; - - particleSystem.disposeOnStop = parsedParticleSystem.disposeOnStop ?? false; - particleSystem.manualEmitCount = parsedParticleSystem.manualEmitCount ?? -1; - } - - /** - * Parses a JSON object to create a particle system. - * @param parsedParticleSystem The JSON object to parse - * @param sceneOrEngine The scene or the engine to create the particle system in - * @param rootUrl The root url to use to load external dependencies like texture - * @param doNotStart Ignore the preventAutoStart attribute and does not start - * @param capacity defines the system capacity (if null or undefined the sotred capacity will be used) - * @returns the Parsed particle system - */ - public static Parse(parsedParticleSystem: any, sceneOrEngine: Scene | AbstractEngine, rootUrl: string, doNotStart = false, capacity?: number): ParticleSystem { - const name = parsedParticleSystem.name; - let custom: Nullable = null; - let program: any = null; - let engine: AbstractEngine; - let scene: Nullable; - - if (sceneOrEngine instanceof AbstractEngine) { - engine = sceneOrEngine; - } else { - scene = sceneOrEngine; - engine = scene.getEngine(); - } - - if (parsedParticleSystem.customShader && (engine as any).createEffectForParticles) { - program = parsedParticleSystem.customShader; - const defines: string = program.shaderOptions.defines.length > 0 ? program.shaderOptions.defines.join("\n") : ""; - custom = (engine as any).createEffectForParticles(program.shaderPath.fragmentElement, program.shaderOptions.uniforms, program.shaderOptions.samplers, defines); - } - const particleSystem = new ParticleSystem(name, capacity || parsedParticleSystem.capacity, sceneOrEngine, custom, parsedParticleSystem.isAnimationSheetEnabled); - particleSystem.customShader = program; - particleSystem._rootUrl = rootUrl; - - if (parsedParticleSystem.id) { - particleSystem.id = parsedParticleSystem.id; - } - - // SubEmitters - if (parsedParticleSystem.subEmitters) { - particleSystem.subEmitters = []; - for (const cell of parsedParticleSystem.subEmitters) { - const cellArray = []; - for (const sub of cell) { - cellArray.push(SubEmitter.Parse(sub, sceneOrEngine, rootUrl)); - } - - particleSystem.subEmitters.push(cellArray); - } - } - - // Attractors - if (parsedParticleSystem.attractors) { - for (const attractor of parsedParticleSystem.attractors) { - const newAttractor = new Attractor(); - newAttractor.position = Vector3.FromArray(attractor.position); - newAttractor.strength = attractor.strength; - particleSystem.addAttractor(newAttractor); - } - } - - ParticleSystem._Parse(parsedParticleSystem, particleSystem, sceneOrEngine, rootUrl); - - if (parsedParticleSystem.textureMask) { - particleSystem.textureMask = Color4.FromArray(parsedParticleSystem.textureMask); - } - - if (parsedParticleSystem.worldOffset) { - particleSystem.worldOffset = Vector3.FromArray(parsedParticleSystem.worldOffset); - } - - // Auto start - if (parsedParticleSystem.preventAutoStart) { - particleSystem.preventAutoStart = parsedParticleSystem.preventAutoStart; - } - - if (parsedParticleSystem.metadata) { - particleSystem.metadata = parsedParticleSystem.metadata; - } - - if (!doNotStart && !particleSystem.preventAutoStart) { - particleSystem.start(); - } - - return particleSystem; - } - - /** - * Serializes the particle system to a JSON object - * @param serializeTexture defines if the texture must be serialized as well - * @returns the JSON object - */ - public override serialize(serializeTexture = false): any { - const serializationObject: any = {}; - - ParticleSystem._Serialize(serializationObject, this, serializeTexture); - - serializationObject.textureMask = this.textureMask.asArray(); - serializationObject.customShader = this.customShader; - serializationObject.preventAutoStart = this.preventAutoStart; - serializationObject.worldOffset = this.worldOffset.asArray(); - - if (this.metadata) { - serializationObject.metadata = this.metadata; - } - - // SubEmitters - if (this.subEmitters) { - serializationObject.subEmitters = []; - - if (!this._subEmitters) { - this._prepareSubEmitterInternalArray(); - } - - for (const subs of this._subEmitters) { - const cell = []; - for (const sub of subs) { - if (!sub.particleSystem.doNotSerialize) { - cell.push(sub.serialize(serializeTexture)); - } - } - - serializationObject.subEmitters.push(cell); - } - } - - // Attractors - if (this._attractors && this._attractors.length) { - serializationObject.attractors = []; - for (const attractor of this._attractors) { - serializationObject.attractors.push(attractor.serialize()); - } - } - - return serializationObject; - } - - /** - * @internal - */ - public static _Serialize(serializationObject: any, particleSystem: IParticleSystem, serializeTexture: boolean) { - serializationObject.name = particleSystem.name; - serializationObject.id = particleSystem.id; - - serializationObject.capacity = particleSystem.getCapacity(); - - serializationObject.disposeOnStop = particleSystem.disposeOnStop; - serializationObject.manualEmitCount = particleSystem.manualEmitCount; - - // Emitter - if ((particleSystem.emitter).position) { - const emitterMesh = particleSystem.emitter; - serializationObject.emitterId = emitterMesh.id; - } else { - const emitterPosition = particleSystem.emitter; - serializationObject.emitter = emitterPosition.asArray(); - } - - // Emitter - if (particleSystem.particleEmitterType) { - serializationObject.particleEmitterType = particleSystem.particleEmitterType.serialize(); - } - - if (particleSystem.particleTexture) { - if (serializeTexture) { - serializationObject.texture = particleSystem.particleTexture.serialize(); - } else { - serializationObject.textureName = particleSystem.particleTexture.name; - serializationObject.invertY = !!(particleSystem.particleTexture as any)._invertY; - } - } - - serializationObject.isLocal = particleSystem.isLocal; - - // Animations - SerializationHelper.AppendSerializedAnimations(particleSystem, serializationObject); - serializationObject.beginAnimationOnStart = particleSystem.beginAnimationOnStart; - serializationObject.beginAnimationFrom = particleSystem.beginAnimationFrom; - serializationObject.beginAnimationTo = particleSystem.beginAnimationTo; - serializationObject.beginAnimationLoop = particleSystem.beginAnimationLoop; - - // Particle system - serializationObject.startDelay = particleSystem.startDelay; - serializationObject.renderingGroupId = particleSystem.renderingGroupId; - serializationObject.isBillboardBased = particleSystem.isBillboardBased; - serializationObject.billboardMode = particleSystem.billboardMode; - serializationObject.minAngularSpeed = particleSystem.minAngularSpeed; - serializationObject.maxAngularSpeed = particleSystem.maxAngularSpeed; - serializationObject.minSize = particleSystem.minSize; - serializationObject.maxSize = particleSystem.maxSize; - serializationObject.minScaleX = particleSystem.minScaleX; - serializationObject.maxScaleX = particleSystem.maxScaleX; - serializationObject.minScaleY = particleSystem.minScaleY; - serializationObject.maxScaleY = particleSystem.maxScaleY; - serializationObject.minEmitPower = particleSystem.minEmitPower; - serializationObject.maxEmitPower = particleSystem.maxEmitPower; - serializationObject.minLifeTime = particleSystem.minLifeTime; - serializationObject.maxLifeTime = particleSystem.maxLifeTime; - serializationObject.emitRate = particleSystem.emitRate; - serializationObject.gravity = particleSystem.gravity.asArray(); - serializationObject.noiseStrength = particleSystem.noiseStrength.asArray(); - serializationObject.color1 = particleSystem.color1.asArray(); - serializationObject.color2 = particleSystem.color2.asArray(); - serializationObject.colorDead = particleSystem.colorDead.asArray(); - serializationObject.updateSpeed = particleSystem.updateSpeed; - serializationObject.targetStopDuration = particleSystem.targetStopDuration; - serializationObject.blendMode = particleSystem.blendMode; - serializationObject.preWarmCycles = particleSystem.preWarmCycles; - serializationObject.preWarmStepOffset = particleSystem.preWarmStepOffset; - serializationObject.minInitialRotation = particleSystem.minInitialRotation; - serializationObject.maxInitialRotation = particleSystem.maxInitialRotation; - serializationObject.startSpriteCellID = particleSystem.startSpriteCellID; - serializationObject.spriteCellLoop = particleSystem.spriteCellLoop; - serializationObject.endSpriteCellID = particleSystem.endSpriteCellID; - serializationObject.spriteCellChangeSpeed = particleSystem.spriteCellChangeSpeed; - serializationObject.spriteCellWidth = particleSystem.spriteCellWidth; - serializationObject.spriteCellHeight = particleSystem.spriteCellHeight; - serializationObject.spriteRandomStartCell = particleSystem.spriteRandomStartCell; - serializationObject.isAnimationSheetEnabled = particleSystem.isAnimationSheetEnabled; - serializationObject.useLogarithmicDepth = particleSystem.useLogarithmicDepth; - - const colorGradients = particleSystem.getColorGradients(); - if (colorGradients) { - serializationObject.colorGradients = []; - for (const colorGradient of colorGradients) { - const serializedGradient: any = { - gradient: colorGradient.gradient, - color1: colorGradient.color1.asArray(), - }; - - if (colorGradient.color2) { - serializedGradient.color2 = colorGradient.color2.asArray(); - } else { - serializedGradient.color2 = colorGradient.color1.asArray(); - } - - serializationObject.colorGradients.push(serializedGradient); - } - } - - const rampGradients = particleSystem.getRampGradients(); - if (rampGradients) { - serializationObject.rampGradients = []; - for (const rampGradient of rampGradients) { - const serializedGradient: any = { - gradient: rampGradient.gradient, - color: rampGradient.color.asArray(), - }; - - serializationObject.rampGradients.push(serializedGradient); - } - serializationObject.useRampGradients = particleSystem.useRampGradients; - } - - const colorRemapGradients = particleSystem.getColorRemapGradients(); - if (colorRemapGradients) { - serializationObject.colorRemapGradients = []; - for (const colorRemapGradient of colorRemapGradients) { - const serializedGradient: any = { - gradient: colorRemapGradient.gradient, - factor1: colorRemapGradient.factor1, - }; - - if (colorRemapGradient.factor2 !== undefined) { - serializedGradient.factor2 = colorRemapGradient.factor2; - } else { - serializedGradient.factor2 = colorRemapGradient.factor1; - } - - serializationObject.colorRemapGradients.push(serializedGradient); - } - } - - const alphaRemapGradients = particleSystem.getAlphaRemapGradients(); - if (alphaRemapGradients) { - serializationObject.alphaRemapGradients = []; - for (const alphaRemapGradient of alphaRemapGradients) { - const serializedGradient: any = { - gradient: alphaRemapGradient.gradient, - factor1: alphaRemapGradient.factor1, - }; - - if (alphaRemapGradient.factor2 !== undefined) { - serializedGradient.factor2 = alphaRemapGradient.factor2; - } else { - serializedGradient.factor2 = alphaRemapGradient.factor1; - } - - serializationObject.alphaRemapGradients.push(serializedGradient); - } - } - - const sizeGradients = particleSystem.getSizeGradients(); - if (sizeGradients) { - serializationObject.sizeGradients = []; - for (const sizeGradient of sizeGradients) { - const serializedGradient: any = { - gradient: sizeGradient.gradient, - factor1: sizeGradient.factor1, - }; - - if (sizeGradient.factor2 !== undefined) { - serializedGradient.factor2 = sizeGradient.factor2; - } else { - serializedGradient.factor2 = sizeGradient.factor1; - } - - serializationObject.sizeGradients.push(serializedGradient); - } - } - - const angularSpeedGradients = particleSystem.getAngularSpeedGradients(); - if (angularSpeedGradients) { - serializationObject.angularSpeedGradients = []; - for (const angularSpeedGradient of angularSpeedGradients) { - const serializedGradient: any = { - gradient: angularSpeedGradient.gradient, - factor1: angularSpeedGradient.factor1, - }; - - if (angularSpeedGradient.factor2 !== undefined) { - serializedGradient.factor2 = angularSpeedGradient.factor2; - } else { - serializedGradient.factor2 = angularSpeedGradient.factor1; - } - - serializationObject.angularSpeedGradients.push(serializedGradient); - } - } - - const velocityGradients = particleSystem.getVelocityGradients(); - if (velocityGradients) { - serializationObject.velocityGradients = []; - for (const velocityGradient of velocityGradients) { - const serializedGradient: any = { - gradient: velocityGradient.gradient, - factor1: velocityGradient.factor1, - }; - - if (velocityGradient.factor2 !== undefined) { - serializedGradient.factor2 = velocityGradient.factor2; - } else { - serializedGradient.factor2 = velocityGradient.factor1; - } - - serializationObject.velocityGradients.push(serializedGradient); - } - } - - const dragGradients = particleSystem.getDragGradients(); - if (dragGradients) { - serializationObject.dragGradients = []; - for (const dragGradient of dragGradients) { - const serializedGradient: any = { - gradient: dragGradient.gradient, - factor1: dragGradient.factor1, - }; - - if (dragGradient.factor2 !== undefined) { - serializedGradient.factor2 = dragGradient.factor2; - } else { - serializedGradient.factor2 = dragGradient.factor1; - } - - serializationObject.dragGradients.push(serializedGradient); - } - } - - const emitRateGradients = particleSystem.getEmitRateGradients(); - if (emitRateGradients) { - serializationObject.emitRateGradients = []; - for (const emitRateGradient of emitRateGradients) { - const serializedGradient: any = { - gradient: emitRateGradient.gradient, - factor1: emitRateGradient.factor1, - }; - - if (emitRateGradient.factor2 !== undefined) { - serializedGradient.factor2 = emitRateGradient.factor2; - } else { - serializedGradient.factor2 = emitRateGradient.factor1; - } - - serializationObject.emitRateGradients.push(serializedGradient); - } - } - - const startSizeGradients = particleSystem.getStartSizeGradients(); - if (startSizeGradients) { - serializationObject.startSizeGradients = []; - for (const startSizeGradient of startSizeGradients) { - const serializedGradient: any = { - gradient: startSizeGradient.gradient, - factor1: startSizeGradient.factor1, - }; - - if (startSizeGradient.factor2 !== undefined) { - serializedGradient.factor2 = startSizeGradient.factor2; - } else { - serializedGradient.factor2 = startSizeGradient.factor1; - } - - serializationObject.startSizeGradients.push(serializedGradient); - } - } - - const lifeTimeGradients = particleSystem.getLifeTimeGradients(); - if (lifeTimeGradients) { - serializationObject.lifeTimeGradients = []; - for (const lifeTimeGradient of lifeTimeGradients) { - const serializedGradient: any = { - gradient: lifeTimeGradient.gradient, - factor1: lifeTimeGradient.factor1, - }; - - if (lifeTimeGradient.factor2 !== undefined) { - serializedGradient.factor2 = lifeTimeGradient.factor2; - } else { - serializedGradient.factor2 = lifeTimeGradient.factor1; - } - - serializationObject.lifeTimeGradients.push(serializedGradient); - } - } - - const limitVelocityGradients = particleSystem.getLimitVelocityGradients(); - if (limitVelocityGradients) { - serializationObject.limitVelocityGradients = []; - for (const limitVelocityGradient of limitVelocityGradients) { - const serializedGradient: any = { - gradient: limitVelocityGradient.gradient, - factor1: limitVelocityGradient.factor1, - }; - - if (limitVelocityGradient.factor2 !== undefined) { - serializedGradient.factor2 = limitVelocityGradient.factor2; - } else { - serializedGradient.factor2 = limitVelocityGradient.factor1; - } - - serializationObject.limitVelocityGradients.push(serializedGradient); - } - - serializationObject.limitVelocityDamping = particleSystem.limitVelocityDamping; - } - - if (particleSystem.noiseTexture) { - serializationObject.noiseTexture = particleSystem.noiseTexture.serialize(); - } - } - - // Clone - /** - * Clones the particle system. - * @param name The name of the cloned object - * @param newEmitter The new emitter to use - * @param cloneTexture Also clone the textures if true - * @returns the cloned particle system - */ - public override clone(name: string, newEmitter: any, cloneTexture = false): ParticleSystem { - const custom = { ...this._customWrappers }; - let program: any = null; - const engine = this._engine; - if (engine.createEffectForParticles) { - if (this.customShader != null) { - program = this.customShader; - const defines: string = program.shaderOptions.defines.length > 0 ? program.shaderOptions.defines.join("\n") : ""; - const effect = engine.createEffectForParticles(program.shaderPath.fragmentElement, program.shaderOptions.uniforms, program.shaderOptions.samplers, defines); - if (!custom[0]) { - this.setCustomEffect(effect, 0); - } else { - custom[0].effect = effect; - } - } - } - - const serialization = this.serialize(cloneTexture); - const result = ParticleSystem.Parse(serialization, this._scene || this._engine, this._rootUrl); - result.name = name; - result.customShader = program; - result._customWrappers = custom; - - if (newEmitter === undefined) { - newEmitter = this.emitter; - } - - if (this.noiseTexture) { - result.noiseTexture = this.noiseTexture.clone(); - } - - result.emitter = newEmitter; - if (!this.preventAutoStart) { - result.start(); - } - - return result; - } -} - -SubEmitter._ParseParticleSystem = ParticleSystem.Parse; diff --git a/tools/src/effect/bjs/subEmitter.ts b/tools/src/effect/bjs/subEmitter.ts deleted file mode 100644 index 047bde1e1..000000000 --- a/tools/src/effect/bjs/subEmitter.ts +++ /dev/null @@ -1,134 +0,0 @@ -import { Vector3 } from "../Maths/math.vector"; -import { _WarnImport } from "../Misc/devTools"; -import type { AbstractEngine } from "../Engines/abstractEngine"; -import { GetClass } from "../Misc/typeStore"; - -import type { Scene } from "../scene"; -import type { AbstractMesh } from "../Meshes/abstractMesh"; -import type { ParticleSystem } from "../Particles/particleSystem"; - -/** - * Type of sub emitter - */ -export const enum SubEmitterType { - /** - * Attached to the particle over it's lifetime - */ - ATTACHED, - /** - * Created when the particle dies - */ - END, -} - -/** - * Sub emitter class used to emit particles from an existing particle - */ -export class SubEmitter { - /** - * Type of the submitter (Default: END) - */ - public type = SubEmitterType.END; - /** - * If the particle should inherit the direction from the particle it's attached to. (+Y will face the direction the particle is moving) (Default: false) - * Note: This only is supported when using an emitter of type Mesh - */ - public inheritDirection = false; - /** - * How much of the attached particles speed should be added to the sub emitted particle (default: 0) - */ - public inheritedVelocityAmount = 0; - - /** - * Creates a sub emitter - * @param particleSystem the particle system to be used by the sub emitter - */ - constructor( - /** - * the particle system to be used by the sub emitter - */ - public particleSystem: ParticleSystem - ) { - // Create mesh as emitter to support rotation - if (!particleSystem.emitter || !(particleSystem.emitter).dispose) { - const internalClass = GetClass("BABYLON.AbstractMesh"); - particleSystem.emitter = new internalClass("SubemitterSystemEmitter", particleSystem.getScene()); - particleSystem._disposeEmitterOnDispose = true; - } - } - /** - * Clones the sub emitter - * @returns the cloned sub emitter - */ - public clone(): SubEmitter { - // Clone particle system - let emitter = this.particleSystem.emitter; - if (!emitter) { - emitter = new Vector3(); - } else if (emitter instanceof Vector3) { - emitter = emitter.clone(); - } else if (emitter.getClassName().indexOf("Mesh") !== -1) { - const internalClass = GetClass("BABYLON.Mesh"); - emitter = new internalClass("", emitter.getScene()); - (emitter! as any).isVisible = false; - } - const clone = new SubEmitter(this.particleSystem.clone(this.particleSystem.name, emitter)); - - // Clone properties - clone.particleSystem.name += "Clone"; - clone.type = this.type; - clone.inheritDirection = this.inheritDirection; - clone.inheritedVelocityAmount = this.inheritedVelocityAmount; - - clone.particleSystem._disposeEmitterOnDispose = true; - clone.particleSystem.disposeOnStop = true; - return clone; - } - - /** - * Serialize current object to a JSON object - * @param serializeTexture defines if the texture must be serialized as well - * @returns the serialized object - */ - public serialize(serializeTexture: boolean = false): any { - const serializationObject: any = {}; - - serializationObject.type = this.type; - serializationObject.inheritDirection = this.inheritDirection; - serializationObject.inheritedVelocityAmount = this.inheritedVelocityAmount; - serializationObject.particleSystem = this.particleSystem.serialize(serializeTexture); - - return serializationObject; - } - - /** - * @internal - */ - // eslint-disable-next-line @typescript-eslint/no-unused-vars - public static _ParseParticleSystem(system: any, sceneOrEngine: Scene | AbstractEngine, rootUrl: string, doNotStart = false): ParticleSystem { - throw _WarnImport("ParseParticle"); - } - - /** - * Creates a new SubEmitter from a serialized JSON version - * @param serializationObject defines the JSON object to read from - * @param sceneOrEngine defines the hosting scene or the hosting engine - * @param rootUrl defines the rootUrl for data loading - * @returns a new SubEmitter - */ - public static Parse(serializationObject: any, sceneOrEngine: Scene | AbstractEngine, rootUrl: string): SubEmitter { - const system = serializationObject.particleSystem; - const subEmitter = new SubEmitter(SubEmitter._ParseParticleSystem(system, sceneOrEngine, rootUrl, true)); - subEmitter.type = serializationObject.type; - subEmitter.inheritDirection = serializationObject.inheritDirection; - subEmitter.inheritedVelocityAmount = serializationObject.inheritedVelocityAmount; - subEmitter.particleSystem._isSubEmitter = true; - - return subEmitter; - } - - /** Release associated resources */ - public dispose() { - this.particleSystem.dispose(); - } -} diff --git a/tools/src/effect/bjs/thinParticleSystem.function.ts b/tools/src/effect/bjs/thinParticleSystem.function.ts deleted file mode 100644 index ac4d38630..000000000 --- a/tools/src/effect/bjs/thinParticleSystem.function.ts +++ /dev/null @@ -1,427 +0,0 @@ -import { Color4 } from "core/Maths/math.color"; -import type { ColorGradient, FactorGradient } from "core/Misc/gradients"; -import { GradientHelper } from "core/Misc/gradients"; -import type { Particle } from "./particle"; -import type { ThinParticleSystem } from "./thinParticleSystem"; -import { Clamp, Lerp, RandomRange } from "core/Maths/math.scalar.functions"; -import { TmpVectors, Vector3, Vector4 } from "core/Maths/math.vector"; - -/** Color */ - -/** @internal */ -export function _CreateColorData(particle: Particle, system: ThinParticleSystem) { - const step = RandomRange(0, 1.0); - - Color4.LerpToRef(system.color1, system.color2, step, particle.color); -} - -/** @internal */ -export function _CreateColorDeadData(particle: Particle, system: ThinParticleSystem) { - system.colorDead.subtractToRef(particle.color, system._colorDiff); - system._colorDiff.scaleToRef(1.0 / particle.lifeTime, particle.colorStep); -} - -/** @internal */ -export function _CreateColorGradientsData(particle: Particle, system: ThinParticleSystem) { - particle._currentColorGradient = system._colorGradients![0]; - particle._currentColorGradient.getColorToRef(particle.color); - particle._currentColor1.copyFrom(particle.color); - - if (system._colorGradients!.length > 1) { - system._colorGradients![1].getColorToRef(particle._currentColor2); - } else { - particle._currentColor2.copyFrom(particle.color); - } -} - -/** @internal */ -export function _ProcessColorGradients(particle: Particle, system: ThinParticleSystem) { - const colorGradients = system._colorGradients; - GradientHelper.GetCurrentGradient(system._ratio, colorGradients!, (currentGradient, nextGradient, scale) => { - if (currentGradient !== particle._currentColorGradient) { - particle._currentColor1.copyFrom(particle._currentColor2); - (nextGradient).getColorToRef(particle._currentColor2); - particle._currentColorGradient = currentGradient; - } - Color4.LerpToRef(particle._currentColor1, particle._currentColor2, scale, particle.color); - - if (particle.color.a < 0) { - particle.color.a = 0; - } - }); -} - -/** @internal */ -export function _ProcessColor(particle: Particle, system: ThinParticleSystem) { - particle.colorStep.scaleToRef(system._scaledUpdateSpeed, system._scaledColorStep); - particle.color.addInPlace(system._scaledColorStep); - - if (particle.color.a < 0) { - particle.color.a = 0; - } -} - -/** Angular speed */ - -/** @internal */ -export function _ProcessAngularSpeedGradients(particle: Particle, system: ThinParticleSystem) { - GradientHelper.GetCurrentGradient(system._ratio, system._angularSpeedGradients!, (currentGradient, nextGradient, scale) => { - if (currentGradient !== particle._currentAngularSpeedGradient) { - particle._currentAngularSpeed1 = particle._currentAngularSpeed2; - particle._currentAngularSpeed2 = (nextGradient).getFactor(); - particle._currentAngularSpeedGradient = currentGradient; - } - particle.angularSpeed = Lerp(particle._currentAngularSpeed1, particle._currentAngularSpeed2, scale); - }); -} - -/** @internal */ -export function _ProcessAngularSpeed(particle: Particle, system: ThinParticleSystem) { - particle.angle += particle.angularSpeed * system._scaledUpdateSpeed; -} - -/** Velocity & direction */ - -/** @internal */ -export function _CreateDirectionData(particle: Particle, system: ThinParticleSystem) { - system.particleEmitterType.startDirectionFunction(system._emitterWorldMatrix, particle.direction, particle, system.isLocal, system._emitterInverseWorldMatrix); -} - -/** @internal */ -export function _CreateCustomDirectionData(particle: Particle, system: ThinParticleSystem) { - system.startDirectionFunction!(system._emitterWorldMatrix, particle.direction, particle, system.isLocal); -} - -/** @internal */ -export function _CreateVelocityGradients(particle: Particle, system: ThinParticleSystem) { - particle._currentVelocityGradient = system._velocityGradients![0]; - particle._currentVelocity1 = particle._currentVelocityGradient.getFactor(); - - if (system._velocityGradients!.length > 1) { - particle._currentVelocity2 = system._velocityGradients![1].getFactor(); - } else { - particle._currentVelocity2 = particle._currentVelocity1; - } -} - -/** @internal */ -export function _CreateLimitVelocityGradients(particle: Particle, system: ThinParticleSystem) { - particle._currentLimitVelocityGradient = system._limitVelocityGradients![0]; - particle._currentLimitVelocity1 = particle._currentLimitVelocityGradient.getFactor(); - - if (system._limitVelocityGradients!.length > 1) { - particle._currentLimitVelocity2 = system._limitVelocityGradients![1].getFactor(); - } else { - particle._currentLimitVelocity2 = particle._currentLimitVelocity1; - } -} - -/** @internal */ -export function _ProcessVelocityGradients(particle: Particle, system: ThinParticleSystem) { - GradientHelper.GetCurrentGradient(system._ratio, system._velocityGradients!, (currentGradient, nextGradient, scale) => { - if (currentGradient !== particle._currentVelocityGradient) { - particle._currentVelocity1 = particle._currentVelocity2; - particle._currentVelocity2 = (nextGradient).getFactor(); - particle._currentVelocityGradient = currentGradient; - } - particle._directionScale *= Lerp(particle._currentVelocity1, particle._currentVelocity2, scale); - }); -} - -/** @internal */ -export function _ProcessLimitVelocityGradients(particle: Particle, system: ThinParticleSystem) { - GradientHelper.GetCurrentGradient(system._ratio, system._limitVelocityGradients!, (currentGradient, nextGradient, scale) => { - if (currentGradient !== particle._currentLimitVelocityGradient) { - particle._currentLimitVelocity1 = particle._currentLimitVelocity2; - particle._currentLimitVelocity2 = (nextGradient).getFactor(); - particle._currentLimitVelocityGradient = currentGradient; - } - - const limitVelocity = Lerp(particle._currentLimitVelocity1, particle._currentLimitVelocity2, scale); - const currentVelocity = particle.direction.length(); - - if (currentVelocity > limitVelocity) { - particle.direction.scaleInPlace(system.limitVelocityDamping); - } - }); -} - -/** @internal */ -export function _ProcessDirection(particle: Particle) { - particle.direction.scaleToRef(particle._directionScale, particle._scaledDirection); -} - -/** Position */ - -/** @internal */ -export function _CreatePositionData(particle: Particle, system: ThinParticleSystem) { - system.particleEmitterType.startPositionFunction(system._emitterWorldMatrix, particle.position, particle, system.isLocal); -} - -/** @internal */ -export function _CreateCustomPositionData(particle: Particle, system: ThinParticleSystem) { - system.startPositionFunction!(system._emitterWorldMatrix, particle.position, particle, system.isLocal); -} - -/** @internal */ -export function _CreateIsLocalData(particle: Particle, system: ThinParticleSystem) { - if (!particle._localPosition) { - particle._localPosition = particle.position.clone(); - } else { - particle._localPosition.copyFrom(particle.position); - } - Vector3.TransformCoordinatesToRef(particle._localPosition, system._emitterWorldMatrix, particle.position); -} - -/** @internal */ -export function _ProcessPosition(particle: Particle, system: ThinParticleSystem) { - if (system.isLocal && particle._localPosition) { - particle._localPosition!.addInPlace(particle._scaledDirection); - Vector3.TransformCoordinatesToRef(particle._localPosition!, system._emitterWorldMatrix, particle.position); - } else { - particle.position.addInPlace(particle._scaledDirection); - } -} - -/** Drag */ - -/** @internal */ -export function _CreateDragData(particle: Particle, system: ThinParticleSystem) { - particle._currentDragGradient = system._dragGradients![0]; - particle._currentDrag1 = particle._currentDragGradient.getFactor(); - - if (system._dragGradients!.length > 1) { - particle._currentDrag2 = system._dragGradients![1].getFactor(); - } else { - particle._currentDrag2 = particle._currentDrag1; - } -} - -/** @internal */ -export function _ProcessDragGradients(particle: Particle, system: ThinParticleSystem) { - GradientHelper.GetCurrentGradient(system._ratio, system._dragGradients!, (currentGradient, nextGradient, scale) => { - if (currentGradient !== particle._currentDragGradient) { - particle._currentDrag1 = particle._currentDrag2; - particle._currentDrag2 = (nextGradient).getFactor(); - particle._currentDragGradient = currentGradient; - } - - const drag = Lerp(particle._currentDrag1, particle._currentDrag2, scale); - - particle._scaledDirection.scaleInPlace(1.0 - drag); - }); -} - -/** Noise */ - -/** @internal */ -export function _CreateNoiseData(particle: Particle, _system: ThinParticleSystem) { - if (particle._randomNoiseCoordinates1 && particle._randomNoiseCoordinates2) { - particle._randomNoiseCoordinates1.copyFromFloats(Math.random(), Math.random(), Math.random()); - particle._randomNoiseCoordinates2.copyFromFloats(Math.random(), Math.random(), Math.random()); - } else { - particle._randomNoiseCoordinates1 = new Vector3(Math.random(), Math.random(), Math.random()); - particle._randomNoiseCoordinates2 = new Vector3(Math.random(), Math.random(), Math.random()); - } -} - -/** @internal */ -export function _ProcessNoise(particle: Particle, system: ThinParticleSystem) { - const noiseTextureData = system._noiseTextureData; - const noiseTextureSize = system._noiseTextureSize; - - if (noiseTextureData && noiseTextureSize && particle._randomNoiseCoordinates1 && particle._randomNoiseCoordinates2) { - const fetchedColorR = system._fetchR( - particle._randomNoiseCoordinates1.x, - particle._randomNoiseCoordinates1.y, - noiseTextureSize.width, - noiseTextureSize.height, - noiseTextureData - ); - const fetchedColorG = system._fetchR( - particle._randomNoiseCoordinates1.z, - particle._randomNoiseCoordinates2.x, - noiseTextureSize.width, - noiseTextureSize.height, - noiseTextureData - ); - const fetchedColorB = system._fetchR( - particle._randomNoiseCoordinates2.y, - particle._randomNoiseCoordinates2.z, - noiseTextureSize.width, - noiseTextureSize.height, - noiseTextureData - ); - - const force = TmpVectors.Vector3[0]; - const scaledForce = TmpVectors.Vector3[1]; - - force.copyFromFloats((2 * fetchedColorR - 1) * system.noiseStrength.x, (2 * fetchedColorG - 1) * system.noiseStrength.y, (2 * fetchedColorB - 1) * system.noiseStrength.z); - - force.scaleToRef(system._tempScaledUpdateSpeed, scaledForce); - particle.direction.addInPlace(scaledForce); - } -} - -/** Gravity */ - -/** @internal */ -export function _ProcessGravity(particle: Particle, system: ThinParticleSystem) { - system.gravity.scaleToRef(system._tempScaledUpdateSpeed, system._scaledGravity); - particle.direction.addInPlace(system._scaledGravity); -} - -/** Size */ - -/** @internal */ -export function _CreateSizeData(particle: Particle, system: ThinParticleSystem) { - particle.size = RandomRange(system.minSize, system.maxSize); - particle.scale.copyFromFloats(RandomRange(system.minScaleX, system.maxScaleX), RandomRange(system.minScaleY, system.maxScaleY)); -} - -/** @internal */ -export function _CreateSizeGradientsData(particle: Particle, system: ThinParticleSystem) { - particle._currentSizeGradient = system._sizeGradients![0]; - particle._currentSize1 = particle._currentSizeGradient.getFactor(); - particle.size = particle._currentSize1; - - if (system._sizeGradients!.length > 1) { - particle._currentSize2 = system._sizeGradients![1].getFactor(); - } else { - particle._currentSize2 = particle._currentSize1; - } - - particle.scale.copyFromFloats(RandomRange(system.minScaleX, system.maxScaleX), RandomRange(system.minScaleY, system.maxScaleY)); -} - -/** @internal */ -export function _CreateStartSizeGradientsData(particle: Particle, system: ThinParticleSystem) { - const ratio = system._actualFrame / system.targetStopDuration; - GradientHelper.GetCurrentGradient(ratio, system._startSizeGradients!, (currentGradient, nextGradient, scale) => { - if (currentGradient !== system._currentStartSizeGradient) { - system._currentStartSize1 = system._currentStartSize2; - system._currentStartSize2 = (nextGradient).getFactor(); - system._currentStartSizeGradient = currentGradient; - } - - const value = Lerp(system._currentStartSize1, system._currentStartSize2, scale); - particle.scale.scaleInPlace(value); - }); -} - -/** @internal */ -export function _ProcessSizeGradients(particle: Particle, system: ThinParticleSystem) { - GradientHelper.GetCurrentGradient(system._ratio, system._sizeGradients!, (currentGradient, nextGradient, scale) => { - if (currentGradient !== particle._currentSizeGradient) { - particle._currentSize1 = particle._currentSize2; - particle._currentSize2 = (nextGradient).getFactor(); - particle._currentSizeGradient = currentGradient; - } - particle.size = Lerp(particle._currentSize1, particle._currentSize2, scale); - }); -} - -/** Ramp */ - -/** @internal */ -export function _CreateRampData(particle: Particle, _system: ThinParticleSystem) { - particle.remapData = new Vector4(0, 1, 0, 1); -} - -/** Remap */ - -/** @internal */ -export function _ProcessRemapGradients(particle: Particle, system: ThinParticleSystem) { - if (system._colorRemapGradients && system._colorRemapGradients.length > 0) { - GradientHelper.GetCurrentGradient(system._ratio, system._colorRemapGradients, (currentGradient, nextGradient, scale) => { - const min = Lerp((currentGradient).factor1, (nextGradient).factor1, scale); - const max = Lerp((currentGradient).factor2!, (nextGradient).factor2!, scale); - - particle.remapData.x = min; - particle.remapData.y = max - min; - }); - } - - if (system._alphaRemapGradients && system._alphaRemapGradients.length > 0) { - GradientHelper.GetCurrentGradient(system._ratio, system._alphaRemapGradients, (currentGradient, nextGradient, scale) => { - const min = Lerp((currentGradient).factor1, (nextGradient).factor1, scale); - const max = Lerp((currentGradient).factor2!, (nextGradient).factor2!, scale); - - particle.remapData.z = min; - particle.remapData.w = max - min; - }); - } -} - -/** Life */ - -/** @internal */ -export function _CreateLifeGradientsData(particle: Particle, system: ThinParticleSystem) { - const ratio = Clamp(system._actualFrame / system.targetStopDuration); - GradientHelper.GetCurrentGradient(ratio, system._lifeTimeGradients!, (currentGradient, nextGradient) => { - const factorGradient1 = currentGradient; - const factorGradient2 = nextGradient; - const lifeTime1 = factorGradient1.getFactor(); - const lifeTime2 = factorGradient2.getFactor(); - const gradient = (ratio - factorGradient1.gradient) / (factorGradient2.gradient - factorGradient1.gradient); - particle.lifeTime = Lerp(lifeTime1, lifeTime2, gradient); - }); - system._emitPower = RandomRange(system.minEmitPower, system.maxEmitPower); -} - -/** @internal */ -export function _CreateLifetimeData(particle: Particle, system: ThinParticleSystem) { - particle.lifeTime = RandomRange(system.minLifeTime, system.maxLifeTime); - system._emitPower = RandomRange(system.minEmitPower, system.maxEmitPower); -} - -/** Emit power */ - -/** @internal */ -export function _CreateEmitPowerData(particle: Particle, system: ThinParticleSystem) { - if (system._emitPower === 0) { - if (!particle._initialDirection) { - particle._initialDirection = particle.direction.clone(); - } else { - particle._initialDirection.copyFrom(particle.direction); - } - particle.direction.set(0, 0, 0); - } else { - particle._initialDirection = null; - particle.direction.scaleInPlace(system._emitPower); - } - - // Inherited Velocity - particle.direction.addInPlace(system._inheritedVelocityOffset); -} - -/** Angle */ - -/** @internal */ -export function _CreateAngleData(particle: Particle, system: ThinParticleSystem) { - particle.angularSpeed = RandomRange(system.minAngularSpeed, system.maxAngularSpeed); - particle.angle = RandomRange(system.minInitialRotation, system.maxInitialRotation); -} - -/** @internal */ -export function _CreateAngleGradientsData(particle: Particle, system: ThinParticleSystem) { - particle._currentAngularSpeedGradient = system._angularSpeedGradients![0]; - particle.angularSpeed = particle._currentAngularSpeedGradient.getFactor(); - particle._currentAngularSpeed1 = particle.angularSpeed; - - if (system._angularSpeedGradients!.length > 1) { - particle._currentAngularSpeed2 = system._angularSpeedGradients![1].getFactor(); - } else { - particle._currentAngularSpeed2 = particle._currentAngularSpeed1; - } - particle.angle = RandomRange(system.minInitialRotation, system.maxInitialRotation); -} - -/** Sheet */ - -/** @internal */ -export function _CreateSheetData(particle: Particle, system: ThinParticleSystem) { - particle._initialStartSpriteCellId = system.startSpriteCellID; - particle._initialEndSpriteCellId = system.endSpriteCellID; - particle._initialSpriteCellLoop = system.spriteCellLoop; -} diff --git a/tools/src/effect/bjs/thinParticleSystem.ts b/tools/src/effect/bjs/thinParticleSystem.ts deleted file mode 100644 index c3fdb6f5e..000000000 --- a/tools/src/effect/bjs/thinParticleSystem.ts +++ /dev/null @@ -1,2422 +0,0 @@ -import type { Immutable, Nullable } from "../types"; -import { FactorGradient, ColorGradient, Color3Gradient, GradientHelper } from "../Misc/gradients"; -import type { Observer } from "../Misc/observable"; -import { Observable } from "../Misc/observable"; -import { Vector3, Matrix, TmpVectors } from "../Maths/math.vector"; -import { VertexBuffer, Buffer } from "../Buffers/buffer"; - -import type { Effect } from "../Materials/effect"; -import { RawTexture } from "../Materials/Textures/rawTexture"; -import { EngineStore } from "../Engines/engineStore"; -import type { IDisposable, Scene } from "../scene"; - -import type { IParticleSystem } from "./IParticleSystem"; -import { BaseParticleSystem } from "./baseParticleSystem"; -import { Particle } from "./particle"; -import { Constants } from "../Engines/constants"; -import type { IAnimatable } from "../Animations/animatable.interface"; -import { DrawWrapper } from "../Materials/drawWrapper"; - -import type { DataBuffer } from "../Buffers/dataBuffer"; -import { Color4, Color3, TmpColors } from "../Maths/math.color"; -import type { ISize } from "../Maths/math.size"; -import type { AbstractEngine } from "../Engines/abstractEngine"; - -import "../Engines/Extensions/engine.alpha"; -import { AddClipPlaneUniforms, PrepareStringDefinesForClipPlanes, BindClipPlane } from "../Materials/clipPlaneMaterialHelper"; - -import type { AbstractMesh } from "../Meshes/abstractMesh"; -import type { ProceduralTexture } from "../Materials/Textures/Procedurals/proceduralTexture"; -import { BindFogParameters, BindLogDepth } from "../Materials/materialHelper.functions"; -import { BoxParticleEmitter } from "./EmitterTypes/boxParticleEmitter"; -import { Lerp } from "../Maths/math.scalar.functions"; -import { PrepareSamplersForImageProcessing, PrepareUniformsForImageProcessing } from "../Materials/imageProcessingConfiguration.functions"; -import type { ThinEngine } from "../Engines/thinEngine"; -import { ShaderLanguage } from "core/Materials/shaderLanguage"; -import { - _CreateAngleData, - _CreateAngleGradientsData, - _CreateColorData, - _CreateColorDeadData, - _CreateColorGradientsData, - _CreateCustomDirectionData, - _CreateCustomPositionData, - _CreateDirectionData, - _CreateDragData, - _CreateEmitPowerData, - _CreateIsLocalData, - _CreateLifeGradientsData, - _CreateLifetimeData, - _CreateLimitVelocityGradients, - _CreateNoiseData, - _CreatePositionData, - _CreateRampData, - _CreateSheetData, - _CreateSizeData, - _CreateSizeGradientsData, - _CreateStartSizeGradientsData, - _CreateVelocityGradients, - _ProcessAngularSpeed, - _ProcessAngularSpeedGradients, - _ProcessColor, - _ProcessColorGradients, - _ProcessDirection, - _ProcessDragGradients, - _ProcessGravity, - _ProcessLimitVelocityGradients, - _ProcessNoise, - _ProcessPosition, - _ProcessRemapGradients, - _ProcessSizeGradients, - _ProcessVelocityGradients, -} from "./thinParticleSystem.function"; -import type { _IExecutionQueueItem } from "./Queue/executionQueue"; -import { _ConnectAfter, _ConnectBefore, _RemoveFromQueue } from "./Queue/executionQueue"; - -/** - * This represents a thin particle system in Babylon. - * Particles are often small sprites used to simulate hard-to-reproduce phenomena like fire, smoke, water, or abstract visual effects like magic glitter and faery dust. - * Particles can take different shapes while emitted like box, sphere, cone or you can write your custom function. - * This thin version contains a limited subset of the total features in order to provide users with a way to get particles but with a smaller footprint - * @example https://doc.babylonjs.com/features/featuresDeepDive/particles/particle_system/particle_system_intro - */ -export class ThinParticleSystem extends BaseParticleSystem implements IDisposable, IAnimatable, IParticleSystem { - /** - * Force all the particle systems to compile to glsl even on WebGPU engines. - * False by default. This is mostly meant for backward compatibility. - */ - public static ForceGLSL = false; - - /** - * This function can be defined to provide custom update for active particles. - * This function will be called instead of regular update (age, position, color, etc.). - * Do not forget that this function will be called on every frame so try to keep it simple and fast :) - */ - public updateFunction: (particles: Particle[]) => void; - - /** @internal */ - public _emitterWorldMatrix: Matrix; - /** @internal */ - public _emitterInverseWorldMatrix: Matrix = Matrix.Identity(); - - private _startDirectionFunction: Nullable<(worldMatrix: Matrix, directionToUpdate: Vector3, particle: Particle, isLocal: boolean) => void> = null; - - /** - * This function can be defined to specify initial direction for every new particle. - * It by default use the emitterType defined function - */ - public get startDirectionFunction(): Nullable<(worldMatrix: Matrix, directionToUpdate: Vector3, particle: Particle, isLocal: boolean) => void> { - return this._startDirectionFunction; - } - - public set startDirectionFunction(value: Nullable<(worldMatrix: Matrix, directionToUpdate: Vector3, particle: Particle, isLocal: boolean) => void>) { - if (this._startDirectionFunction === value) { - return; - } - this._startDirectionFunction = value; - - if (value) { - this._directionProcessing.process = _CreateCustomDirectionData; - } else { - this._directionProcessing.process = _CreateDirectionData; - } - } - - private _startPositionFunction: Nullable<(worldMatrix: Matrix, positionToUpdate: Vector3, particle: Particle, isLocal: boolean) => void> = null; - - /** - * This function can be defined to specify initial position for every new particle. - * It by default use the emitterType defined function - */ - public get startPositionFunction(): Nullable<(worldMatrix: Matrix, positionToUpdate: Vector3, particle: Particle, isLocal: boolean) => void> { - return this._startPositionFunction; - } - - public set startPositionFunction(value: Nullable<(worldMatrix: Matrix, positionToUpdate: Vector3, particle: Particle, isLocal: boolean) => void>) { - if (this._startPositionFunction === value) { - return; - } - this._startPositionFunction = value; - - if (value) { - this._positionCreation.process = _CreateCustomPositionData; - } else { - this._positionCreation.process = _CreatePositionData; - } - } - - /** - * @internal - */ - public _inheritedVelocityOffset = new Vector3(); - /** - * An event triggered when the system is disposed - */ - public onDisposeObservable = new Observable(); - /** - * An event triggered when the system is stopped - */ - public onStoppedObservable = new Observable(); - /** - * An event triggered when the system is started - */ - public onStartedObservable = new Observable(); - - private _onDisposeObserver: Nullable>; - /** - * Sets a callback that will be triggered when the system is disposed - */ - public set onDispose(callback: () => void) { - if (this._onDisposeObserver) { - this.onDisposeObservable.remove(this._onDisposeObserver); - } - this._onDisposeObserver = this.onDisposeObservable.add(callback); - } - - /** @internal */ - public _noiseTextureSize: Nullable = null; - /** @internal */ - public _noiseTextureData: Nullable = null; - private _particles = new Array(); - private _epsilon: number; - private _capacity: number; - private _stockParticles = new Array(); - private _newPartsExcess = 0; - private _vertexData: Float32Array; - private _vertexBuffer: Nullable; - private _vertexBuffers: { [key: string]: VertexBuffer } = {}; - private _spriteBuffer: Nullable; - private _indexBuffer: Nullable; - private _linesIndexBuffer: Nullable; - private _linesIndexBufferUseInstancing: Nullable; - private _drawWrappers: DrawWrapper[][]; // first index is render pass id, second index is blend mode - /** @internal */ - public _customWrappers: { [blendMode: number]: Nullable }; - /** @internal */ - public _scaledColorStep = new Color4(0, 0, 0, 0); - /** @internal */ - public _colorDiff = new Color4(0, 0, 0, 0); - /** @internal */ - public _scaledGravity = Vector3.Zero(); - private _currentRenderId = -1; - private _alive: boolean; - private _useInstancing = false; - private _vertexArrayObject: Nullable; - - private _isDisposed = false; - - /** - * Gets a boolean indicating that the particle system was disposed - */ - public get isDisposed(): boolean { - return this._isDisposed; - } - - private _started = false; - private _stopped = false; - /** @internal */ - public _actualFrame = 0; - /** @internal */ - public _scaledUpdateSpeed: number; - private _vertexBufferSize: number; - - /** @internal */ - public _currentEmitRateGradient: Nullable; - /** @internal */ - public _currentEmitRate1 = 0; - /** @internal */ - public _currentEmitRate2 = 0; - - /** @internal */ - public _currentStartSizeGradient: Nullable; - /** @internal */ - public _currentStartSize1 = 0; - /** @internal */ - public _currentStartSize2 = 0; - - /** Indicates that the update of particles is done in the animate function */ - public readonly updateInAnimate = true; - - private readonly _rawTextureWidth = 256; - private _rampGradientsTexture: Nullable; - private _useRampGradients = false; - - /** @internal */ - public _updateQueueStart: Nullable<_IExecutionQueueItem> = null; - protected _colorProcessing: _IExecutionQueueItem; - protected _angularSpeedGradientProcessing: _IExecutionQueueItem; - protected _angularSpeedProcessing: _IExecutionQueueItem; - protected _velocityGradientProcessing: _IExecutionQueueItem; - protected _directionProcessing: _IExecutionQueueItem; - protected _limitVelocityGradientProcessing: _IExecutionQueueItem; - protected _positionProcessing: _IExecutionQueueItem; - protected _dragGradientProcessing: _IExecutionQueueItem; - protected _noiseProcessing: _IExecutionQueueItem; - protected _gravityProcessing: _IExecutionQueueItem; - protected _sizeGradientProcessing: _IExecutionQueueItem; - protected _remapGradientProcessing: _IExecutionQueueItem; - - /** @internal */ - public _lifeTimeCreation: _IExecutionQueueItem; - /** @internal */ - public _positionCreation: _IExecutionQueueItem; - private _isLocalCreation: _IExecutionQueueItem; - /** @internal */ - public _directionCreation: _IExecutionQueueItem; - private _emitPowerCreation: _IExecutionQueueItem; - /** @internal */ - public _sizeCreation: _IExecutionQueueItem; - private _startSizeCreation: Nullable<_IExecutionQueueItem> = null; - /** @internal */ - public _angleCreation: _IExecutionQueueItem; - private _velocityCreation: _IExecutionQueueItem; - private _limitVelocityCreation: _IExecutionQueueItem; - private _dragCreation: _IExecutionQueueItem; - /** @internal */ - public _colorCreation: _IExecutionQueueItem; - /** @internal */ - public _colorDeadCreation: _IExecutionQueueItem; - private _sheetCreation: _IExecutionQueueItem; - private _rampCreation: _IExecutionQueueItem; - private _noiseCreation: _IExecutionQueueItem; - private _createQueueStart: Nullable<_IExecutionQueueItem> = null; - - /** @internal */ - public _tempScaledUpdateSpeed: number; - /** @internal */ - public _ratio: number; - /** @internal */ - public _emitPower: number; - - /** Gets or sets a matrix to use to compute projection */ - public defaultProjectionMatrix: Matrix; - - /** Gets or sets a matrix to use to compute view */ - public defaultViewMatrix: Matrix; - - /** Gets or sets a boolean indicating that ramp gradients must be used - * @see https://doc.babylonjs.com/features/featuresDeepDive/particles/particle_system/particle_system_intro#ramp-gradients - */ - public get useRampGradients(): boolean { - return this._useRampGradients; - } - - public set useRampGradients(value: boolean) { - if (this._useRampGradients === value) { - return; - } - - this._useRampGradients = value; - - this._resetEffect(); - - if (value) { - this._rampCreation = { - process: _CreateRampData, - previousItem: null, - nextItem: null, - }; - _ConnectAfter(this._rampCreation, this._colorDeadCreation); - this._remapGradientProcessing = { - process: _ProcessRemapGradients, - previousItem: null, - nextItem: null, - }; - _ConnectAfter(this._remapGradientProcessing, this._gravityProcessing); - } else { - _RemoveFromQueue(this._rampCreation); - _RemoveFromQueue(this._remapGradientProcessing); - } - } - - private _isLocal = false; - - /** - * Specifies if the particles are updated in emitter local space or world space - */ - public get isLocal() { - return this._isLocal; - } - - public set isLocal(value: boolean) { - if (this._isLocal === value) { - return; - } - - this._isLocal = value; - - if (value) { - this._isLocalCreation = { - process: _CreateIsLocalData, - previousItem: null, - nextItem: null, - }; - - _ConnectAfter(this._isLocalCreation, this._positionCreation); - } else { - _RemoveFromQueue(this._isLocalCreation); - } - } - - /** Indicates that the particle system is CPU based */ - public readonly isGPU = false; - - /** - * Gets the current list of active particles - */ - public get particles(): Particle[] { - return this._particles; - } - - /** Shader language used by the material */ - protected _shaderLanguage = ShaderLanguage.GLSL; - - /** - * Gets the shader language used in this material. - */ - public get shaderLanguage(): ShaderLanguage { - return this._shaderLanguage; - } - - /** @internal */ - public override get _isAnimationSheetEnabled() { - return this._animationSheetEnabled; - } - - public override set _isAnimationSheetEnabled(value: boolean) { - if (this._animationSheetEnabled === value) { - return; - } - - this._animationSheetEnabled = value; - - if (value) { - this._sheetCreation = { - process: _CreateSheetData, - previousItem: null, - nextItem: null, - }; - - _ConnectAfter(this._sheetCreation, this._colorDeadCreation); - } else { - _RemoveFromQueue(this._sheetCreation); - } - - this._reset(); - } - - /** - * Gets the number of particles active at the same time. - * @returns The number of active particles. - */ - public getActiveCount() { - return this._particles.length; - } - - /** - * Returns the string "ParticleSystem" - * @returns a string containing the class name - */ - public getClassName(): string { - return "ParticleSystem"; - } - - /** - * Gets a boolean indicating that the system is stopping - * @returns true if the system is currently stopping - */ - public isStopping() { - return this._stopped && this.isAlive(); - } - - /** - * Gets the custom effect used to render the particles - * @param blendMode Blend mode for which the effect should be retrieved - * @returns The effect - */ - public getCustomEffect(blendMode: number = 0): Nullable { - return this._customWrappers[blendMode]?.effect ?? this._customWrappers[0]!.effect; - } - - private _getCustomDrawWrapper(blendMode: number = 0): Nullable { - return this._customWrappers[blendMode] ?? this._customWrappers[0]; - } - - /** - * Sets the custom effect used to render the particles - * @param effect The effect to set - * @param blendMode Blend mode for which the effect should be set - */ - public setCustomEffect(effect: Nullable, blendMode: number = 0) { - this._customWrappers[blendMode] = new DrawWrapper(this._engine); - this._customWrappers[blendMode].effect = effect; - if (this._customWrappers[blendMode].drawContext) { - this._customWrappers[blendMode].drawContext.useInstancing = this._useInstancing; - } - } - - /** @internal */ - private _onBeforeDrawParticlesObservable: Nullable>> = null; - - /** - * Observable that will be called just before the particles are drawn - */ - public get onBeforeDrawParticlesObservable(): Observable> { - if (!this._onBeforeDrawParticlesObservable) { - this._onBeforeDrawParticlesObservable = new Observable>(); - } - - return this._onBeforeDrawParticlesObservable; - } - - /** - * Gets the name of the particle vertex shader - */ - public get vertexShaderName(): string { - return "particles"; - } - - /** - * Gets the vertex buffers used by the particle system - */ - public get vertexBuffers(): Immutable<{ [key: string]: VertexBuffer }> { - return this._vertexBuffers; - } - - /** - * Gets the index buffer used by the particle system (or null if no index buffer is used (if _useInstancing=true)) - */ - public get indexBuffer(): Nullable { - return this._indexBuffer; - } - - public override get noiseTexture() { - return this._noiseTexture; - } - - public override set noiseTexture(value: Nullable) { - if (this.noiseTexture === value) { - return; - } - - this._noiseTexture = value; - - if (!value) { - _RemoveFromQueue(this._noiseCreation); - _RemoveFromQueue(this._noiseProcessing); - return; - } - - this._noiseCreation = { - process: _CreateNoiseData, - previousItem: null, - nextItem: null, - }; - _ConnectAfter(this._noiseCreation, this._colorDeadCreation); - - this._noiseProcessing = { - process: _ProcessNoise, - previousItem: null, - nextItem: null, - }; - _ConnectAfter(this._noiseProcessing, this._positionProcessing); - } - - /** - * Instantiates a particle system. - * Particles are often small sprites used to simulate hard-to-reproduce phenomena like fire, smoke, water, or abstract visual effects like magic glitter and faery dust. - * @param name The name of the particle system - * @param capacity The max number of particles alive at the same time - * @param sceneOrEngine The scene the particle system belongs to or the engine to use if no scene - * @param customEffect a custom effect used to change the way particles are rendered by default - * @param isAnimationSheetEnabled Must be true if using a spritesheet to animate the particles texture - * @param epsilon Offset used to render the particles - * @param noUpdateQueue If true, the particle system will start with an empty update queue - */ - constructor( - name: string, - capacity: number, - sceneOrEngine: Scene | AbstractEngine, - customEffect: Nullable = null, - isAnimationSheetEnabled: boolean = false, - epsilon: number = 0.01, - noUpdateQueue: boolean = false - ) { - super(name); - - this._capacity = capacity; - - this._epsilon = epsilon; - - if (!sceneOrEngine || sceneOrEngine.getClassName() === "Scene") { - this._scene = (sceneOrEngine as Scene) || EngineStore.LastCreatedScene; - this._engine = this._scene.getEngine(); - this.uniqueId = this._scene.getUniqueId(); - this._scene.particleSystems.push(this); - } else { - this._engine = sceneOrEngine as AbstractEngine; - this.defaultProjectionMatrix = Matrix.PerspectiveFovLH(0.8, 1, 0.1, 100, this._engine.isNDCHalfZRange); - } - - if (this._engine.getCaps().vertexArrayObject) { - this._vertexArrayObject = null; - } - - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this._initShaderSourceAsync(); - - // Creation queue - this._lifeTimeCreation = { - process: _CreateLifetimeData, - previousItem: null, - nextItem: null, - }; - - this._positionCreation = { - process: _CreatePositionData, - previousItem: null, - nextItem: null, - }; - _ConnectAfter(this._positionCreation, this._lifeTimeCreation); - - this._directionCreation = { - process: _CreateDirectionData, - previousItem: null, - nextItem: null, - }; - _ConnectAfter(this._directionCreation, this._positionCreation); - - this._emitPowerCreation = { - process: _CreateEmitPowerData, - previousItem: null, - nextItem: null, - }; - _ConnectAfter(this._emitPowerCreation, this._directionCreation); - - this._sizeCreation = { - process: _CreateSizeData, - previousItem: null, - nextItem: null, - }; - _ConnectAfter(this._sizeCreation, this._emitPowerCreation); - - this._angleCreation = { - process: _CreateAngleData, - previousItem: null, - nextItem: null, - }; - _ConnectAfter(this._angleCreation, this._sizeCreation); - - this._colorCreation = { - process: _CreateColorData, - previousItem: null, - nextItem: null, - }; - _ConnectAfter(this._colorCreation, this._angleCreation); - - this._colorDeadCreation = { - process: _CreateColorDeadData, - previousItem: null, - nextItem: null, - }; - _ConnectAfter(this._colorDeadCreation, this._colorCreation); - - this._createQueueStart = this._lifeTimeCreation; - - // Processing queue - if (!noUpdateQueue) { - this._colorProcessing = { - process: _ProcessColor, - previousItem: null, - nextItem: null, - }; - - this._angularSpeedProcessing = { - process: _ProcessAngularSpeed, - previousItem: null, - nextItem: null, - }; - _ConnectAfter(this._angularSpeedProcessing, this._colorProcessing); - - this._directionProcessing = { - process: _ProcessDirection, - previousItem: null, - nextItem: null, - }; - _ConnectAfter(this._directionProcessing, this._angularSpeedProcessing); - - this._positionProcessing = { - process: _ProcessPosition, - previousItem: null, - nextItem: null, - }; - _ConnectAfter(this._positionProcessing, this._directionProcessing); - - this._gravityProcessing = { - process: _ProcessGravity, - previousItem: null, - nextItem: null, - }; - - _ConnectAfter(this._gravityProcessing, this._positionProcessing); - - this._updateQueueStart = this._colorProcessing; - } - - this._isAnimationSheetEnabled = isAnimationSheetEnabled; - - // Setup the default processing configuration to the scene. - this._attachImageProcessingConfiguration(null); - - // eslint-disable-next-line @typescript-eslint/naming-convention - this._customWrappers = { 0: new DrawWrapper(this._engine) }; - this._customWrappers[0]!.effect = customEffect; - - this._drawWrappers = []; - this._useInstancing = this._engine.getCaps().instancedArrays; - - this._createIndexBuffer(); - this._createVertexBuffers(); - - // Default emitter type - this.particleEmitterType = new BoxParticleEmitter(); - - // Update - this.updateFunction = (particles: Particle[]): void => { - if (this.noiseTexture) { - // We need to get texture data back to CPU - this._noiseTextureSize = this.noiseTexture.getSize(); - // eslint-disable-next-line @typescript-eslint/no-floating-promises, github/no-then - this.noiseTexture.getContent()?.then((data) => { - this._noiseTextureData = data as Uint8Array; - }); - } - - const sameParticleArray = particles === this._particles; - - for (let index = 0; index < particles.length; index++) { - const particle = particles[index]; - - this._tempScaledUpdateSpeed = this._scaledUpdateSpeed; - const previousAge = particle.age; - particle.age += this._tempScaledUpdateSpeed; - - // Evaluate step to death - if (particle.age > particle.lifeTime) { - const diff = particle.age - previousAge; - const oldDiff = particle.lifeTime - previousAge; - - this._tempScaledUpdateSpeed = (oldDiff * this._tempScaledUpdateSpeed) / diff; - - particle.age = particle.lifeTime; - } - - this._ratio = particle.age / particle.lifeTime; - particle._directionScale = this._tempScaledUpdateSpeed; - - // Processing queue - let currentQueueItem = this._updateQueueStart; - - while (currentQueueItem) { - currentQueueItem.process(particle, this); - currentQueueItem = currentQueueItem.nextItem; - } - - if (this._isAnimationSheetEnabled && !noUpdateQueue) { - particle.updateCellIndex(); - } - - // Update the position of the attached sub-emitters to match their attached particle - particle._inheritParticleInfoToSubEmitters(); - - if (particle.age >= particle.lifeTime) { - // Recycle by swapping with last particle - this._emitFromParticle(particle); - if (particle._attachedSubEmitters) { - for (const subEmitter of particle._attachedSubEmitters) { - subEmitter.particleSystem.disposeOnStop = true; - subEmitter.particleSystem.stop(); - } - particle._attachedSubEmitters = null; - } - this.recycleParticle(particle); - if (sameParticleArray) { - index--; - } - continue; - } - } - }; - } - - /** @internal */ - public _emitFromParticle: (particle: Particle) => void = (_particle) => { - // Do nothing - }; - - serialize(_serializeTexture: boolean) { - throw new Error("Method not implemented."); - } - - /** - * Clones the particle system. - * @param name The name of the cloned object - * @param newEmitter The new emitter to use - * @param _cloneTexture Also clone the textures if true - */ - public clone(name: string, newEmitter: any, _cloneTexture = false): ThinParticleSystem { - throw new Error("Method not implemented."); - } - - private _addFactorGradient(factorGradients: FactorGradient[], gradient: number, factor: number, factor2?: number) { - const newGradient = new FactorGradient(gradient, factor, factor2); - factorGradients.push(newGradient); - - factorGradients.sort((a, b) => { - if (a.gradient < b.gradient) { - return -1; - } else if (a.gradient > b.gradient) { - return 1; - } - - return 0; - }); - } - - private _removeFactorGradient(factorGradients: Nullable, gradient: number) { - if (!factorGradients) { - return; - } - - let index = 0; - for (const factorGradient of factorGradients) { - if (factorGradient.gradient === gradient) { - factorGradients.splice(index, 1); - break; - } - index++; - } - } - - private _syncLifeTimeCreation() { - if (this.targetStopDuration && this._lifeTimeGradients && this._lifeTimeGradients.length > 0) { - this._lifeTimeCreation.process = _CreateLifeGradientsData; - return; - } - - this._lifeTimeCreation.process = _CreateLifetimeData; - } - - private _syncStartSizeCreation() { - if (this._startSizeGradients && this._startSizeGradients[0] && this.targetStopDuration) { - if (!this._startSizeCreation) { - this._startSizeCreation = { - process: _CreateStartSizeGradientsData, - previousItem: null, - nextItem: null, - }; - _ConnectAfter(this._startSizeCreation, this._sizeCreation); - } - return; - } - - if (this._startSizeCreation) { - _RemoveFromQueue(this._startSizeCreation); - this._startSizeCreation = null; - } - } - - public override get targetStopDuration(): number { - return this._targetStopDuration; - } - - public override set targetStopDuration(value: number) { - if (this.targetStopDuration === value) { - return; - } - - this._targetStopDuration = value; - - this._syncLifeTimeCreation(); - this._syncStartSizeCreation(); - } - - /** - * Adds a new life time gradient - * @param gradient defines the gradient to use (between 0 and 1) - * @param factor defines the life time factor to affect to the specified gradient - * @param factor2 defines an additional factor used to define a range ([factor, factor2]) with main value to pick the final value from - * @returns the current particle system - */ - public addLifeTimeGradient(gradient: number, factor: number, factor2?: number): IParticleSystem { - if (!this._lifeTimeGradients) { - this._lifeTimeGradients = []; - } - - this._addFactorGradient(this._lifeTimeGradients, gradient, factor, factor2); - - this._syncLifeTimeCreation(); - - return this; - } - - /** - * Remove a specific life time gradient - * @param gradient defines the gradient to remove - * @returns the current particle system - */ - public removeLifeTimeGradient(gradient: number): IParticleSystem { - this._removeFactorGradient(this._lifeTimeGradients, gradient); - - this._syncLifeTimeCreation(); - - return this; - } - - /** - * Adds a new size gradient - * @param gradient defines the gradient to use (between 0 and 1) - * @param factor defines the size factor to affect to the specified gradient - * @param factor2 defines an additional factor used to define a range ([factor, factor2]) with main value to pick the final value from - * @returns the current particle system - */ - public addSizeGradient(gradient: number, factor: number, factor2?: number): IParticleSystem { - if (!this._sizeGradients) { - this._sizeGradients = []; - } - - if (this._sizeGradients.length === 0) { - this._sizeCreation.process = _CreateSizeGradientsData; - - this._sizeGradientProcessing = { - process: _ProcessSizeGradients, - previousItem: null, - nextItem: null, - }; - _ConnectBefore(this._sizeGradientProcessing, this._gravityProcessing); - } - - this._addFactorGradient(this._sizeGradients, gradient, factor, factor2); - - return this; - } - - /** - * Remove a specific size gradient - * @param gradient defines the gradient to remove - * @returns the current particle system - */ - public removeSizeGradient(gradient: number): IParticleSystem { - this._removeFactorGradient(this._sizeGradients, gradient); - - if (this._sizeGradients?.length === 0) { - _RemoveFromQueue(this._sizeGradientProcessing); - this._sizeCreation.process = _CreateSizeData; - } - - return this; - } - - /** - * Adds a new color remap gradient - * @param gradient defines the gradient to use (between 0 and 1) - * @param min defines the color remap minimal range - * @param max defines the color remap maximal range - * @returns the current particle system - */ - public addColorRemapGradient(gradient: number, min: number, max: number): IParticleSystem { - if (!this._colorRemapGradients) { - this._colorRemapGradients = []; - } - - this._addFactorGradient(this._colorRemapGradients, gradient, min, max); - - return this; - } - - /** - * Remove a specific color remap gradient - * @param gradient defines the gradient to remove - * @returns the current particle system - */ - public removeColorRemapGradient(gradient: number): IParticleSystem { - this._removeFactorGradient(this._colorRemapGradients, gradient); - - return this; - } - - /** - * Adds a new alpha remap gradient - * @param gradient defines the gradient to use (between 0 and 1) - * @param min defines the alpha remap minimal range - * @param max defines the alpha remap maximal range - * @returns the current particle system - */ - public addAlphaRemapGradient(gradient: number, min: number, max: number): IParticleSystem { - if (!this._alphaRemapGradients) { - this._alphaRemapGradients = []; - } - - this._addFactorGradient(this._alphaRemapGradients, gradient, min, max); - - return this; - } - - /** - * Remove a specific alpha remap gradient - * @param gradient defines the gradient to remove - * @returns the current particle system - */ - public removeAlphaRemapGradient(gradient: number): IParticleSystem { - this._removeFactorGradient(this._alphaRemapGradients, gradient); - - return this; - } - - /** - * Adds a new angular speed gradient - * @param gradient defines the gradient to use (between 0 and 1) - * @param factor defines the angular speed to affect to the specified gradient - * @param factor2 defines an additional factor used to define a range ([factor, factor2]) with main value to pick the final value from - * @returns the current particle system - */ - public addAngularSpeedGradient(gradient: number, factor: number, factor2?: number): IParticleSystem { - if (!this._angularSpeedGradients) { - this._angularSpeedGradients = []; - } - - if (this._angularSpeedGradients.length === 0) { - this._angleCreation.process = _CreateAngleGradientsData; - - this._angularSpeedGradientProcessing = { - process: _ProcessAngularSpeedGradients, - previousItem: null, - nextItem: null, - }; - - _ConnectBefore(this._angularSpeedGradientProcessing, this._angularSpeedProcessing); - } - - this._addFactorGradient(this._angularSpeedGradients, gradient, factor, factor2); - - return this; - } - - /** - * Remove a specific angular speed gradient - * @param gradient defines the gradient to remove - * @returns the current particle system - */ - public removeAngularSpeedGradient(gradient: number): IParticleSystem { - this._removeFactorGradient(this._angularSpeedGradients, gradient); - - if (this._angularSpeedGradients?.length === 0) { - this._angleCreation.process = _CreateAngleData; - _RemoveFromQueue(this._angularSpeedGradientProcessing); - } - - return this; - } - - /** - * Adds a new velocity gradient - * @param gradient defines the gradient to use (between 0 and 1) - * @param factor defines the velocity to affect to the specified gradient - * @param factor2 defines an additional factor used to define a range ([factor, factor2]) with main value to pick the final value from - * @returns the current particle system - */ - public addVelocityGradient(gradient: number, factor: number, factor2?: number): IParticleSystem { - if (!this._velocityGradients) { - this._velocityGradients = []; - } - - if (this._velocityGradients.length === 0) { - this._velocityCreation = { - process: _CreateVelocityGradients, - previousItem: null, - nextItem: null, - }; - _ConnectAfter(this._velocityCreation, this._angleCreation); - - this._velocityGradientProcessing = { - process: _ProcessVelocityGradients, - previousItem: null, - nextItem: null, - }; - _ConnectBefore(this._velocityGradientProcessing, this._directionProcessing); - } - - this._addFactorGradient(this._velocityGradients, gradient, factor, factor2); - - return this; - } - - /** - * Remove a specific velocity gradient - * @param gradient defines the gradient to remove - * @returns the current particle system - */ - public removeVelocityGradient(gradient: number): IParticleSystem { - this._removeFactorGradient(this._velocityGradients, gradient); - - if (this._velocityGradients?.length === 0) { - _RemoveFromQueue(this._velocityCreation); - _RemoveFromQueue(this._velocityGradientProcessing); - } - - return this; - } - - /** - * Adds a new limit velocity gradient - * @param gradient defines the gradient to use (between 0 and 1) - * @param factor defines the limit velocity value to affect to the specified gradient - * @param factor2 defines an additional factor used to define a range ([factor, factor2]) with main value to pick the final value from - * @returns the current particle system - */ - public addLimitVelocityGradient(gradient: number, factor: number, factor2?: number): IParticleSystem { - if (!this._limitVelocityGradients) { - this._limitVelocityGradients = []; - } - - if (this._limitVelocityGradients.length === 0) { - this._limitVelocityCreation = { - process: _CreateLimitVelocityGradients, - previousItem: null, - nextItem: null, - }; - _ConnectAfter(this._limitVelocityCreation, this._angleCreation); - - this._limitVelocityGradientProcessing = { - process: _ProcessLimitVelocityGradients, - previousItem: null, - nextItem: null, - }; - _ConnectAfter(this._limitVelocityGradientProcessing, this._directionProcessing); - } - - this._addFactorGradient(this._limitVelocityGradients, gradient, factor, factor2); - - return this; - } - - /** - * Remove a specific limit velocity gradient - * @param gradient defines the gradient to remove - * @returns the current particle system - */ - public removeLimitVelocityGradient(gradient: number): IParticleSystem { - this._removeFactorGradient(this._limitVelocityGradients, gradient); - - if (this._limitVelocityGradients?.length === 0) { - _RemoveFromQueue(this._limitVelocityCreation); - _RemoveFromQueue(this._limitVelocityGradientProcessing); - } - - return this; - } - - /** - * Adds a new drag gradient - * @param gradient defines the gradient to use (between 0 and 1) - * @param factor defines the drag value to affect to the specified gradient - * @param factor2 defines an additional factor used to define a range ([factor, factor2]) with main value to pick the final value from - * @returns the current particle system - */ - public addDragGradient(gradient: number, factor: number, factor2?: number): IParticleSystem { - if (!this._dragGradients) { - this._dragGradients = []; - } - - if (this._dragGradients.length === 0) { - this._dragCreation = { - process: _CreateDragData, - previousItem: null, - nextItem: null, - }; - _ConnectBefore(this._dragCreation, this._colorDeadCreation); - - this._dragGradientProcessing = { - process: _ProcessDragGradients, - previousItem: null, - nextItem: null, - }; - _ConnectBefore(this._dragGradientProcessing, this._positionProcessing); - } - - this._addFactorGradient(this._dragGradients, gradient, factor, factor2); - - return this; - } - - /** - * Remove a specific drag gradient - * @param gradient defines the gradient to remove - * @returns the current particle system - */ - public removeDragGradient(gradient: number): IParticleSystem { - this._removeFactorGradient(this._dragGradients, gradient); - - if (this._dragGradients?.length === 0) { - _RemoveFromQueue(this._dragCreation); - _RemoveFromQueue(this._dragGradientProcessing); - } - - return this; - } - - /** - * Adds a new emit rate gradient (please note that this will only work if you set the targetStopDuration property) - * @param gradient defines the gradient to use (between 0 and 1) - * @param factor defines the emit rate value to affect to the specified gradient - * @param factor2 defines an additional factor used to define a range ([factor, factor2]) with main value to pick the final value from - * @returns the current particle system - */ - public addEmitRateGradient(gradient: number, factor: number, factor2?: number): IParticleSystem { - if (!this._emitRateGradients) { - this._emitRateGradients = []; - } - - this._addFactorGradient(this._emitRateGradients, gradient, factor, factor2); - return this; - } - - /** - * Remove a specific emit rate gradient - * @param gradient defines the gradient to remove - * @returns the current particle system - */ - public removeEmitRateGradient(gradient: number): IParticleSystem { - this._removeFactorGradient(this._emitRateGradients, gradient); - - return this; - } - - /** - * Adds a new start size gradient (please note that this will only work if you set the targetStopDuration property) - * @param gradient defines the gradient to use (between 0 and 1) - * @param factor defines the start size value to affect to the specified gradient - * @param factor2 defines an additional factor used to define a range ([factor, factor2]) with main value to pick the final value from - * @returns the current particle system - */ - public addStartSizeGradient(gradient: number, factor: number, factor2?: number): IParticleSystem { - if (!this._startSizeGradients) { - this._startSizeGradients = []; - } - - this._addFactorGradient(this._startSizeGradients, gradient, factor, factor2); - - this._syncStartSizeCreation(); - - return this; - } - - /** - * Remove a specific start size gradient - * @param gradient defines the gradient to remove - * @returns the current particle system - */ - public removeStartSizeGradient(gradient: number): IParticleSystem { - this._removeFactorGradient(this._startSizeGradients, gradient); - - this._syncStartSizeCreation(); - - return this; - } - - private _createRampGradientTexture() { - if (!this._rampGradients || !this._rampGradients.length || this._rampGradientsTexture || !this._scene) { - return; - } - - const data = new Uint8Array(this._rawTextureWidth * 4); - const tmpColor = TmpColors.Color3[0]; - - for (let x = 0; x < this._rawTextureWidth; x++) { - const ratio = x / this._rawTextureWidth; - - GradientHelper.GetCurrentGradient(ratio, this._rampGradients, (currentGradient, nextGradient, scale) => { - Color3.LerpToRef((currentGradient).color, (nextGradient).color, scale, tmpColor); - data[x * 4] = tmpColor.r * 255; - data[x * 4 + 1] = tmpColor.g * 255; - data[x * 4 + 2] = tmpColor.b * 255; - data[x * 4 + 3] = 255; - }); - } - - this._rampGradientsTexture = RawTexture.CreateRGBATexture(data, this._rawTextureWidth, 1, this._scene, false, false, Constants.TEXTURE_NEAREST_SAMPLINGMODE); - } - - /** - * Gets the current list of ramp gradients. - * You must use addRampGradient and removeRampGradient to update this list - * @returns the list of ramp gradients - */ - public getRampGradients(): Nullable> { - return this._rampGradients; - } - - /** Force the system to rebuild all gradients that need to be resync */ - public forceRefreshGradients() { - this._syncRampGradientTexture(); - } - - private _syncRampGradientTexture() { - if (!this._rampGradients) { - return; - } - - this._rampGradients.sort((a, b) => { - if (a.gradient < b.gradient) { - return -1; - } else if (a.gradient > b.gradient) { - return 1; - } - - return 0; - }); - - if (this._rampGradientsTexture) { - this._rampGradientsTexture.dispose(); - this._rampGradientsTexture = null; - } - - this._createRampGradientTexture(); - } - - /** - * Adds a new ramp gradient used to remap particle colors - * @param gradient defines the gradient to use (between 0 and 1) - * @param color defines the color to affect to the specified gradient - * @returns the current particle system - */ - public addRampGradient(gradient: number, color: Color3): ThinParticleSystem { - if (!this._rampGradients) { - this._rampGradients = []; - } - - const rampGradient = new Color3Gradient(gradient, color); - this._rampGradients.push(rampGradient); - - this._syncRampGradientTexture(); - - return this; - } - - /** - * Remove a specific ramp gradient - * @param gradient defines the gradient to remove - * @returns the current particle system - */ - public removeRampGradient(gradient: number): ThinParticleSystem { - this._removeGradientAndTexture(gradient, this._rampGradients, this._rampGradientsTexture); - this._rampGradientsTexture = null; - - if (this._rampGradients && this._rampGradients.length > 0) { - this._createRampGradientTexture(); - } - - return this; - } - - /** - * Adds a new color gradient - * @param gradient defines the gradient to use (between 0 and 1) - * @param color1 defines the color to affect to the specified gradient - * @param color2 defines an additional color used to define a range ([color, color2]) with main color to pick the final color from - * @returns this particle system - */ - public addColorGradient(gradient: number, color1: Color4, color2?: Color4): IParticleSystem { - if (!this._colorGradients) { - this._colorGradients = []; - } - - if (this._colorGradients.length === 0) { - this._colorCreation.process = _CreateColorGradientsData; - this._colorProcessing.process = _ProcessColorGradients; - } - - const colorGradient = new ColorGradient(gradient, color1, color2); - this._colorGradients.push(colorGradient); - - this._colorGradients.sort((a, b) => { - if (a.gradient < b.gradient) { - return -1; - } else if (a.gradient > b.gradient) { - return 1; - } - - return 0; - }); - - return this; - } - - /** - * Remove a specific color gradient - * @param gradient defines the gradient to remove - * @returns this particle system - */ - public removeColorGradient(gradient: number): IParticleSystem { - if (!this._colorGradients) { - return this; - } - - let index = 0; - for (const colorGradient of this._colorGradients) { - if (colorGradient.gradient === gradient) { - this._colorGradients.splice(index, 1); - break; - } - index++; - } - - if (this._colorGradients.length === 0) { - this._colorCreation.process = _CreateColorData; - this._colorProcessing.process = _ProcessColor; - } - - return this; - } - - /** - * Resets the draw wrappers cache - */ - public resetDrawCache(): void { - if (!this._drawWrappers) { - return; - } - for (const drawWrappers of this._drawWrappers) { - if (drawWrappers) { - for (const drawWrapper of drawWrappers) { - drawWrapper?.dispose(); - } - } - } - - this._drawWrappers = []; - } - - /** @internal */ - public _fetchR(u: number, v: number, width: number, height: number, pixels: Uint8Array | Uint8ClampedArray): number { - u = Math.abs(u) * 0.5 + 0.5; - v = Math.abs(v) * 0.5 + 0.5; - - const wrappedU = (u * width) % width | 0; - const wrappedV = (v * height) % height | 0; - - const position = (wrappedU + wrappedV * width) * 4; - return pixels[position] / 255; - } - - protected override _reset() { - this._resetEffect(); - } - - private _resetEffect() { - if (this._vertexBuffer) { - this._vertexBuffer.dispose(); - this._vertexBuffer = null; - } - - if (this._spriteBuffer) { - this._spriteBuffer.dispose(); - this._spriteBuffer = null; - } - - if (this._vertexArrayObject) { - (this._engine as ThinEngine).releaseVertexArrayObject(this._vertexArrayObject); - this._vertexArrayObject = null; - } - - this._createVertexBuffers(); - } - - private _createVertexBuffers() { - this._vertexBufferSize = this._useInstancing ? 10 : 12; - if (this._isAnimationSheetEnabled) { - this._vertexBufferSize += 1; - } - - if ( - !this._isBillboardBased || - this.billboardMode === Constants.PARTICLES_BILLBOARDMODE_STRETCHED || - this.billboardMode === Constants.PARTICLES_BILLBOARDMODE_STRETCHED_LOCAL - ) { - this._vertexBufferSize += 3; - } - - if (this._useRampGradients) { - this._vertexBufferSize += 4; - } - - const engine = this._engine; - const vertexSize = this._vertexBufferSize * (this._useInstancing ? 1 : 4); - this._vertexData = new Float32Array(this._capacity * vertexSize); - this._vertexBuffer = new Buffer(engine, this._vertexData, true, vertexSize); - - let dataOffset = 0; - const positions = this._vertexBuffer.createVertexBuffer(VertexBuffer.PositionKind, dataOffset, 3, this._vertexBufferSize, this._useInstancing); - this._vertexBuffers[VertexBuffer.PositionKind] = positions; - dataOffset += 3; - - const colors = this._vertexBuffer.createVertexBuffer(VertexBuffer.ColorKind, dataOffset, 4, this._vertexBufferSize, this._useInstancing); - this._vertexBuffers[VertexBuffer.ColorKind] = colors; - dataOffset += 4; - - const options = this._vertexBuffer.createVertexBuffer("angle", dataOffset, 1, this._vertexBufferSize, this._useInstancing); - this._vertexBuffers["angle"] = options; - dataOffset += 1; - - const size = this._vertexBuffer.createVertexBuffer("size", dataOffset, 2, this._vertexBufferSize, this._useInstancing); - this._vertexBuffers["size"] = size; - dataOffset += 2; - - if (this._isAnimationSheetEnabled) { - const cellIndexBuffer = this._vertexBuffer.createVertexBuffer("cellIndex", dataOffset, 1, this._vertexBufferSize, this._useInstancing); - this._vertexBuffers["cellIndex"] = cellIndexBuffer; - dataOffset += 1; - } - - if ( - !this._isBillboardBased || - this.billboardMode === Constants.PARTICLES_BILLBOARDMODE_STRETCHED || - this.billboardMode === Constants.PARTICLES_BILLBOARDMODE_STRETCHED_LOCAL - ) { - const directionBuffer = this._vertexBuffer.createVertexBuffer("direction", dataOffset, 3, this._vertexBufferSize, this._useInstancing); - this._vertexBuffers["direction"] = directionBuffer; - dataOffset += 3; - } - - if (this._useRampGradients) { - const rampDataBuffer = this._vertexBuffer.createVertexBuffer("remapData", dataOffset, 4, this._vertexBufferSize, this._useInstancing); - this._vertexBuffers["remapData"] = rampDataBuffer; - dataOffset += 4; - } - - let offsets: VertexBuffer; - if (this._useInstancing) { - const spriteData = new Float32Array([0, 0, 1, 0, 0, 1, 1, 1]); - this._spriteBuffer = new Buffer(engine, spriteData, false, 2); - offsets = this._spriteBuffer.createVertexBuffer("offset", 0, 2); - } else { - offsets = this._vertexBuffer.createVertexBuffer("offset", dataOffset, 2, this._vertexBufferSize, this._useInstancing); - dataOffset += 2; - } - this._vertexBuffers["offset"] = offsets; - - this.resetDrawCache(); - } - - private _createIndexBuffer() { - if (this._useInstancing) { - this._linesIndexBufferUseInstancing = this._engine.createIndexBuffer(new Uint32Array([0, 1, 1, 3, 3, 2, 2, 0, 0, 3])); - return; - } - const indices = []; - const indicesWireframe = []; - let index = 0; - for (let count = 0; count < this._capacity; count++) { - indices.push(index); - indices.push(index + 1); - indices.push(index + 2); - indices.push(index); - indices.push(index + 2); - indices.push(index + 3); - indicesWireframe.push(index, index + 1, index + 1, index + 2, index + 2, index + 3, index + 3, index, index, index + 3); - index += 4; - } - - this._indexBuffer = this._engine.createIndexBuffer(indices); - this._linesIndexBuffer = this._engine.createIndexBuffer(indicesWireframe); - } - - /** - * Gets the maximum number of particles active at the same time. - * @returns The max number of active particles. - */ - public getCapacity(): number { - return this._capacity; - } - - /** - * Gets whether there are still active particles in the system. - * @returns True if it is alive, otherwise false. - */ - public isAlive(): boolean { - return this._alive; - } - - /** - * Gets if the system has been started. (Note: this will still be true after stop is called) - * @returns True if it has been started, otherwise false. - */ - public isStarted(): boolean { - return this._started; - } - - /** @internal */ - public _preStart() { - // Do nothing - } - - /** - * Starts the particle system and begins to emit - * @param delay defines the delay in milliseconds before starting the system (this.startDelay by default) - */ - public start(delay = this.startDelay): void { - if (!this.targetStopDuration && this._hasTargetStopDurationDependantGradient()) { - // eslint-disable-next-line no-throw-literal - throw "Particle system started with a targetStopDuration dependant gradient (eg. startSizeGradients) but no targetStopDuration set"; - } - if (delay) { - this.startDelay = delay; - setTimeout(() => { - this.start(0); - }, delay); - return; - } - this._started = true; - this._stopped = false; - this._actualFrame = 0; - - this._preStart(); - - // Reset emit gradient so it acts the same on every start - if (this._emitRateGradients) { - if (this._emitRateGradients.length > 0) { - this._currentEmitRateGradient = this._emitRateGradients[0]; - this._currentEmitRate1 = this._currentEmitRateGradient.getFactor(); - this._currentEmitRate2 = this._currentEmitRate1; - } - if (this._emitRateGradients.length > 1) { - this._currentEmitRate2 = this._emitRateGradients[1].getFactor(); - } - } - // Reset start size gradient so it acts the same on every start - if (this._startSizeGradients) { - if (this._startSizeGradients.length > 0) { - this._currentStartSizeGradient = this._startSizeGradients[0]; - this._currentStartSize1 = this._currentStartSizeGradient.getFactor(); - this._currentStartSize2 = this._currentStartSize1; - } - if (this._startSizeGradients.length > 1) { - this._currentStartSize2 = this._startSizeGradients[1].getFactor(); - } - } - - if (this.preWarmCycles) { - if (this.emitter?.getClassName().indexOf("Mesh") !== -1) { - (this.emitter as any).computeWorldMatrix(true); - } - - const noiseTextureAsProcedural = this.noiseTexture as ProceduralTexture; - - if (noiseTextureAsProcedural && noiseTextureAsProcedural.onGeneratedObservable) { - noiseTextureAsProcedural.onGeneratedObservable.addOnce(() => { - setTimeout(() => { - for (let index = 0; index < this.preWarmCycles; index++) { - this.animate(true); - noiseTextureAsProcedural.render(); - } - }); - }); - } else { - for (let index = 0; index < this.preWarmCycles; index++) { - this.animate(true); - } - } - } - - // Animations - if (this.beginAnimationOnStart && this.animations && this.animations.length > 0 && this._scene) { - this._scene.beginAnimation(this, this.beginAnimationFrom, this.beginAnimationTo, this.beginAnimationLoop); - } - - this.onStartedObservable.notifyObservers(this); - } - - /** - * Stops the particle system. - * @param stopSubEmitters if true it will stop the current system and all created sub-Systems if false it will stop the current root system only, this param is used by the root particle system only. The default value is true. - */ - public stop(stopSubEmitters = true): void { - if (this._stopped) { - return; - } - - this.onStoppedObservable.notifyObservers(this); - - this._stopped = true; - - this._postStop(stopSubEmitters); - } - - /** @internal */ - public _postStop(_stopSubEmitters: boolean) { - // Do nothing - } - - // Animation sheet - - /** - * Remove all active particles - */ - public reset(): void { - this._stockParticles.length = 0; - this._particles.length = 0; - } - - /** - * @internal (for internal use only) - */ - public _appendParticleVertex(index: number, particle: Particle, offsetX: number, offsetY: number): void { - let offset = index * this._vertexBufferSize; - - const floatingOriginOffset = TmpVectors.Vector3[0].copyFrom(this._scene?.floatingOriginOffset || Vector3.ZeroReadOnly); - this._vertexData[offset++] = particle.position.x + this.worldOffset.x - floatingOriginOffset.x; - this._vertexData[offset++] = particle.position.y + this.worldOffset.y - floatingOriginOffset.y; - this._vertexData[offset++] = particle.position.z + this.worldOffset.z - floatingOriginOffset.z; - this._vertexData[offset++] = particle.color.r; - this._vertexData[offset++] = particle.color.g; - this._vertexData[offset++] = particle.color.b; - this._vertexData[offset++] = particle.color.a; - this._vertexData[offset++] = particle.angle; - - this._vertexData[offset++] = particle.scale.x * particle.size; - this._vertexData[offset++] = particle.scale.y * particle.size; - - if (this._isAnimationSheetEnabled) { - this._vertexData[offset++] = particle.cellIndex; - } - - if (!this._isBillboardBased) { - if (particle._initialDirection) { - let initialDirection = particle._initialDirection; - if (this.isLocal) { - Vector3.TransformNormalToRef(initialDirection, this._emitterWorldMatrix, TmpVectors.Vector3[0]); - initialDirection = TmpVectors.Vector3[0]; - } - if (initialDirection.x === 0 && initialDirection.z === 0) { - initialDirection.x = 0.001; - } - - this._vertexData[offset++] = initialDirection.x; - this._vertexData[offset++] = initialDirection.y; - this._vertexData[offset++] = initialDirection.z; - } else { - let direction = particle.direction; - if (this.isLocal) { - Vector3.TransformNormalToRef(direction, this._emitterWorldMatrix, TmpVectors.Vector3[0]); - direction = TmpVectors.Vector3[0]; - } - - if (direction.x === 0 && direction.z === 0) { - direction.x = 0.001; - } - this._vertexData[offset++] = direction.x; - this._vertexData[offset++] = direction.y; - this._vertexData[offset++] = direction.z; - } - } else if (this.billboardMode === Constants.PARTICLES_BILLBOARDMODE_STRETCHED || this.billboardMode === Constants.PARTICLES_BILLBOARDMODE_STRETCHED_LOCAL) { - this._vertexData[offset++] = particle.direction.x; - this._vertexData[offset++] = particle.direction.y; - this._vertexData[offset++] = particle.direction.z; - } - - if (this._useRampGradients && particle.remapData) { - this._vertexData[offset++] = particle.remapData.x; - this._vertexData[offset++] = particle.remapData.y; - this._vertexData[offset++] = particle.remapData.z; - this._vertexData[offset++] = particle.remapData.w; - } - - if (!this._useInstancing) { - if (this._isAnimationSheetEnabled) { - if (offsetX === 0) { - offsetX = this._epsilon; - } else if (offsetX === 1) { - offsetX = 1 - this._epsilon; - } - - if (offsetY === 0) { - offsetY = this._epsilon; - } else if (offsetY === 1) { - offsetY = 1 - this._epsilon; - } - } - - this._vertexData[offset++] = offsetX; - this._vertexData[offset++] = offsetY; - } - } - - // start of sub system methods - - /** - * "Recycles" one of the particle by copying it back to the "stock" of particles and removing it from the active list. - * Its lifetime will start back at 0. - * @param particle - */ - public recycleParticle: (particle: Particle) => void = (particle) => { - // move particle from activeParticle list to stock particles - const lastParticle = this._particles.pop(); - if (lastParticle !== particle) { - lastParticle.copyTo(particle); - } - this._stockParticles.push(lastParticle); - }; - - private _createParticle: () => Particle = () => { - let particle: Particle; - if (this._stockParticles.length !== 0) { - particle = this._stockParticles.pop(); - particle._reset(); - } else { - particle = new Particle(this); - } - - this._prepareParticle(particle); - return particle; - }; - - /** @internal */ - public _prepareParticle(_particle: Particle) { - //Do nothing - } - - private _createNewOnes(newParticles: number) { - // Add new ones - let particle: Particle; - for (let index = 0; index < newParticles; index++) { - if (this._particles.length === this._capacity) { - break; - } - - particle = this._createParticle(); - - this._particles.push(particle); - - // Creation queue - let currentQueueItem = this._createQueueStart; - - while (currentQueueItem) { - currentQueueItem.process(particle, this); - currentQueueItem = currentQueueItem.nextItem; - } - - // Update the position of the attached sub-emitters to match their attached particle - particle._inheritParticleInfoToSubEmitters(); - } - } - - private _update(newParticles: number): void { - // Update current - this._alive = this._particles.length > 0; - - if ((this.emitter).position) { - const emitterMesh = this.emitter; - this._emitterWorldMatrix = emitterMesh.getWorldMatrix(); - } else { - const emitterPosition = this.emitter; - this._emitterWorldMatrix = Matrix.Translation(emitterPosition.x, emitterPosition.y, emitterPosition.z); - } - - this._emitterWorldMatrix.invertToRef(this._emitterInverseWorldMatrix); - this.updateFunction(this._particles); - - this._createNewOnes(newParticles); - } - - /** - * @internal - */ - public static _GetAttributeNamesOrOptions(isAnimationSheetEnabled = false, isBillboardBased = false, useRampGradients = false): string[] { - const attributeNamesOrOptions = [VertexBuffer.PositionKind, VertexBuffer.ColorKind, "angle", "offset", "size"]; - - if (isAnimationSheetEnabled) { - attributeNamesOrOptions.push("cellIndex"); - } - - if (!isBillboardBased) { - attributeNamesOrOptions.push("direction"); - } - - if (useRampGradients) { - attributeNamesOrOptions.push("remapData"); - } - - return attributeNamesOrOptions; - } - - /** - * @internal - */ - public static _GetEffectCreationOptions(isAnimationSheetEnabled = false, useLogarithmicDepth = false, applyFog = false): string[] { - const effectCreationOption = ["invView", "view", "projection", "textureMask", "translationPivot", "eyePosition"]; - - AddClipPlaneUniforms(effectCreationOption); - - if (isAnimationSheetEnabled) { - effectCreationOption.push("particlesInfos"); - } - if (useLogarithmicDepth) { - effectCreationOption.push("logarithmicDepthConstant"); - } - - if (applyFog) { - effectCreationOption.push("vFogInfos"); - effectCreationOption.push("vFogColor"); - } - - return effectCreationOption; - } - - /** - * Fill the defines array according to the current settings of the particle system - * @param defines Array to be updated - * @param blendMode blend mode to take into account when updating the array - * @param fillImageProcessing fills the image processing defines - */ - public fillDefines(defines: Array, blendMode: number, fillImageProcessing: boolean = true): void { - if (this._scene) { - PrepareStringDefinesForClipPlanes(this, this._scene, defines); - if (this.applyFog && this._scene.fogEnabled && this._scene.fogMode !== Constants.FOGMODE_NONE) { - defines.push("#define FOG"); - } - } - - if (this._isAnimationSheetEnabled) { - defines.push("#define ANIMATESHEET"); - } - - if (this.useLogarithmicDepth) { - defines.push("#define LOGARITHMICDEPTH"); - } - - if (blendMode === BaseParticleSystem.BLENDMODE_MULTIPLY) { - defines.push("#define BLENDMULTIPLYMODE"); - } - - if (this._useRampGradients) { - defines.push("#define RAMPGRADIENT"); - } - - if (this._isBillboardBased) { - defines.push("#define BILLBOARD"); - - switch (this.billboardMode) { - case Constants.PARTICLES_BILLBOARDMODE_Y: - defines.push("#define BILLBOARDY"); - break; - case Constants.PARTICLES_BILLBOARDMODE_STRETCHED: - case Constants.PARTICLES_BILLBOARDMODE_STRETCHED_LOCAL: - defines.push("#define BILLBOARDSTRETCHED"); - if (this.billboardMode === Constants.PARTICLES_BILLBOARDMODE_STRETCHED_LOCAL) { - defines.push("#define BILLBOARDSTRETCHED_LOCAL"); - } - break; - case Constants.PARTICLES_BILLBOARDMODE_ALL: - defines.push("#define BILLBOARDMODE_ALL"); - break; - default: - break; - } - } - - if (fillImageProcessing && this._imageProcessingConfiguration) { - this._imageProcessingConfiguration.prepareDefines(this._imageProcessingConfigurationDefines); - defines.push(this._imageProcessingConfigurationDefines.toString()); - } - } - - /** - * Fill the uniforms, attributes and samplers arrays according to the current settings of the particle system - * @param uniforms Uniforms array to fill - * @param attributes Attributes array to fill - * @param samplers Samplers array to fill - */ - public fillUniformsAttributesAndSamplerNames(uniforms: Array, attributes: Array, samplers: Array) { - attributes.push( - ...ThinParticleSystem._GetAttributeNamesOrOptions( - this._isAnimationSheetEnabled, - this._isBillboardBased && - this.billboardMode !== Constants.PARTICLES_BILLBOARDMODE_STRETCHED && - this.billboardMode !== Constants.PARTICLES_BILLBOARDMODE_STRETCHED_LOCAL, - this._useRampGradients - ) - ); - - uniforms.push(...ThinParticleSystem._GetEffectCreationOptions(this._isAnimationSheetEnabled, this.useLogarithmicDepth, this.applyFog)); - - samplers.push("diffuseSampler", "rampSampler"); - - if (this._imageProcessingConfiguration) { - PrepareUniformsForImageProcessing(uniforms, this._imageProcessingConfigurationDefines); - PrepareSamplersForImageProcessing(samplers, this._imageProcessingConfigurationDefines); - } - } - - /** - * @internal - */ - private _getWrapper(blendMode: number): DrawWrapper { - const customWrapper = this._getCustomDrawWrapper(blendMode); - - if (customWrapper?.effect) { - return customWrapper; - } - - const defines: Array = []; - - this.fillDefines(defines, blendMode); - - // Effect - const currentRenderPassId = this._engine._features.supportRenderPasses ? this._engine.currentRenderPassId : Constants.RENDERPASS_MAIN; - let drawWrappers = this._drawWrappers[currentRenderPassId]; - if (!drawWrappers) { - drawWrappers = this._drawWrappers[currentRenderPassId] = []; - } - let drawWrapper = drawWrappers[blendMode]; - if (!drawWrapper) { - drawWrapper = new DrawWrapper(this._engine); - if (drawWrapper.drawContext) { - drawWrapper.drawContext.useInstancing = this._useInstancing; - } - drawWrappers[blendMode] = drawWrapper; - } - - const join = defines.join("\n"); - if (drawWrapper.defines !== join) { - const attributesNamesOrOptions: Array = []; - const effectCreationOption: Array = []; - const samplers: Array = []; - - this.fillUniformsAttributesAndSamplerNames(effectCreationOption, attributesNamesOrOptions, samplers); - - drawWrapper.setEffect( - this._engine.createEffect( - "particles", - attributesNamesOrOptions, - effectCreationOption, - samplers, - join, - undefined, - undefined, - undefined, - undefined, - this._shaderLanguage - ), - join - ); - } - - return drawWrapper; - } - - /** - * Gets or sets a boolean indicating that the particle system is paused (no animation will be done). - */ - public paused = false; - - /** - * Animates the particle system for the current frame by emitting new particles and or animating the living ones. - * @param preWarmOnly will prevent the system from updating the vertex buffer (default is false) - */ - public animate(preWarmOnly = false): void { - if (!this._started || this.paused) { - return; - } - - if (!preWarmOnly && this._scene) { - // Check - if (!this.isReady()) { - return; - } - - if (this._currentRenderId === this._scene.getFrameId()) { - return; - } - this._currentRenderId = this._scene.getFrameId(); - } - - this._scaledUpdateSpeed = this.updateSpeed * (preWarmOnly ? this.preWarmStepOffset : this._scene?.getAnimationRatio() || 1); - - // Determine the number of particles we need to create - let newParticles; - - if (this.manualEmitCount > -1) { - newParticles = this.manualEmitCount; - this._newPartsExcess = 0; - this.manualEmitCount = 0; - } else { - const rate = this._calculateEmitRate(); - newParticles = (rate * this._scaledUpdateSpeed) >> 0; - this._newPartsExcess += rate * this._scaledUpdateSpeed - newParticles; - } - - if (this._newPartsExcess > 1.0) { - newParticles += this._newPartsExcess >> 0; - this._newPartsExcess -= this._newPartsExcess >> 0; - } - - this._alive = false; - - if (!this._stopped) { - this._actualFrame += this._scaledUpdateSpeed; - - if (this.targetStopDuration && this._actualFrame >= this.targetStopDuration) { - this.stop(); - } - } else { - newParticles = 0; - } - this._update(newParticles); - - // Stopped? - if (this._stopped) { - if (!this._alive) { - this._started = false; - if (this.onAnimationEnd) { - this.onAnimationEnd(); - } - if (this.disposeOnStop && this._scene) { - this._scene._toBeDisposed.push(this); - } - } - } - - if (!preWarmOnly) { - // Update VBO - let offset = 0; - for (let index = 0; index < this._particles.length; index++) { - const particle = this._particles[index]; - this._appendParticleVertices(offset, particle); - offset += this._useInstancing ? 1 : 4; - } - - if (this._vertexBuffer) { - this._vertexBuffer.updateDirectly(this._vertexData, 0, this._particles.length); - } - } - - if (this.manualEmitCount === 0 && this.disposeOnStop) { - this.stop(); - } - } - - /** - * Internal only. Calculates the current emit rate based on the gradients if any. - * @returns The emit rate - * @internal - */ - public _calculateEmitRate(): number { - let rate = this.emitRate; - - if (this._emitRateGradients && this._emitRateGradients.length > 0 && this.targetStopDuration) { - const ratio = this._actualFrame / this.targetStopDuration; - GradientHelper.GetCurrentGradient(ratio, this._emitRateGradients, (currentGradient, nextGradient, scale) => { - if (currentGradient !== this._currentEmitRateGradient) { - this._currentEmitRate1 = this._currentEmitRate2; - this._currentEmitRate2 = (nextGradient).getFactor(); - this._currentEmitRateGradient = currentGradient; - } - - rate = Lerp(this._currentEmitRate1, this._currentEmitRate2, scale); - }); - } - - return rate; - } - - private _appendParticleVertices(offset: number, particle: Particle) { - this._appendParticleVertex(offset++, particle, 0, 0); - if (!this._useInstancing) { - this._appendParticleVertex(offset++, particle, 1, 0); - this._appendParticleVertex(offset++, particle, 1, 1); - this._appendParticleVertex(offset++, particle, 0, 1); - } - } - - /** - * Rebuilds the particle system. - */ - public rebuild(): void { - if (this._engine.getCaps().vertexArrayObject) { - this._vertexArrayObject = null; - } - - this._createIndexBuffer(); - - this._spriteBuffer?._rebuild(); - - this._createVertexBuffers(); - - this.resetDrawCache(); - } - - private _shadersLoaded = false; - private async _initShaderSourceAsync() { - const engine = this._engine; - - if (engine.isWebGPU && !ThinParticleSystem.ForceGLSL) { - this._shaderLanguage = ShaderLanguage.WGSL; - - await Promise.all([import("../ShadersWGSL/particles.vertex"), import("../ShadersWGSL/particles.fragment")]); - } else { - await Promise.all([import("../Shaders/particles.vertex"), import("../Shaders/particles.fragment")]); - } - - this._shadersLoaded = true; - } - - /** - * Is this system ready to be used/rendered - * @returns true if the system is ready - */ - public isReady(): boolean { - if (!this._shadersLoaded) { - return false; - } - if (!this.emitter || (this._imageProcessingConfiguration && !this._imageProcessingConfiguration.isReady()) || !this.particleTexture || !this.particleTexture.isReady()) { - return false; - } - - if (this.blendMode !== BaseParticleSystem.BLENDMODE_MULTIPLYADD) { - if (!this._getWrapper(this.blendMode).effect!.isReady()) { - return false; - } - } else { - if (!this._getWrapper(BaseParticleSystem.BLENDMODE_MULTIPLY).effect!.isReady()) { - return false; - } - if (!this._getWrapper(BaseParticleSystem.BLENDMODE_ADD).effect!.isReady()) { - return false; - } - } - - return true; - } - - private _render(blendMode: number) { - const drawWrapper = this._getWrapper(blendMode); - const effect = drawWrapper.effect!; - - const engine = this._engine; - - // Render - engine.enableEffect(drawWrapper); - - const viewMatrix = this.defaultViewMatrix ?? this._scene!.getViewMatrix(); - effect.setTexture("diffuseSampler", this.particleTexture); - effect.setMatrix("view", viewMatrix); - effect.setMatrix("projection", this.defaultProjectionMatrix ?? this._scene!.getProjectionMatrix()); - - if (this._isAnimationSheetEnabled && this.particleTexture) { - const baseSize = this.particleTexture.getBaseSize(); - effect.setFloat3("particlesInfos", this.spriteCellWidth / baseSize.width, this.spriteCellHeight / baseSize.height, this.spriteCellWidth / baseSize.width); - } - - effect.setVector2("translationPivot", this.translationPivot); - effect.setFloat4("textureMask", this.textureMask.r, this.textureMask.g, this.textureMask.b, this.textureMask.a); - - if (this._isBillboardBased && this._scene) { - const camera = this._scene.activeCamera!; - effect.setVector3("eyePosition", camera.globalPosition); - } - - if (this._rampGradientsTexture) { - if (!this._rampGradients || !this._rampGradients.length) { - this._rampGradientsTexture.dispose(); - this._rampGradientsTexture = null; - } - effect.setTexture("rampSampler", this._rampGradientsTexture); - } - - const defines = effect.defines; - - if (this._scene) { - BindClipPlane(effect, this, this._scene); - - if (this.applyFog) { - BindFogParameters(this._scene, undefined, effect); - } - } - - if (defines.indexOf("#define BILLBOARDMODE_ALL") >= 0) { - viewMatrix.invertToRef(TmpVectors.Matrix[0]); - effect.setMatrix("invView", TmpVectors.Matrix[0]); - } - - if (this._vertexArrayObject !== undefined) { - if (this._scene?.forceWireframe) { - engine.bindBuffers(this._vertexBuffers, this._linesIndexBufferUseInstancing, effect); - } else { - if (!this._vertexArrayObject) { - this._vertexArrayObject = (this._engine as ThinEngine).recordVertexArrayObject(this._vertexBuffers, this._indexBuffer, effect); - } - - (this._engine as ThinEngine).bindVertexArrayObject(this._vertexArrayObject, this._indexBuffer); - } - } else { - if (!this._indexBuffer) { - // Use instancing mode - engine.bindBuffers(this._vertexBuffers, this._scene?.forceWireframe ? this._linesIndexBufferUseInstancing : null, effect); - } else { - engine.bindBuffers(this._vertexBuffers, this._scene?.forceWireframe ? this._linesIndexBuffer : this._indexBuffer, effect); - } - } - - // Log. depth - if (this.useLogarithmicDepth && this._scene) { - BindLogDepth(defines, effect, this._scene); - } - - // image processing - if (this._imageProcessingConfiguration && !this._imageProcessingConfiguration.applyByPostProcess) { - this._imageProcessingConfiguration.bind(effect); - } - - // Draw order - this._setEngineBasedOnBlendMode(blendMode); - - if (this._onBeforeDrawParticlesObservable) { - this._onBeforeDrawParticlesObservable.notifyObservers(effect); - } - - if (this._useInstancing) { - if (this._scene?.forceWireframe) { - engine.drawElementsType(Constants.MATERIAL_LineStripDrawMode, 0, 10, this._particles.length); - } else { - engine.drawArraysType(Constants.MATERIAL_TriangleStripDrawMode, 0, 4, this._particles.length); - } - } else { - if (this._scene?.forceWireframe) { - engine.drawElementsType(Constants.MATERIAL_WireFrameFillMode, 0, this._particles.length * 10); - } else { - engine.drawElementsType(Constants.MATERIAL_TriangleFillMode, 0, this._particles.length * 6); - } - } - - return this._particles.length; - } - - /** - * Renders the particle system in its current state. - * @returns the current number of particles - */ - public render(): number { - // Check - if (!this.isReady() || !this._particles.length) { - return 0; - } - - const engine = this._engine as any; - if (engine.setState) { - engine.setState(false); - - if (this.forceDepthWrite) { - engine.setDepthWrite(true); - } - } - - let outparticles = 0; - - if (this.blendMode === BaseParticleSystem.BLENDMODE_MULTIPLYADD) { - outparticles = this._render(BaseParticleSystem.BLENDMODE_MULTIPLY) + this._render(BaseParticleSystem.BLENDMODE_ADD); - } else { - outparticles = this._render(this.blendMode); - } - - this._engine.unbindInstanceAttributes(); - this._engine.setAlphaMode(Constants.ALPHA_DISABLE); - - return outparticles; - } - - /** @internal */ - public _onDispose(_disposeAttachedSubEmitters = false, _disposeEndSubEmitters = false) { - // Do Nothing - } - - /** - * Disposes the particle system and free the associated resources - * @param disposeTexture defines if the particle texture must be disposed as well (true by default) - * @param disposeAttachedSubEmitters defines if the attached sub-emitters must be disposed as well (false by default) - * @param disposeEndSubEmitters defines if the end type sub-emitters must be disposed as well (false by default) - */ - public dispose(disposeTexture = true, disposeAttachedSubEmitters = false, disposeEndSubEmitters = false): void { - this.resetDrawCache(); - - if (this._vertexBuffer) { - this._vertexBuffer.dispose(); - this._vertexBuffer = null; - } - - if (this._spriteBuffer) { - this._spriteBuffer.dispose(); - this._spriteBuffer = null; - } - - if (this._indexBuffer) { - this._engine._releaseBuffer(this._indexBuffer); - this._indexBuffer = null; - } - - if (this._linesIndexBuffer) { - this._engine._releaseBuffer(this._linesIndexBuffer); - this._linesIndexBuffer = null; - } - - if (this._linesIndexBufferUseInstancing) { - this._engine._releaseBuffer(this._linesIndexBufferUseInstancing); - this._linesIndexBufferUseInstancing = null; - } - - if (this._vertexArrayObject) { - (this._engine as ThinEngine).releaseVertexArrayObject(this._vertexArrayObject); - this._vertexArrayObject = null; - } - - if (disposeTexture && this.particleTexture) { - this.particleTexture.dispose(); - this.particleTexture = null; - } - - if (disposeTexture && this.noiseTexture) { - this.noiseTexture.dispose(); - this.noiseTexture = null; - } - - if (this._rampGradientsTexture) { - this._rampGradientsTexture.dispose(); - this._rampGradientsTexture = null; - } - - this._onDispose(disposeAttachedSubEmitters, disposeEndSubEmitters); - - if (this._onBeforeDrawParticlesObservable) { - this._onBeforeDrawParticlesObservable.clear(); - } - - // Remove from scene - if (this._scene) { - const index = this._scene.particleSystems.indexOf(this); - if (index > -1) { - this._scene.particleSystems.splice(index, 1); - } - - this._scene._activeParticleSystems.dispose(); - } - - // Callback - this.onDisposeObservable.notifyObservers(this); - this.onDisposeObservable.clear(); - this.onStoppedObservable.clear(); - this.onStartedObservable.clear(); - - this.reset(); - - this._isDisposed = true; - } -} From 7a3d382b0df5be478e5fc2a9bc25749221ea1349 Mon Sep 17 00:00:00 2001 From: Mikalai Lazitski Date: Thu, 18 Dec 2025 09:58:54 +0300 Subject: [PATCH 44/62] refactor: rename particle system creation methods for consistency and introduce emission burst handling as gradients to enhance effect system functionality --- tools/src/effect/factories/systemFactory.ts | 108 +++++++----------- .../systems/effectSolidParticleSystem.ts | 26 +---- 2 files changed, 41 insertions(+), 93 deletions(-) diff --git a/tools/src/effect/factories/systemFactory.ts b/tools/src/effect/factories/systemFactory.ts index 56a8893b0..4219741bf 100644 --- a/tools/src/effect/factories/systemFactory.ts +++ b/tools/src/effect/factories/systemFactory.ts @@ -83,7 +83,7 @@ export class SystemFactory { * Process a Emitter object */ private _processEmitter(emitter: IEmitter, parentGroup: Nullable, depth: number, particleSystems: (EffectParticleSystem | EffectSolidParticleSystem)[]): void { - const particleSystem = this._createParticleSystem(emitter, parentGroup, depth); + const particleSystem = this._createEffectSystem(emitter, parentGroup, depth); if (particleSystem) { particleSystems.push(particleSystem); } @@ -129,7 +129,7 @@ export class SystemFactory { /** * Create a particle system from a Emitter */ - private _createParticleSystem(emitter: IEmitter, parentGroup: Nullable, depth: number): Nullable { + private _createEffectSystem(emitter: IEmitter, parentGroup: Nullable, depth: number): Nullable { const indent = " ".repeat(depth); const parentName = parentGroup ? parentGroup.name : "none"; this._logger.log(`${indent}Processing emitter: ${emitter.name} (parent: ${parentName})`); @@ -155,9 +155,9 @@ export class SystemFactory { try { if (systemType === "solid") { - particleSystem = this._createSolidParticleSystem(emitter, parentGroup); + particleSystem = this._createEffectSolidParticleSystem(emitter, parentGroup); } else { - particleSystem = this._createParticleSystemInstance(emitter, parentGroup, cumulativeScale, depth); + particleSystem = this._createEffectParticleSystem(emitter, parentGroup, cumulativeScale, depth); } } catch (error) { this._logger.error(`${indent}Failed to create ${systemType} system for emitter ${emitter.name}: ${error instanceof Error ? error.message : String(error)}`); @@ -283,10 +283,36 @@ export class SystemFactory { } } + /** + * Apply emission bursts by converting them to emit rate gradients + * Unified approach for both ParticleSystem and SolidParticleSystem + */ + private _applyEmissionBursts(system: EffectParticleSystem | EffectSolidParticleSystem, config: IParticleSystemConfig, duration: number): void { + if (!config.emissionBursts || config.emissionBursts.length === 0) { + return; + } + + const baseEmitRate = config.emitRate || 10; + for (const burst of config.emissionBursts) { + if (burst.time !== undefined && burst.count !== undefined) { + const burstTime = ValueUtils.parseConstantValue(burst.time); + const burstCount = ValueUtils.parseConstantValue(burst.count); + const timeRatio = Math.min(Math.max(burstTime / duration, 0), 1); + const windowSize = 0.02; + const burstEmitRate = burstCount / windowSize; + const beforeTime = Math.max(0, timeRatio - windowSize); + const afterTime = Math.min(1, timeRatio + windowSize); + system.addEmitRateGradient(beforeTime, baseEmitRate); + system.addEmitRateGradient(timeRatio, burstEmitRate); + system.addEmitRateGradient(afterTime, baseEmitRate); + } + } + } + /** * Create a ParticleSystem instance */ - private _createParticleSystemInstance(emitter: IEmitter, _parentGroup: Nullable, cumulativeScale: Vector3, _depth: number): Nullable { + private _createEffectParticleSystem(emitter: IEmitter, _parentGroup: Nullable, cumulativeScale: Vector3, _depth: number): Nullable { const { name, config } = emitter; this._logger.log(`Creating ParticleSystem: ${name}`); @@ -332,30 +358,14 @@ export class SystemFactory { // Apply common rendering and behavior options this._applyCommonOptions(particleSystem, config); + // Apply emission bursts (converted to gradients) + this._applyEmissionBursts(particleSystem, config, duration); + // ParticleSystem-specific: billboard mode if (config.billboardMode !== undefined) { particleSystem.billboardMode = config.billboardMode; } - // === Настройка emission bursts === - if (config.emissionBursts && config.emissionBursts.length > 0) { - const baseEmitRate = config.emitRate || 10; - for (const burst of config.emissionBursts) { - if (burst.time !== undefined && burst.count !== undefined) { - const burstTime = ValueUtils.parseConstantValue(burst.time); - const burstCount = ValueUtils.parseConstantValue(burst.count); - const timeRatio = Math.min(Math.max(burstTime / duration, 0), 1); - const windowSize = 0.02; - const burstEmitRate = burstCount / windowSize; - const beforeTime = Math.max(0, timeRatio - windowSize); - const afterTime = Math.min(1, timeRatio + windowSize); - particleSystem.addEmitRateGradient(beforeTime, baseEmitRate); - particleSystem.addEmitRateGradient(timeRatio, burstEmitRate); - particleSystem.addEmitRateGradient(afterTime, baseEmitRate); - } - } - } - // === Создание emitter === const rotationMatrix = emitter.matrix ? MatrixUtils.extractRotationMatrix(emitter.matrix) : null; particleSystem.configureEmitterFromShape(config.shape, cumulativeScale, rotationMatrix); @@ -367,14 +377,11 @@ export class SystemFactory { /** * Create a SolidParticleSystem instance */ - private _createSolidParticleSystem(emitter: IEmitter, parentGroup: Nullable): Nullable { + private _createEffectSolidParticleSystem(emitter: IEmitter, parentGroup: Nullable): Nullable { const { name, config } = emitter; this._logger.log(`Creating SolidParticleSystem: ${name}`); - // Get transform - const transform = emitter.transform || null; - // Create or load particle mesh const particleMesh = this._geometryFactory.createParticleMesh(config, name, this._scene); if (!particleMesh) { @@ -396,7 +403,6 @@ export class SystemFactory { enableDepthSort: false, particleIntersection: false, useModelMaterial: true, - transform, }); // Set particle mesh and emitter (like ParticleSystem interface) @@ -412,49 +418,15 @@ export class SystemFactory { // Apply common rendering and behavior options this._applyCommonOptions(sps, config); + // Apply emission bursts (converted to gradients) + const duration = config.targetStopDuration !== undefined && config.targetStopDuration > 0 ? config.targetStopDuration : 5; + this._applyEmissionBursts(sps, config, duration); + // === SolidParticleSystem-specific properties === - if (config.shape !== undefined) { - sps.shape = config.shape; - } + // Distance-based emission if (config.emissionOverDistance !== undefined) { sps.emissionOverDistance = config.emissionOverDistance; } - if (config.emissionBursts !== undefined) { - sps.emissionBursts = config.emissionBursts; - } - if (config.onlyUsedByOther !== undefined) { - sps.onlyUsedByOther = config.onlyUsedByOther; - } - if (config.instancingGeometry !== undefined) { - sps.instancingGeometry = config.instancingGeometry; - } - if (config.rendererEmitterSettings !== undefined) { - sps.rendererEmitterSettings = config.rendererEmitterSettings; - } - if (config.material !== undefined) { - sps.material = config.material; - } - if (config.startTileIndex !== undefined) { - sps.startTileIndex = config.startTileIndex; - } - if (config.uTileCount !== undefined) { - sps.uTileCount = config.uTileCount; - } - if (config.vTileCount !== undefined) { - sps.vTileCount = config.vTileCount; - } - if (config.blendTiles !== undefined) { - sps.blendTiles = config.blendTiles; - } - if (config.softParticles !== undefined) { - sps.softParticles = config.softParticles; - } - if (config.softFarFade !== undefined) { - sps.softFarFade = config.softFarFade; - } - if (config.softNearFade !== undefined) { - sps.softNearFade = config.softNearFade; - } // === Создание emitter === sps.configureEmitterFromShape(config.shape); diff --git a/tools/src/effect/systems/effectSolidParticleSystem.ts b/tools/src/effect/systems/effectSolidParticleSystem.ts index e14080d10..c27053145 100644 --- a/tools/src/effect/systems/effectSolidParticleSystem.ts +++ b/tools/src/effect/systems/effectSolidParticleSystem.ts @@ -11,7 +11,6 @@ import type { PerSolidParticleBehaviorFunction, ISystem, SolidParticleWithSystem, - IShape, Value, } from "../types"; import { SolidPointParticleEmitter, SolidSphereParticleEmitter, SolidConeParticleEmitter } from "../emitters"; @@ -51,7 +50,6 @@ export class EffectSolidParticleSystem extends SolidParticleSystem implements IS private _emissionState: IEmissionState; private _behaviors: PerSolidParticleBehaviorFunction[]; public particleEmitterType: ISolidParticleEmitterType | null; - private _transform: { position: Vector3; rotation: Quaternion; scale: Vector3 } | null; private _emitEnded: boolean; private _emitter: AbstractMesh | null; @@ -98,23 +96,11 @@ export class EffectSolidParticleSystem extends SolidParticleSystem implements IS private _emitRateGradients: NumberGradientSystem; // === Other properties === - public shape?: IShape; public emissionOverDistance?: Value; // For distance-based emission - public emissionBursts?: IEmissionBurst[]; - public onlyUsedByOther: boolean; - public instancingGeometry?: string; + public emissionBursts?: IEmissionBurst[]; // Legacy: converted to gradients in Factory public renderOrder?: number; - public rendererEmitterSettings?: Record; - public material?: string; public layers?: number; public isBillboardBased?: boolean; - public startTileIndex?: Value; - public uTileCount?: number; - public vTileCount?: number; - public blendTiles?: boolean; - public softParticles: boolean; - public softFarFade?: number; - public softNearFade?: number; private _behaviorConfigs: Behavior[]; /** @@ -386,7 +372,6 @@ export class EffectSolidParticleSystem extends SolidParticleSystem implements IS enableDepthSort?: boolean; particleIntersection?: boolean; useModelMaterial?: boolean; - transform?: { position: Vector3; rotation: Quaternion; scale: Vector3 } | null; } ) { super(name, scene, options); @@ -394,8 +379,6 @@ export class EffectSolidParticleSystem extends SolidParticleSystem implements IS this.name = name; this._behaviors = []; this.particleEmitterType = null; - this.onlyUsedByOther = false; - this.softParticles = false; this._emitter = null; // Gradient systems for "OverLife" behaviors @@ -414,7 +397,6 @@ export class EffectSolidParticleSystem extends SolidParticleSystem implements IS this._behaviorConfigs = []; this._behaviors = []; - this._transform = options?.transform ?? null; this._emitEnded = false; this._normalMatrix = new Matrix(); this._tempVec = Vector3.Zero(); @@ -809,12 +791,6 @@ export class EffectSolidParticleSystem extends SolidParticleSystem implements IS if (this._emitter) { this.mesh.setParent(this._emitter, false, true); } - - if (this._transform) { - this.mesh.position.copyFrom(this._transform.position); - this.mesh.rotationQuaternion = this._transform.rotation.clone(); - this.mesh.scaling.copyFrom(this._transform.scale); - } } private _initializeDeadParticles(): void { From e0d17f797f88ea39deb2b3ab6d89a4f081a27a72 Mon Sep 17 00:00:00 2001 From: Mikalai Lazitski Date: Thu, 18 Dec 2025 10:06:38 +0300 Subject: [PATCH 45/62] refactor: standardize property assignment syntax in effect systems for improved readability and maintainability --- tools/src/effect/effect.ts | 39 ++++++++------- tools/src/effect/factories/systemFactory.ts | 54 ++++++++++----------- 2 files changed, 46 insertions(+), 47 deletions(-) diff --git a/tools/src/effect/effect.ts b/tools/src/effect/effect.ts index 66e35697f..fd1d31984 100644 --- a/tools/src/effect/effect.ts +++ b/tools/src/effect/effect.ts @@ -1,4 +1,4 @@ -import { Scene, Tools, IDisposable, TransformNode, MeshBuilder, Texture, AbstractMesh } from "babylonjs"; +import { Scene, Tools, IDisposable, TransformNode, MeshBuilder, Texture, Color4, AbstractMesh } from "babylonjs"; import type { IQuarksJSON } from "./types/quarksTypes"; import type { ILoaderOptions } from "./types/loader"; import { Parser } from "./parsers/parser"; @@ -6,7 +6,6 @@ import { EffectParticleSystem } from "./systems/effectParticleSystem"; import { EffectSolidParticleSystem } from "./systems/effectSolidParticleSystem"; import type { IGroup, IEmitter, IData } from "./types/hierarchy"; import type { IParticleSystemConfig } from "./types/emitter"; -import { Color4 } from "babylonjs"; import { isSystem } from "./types/system"; /** @@ -669,24 +668,24 @@ export class Effect implements IDisposable { system.name = uniqueName; system.emitter = parent.group as AbstractMesh; // === Assign native properties (shared by both systems) === - if (config.minSize !== undefined) system.minSize = config.minSize; - if (config.maxSize !== undefined) system.maxSize = config.maxSize; - if (config.minLifeTime !== undefined) system.minLifeTime = config.minLifeTime; - if (config.maxLifeTime !== undefined) system.maxLifeTime = config.maxLifeTime; - if (config.minEmitPower !== undefined) system.minEmitPower = config.minEmitPower; - if (config.maxEmitPower !== undefined) system.maxEmitPower = config.maxEmitPower; - if (config.emitRate !== undefined) system.emitRate = config.emitRate; - if (config.targetStopDuration !== undefined) system.targetStopDuration = config.targetStopDuration; - if (config.manualEmitCount !== undefined) system.manualEmitCount = config.manualEmitCount; - if (config.preWarmCycles !== undefined) system.preWarmCycles = config.preWarmCycles; - if (config.preWarmStepOffset !== undefined) system.preWarmStepOffset = config.preWarmStepOffset; - if (config.color1 !== undefined) system.color1 = config.color1; - if (config.color2 !== undefined) system.color2 = config.color2; - if (config.colorDead !== undefined) system.colorDead = config.colorDead; - if (config.minInitialRotation !== undefined) system.minInitialRotation = config.minInitialRotation; - if (config.maxInitialRotation !== undefined) system.maxInitialRotation = config.maxInitialRotation; - if (config.isLocal !== undefined) system.isLocal = config.isLocal; - if (config.disposeOnStop !== undefined) system.disposeOnStop = config.disposeOnStop; + if (config.minSize !== undefined) {system.minSize = config.minSize;} + if (config.maxSize !== undefined) {system.maxSize = config.maxSize;} + if (config.minLifeTime !== undefined) {system.minLifeTime = config.minLifeTime;} + if (config.maxLifeTime !== undefined) {system.maxLifeTime = config.maxLifeTime;} + if (config.minEmitPower !== undefined) {system.minEmitPower = config.minEmitPower;} + if (config.maxEmitPower !== undefined) {system.maxEmitPower = config.maxEmitPower;} + if (config.emitRate !== undefined) {system.emitRate = config.emitRate;} + if (config.targetStopDuration !== undefined) {system.targetStopDuration = config.targetStopDuration;} + if (config.manualEmitCount !== undefined) {system.manualEmitCount = config.manualEmitCount;} + if (config.preWarmCycles !== undefined) {system.preWarmCycles = config.preWarmCycles;} + if (config.preWarmStepOffset !== undefined) {system.preWarmStepOffset = config.preWarmStepOffset;} + if (config.color1 !== undefined) {system.color1 = config.color1;} + if (config.color2 !== undefined) {system.color2 = config.color2;} + if (config.colorDead !== undefined) {system.colorDead = config.colorDead;} + if (config.minInitialRotation !== undefined) {system.minInitialRotation = config.minInitialRotation;} + if (config.maxInitialRotation !== undefined) {system.maxInitialRotation = config.maxInitialRotation;} + if (config.isLocal !== undefined) {system.isLocal = config.isLocal;} + if (config.disposeOnStop !== undefined) {system.disposeOnStop = config.disposeOnStop;} // === Apply gradients (shared by both systems) === if (config.startSizeGradients) { diff --git a/tools/src/effect/factories/systemFactory.ts b/tools/src/effect/factories/systemFactory.ts index 4219741bf..eb2f0342a 100644 --- a/tools/src/effect/factories/systemFactory.ts +++ b/tools/src/effect/factories/systemFactory.ts @@ -202,33 +202,33 @@ export class SystemFactory { * Apply common native properties to both ParticleSystem and SolidParticleSystem */ private _applyCommonProperties(system: EffectParticleSystem | EffectSolidParticleSystem, config: IParticleSystemConfig): void { - if (config.minSize !== undefined) system.minSize = config.minSize; - if (config.maxSize !== undefined) system.maxSize = config.maxSize; - if (config.minLifeTime !== undefined) system.minLifeTime = config.minLifeTime; - if (config.maxLifeTime !== undefined) system.maxLifeTime = config.maxLifeTime; - if (config.minEmitPower !== undefined) system.minEmitPower = config.minEmitPower; - if (config.maxEmitPower !== undefined) system.maxEmitPower = config.maxEmitPower; - if (config.emitRate !== undefined) system.emitRate = config.emitRate; - if (config.targetStopDuration !== undefined) system.targetStopDuration = config.targetStopDuration; - if (config.manualEmitCount !== undefined) system.manualEmitCount = config.manualEmitCount; - if (config.preWarmCycles !== undefined) system.preWarmCycles = config.preWarmCycles; - if (config.preWarmStepOffset !== undefined) system.preWarmStepOffset = config.preWarmStepOffset; - if (config.color1 !== undefined) system.color1 = config.color1; - if (config.color2 !== undefined) system.color2 = config.color2; - if (config.colorDead !== undefined) system.colorDead = config.colorDead; - if (config.minInitialRotation !== undefined) system.minInitialRotation = config.minInitialRotation; - if (config.maxInitialRotation !== undefined) system.maxInitialRotation = config.maxInitialRotation; - if (config.isLocal !== undefined) system.isLocal = config.isLocal; - if (config.disposeOnStop !== undefined) system.disposeOnStop = config.disposeOnStop; - if (config.gravity !== undefined) system.gravity = config.gravity; - if (config.noiseStrength !== undefined) system.noiseStrength = config.noiseStrength; - if (config.updateSpeed !== undefined) system.updateSpeed = config.updateSpeed; - if (config.minAngularSpeed !== undefined) system.minAngularSpeed = config.minAngularSpeed; - if (config.maxAngularSpeed !== undefined) system.maxAngularSpeed = config.maxAngularSpeed; - if (config.minScaleX !== undefined) system.minScaleX = config.minScaleX; - if (config.maxScaleX !== undefined) system.maxScaleX = config.maxScaleX; - if (config.minScaleY !== undefined) system.minScaleY = config.minScaleY; - if (config.maxScaleY !== undefined) system.maxScaleY = config.maxScaleY; + if (config.minSize !== undefined) {system.minSize = config.minSize;} + if (config.maxSize !== undefined) {system.maxSize = config.maxSize;} + if (config.minLifeTime !== undefined) {system.minLifeTime = config.minLifeTime;} + if (config.maxLifeTime !== undefined) {system.maxLifeTime = config.maxLifeTime;} + if (config.minEmitPower !== undefined) {system.minEmitPower = config.minEmitPower;} + if (config.maxEmitPower !== undefined) {system.maxEmitPower = config.maxEmitPower;} + if (config.emitRate !== undefined) {system.emitRate = config.emitRate;} + if (config.targetStopDuration !== undefined) {system.targetStopDuration = config.targetStopDuration;} + if (config.manualEmitCount !== undefined) {system.manualEmitCount = config.manualEmitCount;} + if (config.preWarmCycles !== undefined) {system.preWarmCycles = config.preWarmCycles;} + if (config.preWarmStepOffset !== undefined) {system.preWarmStepOffset = config.preWarmStepOffset;} + if (config.color1 !== undefined) {system.color1 = config.color1;} + if (config.color2 !== undefined) {system.color2 = config.color2;} + if (config.colorDead !== undefined) {system.colorDead = config.colorDead;} + if (config.minInitialRotation !== undefined) {system.minInitialRotation = config.minInitialRotation;} + if (config.maxInitialRotation !== undefined) {system.maxInitialRotation = config.maxInitialRotation;} + if (config.isLocal !== undefined) {system.isLocal = config.isLocal;} + if (config.disposeOnStop !== undefined) {system.disposeOnStop = config.disposeOnStop;} + if (config.gravity !== undefined) {system.gravity = config.gravity;} + if (config.noiseStrength !== undefined) {system.noiseStrength = config.noiseStrength;} + if (config.updateSpeed !== undefined) {system.updateSpeed = config.updateSpeed;} + if (config.minAngularSpeed !== undefined) {system.minAngularSpeed = config.minAngularSpeed;} + if (config.maxAngularSpeed !== undefined) {system.maxAngularSpeed = config.maxAngularSpeed;} + if (config.minScaleX !== undefined) {system.minScaleX = config.minScaleX;} + if (config.maxScaleX !== undefined) {system.maxScaleX = config.maxScaleX;} + if (config.minScaleY !== undefined) {system.minScaleY = config.minScaleY;} + if (config.maxScaleY !== undefined) {system.maxScaleY = config.maxScaleY;} } /** From a3109ff51186650fb8175d11f2c51c37e9edab62 Mon Sep 17 00:00:00 2001 From: Mikalai Lazitski Date: Thu, 18 Dec 2025 12:34:51 +0300 Subject: [PATCH 46/62] refactor: enhance effect system configuration by standardizing property assignments, improving gradient handling, and consolidating type definitions for clarity and maintainability --- .../layout/inspector/fields/geometry.tsx | 9 +- .../layout/inspector/fields/gradient.tsx | 3 +- .../windows/effect-editor/editors/bezier.tsx | 8 +- .../effect-editor/properties/emission.tsx | 82 ++++--- .../properties/initialization.tsx | 216 ++++++------------ .../effect-editor/properties/renderer.tsx | 68 ++---- editor/src/ui/gradient-picker.tsx | 5 +- tools/src/effect/effect.ts | 72 ++++-- tools/src/effect/factories/systemFactory.ts | 108 ++++++--- tools/src/effect/parsers/dataConverter.ts | 206 ++++++++++++----- tools/src/effect/types/emitter.ts | 11 +- 11 files changed, 441 insertions(+), 347 deletions(-) diff --git a/editor/src/editor/layout/inspector/fields/geometry.tsx b/editor/src/editor/layout/inspector/fields/geometry.tsx index 0edb73784..8e1f856ed 100644 --- a/editor/src/editor/layout/inspector/fields/geometry.tsx +++ b/editor/src/editor/layout/inspector/fields/geometry.tsx @@ -13,7 +13,6 @@ import { registerUndoRedo } from "../../../../tools/undoredo"; import { configureImportedNodeIds, loadImportedSceneFile } from "../../preview/import/import"; import { EditorInspectorNumberField } from "./number"; -import { isMesh } from "babylonjs-editor-tools"; export interface IEditorInspectorGeometryFieldProps extends PropsWithChildren { title: string; @@ -163,14 +162,14 @@ export class EditorInspectorGeometryField extends Component 0 && isMesh(result.meshes[0])) { - importedMesh = result.meshes[0] as Mesh; + if (!importedMesh && result.meshes.length > 0 && result.meshes[0] instanceof Mesh) { + importedMesh = result.meshes[0]; } if (!importedMesh) { @@ -210,7 +209,7 @@ export class EditorInspectorGeometryField extends Component void): const system = nodeData.system; + // Proxy for looping (targetStopDuration === 0 means looping) + const loopingProxy = { + get isLooping() { + return system.targetStopDuration === 0; + }, + set isLooping(value: boolean) { + if (value) { + system.targetStopDuration = 0; + } else if (system.targetStopDuration === 0) { + system.targetStopDuration = 5; // Default duration + } + }, + }; + + // Proxy for prewarm (preWarmCycles > 0 means prewarm enabled) + const prewarmProxy = { + get prewarm() { + return system.preWarmCycles > 0; + }, + set prewarm(value: boolean) { + if (value && system.preWarmCycles === 0) { + system.preWarmCycles = Math.ceil((system.targetStopDuration || 5) * 60); + system.preWarmStepOffset = 1 / 60; + } else if (!value) { + system.preWarmCycles = 0; + } + }, + }; + return ( <> - - - - - - - { - (system as any).emissionOverTime = val; - onChange(); - }} - /> - - - - { - (system as any).emissionOverDistance = val; - onChange(); - }} - /> - + + + + + {/* Emit Rate (native Babylon.js property) */} + + + {/* Emit Over Distance - only for SolidParticleSystem */} + {system instanceof EffectSolidParticleSystem && ( + + { + (system as EffectSolidParticleSystem).emissionOverDistance = val as Value; + onChange(); + }} + /> + + )} {system instanceof EffectParticleSystem && ( diff --git a/editor/src/editor/windows/effect-editor/properties/initialization.tsx b/editor/src/editor/windows/effect-editor/properties/initialization.tsx index 4e938e758..ef795edfd 100644 --- a/editor/src/editor/windows/effect-editor/properties/initialization.tsx +++ b/editor/src/editor/windows/effect-editor/properties/initialization.tsx @@ -2,10 +2,9 @@ import { ReactNode } from "react"; import { EditorInspectorBlockField } from "../../../layout/inspector/fields/block"; -import { type IEffectNode, EffectSolidParticleSystem, EffectParticleSystem, ValueUtils, Value, Color, Rotation } from "babylonjs-editor-tools"; +import { type IEffectNode, ValueUtils, Value, Color, Rotation } from "babylonjs-editor-tools"; import { EffectValueEditor, type IVec3Function } from "../editors/value"; import { EffectColorEditor } from "../editors/color"; -import { EffectRotationEditor } from "../editors/rotation"; export interface IEffectEditorParticleInitializationPropertiesProps { nodeData: IEffectNode; @@ -22,150 +21,93 @@ export function EffectEditorParticleInitializationProperties(props: IEffectEdito const system = nodeData.system; - // Helper to get/set startLife as VEffectValue for both systems + // Helper to get/set startLife - both systems use native minLifeTime/maxLifeTime const getStartLife = (): Value | undefined => { - if (system instanceof EffectSolidParticleSystem) { - return system.startLife; - } - // For VEffectParticleSystem, convert minLifeTime/maxLifeTime to IntervalValue - if (system instanceof EffectParticleSystem) { - return { type: "IntervalValue", min: system.minLifeTime, max: system.maxLifeTime }; - } - return undefined; + // Both systems have native minLifeTime/maxLifeTime properties + return { type: "IntervalValue", min: system.minLifeTime, max: system.maxLifeTime }; }; const setStartLife = (value: Value): void => { - if (system instanceof EffectSolidParticleSystem) { - system.startLife = value; - } else if (system instanceof EffectParticleSystem) { - const interval = ValueUtils.parseIntervalValue(value); - system.minLifeTime = interval.min; - system.maxLifeTime = interval.max; - } + const interval = ValueUtils.parseIntervalValue(value); + system.minLifeTime = interval.min; + system.maxLifeTime = interval.max; onChange(); }; - // Helper to get/set startSize as VEffectValue | IVec3Function for both systems + // Helper to get/set startSize - both systems use native minSize/maxSize const getStartSize = (): Value | IVec3Function | undefined => { - if (system instanceof EffectSolidParticleSystem) { - return system.startSize; - } - // For VEffectParticleSystem, convert minSize/maxSize to IntervalValue - if (system instanceof EffectParticleSystem) { - return { type: "IntervalValue", min: system.minSize, max: system.maxSize }; - } - return undefined; + // Both systems have native minSize/maxSize properties + return { type: "IntervalValue", min: system.minSize, max: system.maxSize }; }; const setStartSize = (value: Value | IVec3Function): void => { - if (system instanceof EffectSolidParticleSystem) { - // For Vec3Function, we need to handle it differently - but VEffectSolidParticleSystem doesn't support Vec3Function yet - // For now, convert Vec3Function to a single value - if (typeof value === "object" && "type" in value && value.type === "Vec3Function") { - const x = ValueUtils.parseConstantValue(value.x); - const y = ValueUtils.parseConstantValue(value.y); - const z = ValueUtils.parseConstantValue(value.z); - const avg = (x + y + z) / 3; - system.startSize = { type: "ConstantValue", value: avg }; - } else { - system.startSize = value as Value; - } - } else if (system instanceof EffectParticleSystem) { - if (typeof value === "object" && "type" in value && value.type === "Vec3Function") { - // For Vec3Function, use average of x, y, z - const x = ValueUtils.parseConstantValue(value.x); - const y = ValueUtils.parseConstantValue(value.y); - const z = ValueUtils.parseConstantValue(value.z); - const avg = (x + y + z) / 3; - system.minSize = avg; - system.maxSize = avg; - } else { - const interval = ValueUtils.parseIntervalValue(value as Value); - system.minSize = interval.min; - system.maxSize = interval.max; - } + if (typeof value === "object" && "type" in value && value.type === "Vec3Function") { + // For Vec3Function, use average of x, y, z + const x = ValueUtils.parseConstantValue(value.x); + const y = ValueUtils.parseConstantValue(value.y); + const z = ValueUtils.parseConstantValue(value.z); + const avg = (x + y + z) / 3; + system.minSize = avg; + system.maxSize = avg; + } else { + const interval = ValueUtils.parseIntervalValue(value as Value); + system.minSize = interval.min; + system.maxSize = interval.max; } onChange(); }; - // Helper to get/set startSpeed as VEffectValue for both systems + // Helper to get/set startSpeed - both systems use native minEmitPower/maxEmitPower const getStartSpeed = (): Value | undefined => { - if (system instanceof EffectSolidParticleSystem) { - return system.startSpeed; - } - // For VEffectParticleSystem, convert minEmitPower/maxEmitPower to IntervalValue - if (system instanceof EffectParticleSystem) { - return { type: "IntervalValue", min: system.minEmitPower, max: system.maxEmitPower }; - } - return undefined; + // Both systems have native minEmitPower/maxEmitPower properties + return { type: "IntervalValue", min: system.minEmitPower, max: system.maxEmitPower }; }; const setStartSpeed = (value: Value): void => { - if (system instanceof EffectSolidParticleSystem) { - system.startSpeed = value; - } else if (system instanceof EffectParticleSystem) { - const interval = ValueUtils.parseIntervalValue(value); - system.minEmitPower = interval.min; - system.maxEmitPower = interval.max; - } + const interval = ValueUtils.parseIntervalValue(value); + system.minEmitPower = interval.min; + system.maxEmitPower = interval.max; onChange(); }; - // Helper to get/set startColor as VEffectColor for both systems + // Helper to get/set startColor - both systems use native color1 const getStartColor = (): Color | undefined => { - if (system instanceof EffectSolidParticleSystem) { - return system.startColor; - } - // For VEffectParticleSystem, convert Color4 to ConstantColor - if (system instanceof EffectParticleSystem && system.color1) { + // Both systems have native color1 property + if (system.color1) { return { type: "ConstantColor", value: [system.color1.r, system.color1.g, system.color1.b, system.color1.a] }; } return undefined; }; const setStartColor = (value: Color): void => { - if (system instanceof EffectSolidParticleSystem) { - system.startColor = value; - } else if (system instanceof EffectParticleSystem) { - const color = ValueUtils.parseConstantColor(value); - system.color1 = color; - } + const color = ValueUtils.parseConstantColor(value); + system.color1 = color; onChange(); }; - // Helper to get/set startRotation as VEffectRotation for both systems + // Helper to get/set startRotation - both systems use native minInitialRotation/maxInitialRotation const getStartRotation = (): Rotation | undefined => { - if (system instanceof EffectSolidParticleSystem) { - return system.startRotation; - } - // For VEffectParticleSystem, convert minInitialRotation/maxInitialRotation to Euler with angleZ - if (system instanceof EffectParticleSystem) { - return { - type: "Euler", - angleZ: { type: "IntervalValue", min: system.minInitialRotation, max: system.maxInitialRotation }, - order: "xyz", - }; - } - return undefined; + // Both systems have native minInitialRotation/maxInitialRotation properties + return { + type: "Euler", + angleZ: { type: "IntervalValue", min: system.minInitialRotation, max: system.maxInitialRotation }, + order: "xyz", + }; }; const setStartRotation = (value: Rotation): void => { - if (system instanceof EffectSolidParticleSystem) { - system.startRotation = value; - } else if (system instanceof EffectParticleSystem) { - // Extract angleZ from rotation for VEffectParticleSystem - if (typeof value === "object" && "type" in value && value.type === "Euler" && value.angleZ) { - const interval = ValueUtils.parseIntervalValue(value.angleZ); - system.minInitialRotation = interval.min; - system.maxInitialRotation = interval.max; - } else if ( - typeof value === "number" || - (typeof value === "object" && "type" in value && (value.type === "ConstantValue" || value.type === "IntervalValue" || value.type === "PiecewiseBezier")) - ) { - const interval = ValueUtils.parseIntervalValue(value as Value); - system.minInitialRotation = interval.min; - system.maxInitialRotation = interval.max; - } + // Extract angleZ from rotation + if (typeof value === "object" && "type" in value && value.type === "Euler" && value.angleZ) { + const interval = ValueUtils.parseIntervalValue(value.angleZ); + system.minInitialRotation = interval.min; + system.maxInitialRotation = interval.max; + } else if ( + typeof value === "number" || + (typeof value === "object" && "type" in value && (value.type === "ConstantValue" || value.type === "IntervalValue" || value.type === "PiecewiseBezier")) + ) { + const interval = ValueUtils.parseIntervalValue(value as Value); + system.minInitialRotation = interval.min; + system.maxInitialRotation = interval.max; } onChange(); }; @@ -200,38 +142,28 @@ export function EffectEditorParticleInitializationProperties(props: IEffectEdito
Start Rotation
- {system instanceof EffectSolidParticleSystem ? ( - - ) : ( - (() => { - // For VEffectParticleSystem, extract angleZ from rotation - const rotation = getStartRotation(); - const angleZ = - rotation && typeof rotation === "object" && "type" in rotation && rotation.type === "Euler" && rotation.angleZ - ? rotation.angleZ - : rotation && - (typeof rotation === "number" || - (typeof rotation === "object" && - "type" in rotation && - (rotation.type === "ConstantValue" || rotation.type === "IntervalValue" || rotation.type === "PiecewiseBezier"))) - ? (rotation as Value) - : { type: "IntervalValue" as const, min: 0, max: 0 }; - return ( - { - setStartRotation({ - type: "Euler", - angleZ: newAngleZ as Value, - order: "xyz", - }); - }} - availableTypes={["ConstantValue", "IntervalValue", "PiecewiseBezier"]} - step={0.1} - /> - ); - })() - )} + {(() => { + // Both systems use native minInitialRotation/maxInitialRotation + const rotation = getStartRotation(); + const angleZ = + rotation && typeof rotation === "object" && "type" in rotation && rotation.type === "Euler" && rotation.angleZ + ? rotation.angleZ + : { type: "IntervalValue" as const, min: 0, max: 0 }; + return ( + { + setStartRotation({ + type: "Euler", + angleZ: newAngleZ as Value, + order: "xyz", + }); + }} + availableTypes={["ConstantValue", "IntervalValue", "PiecewiseBezier"]} + step={0.1} + /> + ); + })()}
); diff --git a/editor/src/editor/windows/effect-editor/properties/renderer.tsx b/editor/src/editor/windows/effect-editor/properties/renderer.tsx index ed2b29cc1..a20fc097f 100644 --- a/editor/src/editor/windows/effect-editor/properties/renderer.tsx +++ b/editor/src/editor/windows/effect-editor/properties/renderer.tsx @@ -25,7 +25,6 @@ import { EditorGradientMaterialInspector } from "../../../layout/inspector/mater import { type IEffectNode, EffectSolidParticleSystem, EffectParticleSystem } from "babylonjs-editor-tools"; import { IEffectEditor } from ".."; -import { EffectValueEditor } from "../editors/value"; import { CellMaterial, FireMaterial, GradientMaterial, GridMaterial, LavaMaterial, NormalMaterial, SkyMaterial, TriPlanarMaterial, WaterMaterial } from "babylonjs-materials"; export interface IEffectEditorParticleRendererPropertiesProps { @@ -82,21 +81,18 @@ export class EffectEditorParticleRendererProperties extends Component )} - {/* World Space */} - {isEffectParticleSystem && - (() => { - // Для VEffectParticleSystem, worldSpace = !isLocal - const proxy = { - get worldSpace() { - return !system.isLocal; - }, - set worldSpace(value: boolean) { - system.isLocal = !value; - }, - }; - return this.props.onChange()} />; - })()} - {isEffectSolidParticleSystem && this.props.onChange()} />} + {/* World Space (isLocal inverted) */} + {(() => { + const proxy = { + get worldSpace() { + return !system.isLocal; + }, + set worldSpace(value: boolean) { + system.isLocal = !value; + }, + }; + return this.props.onChange()} />; + })()} {/* Material Inspector - только для solid с материалом */} {isEffectSolidParticleSystem && this._getMaterialInspector()} @@ -130,11 +126,8 @@ export class EffectEditorParticleRendererProperties extends Component this.props.onChange()} />} - {isEffectSolidParticleSystem && ( - this.props.onChange()} /> - )} {/* Geometry - только для solid */} {isEffectSolidParticleSystem && this._getGeometryField()} @@ -268,30 +261,17 @@ export class EffectEditorParticleRendererProperties extends Component this.props.onChange()} /> this.props.onChange()} /> - {/* TODO: Add blendTiles if available for VEffectParticleSystem */} - - ); - } - - // Для VEffectSolidParticleSystem, используем uTileCount и vTileCount - if (system instanceof EffectSolidParticleSystem) { - return ( - - this.props.onChange()} /> - this.props.onChange()} /> - {system.blendTiles !== undefined && ( - this.props.onChange()} /> - )} ); } + // SolidParticleSystem uses mesh UVs, no tile settings return null; } @@ -304,26 +284,12 @@ export class EffectEditorParticleRendererProperties extends Component this.props.onChange()} />; } - // Для VEffectSolidParticleSystem, используем startTileIndex (VEffectValue) - if (system instanceof EffectSolidParticleSystem && system.startTileIndex !== undefined) { - const getValue = () => system.startTileIndex!; - const setValue = (value: any) => { - system.startTileIndex = value; - this.props.onChange(); - }; - - return ( -
- -
- ); - } - + // SolidParticleSystem uses mesh UVs, no tile index return null; } diff --git a/editor/src/ui/gradient-picker.tsx b/editor/src/ui/gradient-picker.tsx index ca84d9d92..25f02aeed 100644 --- a/editor/src/ui/gradient-picker.tsx +++ b/editor/src/ui/gradient-picker.tsx @@ -53,7 +53,7 @@ export function GradientPicker(props: IGradientPickerProps): ReactNode { const r = key.value.r * 255; const g = key.value.g * 255; const b = key.value.b * 255; - const a = ("a" in key.value ? key.value.a : 1) * 255; + const a = ("a" in key.value && key.value.a !== undefined ? key.value.a : 1) * 255; color = `rgba(${r}, ${g}, ${b}, ${a / 255})`; } return `${color} ${pos}%`; @@ -141,7 +141,8 @@ export function GradientPicker(props: IGradientPickerProps): ReactNode { // Interpolate color at position const color = interpolateColorAtPosition(sortedColorKeys, pos); - const newColorKeys = [...colorKeys, { pos, value: [color.r, color.g, color.b, color.a] }]; + const newKey: IGradientKey = { pos, value: [color.r, color.g, color.b, color.a] as [number, number, number, number] }; + const newColorKeys: IGradientKey[] = [...colorKeys, newKey]; const sorted = newColorKeys.sort((a, b) => (a.pos || 0) - (b.pos || 0)); const newIndex = sorted.findIndex((key) => key.pos === pos); setSelectedKeyIndex(newIndex); diff --git a/tools/src/effect/effect.ts b/tools/src/effect/effect.ts index fd1d31984..2fad1da6c 100644 --- a/tools/src/effect/effect.ts +++ b/tools/src/effect/effect.ts @@ -668,24 +668,60 @@ export class Effect implements IDisposable { system.name = uniqueName; system.emitter = parent.group as AbstractMesh; // === Assign native properties (shared by both systems) === - if (config.minSize !== undefined) {system.minSize = config.minSize;} - if (config.maxSize !== undefined) {system.maxSize = config.maxSize;} - if (config.minLifeTime !== undefined) {system.minLifeTime = config.minLifeTime;} - if (config.maxLifeTime !== undefined) {system.maxLifeTime = config.maxLifeTime;} - if (config.minEmitPower !== undefined) {system.minEmitPower = config.minEmitPower;} - if (config.maxEmitPower !== undefined) {system.maxEmitPower = config.maxEmitPower;} - if (config.emitRate !== undefined) {system.emitRate = config.emitRate;} - if (config.targetStopDuration !== undefined) {system.targetStopDuration = config.targetStopDuration;} - if (config.manualEmitCount !== undefined) {system.manualEmitCount = config.manualEmitCount;} - if (config.preWarmCycles !== undefined) {system.preWarmCycles = config.preWarmCycles;} - if (config.preWarmStepOffset !== undefined) {system.preWarmStepOffset = config.preWarmStepOffset;} - if (config.color1 !== undefined) {system.color1 = config.color1;} - if (config.color2 !== undefined) {system.color2 = config.color2;} - if (config.colorDead !== undefined) {system.colorDead = config.colorDead;} - if (config.minInitialRotation !== undefined) {system.minInitialRotation = config.minInitialRotation;} - if (config.maxInitialRotation !== undefined) {system.maxInitialRotation = config.maxInitialRotation;} - if (config.isLocal !== undefined) {system.isLocal = config.isLocal;} - if (config.disposeOnStop !== undefined) {system.disposeOnStop = config.disposeOnStop;} + if (config.minSize !== undefined) { + system.minSize = config.minSize; + } + if (config.maxSize !== undefined) { + system.maxSize = config.maxSize; + } + if (config.minLifeTime !== undefined) { + system.minLifeTime = config.minLifeTime; + } + if (config.maxLifeTime !== undefined) { + system.maxLifeTime = config.maxLifeTime; + } + if (config.minEmitPower !== undefined) { + system.minEmitPower = config.minEmitPower; + } + if (config.maxEmitPower !== undefined) { + system.maxEmitPower = config.maxEmitPower; + } + if (config.emitRate !== undefined) { + system.emitRate = config.emitRate; + } + if (config.targetStopDuration !== undefined) { + system.targetStopDuration = config.targetStopDuration; + } + if (config.manualEmitCount !== undefined) { + system.manualEmitCount = config.manualEmitCount; + } + if (config.preWarmCycles !== undefined) { + system.preWarmCycles = config.preWarmCycles; + } + if (config.preWarmStepOffset !== undefined) { + system.preWarmStepOffset = config.preWarmStepOffset; + } + if (config.color1 !== undefined) { + system.color1 = config.color1; + } + if (config.color2 !== undefined) { + system.color2 = config.color2; + } + if (config.colorDead !== undefined) { + system.colorDead = config.colorDead; + } + if (config.minInitialRotation !== undefined) { + system.minInitialRotation = config.minInitialRotation; + } + if (config.maxInitialRotation !== undefined) { + system.maxInitialRotation = config.maxInitialRotation; + } + if (config.isLocal !== undefined) { + system.isLocal = config.isLocal; + } + if (config.disposeOnStop !== undefined) { + system.disposeOnStop = config.disposeOnStop; + } // === Apply gradients (shared by both systems) === if (config.startSizeGradients) { diff --git a/tools/src/effect/factories/systemFactory.ts b/tools/src/effect/factories/systemFactory.ts index eb2f0342a..94db6a31e 100644 --- a/tools/src/effect/factories/systemFactory.ts +++ b/tools/src/effect/factories/systemFactory.ts @@ -202,33 +202,87 @@ export class SystemFactory { * Apply common native properties to both ParticleSystem and SolidParticleSystem */ private _applyCommonProperties(system: EffectParticleSystem | EffectSolidParticleSystem, config: IParticleSystemConfig): void { - if (config.minSize !== undefined) {system.minSize = config.minSize;} - if (config.maxSize !== undefined) {system.maxSize = config.maxSize;} - if (config.minLifeTime !== undefined) {system.minLifeTime = config.minLifeTime;} - if (config.maxLifeTime !== undefined) {system.maxLifeTime = config.maxLifeTime;} - if (config.minEmitPower !== undefined) {system.minEmitPower = config.minEmitPower;} - if (config.maxEmitPower !== undefined) {system.maxEmitPower = config.maxEmitPower;} - if (config.emitRate !== undefined) {system.emitRate = config.emitRate;} - if (config.targetStopDuration !== undefined) {system.targetStopDuration = config.targetStopDuration;} - if (config.manualEmitCount !== undefined) {system.manualEmitCount = config.manualEmitCount;} - if (config.preWarmCycles !== undefined) {system.preWarmCycles = config.preWarmCycles;} - if (config.preWarmStepOffset !== undefined) {system.preWarmStepOffset = config.preWarmStepOffset;} - if (config.color1 !== undefined) {system.color1 = config.color1;} - if (config.color2 !== undefined) {system.color2 = config.color2;} - if (config.colorDead !== undefined) {system.colorDead = config.colorDead;} - if (config.minInitialRotation !== undefined) {system.minInitialRotation = config.minInitialRotation;} - if (config.maxInitialRotation !== undefined) {system.maxInitialRotation = config.maxInitialRotation;} - if (config.isLocal !== undefined) {system.isLocal = config.isLocal;} - if (config.disposeOnStop !== undefined) {system.disposeOnStop = config.disposeOnStop;} - if (config.gravity !== undefined) {system.gravity = config.gravity;} - if (config.noiseStrength !== undefined) {system.noiseStrength = config.noiseStrength;} - if (config.updateSpeed !== undefined) {system.updateSpeed = config.updateSpeed;} - if (config.minAngularSpeed !== undefined) {system.minAngularSpeed = config.minAngularSpeed;} - if (config.maxAngularSpeed !== undefined) {system.maxAngularSpeed = config.maxAngularSpeed;} - if (config.minScaleX !== undefined) {system.minScaleX = config.minScaleX;} - if (config.maxScaleX !== undefined) {system.maxScaleX = config.maxScaleX;} - if (config.minScaleY !== undefined) {system.minScaleY = config.minScaleY;} - if (config.maxScaleY !== undefined) {system.maxScaleY = config.maxScaleY;} + if (config.minSize !== undefined) { + system.minSize = config.minSize; + } + if (config.maxSize !== undefined) { + system.maxSize = config.maxSize; + } + if (config.minLifeTime !== undefined) { + system.minLifeTime = config.minLifeTime; + } + if (config.maxLifeTime !== undefined) { + system.maxLifeTime = config.maxLifeTime; + } + if (config.minEmitPower !== undefined) { + system.minEmitPower = config.minEmitPower; + } + if (config.maxEmitPower !== undefined) { + system.maxEmitPower = config.maxEmitPower; + } + if (config.emitRate !== undefined) { + system.emitRate = config.emitRate; + } + if (config.targetStopDuration !== undefined) { + system.targetStopDuration = config.targetStopDuration; + } + if (config.manualEmitCount !== undefined) { + system.manualEmitCount = config.manualEmitCount; + } + if (config.preWarmCycles !== undefined) { + system.preWarmCycles = config.preWarmCycles; + } + if (config.preWarmStepOffset !== undefined) { + system.preWarmStepOffset = config.preWarmStepOffset; + } + if (config.color1 !== undefined) { + system.color1 = config.color1; + } + if (config.color2 !== undefined) { + system.color2 = config.color2; + } + if (config.colorDead !== undefined) { + system.colorDead = config.colorDead; + } + if (config.minInitialRotation !== undefined) { + system.minInitialRotation = config.minInitialRotation; + } + if (config.maxInitialRotation !== undefined) { + system.maxInitialRotation = config.maxInitialRotation; + } + if (config.isLocal !== undefined) { + system.isLocal = config.isLocal; + } + if (config.disposeOnStop !== undefined) { + system.disposeOnStop = config.disposeOnStop; + } + if (config.gravity !== undefined) { + system.gravity = config.gravity; + } + if (config.noiseStrength !== undefined) { + system.noiseStrength = config.noiseStrength; + } + if (config.updateSpeed !== undefined) { + system.updateSpeed = config.updateSpeed; + } + if (config.minAngularSpeed !== undefined) { + system.minAngularSpeed = config.minAngularSpeed; + } + if (config.maxAngularSpeed !== undefined) { + system.maxAngularSpeed = config.maxAngularSpeed; + } + if (config.minScaleX !== undefined) { + system.minScaleX = config.minScaleX; + } + if (config.maxScaleX !== undefined) { + system.maxScaleX = config.maxScaleX; + } + if (config.minScaleY !== undefined) { + system.minScaleY = config.minScaleY; + } + if (config.maxScaleY !== undefined) { + system.maxScaleY = config.maxScaleY; + } } /** diff --git a/tools/src/effect/parsers/dataConverter.ts b/tools/src/effect/parsers/dataConverter.ts index 7ff76468e..4218f6d51 100644 --- a/tools/src/effect/parsers/dataConverter.ts +++ b/tools/src/effect/parsers/dataConverter.ts @@ -41,8 +41,6 @@ import type { ISizeBySpeedBehavior, } from "../types/behaviors"; import type { Value } from "../types/values"; -import type { Color } from "../types/colors"; -import type { Rotation } from "../types/rotations"; import type { IGradientKey } from "../types/gradients"; import type { IShape } from "../types/shapes"; import { Logger } from "../loggers/logger"; @@ -280,42 +278,72 @@ export class DataConverter { isLocal, disposeOnStop, // Other properties - onlyUsedByOther: IQuarksConfig.onlyUsedByOther, - instancingGeometry: IQuarksConfig.instancingGeometry, + instancingGeometry: IQuarksConfig.instancingGeometry, // Custom geometry for SPS renderOrder: IQuarksConfig.renderOrder, - rendererEmitterSettings: IQuarksConfig.rendererEmitterSettings, - material: IQuarksConfig.material, layers: IQuarksConfig.layers, + // Sprite animation (ParticleSystem only) uTileCount: IQuarksConfig.uTileCount, vTileCount: IQuarksConfig.vTileCount, - blendTiles: IQuarksConfig.blendTiles, - softParticles: IQuarksConfig.softParticles, - softFarFade: IQuarksConfig.softFarFade, - softNearFade: IQuarksConfig.softNearFade, }; - // Convert values + // === Convert Quarks values to native Babylon.js properties === + + // Convert startLife → minLifeTime, maxLifeTime, lifeTimeGradients if (IQuarksConfig.startLife !== undefined) { - Config.startLife = this._convertValue(IQuarksConfig.startLife); + const lifeResult = this._convertValueToMinMax(IQuarksConfig.startLife); + Config.minLifeTime = lifeResult.min; + Config.maxLifeTime = lifeResult.max; + if (lifeResult.gradients) { + Config.lifeTimeGradients = lifeResult.gradients; + } } + + // Convert startSpeed → minEmitPower, maxEmitPower if (IQuarksConfig.startSpeed !== undefined) { - Config.startSpeed = this._convertValue(IQuarksConfig.startSpeed); - } - if (IQuarksConfig.startRotation !== undefined) { - Config.startRotation = this._convertRotation(IQuarksConfig.startRotation); + const speedResult = this._convertValueToMinMax(IQuarksConfig.startSpeed); + Config.minEmitPower = speedResult.min; + Config.maxEmitPower = speedResult.max; } + + // Convert startSize → minSize, maxSize, startSizeGradients if (IQuarksConfig.startSize !== undefined) { - Config.startSize = this._convertValue(IQuarksConfig.startSize); + const sizeResult = this._convertValueToMinMax(IQuarksConfig.startSize); + Config.minSize = sizeResult.min; + Config.maxSize = sizeResult.max; + if (sizeResult.gradients) { + Config.startSizeGradients = sizeResult.gradients; + } + } + + // Convert startRotation → minInitialRotation, maxInitialRotation + if (IQuarksConfig.startRotation !== undefined) { + const rotResult = this._convertRotationToMinMax(IQuarksConfig.startRotation); + Config.minInitialRotation = rotResult.min; + Config.maxInitialRotation = rotResult.max; } + + // Convert startColor → color1, color2 if (IQuarksConfig.startColor !== undefined) { - Config.startColor = this._convertColor(IQuarksConfig.startColor); + const colorResult = this._convertColorToColor4(IQuarksConfig.startColor); + Config.color1 = colorResult.color1; + Config.color2 = colorResult.color2; } + + // Convert emissionOverTime → emitRate, emitRateGradients if (IQuarksConfig.emissionOverTime !== undefined) { - Config.emissionOverTime = this._convertValue(IQuarksConfig.emissionOverTime); + const emitResult = this._convertValueToMinMax(IQuarksConfig.emissionOverTime); + Config.emitRate = emitResult.min; // Use min as base rate + if (emitResult.gradients) { + Config.emitRateGradients = emitResult.gradients; + } } + + // emissionOverDistance - only for SPS, keep as Value if (IQuarksConfig.emissionOverDistance !== undefined) { Config.emissionOverDistance = this._convertValue(IQuarksConfig.emissionOverDistance); } + + // startTileIndex - for sprite animation (ParticleSystem only) if (IQuarksConfig.startTileIndex !== undefined) { Config.startTileIndex = this._convertValue(IQuarksConfig.startTileIndex); } @@ -414,54 +442,122 @@ export class DataConverter { } /** - * Convert IQuarks color to color + * Convert IQuarks value to native Babylon.js min/max + gradients + * - ConstantValue → min = max = value + * - IntervalValue → min = a, max = b + * - PiecewiseBezier → gradients array */ - private _convertColor(IQuarksColor: IQuarksColor): Color { - if (typeof IQuarksColor === "string" || Array.isArray(IQuarksColor)) { - return IQuarksColor; + private _convertValueToMinMax(IQuarksValue: IQuarksValue): { min: number; max: number; gradients?: Array<{ gradient: number; factor: number; factor2?: number }> } { + if (typeof IQuarksValue === "number") { + return { min: IQuarksValue, max: IQuarksValue }; } - if (IQuarksColor.type === "ConstantColor") { - if (IQuarksColor.value && Array.isArray(IQuarksColor.value)) { - return { - type: "ConstantColor", - value: IQuarksColor.value, - }; + if (IQuarksValue.type === "ConstantValue") { + return { min: IQuarksValue.value, max: IQuarksValue.value }; + } + if (IQuarksValue.type === "IntervalValue") { + return { min: IQuarksValue.a ?? 0, max: IQuarksValue.b ?? 0 }; + } + if (IQuarksValue.type === "PiecewiseBezier" && IQuarksValue.functions) { + // Convert PiecewiseBezier to gradients + const gradients: Array<{ gradient: number; factor: number; factor2?: number }> = []; + let minVal = Infinity; + let maxVal = -Infinity; + + for (const func of IQuarksValue.functions) { + const startTime = func.start; + // Evaluate bezier at start and end points + const startValue = this._evaluateBezierAt(func.function, 0); + const endValue = this._evaluateBezierAt(func.function, 1); + + gradients.push({ gradient: startTime, factor: startValue }); + + // Track min/max for fallback + minVal = Math.min(minVal, startValue, endValue); + maxVal = Math.max(maxVal, startValue, endValue); } - if (IQuarksColor.color) { - return { - type: "ConstantColor", - value: [IQuarksColor.color.r || 0, IQuarksColor.color.g || 0, IQuarksColor.color.b || 0, IQuarksColor.color.a !== undefined ? IQuarksColor.color.a : 1], - }; + + // Add final point at gradient 1.0 if not present + if (gradients.length > 0 && gradients[gradients.length - 1].gradient < 1) { + const lastFunc = IQuarksValue.functions[IQuarksValue.functions.length - 1]; + const endValue = this._evaluateBezierAt(lastFunc.function, 1); + gradients.push({ gradient: 1, factor: endValue }); } - // Fallback: return default color if neither value nor color is present + return { - type: "ConstantColor", - value: [1, 1, 1, 1], + min: minVal === Infinity ? 1 : minVal, + max: maxVal === -Infinity ? 1 : maxVal, + gradients: gradients.length > 0 ? gradients : undefined, }; } - return IQuarksColor as Color; + return { min: 1, max: 1 }; } /** - * Convert IQuarks rotation to rotation + * Evaluate bezier curve at time t + * Bezier format: { p0, p1, p2, p3 } for cubic bezier */ - private _convertRotation(IQuarksRotation: IQuarksRotation): Rotation { - if ( - typeof IQuarksRotation === "number" || - (typeof IQuarksRotation === "object" && IQuarksRotation !== null && "type" in IQuarksRotation && IQuarksRotation.type !== "Euler") - ) { - return this._convertValue(IQuarksRotation as IQuarksValue); - } - if (typeof IQuarksRotation === "object" && IQuarksRotation !== null && "type" in IQuarksRotation && IQuarksRotation.type === "Euler") { - return { - type: "Euler", - angleX: IQuarksRotation.angleX !== undefined ? this._convertValue(IQuarksRotation.angleX) : undefined, - angleY: IQuarksRotation.angleY !== undefined ? this._convertValue(IQuarksRotation.angleY) : undefined, - angleZ: IQuarksRotation.angleZ !== undefined ? this._convertValue(IQuarksRotation.angleZ) : undefined, - order: (IQuarksRotation as any).order || "xyz", // Default to xyz if not specified - }; + private _evaluateBezierAt(bezier: { p0: number; p1: number; p2: number; p3: number }, t: number): number { + const { p0, p1, p2, p3 } = bezier; + const t2 = t * t; + const t3 = t2 * t; + const mt = 1 - t; + const mt2 = mt * mt; + const mt3 = mt2 * mt; + return mt3 * p0 + 3 * mt2 * t * p1 + 3 * mt * t2 * p2 + t3 * p3; + } + + /** + * Convert IQuarks rotation to native min/max radians + */ + private _convertRotationToMinMax(IQuarksRotation: IQuarksRotation): { min: number; max: number } { + if (typeof IQuarksRotation === "number") { + return { min: IQuarksRotation, max: IQuarksRotation }; + } + if (typeof IQuarksRotation === "object" && IQuarksRotation !== null && "type" in IQuarksRotation) { + if (IQuarksRotation.type === "ConstantValue") { + const val = (IQuarksRotation as any).value ?? 0; + return { min: val, max: val }; + } + if (IQuarksRotation.type === "IntervalValue") { + return { min: (IQuarksRotation as any).a ?? 0, max: (IQuarksRotation as any).b ?? 0 }; + } + } + return { min: 0, max: 0 }; + } + + /** + * Convert IQuarks color to native Babylon.js Color4 (color1, color2) + */ + private _convertColorToColor4(IQuarksColor: IQuarksColor): { color1: import("babylonjs").Color4; color2: import("babylonjs").Color4 } { + const { Color4 } = require("babylonjs"); + + if (Array.isArray(IQuarksColor)) { + const c = new Color4(IQuarksColor[0] || 1, IQuarksColor[1] || 1, IQuarksColor[2] || 1, IQuarksColor[3] ?? 1); + return { color1: c, color2: c }; + } + + if (typeof IQuarksColor === "object" && IQuarksColor !== null && "type" in IQuarksColor) { + if (IQuarksColor.type === "ConstantColor") { + if (IQuarksColor.value && Array.isArray(IQuarksColor.value)) { + const c = new Color4(IQuarksColor.value[0] || 1, IQuarksColor.value[1] || 1, IQuarksColor.value[2] || 1, IQuarksColor.value[3] ?? 1); + return { color1: c, color2: c }; + } + if (IQuarksColor.color) { + const c = new Color4(IQuarksColor.color.r || 1, IQuarksColor.color.g || 1, IQuarksColor.color.b || 1, IQuarksColor.color.a ?? 1); + return { color1: c, color2: c }; + } + } + // Handle RandomColor (interpolation between two colors) + const anyColor = IQuarksColor as any; + if (anyColor.type === "RandomColor" && anyColor.a && anyColor.b) { + const color1 = new Color4(anyColor.a[0] || 1, anyColor.a[1] || 1, anyColor.a[2] || 1, anyColor.a[3] ?? 1); + const color2 = new Color4(anyColor.b[0] || 1, anyColor.b[1] || 1, anyColor.b[2] || 1, anyColor.b[3] ?? 1); + return { color1, color2 }; + } } - return this._convertValue(IQuarksRotation as IQuarksValue); + + // Default white + return { color1: new Color4(1, 1, 1, 1), color2: new Color4(1, 1, 1, 1) }; } /** diff --git a/tools/src/effect/types/emitter.ts b/tools/src/effect/types/emitter.ts index 0584c7e84..a2b608f02 100644 --- a/tools/src/effect/types/emitter.ts +++ b/tools/src/effect/types/emitter.ts @@ -75,21 +75,16 @@ export interface IParticleSystemConfig { shape?: IShape; emissionBursts?: IEmissionBurst[]; emissionOverDistance?: Value; // For solid system only - onlyUsedByOther?: boolean; - instancingGeometry?: string; + instancingGeometry?: string; // Custom geometry ID for SPS renderOrder?: number; - rendererEmitterSettings?: Record; - material?: string; layers?: number; isBillboardBased?: boolean; billboardMode?: number; + // Sprite animation (ParticleSystem only) startTileIndex?: Value; uTileCount?: number; vTileCount?: number; - blendTiles?: boolean; - softParticles?: boolean; - softFarFade?: number; - softNearFade?: number; + // Behaviors behaviors?: Behavior[]; } From 80928c479d1136dbed693dbc6b05530b19dc9cb2 Mon Sep 17 00:00:00 2001 From: Mikalai Lazitski Date: Thu, 18 Dec 2025 17:37:23 +0300 Subject: [PATCH 47/62] refactor: update effect editor to improve node handling and enhance emitter type consistency in properties --- editor/src/editor/layout/toolbar.tsx | 2 +- .../editor/windows/effect-editor/graph.tsx | 56 ++++++++++++++----- .../effect-editor/properties/emission.tsx | 8 +-- 3 files changed, 47 insertions(+), 19 deletions(-) diff --git a/editor/src/editor/layout/toolbar.tsx b/editor/src/editor/layout/toolbar.tsx index c0692369f..18fd06162 100644 --- a/editor/src/editor/layout/toolbar.tsx +++ b/editor/src/editor/layout/toolbar.tsx @@ -265,7 +265,7 @@ export class EditorToolbar extends Component { } private _handleOpenFXEditor(): void { - ipcRenderer.send("window:open", "build/src/editor/windows/fx-editor", { + ipcRenderer.send("window:open", "build/src/editor/windows/effect-editor", { projectConfiguration: { ...projectConfiguration }, }); } diff --git a/editor/src/editor/windows/effect-editor/graph.tsx b/editor/src/editor/windows/effect-editor/graph.tsx index ab51dbd76..097ac0159 100644 --- a/editor/src/editor/windows/effect-editor/graph.tsx +++ b/editor/src/editor/windows/effect-editor/graph.tsx @@ -653,9 +653,9 @@ export class EffectEditorGraph extends Component { - // Remove from children - const index = current.children.findIndex((child) => child === nodeData || child.uuid === nodeData.uuid || child.name === nodeData.name); - if (index !== -1) { - const removedNode = current.children[index]; - // Dispose system if it's a particle system - if (removedNode.system) { - removedNode.system.dispose(); + // Remove from children - use instance comparison primarily + const index = current.children.findIndex((child) => { + // Primary: instance comparison + if (child === nodeData) { + return true; } - // Dispose group if it's a group - if (removedNode.group) { - removedNode.group.dispose(); + // Fallback: uuid comparison (if both have uuid) + if (child.uuid && nodeData.uuid && child.uuid === nodeData.uuid) { + return true; } + return false; + }); + + if (index !== -1) { + const removedNode = current.children[index]; + // Recursively dispose all children first + this._disposeNodeRecursive(removedNode); current.children.splice(index, 1); return true; } @@ -705,7 +710,30 @@ export class EffectEditorGraph extends Component = { - SolidPointParticleEmitter: "Point", - SolidSphereParticleEmitter: "Sphere", - SolidConeParticleEmitter: "Cone", + SolidPointParticleEmitter: "point", + SolidSphereParticleEmitter: "sphere", + SolidConeParticleEmitter: "cone", }; - const currentType = emitterTypeMap[emitterType] || "Point"; + const currentType = emitterTypeMap[emitterType] || "point"; const emitterTypes = [ { text: "Point", value: "point" }, { text: "Sphere", value: "sphere" }, From fc3a872396ef1b93171e791ecdb874c8a0475f04 Mon Sep 17 00:00:00 2001 From: Mikalai Lazitski Date: Thu, 18 Dec 2025 19:04:45 +0300 Subject: [PATCH 48/62] refactor: enhance effect editor and solid particle system by introducing new emitter types, improving property handling, and refining particle initialization methods for better consistency and functionality --- .../editor/windows/effect-editor/graph.tsx | 31 +++- .../effect-editor/properties/emission.tsx | 90 +++++++-- .../properties/initialization.tsx | 51 +++++ tools/src/effect/effect.ts | 2 +- tools/src/effect/emitters/index.ts | 3 + tools/src/effect/emitters/solidBoxEmitter.ts | 75 ++++++++ .../effect/emitters/solidCylinderEmitter.ts | 83 +++++++++ .../emitters/solidHemisphericEmitter.ts | 69 +++++++ .../systems/effectSolidParticleSystem.ts | 175 ++++++++++++++---- 9 files changed, 525 insertions(+), 54 deletions(-) create mode 100644 tools/src/effect/emitters/solidBoxEmitter.ts create mode 100644 tools/src/effect/emitters/solidCylinderEmitter.ts create mode 100644 tools/src/effect/emitters/solidHemisphericEmitter.ts diff --git a/editor/src/editor/windows/effect-editor/graph.tsx b/editor/src/editor/windows/effect-editor/graph.tsx index 097ac0159..694a8ffd4 100644 --- a/editor/src/editor/windows/effect-editor/graph.tsx +++ b/editor/src/editor/windows/effect-editor/graph.tsx @@ -19,7 +19,7 @@ import { IEffectEditor } from "."; import { saveSingleFileDialog } from "../../../tools/dialog"; import { writeJSON } from "fs-extra"; import { toast } from "sonner"; -import { Effect, type IEffectNode } from "babylonjs-editor-tools"; +import { Effect, type IEffectNode, EffectSolidParticleSystem } from "babylonjs-editor-tools"; export interface IEffectEditorGraphProps { filePath: string | null; @@ -201,16 +201,31 @@ export class EffectEditorGraph extends Component 0 ? Node.children.map((child) => this._convertNodeToTreeNode(child, false)) : undefined; + // Check if solid particle system + const isSolid = Node.system instanceof EffectSolidParticleSystem; + + // Determine icon based on node type (sparkles for all particles, with color coding) + let icon: JSX.Element; + if (isEffectRoot) { + icon = ; + } else if (Node.type === "particle") { + icon = ; + } else { + icon = ; + } + + // Get system type label for particles + const secondaryLabel = Node.type === "particle" ? ( + + {isSolid ? "Solid" : "Base"} + + ) : undefined; + return { id: nodeId, label: this._getNodeLabelComponent({ id: nodeId, nodeData: Node } as any, Node.name), - icon: isEffectRoot ? ( - - ) : Node.type === "particle" ? ( - - ) : ( - - ), + icon, + secondaryLabel, isExpanded: isEffectRoot || Node.type === "group", childNodes, isSelected: false, diff --git a/editor/src/editor/windows/effect-editor/properties/emission.tsx b/editor/src/editor/windows/effect-editor/properties/emission.tsx index 776c73321..82e32fe9e 100644 --- a/editor/src/editor/windows/effect-editor/properties/emission.tsx +++ b/editor/src/editor/windows/effect-editor/properties/emission.tsx @@ -15,6 +15,9 @@ import { EffectParticleSystem, SolidSphereParticleEmitter, SolidConeParticleEmitter, + SolidBoxParticleEmitter, + SolidHemisphericParticleEmitter, + SolidCylinderParticleEmitter, Value, } from "babylonjs-editor-tools"; import { EffectValueEditor } from "../editors/value"; @@ -35,15 +38,46 @@ function renderSolidParticleSystemEmitter(system: EffectSolidParticleSystem, onC SolidPointParticleEmitter: "point", SolidSphereParticleEmitter: "sphere", SolidConeParticleEmitter: "cone", + SolidBoxParticleEmitter: "box", + SolidHemisphericParticleEmitter: "hemisphere", + SolidCylinderParticleEmitter: "cylinder", }; const currentType = emitterTypeMap[emitterType] || "point"; const emitterTypes = [ { text: "Point", value: "point" }, + { text: "Box", value: "box" }, { text: "Sphere", value: "sphere" }, { text: "Cone", value: "cone" }, + { text: "Hemisphere", value: "hemisphere" }, + { text: "Cylinder", value: "cylinder" }, ]; + // Helper to get current values from various emitter types + const getRadius = (): number => { + if (emitter instanceof SolidSphereParticleEmitter || emitter instanceof SolidConeParticleEmitter) { + return emitter.radius; + } + if (emitter instanceof SolidHemisphericParticleEmitter || emitter instanceof SolidCylinderParticleEmitter) { + return emitter.radius; + } + return 1; + }; + + const getRadiusRange = (): number => { + if (emitter instanceof SolidHemisphericParticleEmitter || emitter instanceof SolidCylinderParticleEmitter) { + return emitter.radiusRange; + } + return 1; + }; + + const getDirectionRandomizer = (): number => { + if (emitter instanceof SolidHemisphericParticleEmitter || emitter instanceof SolidCylinderParticleEmitter) { + return emitter.directionRandomizer; + } + return 0; + }; + return ( <> ({ text: t.text, value: t.value }))} onChange={(value) => { - const currentRadius = emitter instanceof SolidSphereParticleEmitter || emitter instanceof SolidConeParticleEmitter ? emitter.radius : 1; + const currentRadius = getRadius(); const currentArc = emitter instanceof SolidSphereParticleEmitter || emitter instanceof SolidConeParticleEmitter ? emitter.arc : Math.PI * 2; const currentThickness = emitter instanceof SolidSphereParticleEmitter || emitter instanceof SolidConeParticleEmitter ? emitter.thickness : 1; const currentAngle = emitter instanceof SolidConeParticleEmitter ? emitter.angle : Math.PI / 6; + const currentHeight = emitter instanceof SolidCylinderParticleEmitter ? emitter.height : 1; + const currentRadiusRange = getRadiusRange(); + const currentDirRandomizer = getDirectionRandomizer(); switch (value) { case "point": system.createPointEmitter(); break; + case "box": + system.createBoxEmitter(); + break; case "sphere": system.createSphereEmitter(currentRadius, currentArc, currentThickness); break; case "cone": system.createConeEmitter(currentRadius, currentArc, currentThickness, currentAngle); break; + case "hemisphere": + system.createHemisphericEmitter(currentRadius, currentRadiusRange, currentDirRandomizer); + break; + case "cylinder": + system.createCylinderEmitter(currentRadius, currentHeight, currentRadiusRange, currentDirRandomizer); + break; } onChange(); }} @@ -88,6 +134,38 @@ function renderSolidParticleSystemEmitter(system: EffectSolidParticleSystem, onC )} + + {emitter instanceof SolidBoxParticleEmitter && ( + <> + +
Direction
+ + +
+ +
Emit Box
+ + +
+ + )} + + {emitter instanceof SolidHemisphericParticleEmitter && ( + <> + + + + + )} + + {emitter instanceof SolidCylinderParticleEmitter && ( + <> + + + + + + )} ); } @@ -400,16 +478,6 @@ function renderEmissionParameters(nodeData: IEffectNode, onChange: () => void): )} - {system instanceof EffectParticleSystem && ( - -
Emit Power
-
- - -
-
- )} - {renderBursts(system as any, onChange)} ); diff --git a/editor/src/editor/windows/effect-editor/properties/initialization.tsx b/editor/src/editor/windows/effect-editor/properties/initialization.tsx index ef795edfd..7c2f171f2 100644 --- a/editor/src/editor/windows/effect-editor/properties/initialization.tsx +++ b/editor/src/editor/windows/effect-editor/properties/initialization.tsx @@ -112,6 +112,42 @@ export function EffectEditorParticleInitializationProperties(props: IEffectEdito onChange(); }; + // Helper to get/set angular speed - both systems use native minAngularSpeed/maxAngularSpeed + const getAngularSpeed = (): Value | undefined => { + return { type: "IntervalValue", min: system.minAngularSpeed, max: system.maxAngularSpeed }; + }; + + const setAngularSpeed = (value: Value): void => { + const interval = ValueUtils.parseIntervalValue(value); + system.minAngularSpeed = interval.min; + system.maxAngularSpeed = interval.max; + onChange(); + }; + + // Helper to get/set scale X - both systems use native minScaleX/maxScaleX + const getScaleX = (): Value | undefined => { + return { type: "IntervalValue", min: system.minScaleX, max: system.maxScaleX }; + }; + + const setScaleX = (value: Value): void => { + const interval = ValueUtils.parseIntervalValue(value); + system.minScaleX = interval.min; + system.maxScaleX = interval.max; + onChange(); + }; + + // Helper to get/set scale Y - both systems use native minScaleY/maxScaleY + const getScaleY = (): Value | undefined => { + return { type: "IntervalValue", min: system.minScaleY, max: system.maxScaleY }; + }; + + const setScaleY = (value: Value): void => { + const interval = ValueUtils.parseIntervalValue(value); + system.minScaleY = interval.min; + system.maxScaleY = interval.max; + onChange(); + }; + return ( <> @@ -130,6 +166,16 @@ export function EffectEditorParticleInitializationProperties(props: IEffectEdito /> + +
Scale X
+ +
+ + +
Scale Y
+ +
+
Start Speed
@@ -165,6 +211,11 @@ export function EffectEditorParticleInitializationProperties(props: IEffectEdito ); })()}
+ + +
Angular Speed
+ +
); } diff --git a/tools/src/effect/effect.ts b/tools/src/effect/effect.ts index 2fad1da6c..5f2eea98c 100644 --- a/tools/src/effect/effect.ts +++ b/tools/src/effect/effect.ts @@ -656,7 +656,7 @@ export class Effect implements IDisposable { particleIntersection: false, useModelMaterial: true, }); - const particleMesh = MeshBuilder.CreatePlane("particleMesh", { size: 1 }, this._scene); + const particleMesh = MeshBuilder.CreateSphere("particleMesh", { segments: 16, diameter: 1 }, this._scene); system.particleMesh = particleMesh; } else { const capacity = 500; diff --git a/tools/src/effect/emitters/index.ts b/tools/src/effect/emitters/index.ts index 2576aea7e..2c35fc332 100644 --- a/tools/src/effect/emitters/index.ts +++ b/tools/src/effect/emitters/index.ts @@ -1,3 +1,6 @@ export { SolidPointParticleEmitter } from "./solidPointEmitter"; export { SolidSphereParticleEmitter } from "./solidSphereEmitter"; export { SolidConeParticleEmitter } from "./solidConeEmitter"; +export { SolidBoxParticleEmitter } from "./solidBoxEmitter"; +export { SolidHemisphericParticleEmitter } from "./solidHemisphericEmitter"; +export { SolidCylinderParticleEmitter } from "./solidCylinderEmitter"; diff --git a/tools/src/effect/emitters/solidBoxEmitter.ts b/tools/src/effect/emitters/solidBoxEmitter.ts new file mode 100644 index 000000000..0beca0d8d --- /dev/null +++ b/tools/src/effect/emitters/solidBoxEmitter.ts @@ -0,0 +1,75 @@ +import { SolidParticle, Vector3 } from "babylonjs"; +import { ISolidParticleEmitterType } from "../types"; + +/** + * Box emitter for SolidParticleSystem + * Emits particles from inside a box with random direction between direction1 and direction2 + */ +export class SolidBoxParticleEmitter implements ISolidParticleEmitterType { + /** + * Random direction of each particle after it has been emitted, between direction1 and direction2 vectors. + */ + public direction1: Vector3 = new Vector3(0, 1, 0); + + /** + * Random direction of each particle after it has been emitted, between direction1 and direction2 vectors. + */ + public direction2: Vector3 = new Vector3(0, 1, 0); + + /** + * Minimum box point around the emitter center. + */ + public minEmitBox: Vector3 = new Vector3(-0.5, -0.5, -0.5); + + /** + * Maximum box point around the emitter center. + */ + public maxEmitBox: Vector3 = new Vector3(0.5, 0.5, 0.5); + + constructor( + direction1?: Vector3, + direction2?: Vector3, + minEmitBox?: Vector3, + maxEmitBox?: Vector3 + ) { + if (direction1) { + this.direction1 = direction1; + } + if (direction2) { + this.direction2 = direction2; + } + if (minEmitBox) { + this.minEmitBox = minEmitBox; + } + if (maxEmitBox) { + this.maxEmitBox = maxEmitBox; + } + } + + /** + * Random range helper + */ + private _randomRange(min: number, max: number): number { + return min + Math.random() * (max - min); + } + + /** + * Initialize particle position and velocity + * Note: Direction is NOT normalized, matching ParticleSystem behavior. + * The direction vector magnitude affects final velocity. + */ + public initializeParticle(particle: SolidParticle, startSpeed: number): void { + // Random position within the box + const randX = this._randomRange(this.minEmitBox.x, this.maxEmitBox.x); + const randY = this._randomRange(this.minEmitBox.y, this.maxEmitBox.y); + const randZ = this._randomRange(this.minEmitBox.z, this.maxEmitBox.z); + particle.position.set(randX, randY, randZ); + + // Random direction between direction1 and direction2 (NOT normalized, like ParticleSystem) + const dirX = this._randomRange(this.direction1.x, this.direction2.x); + const dirY = this._randomRange(this.direction1.y, this.direction2.y); + const dirZ = this._randomRange(this.direction1.z, this.direction2.z); + particle.velocity.set(dirX * startSpeed, dirY * startSpeed, dirZ * startSpeed); + } +} + diff --git a/tools/src/effect/emitters/solidCylinderEmitter.ts b/tools/src/effect/emitters/solidCylinderEmitter.ts new file mode 100644 index 000000000..90aa083bd --- /dev/null +++ b/tools/src/effect/emitters/solidCylinderEmitter.ts @@ -0,0 +1,83 @@ +import { SolidParticle, Vector3 } from "babylonjs"; +import { ISolidParticleEmitterType } from "../types"; + +/** + * Cylinder emitter for SolidParticleSystem + * Emits particles from inside a cylinder + */ +export class SolidCylinderParticleEmitter implements ISolidParticleEmitterType { + /** + * The radius of the emission cylinder + */ + public radius: number; + + /** + * The height of the emission cylinder + */ + public height: number; + + /** + * The range of emission [0-1] 0 Surface only, 1 Entire Radius + */ + public radiusRange: number; + + /** + * How much to randomize the particle direction [0-1] + */ + public directionRandomizer: number; + + private _tempVector: Vector3 = Vector3.Zero(); + + constructor(radius: number = 1, height: number = 1, radiusRange: number = 1, directionRandomizer: number = 0) { + this.radius = radius; + this.height = height; + this.radiusRange = radiusRange; + this.directionRandomizer = directionRandomizer; + } + + /** + * Random range helper + */ + private _randomRange(min: number, max: number): number { + return min + Math.random() * (max - min); + } + + /** + * Initialize particle position and velocity + */ + public initializeParticle(particle: SolidParticle, startSpeed: number): void { + // Random height position + const yPos = this._randomRange(-this.height / 2, this.height / 2); + + // Random angle around cylinder + const angle = this._randomRange(0, 2 * Math.PI); + + // Pick a properly distributed point within the circle + // https://programming.guide/random-point-within-circle.html + const radiusDistribution = this._randomRange((1 - this.radiusRange) * (1 - this.radiusRange), 1); + const positionRadius = Math.sqrt(radiusDistribution) * this.radius; + + const xPos = positionRadius * Math.cos(angle); + const zPos = positionRadius * Math.sin(angle); + + particle.position.set(xPos, yPos, zPos); + + // Direction is outward from cylinder axis with randomization + this._tempVector.set(xPos, 0, zPos); + this._tempVector.normalize(); + + // Apply direction randomization + const randY = this._randomRange(-this.directionRandomizer / 2, this.directionRandomizer / 2); + let dirAngle = Math.atan2(this._tempVector.x, this._tempVector.z); + dirAngle += this._randomRange(-Math.PI / 2, Math.PI / 2) * this.directionRandomizer; + + particle.velocity.set( + Math.sin(dirAngle), + randY, + Math.cos(dirAngle) + ); + particle.velocity.normalize(); + particle.velocity.scaleInPlace(startSpeed); + } +} + diff --git a/tools/src/effect/emitters/solidHemisphericEmitter.ts b/tools/src/effect/emitters/solidHemisphericEmitter.ts new file mode 100644 index 000000000..e20e033e7 --- /dev/null +++ b/tools/src/effect/emitters/solidHemisphericEmitter.ts @@ -0,0 +1,69 @@ +import { SolidParticle } from "babylonjs"; +import { ISolidParticleEmitterType } from "../types"; + +/** + * Hemispheric emitter for SolidParticleSystem + * Emits particles from the inside of a hemisphere (upper half of a sphere) + */ +export class SolidHemisphericParticleEmitter implements ISolidParticleEmitterType { + /** + * The radius of the emission hemisphere + */ + public radius: number; + + /** + * The range of emission [0-1] 0 Surface only, 1 Entire Radius + */ + public radiusRange: number; + + /** + * How much to randomize the particle direction [0-1] + */ + public directionRandomizer: number; + + constructor(radius: number = 1, radiusRange: number = 1, directionRandomizer: number = 0) { + this.radius = radius; + this.radiusRange = radiusRange; + this.directionRandomizer = directionRandomizer; + } + + /** + * Random range helper + */ + private _randomRange(min: number, max: number): number { + return min + Math.random() * (max - min); + } + + /** + * Initialize particle position and velocity + */ + public initializeParticle(particle: SolidParticle, startSpeed: number): void { + // Calculate random position within hemisphere + const randRadius = this.radius - this._randomRange(0, this.radius * this.radiusRange); + const v = Math.random(); + const phi = this._randomRange(0, 2 * Math.PI); + const theta = Math.acos(2 * v - 1); + + const x = randRadius * Math.cos(phi) * Math.sin(theta); + const y = randRadius * Math.cos(theta); + const z = randRadius * Math.sin(phi) * Math.sin(theta); + + // Use absolute y to keep particles in upper hemisphere + particle.position.set(x, Math.abs(y), z); + + // Direction is outward from center with optional randomization + particle.velocity.copyFrom(particle.position); + particle.velocity.normalize(); + + // Apply direction randomization + if (this.directionRandomizer > 0) { + particle.velocity.x += this._randomRange(0, this.directionRandomizer); + particle.velocity.y += this._randomRange(0, this.directionRandomizer); + particle.velocity.z += this._randomRange(0, this.directionRandomizer); + particle.velocity.normalize(); + } + + particle.velocity.scaleInPlace(startSpeed); + } +} + diff --git a/tools/src/effect/systems/effectSolidParticleSystem.ts b/tools/src/effect/systems/effectSolidParticleSystem.ts index c27053145..fa2e3c3e4 100644 --- a/tools/src/effect/systems/effectSolidParticleSystem.ts +++ b/tools/src/effect/systems/effectSolidParticleSystem.ts @@ -13,7 +13,14 @@ import type { SolidParticleWithSystem, Value, } from "../types"; -import { SolidPointParticleEmitter, SolidSphereParticleEmitter, SolidConeParticleEmitter } from "../emitters"; +import { + SolidPointParticleEmitter, + SolidSphereParticleEmitter, + SolidConeParticleEmitter, + SolidBoxParticleEmitter, + SolidHemisphericParticleEmitter, + SolidCylinderParticleEmitter, +} from "../emitters"; import { ValueUtils, CapacityCalculator, ColorGradientSystem, NumberGradientSystem } from "../utils"; import { applyColorBySpeedSPS, @@ -82,7 +89,8 @@ export class EffectSolidParticleSystem extends SolidParticleSystem implements IS public disposeOnStop: boolean = false; public gravity?: Vector3; public noiseStrength?: Vector3; - public updateSpeed: number = 1; + // Note: inherited from SolidParticleSystem, default is 0.01 + // We don't override it, using the base class default public minAngularSpeed: number = 0; public maxAngularSpeed: number = 0; public minScaleX: number = 1; @@ -355,6 +363,11 @@ export class EffectSolidParticleSystem extends SolidParticleSystem implements IS this.buildMesh(); this._setupMeshProperties(); + + // Initialize all particles as dead/invisible immediately after build + this._initializeDeadParticles(); + this.setParticles(); // Apply visibility changes to mesh + particleMesh.dispose(); } @@ -378,7 +391,7 @@ export class EffectSolidParticleSystem extends SolidParticleSystem implements IS this.name = name; this._behaviors = []; - this.particleEmitterType = null; + this.particleEmitterType = new SolidBoxParticleEmitter(); // Default emitter (like ParticleSystem) this._emitter = null; // Gradient systems for "OverLife" behaviors @@ -469,24 +482,12 @@ export class EffectSolidParticleSystem extends SolidParticleSystem implements IS /** * Initialize particle speed + * Uses minEmitPower/maxEmitPower like ParticleSystem */ - private _initializeParticleSpeed(particle: SolidParticle, normalizedTime: number): void { + private _initializeParticleSpeed(particle: SolidParticle): void { const props = particle.props!; - // Use min/max or gradient - let speedValue: number; - const emitRateGradients = this._emitRateGradients.getGradients(); - if (emitRateGradients.length > 0 && this.targetStopDuration > 0) { - const ratio = Math.max(0, Math.min(1, normalizedTime)); - const gradientValue = this._emitRateGradients.getValue(ratio); - if (gradientValue !== null) { - speedValue = gradientValue; - } else { - speedValue = this._randomRange(this.minEmitPower, this.maxEmitPower); - } - } else { - speedValue = this._randomRange(this.minEmitPower, this.maxEmitPower); - } - props.startSpeed = speedValue; + // Simply use random between min and max emit power (like ParticleSystem) + props.startSpeed = this._randomRange(this.minEmitPower, this.maxEmitPower); } /** @@ -508,10 +509,11 @@ export class EffectSolidParticleSystem extends SolidParticleSystem implements IS /** * Initialize particle size + * Uses minSize/maxSize and minScaleX/maxScaleX/minScaleY/maxScaleY (like ParticleSystem) */ private _initializeParticleSize(particle: SolidParticle, normalizedTime: number): void { const props = particle.props!; - // Use min/max or gradient + // Use min/max or gradient for base size let sizeValue: number; const startSizeGradients = this._startSizeGradients.getGradients(); if (startSizeGradients.length > 0 && this.targetStopDuration > 0) { @@ -526,7 +528,13 @@ export class EffectSolidParticleSystem extends SolidParticleSystem implements IS sizeValue = this._randomRange(this.minSize, this.maxSize); } props.startSize = sizeValue; - particle.scaling.setAll(sizeValue); + + // Apply scale modifiers (like ParticleSystem: scale.copyFromFloats) + const scaleX = this._randomRange(this.minScaleX, this.maxScaleX); + const scaleY = this._randomRange(this.minScaleY, this.maxScaleY); + props.startScaleX = scaleX; + props.startScaleY = scaleY; + particle.scaling.set(sizeValue * scaleX, sizeValue * scaleY, sizeValue); } /** @@ -537,12 +545,15 @@ export class EffectSolidParticleSystem extends SolidParticleSystem implements IS } /** - * Initialize particle rotation - * Uses minInitialRotation/maxInitialRotation (like ParticleSystem) + * Initialize particle rotation and angular speed + * Uses minInitialRotation/maxInitialRotation and minAngularSpeed/maxAngularSpeed (like ParticleSystem) */ private _initializeParticleRotation(particle: SolidParticle, _normalizedTime: number): void { + const props = particle.props!; const angleZ = this._randomRange(this.minInitialRotation, this.maxInitialRotation); particle.rotation.set(0, 0, angleZ); + // Store angular speed for per-frame rotation (like ParticleSystem) + props.startAngularSpeed = this._randomRange(this.minAngularSpeed, this.maxAngularSpeed); } /** @@ -574,7 +585,7 @@ export class EffectSolidParticleSystem extends SolidParticleSystem implements IS this._resetParticle(particle); this._initializeParticleColor(particle); - this._initializeParticleSpeed(particle, normalizedTime); + this._initializeParticleSpeed(particle); this._initializeParticleLife(particle, normalizedTime); this._initializeParticleSize(particle, normalizedTime); this._initializeParticleRotation(particle, normalizedTime); @@ -623,6 +634,38 @@ export class EffectSolidParticleSystem extends SolidParticleSystem implements IS return emitter; } + /** + * Create box emitter for SolidParticleSystem + */ + public createBoxEmitter( + direction1: Vector3 = new Vector3(0, 1, 0), + direction2: Vector3 = new Vector3(0, 1, 0), + minEmitBox: Vector3 = new Vector3(-0.5, -0.5, -0.5), + maxEmitBox: Vector3 = new Vector3(0.5, 0.5, 0.5) + ): SolidBoxParticleEmitter { + const emitter = new SolidBoxParticleEmitter(direction1, direction2, minEmitBox, maxEmitBox); + this.particleEmitterType = emitter; + return emitter; + } + + /** + * Create hemispheric emitter for SolidParticleSystem + */ + public createHemisphericEmitter(radius: number = 1, radiusRange: number = 1, directionRandomizer: number = 0): SolidHemisphericParticleEmitter { + const emitter = new SolidHemisphericParticleEmitter(radius, radiusRange, directionRandomizer); + this.particleEmitterType = emitter; + return emitter; + } + + /** + * Create cylinder emitter for SolidParticleSystem + */ + public createCylinderEmitter(radius: number = 1, height: number = 1, radiusRange: number = 1, directionRandomizer: number = 0): SolidCylinderParticleEmitter { + const emitter = new SolidCylinderParticleEmitter(radius, height, radiusRange, directionRandomizer); + this.particleEmitterType = emitter; + return emitter; + } + /** * Configure emitter from shape config * This replaces the need for EmitterFactory @@ -638,6 +681,9 @@ export class EffectSolidParticleSystem extends SolidParticleSystem implements IS const arc = shape.arc ?? Math.PI * 2; const thickness = shape.thickness ?? 1; const angle = shape.angle ?? Math.PI / 6; + const height = shape.height ?? 1; + const radiusRange = shape.radiusRange ?? 1; + const directionRandomizer = shape.directionRandomizer ?? 0; switch (shapeType) { case "sphere": @@ -646,6 +692,22 @@ export class EffectSolidParticleSystem extends SolidParticleSystem implements IS case "cone": this.createConeEmitter(radius, arc, thickness, angle); break; + case "box": { + const minEmitBox = shape.minEmitBox + ? new Vector3(shape.minEmitBox[0] ?? -0.5, shape.minEmitBox[1] ?? -0.5, shape.minEmitBox[2] ?? -0.5) + : new Vector3(-0.5, -0.5, -0.5); + const maxEmitBox = shape.maxEmitBox ? new Vector3(shape.maxEmitBox[0] ?? 0.5, shape.maxEmitBox[1] ?? 0.5, shape.maxEmitBox[2] ?? 0.5) : new Vector3(0.5, 0.5, 0.5); + const direction1 = shape.direction1 ? new Vector3(shape.direction1[0] ?? 0, shape.direction1[1] ?? 1, shape.direction1[2] ?? 0) : new Vector3(0, 1, 0); + const direction2 = shape.direction2 ? new Vector3(shape.direction2[0] ?? 0, shape.direction2[1] ?? 1, shape.direction2[2] ?? 0) : new Vector3(0, 1, 0); + this.createBoxEmitter(direction1, direction2, minEmitBox, maxEmitBox); + break; + } + case "hemisphere": + this.createHemisphericEmitter(radius, radiusRange, directionRandomizer); + break; + case "cylinder": + this.createCylinderEmitter(radius, height, radiusRange, directionRandomizer); + break; case "point": this.createPointEmitter(); break; @@ -715,6 +777,14 @@ export class EffectSolidParticleSystem extends SolidParticleSystem implements IS return; } + // Check for manual emit count (like ParticleSystem) + // When manualEmitCount > -1, emit that exact number and reset to 0 + if (this.manualEmitCount > -1) { + emissionState.waitEmiting = this.manualEmitCount; + this.manualEmitCount = 0; + return; + } + // Get emit rate (use gradient if available) let emissionRate = this.emitRate; const emitRateGradients = this._emitRateGradients.getGradients(); @@ -937,6 +1007,7 @@ export class EffectSolidParticleSystem extends SolidParticleSystem implements IS public override beforeUpdateParticles(start?: number, stop?: number, update?: boolean): void { super.beforeUpdateParticles(start, stop, update); + // Hide particles when stopped if (this._stopped) { const particles = this.particles; const nbParticles = this.nbParticles; @@ -946,14 +1017,37 @@ export class EffectSolidParticleSystem extends SolidParticleSystem implements IS particle.isVisible = false; } } - return; } + } + + /** + * Called AFTER particle updates in setParticles(). + * This is the correct place for emission because _scaledUpdateSpeed is already calculated. + */ + public override afterUpdateParticles(start?: number, stop?: number, update?: boolean): void { + super.afterUpdateParticles(start, stop, update); - if (!this._started) { + if (this._stopped || !this._started) { return; } - const deltaTime = this._scaledUpdateSpeed || 0.016; + // Use _scaledUpdateSpeed for emission (same as ThinParticleSystem) + // Now it's properly calculated by the base class + const deltaTime = this._scaledUpdateSpeed; + + // Debug logging (aggregated per second) + this._debugFrameCount++; + this._debugDeltaSum += deltaTime; + const now = performance.now(); + if (now - this._debugLastLog > 1000) { + const aliveCount = this.particles.filter((p) => p.alive).length; + console.log(`[SPS] emitRate=${this.emitRate}, updateSpeed=${this.updateSpeed}`); + console.log(` _scaledUpdateSpeed=${this._scaledUpdateSpeed?.toFixed(4)}, avgDelta=${(this._debugDeltaSum / this._debugFrameCount).toFixed(4)}`); + console.log(` waitEmiting=${this._emissionState.waitEmiting.toFixed(2)}, alive=${aliveCount}/${this.nbParticles}`); + this._debugLastLog = now; + this._debugFrameCount = 0; + this._debugDeltaSum = 0; + } this._emissionState.time += deltaTime; @@ -962,6 +1056,10 @@ export class EffectSolidParticleSystem extends SolidParticleSystem implements IS this._handleEmissionLooping(); } + private _debugLastLog = 0; + private _debugFrameCount = 0; + private _debugDeltaSum = 0; + private _updateParticle(particle: SolidParticle): SolidParticle { if (!particle.alive) { particle.isVisible = false; @@ -994,8 +1092,9 @@ export class EffectSolidParticleSystem extends SolidParticleSystem implements IS const props = particle.props; const speedModifier = props?.speedModifier ?? 1.0; - const updateSpeed = this.updateSpeed; - particle.position.addInPlace(particle.velocity.scale(updateSpeed * speedModifier)); + // Use _scaledUpdateSpeed for FPS-independent movement (like ParticleSystem) + const deltaTime = this._scaledUpdateSpeed || this.updateSpeed; + particle.position.addInPlace(particle.velocity.scale(deltaTime * speedModifier)); return particle; } @@ -1005,7 +1104,8 @@ export class EffectSolidParticleSystem extends SolidParticleSystem implements IS */ private _applyGradients(particle: SolidParticle, lifeRatio: number): void { const props = (particle.props ||= {}); - const updateSpeed = this.updateSpeed; + // Use _scaledUpdateSpeed for FPS-independent gradients + const deltaTime = this._scaledUpdateSpeed || this.updateSpeed; const color = this._colorGradients.getValue(lifeRatio); if (color && particle.color) { @@ -1020,9 +1120,12 @@ export class EffectSolidParticleSystem extends SolidParticleSystem implements IS } } + // Apply size gradients with scale modifiers (like ParticleSystem) const size = this._sizeGradients.getValue(lifeRatio); if (size !== null && props.startSize !== undefined) { - particle.scaling.setAll(props.startSize * size); + const scaleX = props.startScaleX ?? 1; + const scaleY = props.startScaleY ?? 1; + particle.scaling.set(props.startSize * size * scaleX, props.startSize * size * scaleY, props.startSize * size); } const velocity = this._velocityGradients.getValue(lifeRatio); @@ -1030,9 +1133,13 @@ export class EffectSolidParticleSystem extends SolidParticleSystem implements IS props.speedModifier = velocity; } - const angularSpeed = this._angularSpeedGradients.getValue(lifeRatio); - if (angularSpeed !== null) { - particle.rotation.z += angularSpeed * updateSpeed; + // Apply angular speed: use gradient if available, otherwise use particle's startAngularSpeed (like ParticleSystem) + const angularSpeedFromGradient = this._angularSpeedGradients.getValue(lifeRatio); + if (angularSpeedFromGradient !== null) { + particle.rotation.z += angularSpeedFromGradient * deltaTime; + } else if (props.startAngularSpeed !== undefined && props.startAngularSpeed !== 0) { + // Apply base angular speed (like ParticleSystem._ProcessAngularSpeed) + particle.rotation.z += props.startAngularSpeed * deltaTime; } const limitVelocity = this._limitVelocityGradients.getValue(lifeRatio); From 3170af87cd1215dc123373ddd138880f5fc75bc6 Mon Sep 17 00:00:00 2001 From: Mikalai Lazitski Date: Fri, 19 Dec 2025 05:47:32 +0300 Subject: [PATCH 49/62] refactor: enhance data conversion and solid particle system behavior by adding support for new rotation types, improving color parsing, and optimizing force application for better performance and maintainability --- tools/src/effect/parsers/dataConverter.ts | 72 +++++-- .../systems/effectSolidParticleSystem.ts | 185 ++++++++++++++---- tools/src/effect/utils/valueParser.ts | 22 ++- 3 files changed, 228 insertions(+), 51 deletions(-) diff --git a/tools/src/effect/parsers/dataConverter.ts b/tools/src/effect/parsers/dataConverter.ts index 4218f6d51..153730995 100644 --- a/tools/src/effect/parsers/dataConverter.ts +++ b/tools/src/effect/parsers/dataConverter.ts @@ -1,4 +1,4 @@ -import { Vector3, Matrix, Quaternion, Color3, Texture as BabylonTexture, ParticleSystem } from "babylonjs"; +import { Vector3, Matrix, Quaternion, Color3, Texture as BabylonTexture, ParticleSystem, Color4 } from "babylonjs"; import type { ILoaderOptions } from "../types/loader"; import type { IQuarksJSON, @@ -327,6 +327,7 @@ export class DataConverter { const colorResult = this._convertColorToColor4(IQuarksConfig.startColor); Config.color1 = colorResult.color1; Config.color2 = colorResult.color2; + } else { } // Convert emissionOverTime → emitRate, emitRateGradients @@ -508,50 +509,95 @@ export class DataConverter { /** * Convert IQuarks rotation to native min/max radians + * Supports: number, ConstantValue, IntervalValue, Euler, AxisAngle, RandomQuat */ private _convertRotationToMinMax(IQuarksRotation: IQuarksRotation): { min: number; max: number } { if (typeof IQuarksRotation === "number") { return { min: IQuarksRotation, max: IQuarksRotation }; } + if (typeof IQuarksRotation === "object" && IQuarksRotation !== null && "type" in IQuarksRotation) { - if (IQuarksRotation.type === "ConstantValue") { + const rotationType = IQuarksRotation.type; + + if (rotationType === "ConstantValue") { const val = (IQuarksRotation as any).value ?? 0; return { min: val, max: val }; } - if (IQuarksRotation.type === "IntervalValue") { + + if (rotationType === "IntervalValue") { return { min: (IQuarksRotation as any).a ?? 0, max: (IQuarksRotation as any).b ?? 0 }; } + + // Handle Euler type - for 2D/billboard particles we use angleZ + if (rotationType === "Euler") { + const euler = IQuarksRotation as any; + // angleZ is the rotation around forward axis (most common for 2D particles) + const angleZ = euler.angleZ; + if (angleZ) { + if (typeof angleZ === "number") { + return { min: angleZ, max: angleZ }; + } + if (angleZ.type === "ConstantValue") { + const val = angleZ.value ?? 0; + return { min: val, max: val }; + } + if (angleZ.type === "IntervalValue") { + return { min: angleZ.a ?? 0, max: angleZ.b ?? 0 }; + } + } + // Fallback to angleX if no angleZ (for different orientations) + const angleX = euler.angleX; + if (angleX) { + if (typeof angleX === "number") { + return { min: angleX, max: angleX }; + } + if (angleX.type === "ConstantValue") { + const val = angleX.value ?? 0; + return { min: val, max: val }; + } + if (angleX.type === "IntervalValue") { + return { min: angleX.a ?? 0, max: angleX.b ?? 0 }; + } + } + return { min: 0, max: 0 }; + } } + return { min: 0, max: 0 }; } /** * Convert IQuarks color to native Babylon.js Color4 (color1, color2) */ - private _convertColorToColor4(IQuarksColor: IQuarksColor): { color1: import("babylonjs").Color4; color2: import("babylonjs").Color4 } { - const { Color4 } = require("babylonjs"); - + private _convertColorToColor4(IQuarksColor: IQuarksColor): { color1: Color4; color2: Color4 } { if (Array.isArray(IQuarksColor)) { - const c = new Color4(IQuarksColor[0] || 1, IQuarksColor[1] || 1, IQuarksColor[2] || 1, IQuarksColor[3] ?? 1); + const c = new Color4(IQuarksColor[0] ?? 1, IQuarksColor[1] ?? 1, IQuarksColor[2] ?? 1, IQuarksColor[3] ?? 1); return { color1: c, color2: c }; } if (typeof IQuarksColor === "object" && IQuarksColor !== null && "type" in IQuarksColor) { if (IQuarksColor.type === "ConstantColor") { - if (IQuarksColor.value && Array.isArray(IQuarksColor.value)) { - const c = new Color4(IQuarksColor.value[0] || 1, IQuarksColor.value[1] || 1, IQuarksColor.value[2] || 1, IQuarksColor.value[3] ?? 1); + const constColor = IQuarksColor as any; + if (constColor.value && Array.isArray(constColor.value)) { + const c = new Color4(constColor.value[0] ?? 1, constColor.value[1] ?? 1, constColor.value[2] ?? 1, constColor.value[3] ?? 1); return { color1: c, color2: c }; } - if (IQuarksColor.color) { - const c = new Color4(IQuarksColor.color.r || 1, IQuarksColor.color.g || 1, IQuarksColor.color.b || 1, IQuarksColor.color.a ?? 1); + if (constColor.color) { + const colorObj = constColor.color; + const c = new Color4( + colorObj.r !== undefined ? colorObj.r : 1, + colorObj.g !== undefined ? colorObj.g : 1, + colorObj.b !== undefined ? colorObj.b : 1, + colorObj.a !== undefined ? colorObj.a : 1 + ); return { color1: c, color2: c }; } } // Handle RandomColor (interpolation between two colors) const anyColor = IQuarksColor as any; if (anyColor.type === "RandomColor" && anyColor.a && anyColor.b) { - const color1 = new Color4(anyColor.a[0] || 1, anyColor.a[1] || 1, anyColor.a[2] || 1, anyColor.a[3] ?? 1); - const color2 = new Color4(anyColor.b[0] || 1, anyColor.b[1] || 1, anyColor.b[2] || 1, anyColor.b[3] ?? 1); + const color1 = new Color4(anyColor.a[0] ?? 1, anyColor.a[1] ?? 1, anyColor.a[2] ?? 1, anyColor.a[3] ?? 1); + const color2 = new Color4(anyColor.b[0] ?? 1, anyColor.b[1] ?? 1, anyColor.b[2] ?? 1, anyColor.b[3] ?? 1); return { color1, color2 }; } } diff --git a/tools/src/effect/systems/effectSolidParticleSystem.ts b/tools/src/effect/systems/effectSolidParticleSystem.ts index fa2e3c3e4..5e6640b98 100644 --- a/tools/src/effect/systems/effectSolidParticleSystem.ts +++ b/tools/src/effect/systems/effectSolidParticleSystem.ts @@ -23,15 +23,14 @@ import { } from "../emitters"; import { ValueUtils, CapacityCalculator, ColorGradientSystem, NumberGradientSystem } from "../utils"; import { - applyColorBySpeedSPS, - applySizeBySpeedSPS, - applyRotationBySpeedSPS, - applyOrbitOverLifeSPS, applyColorOverLifeSPS, applyLimitSpeedOverLifeSPS, applyRotationOverLifeSPS, applySizeOverLifeSPS, applySpeedOverLifeSPS, + interpolateColorKeys, + interpolateGradientKeys, + extractNumberFromValue, } from "../behaviors"; /** @@ -238,9 +237,6 @@ export class EffectSolidParticleSystem extends SolidParticleSystem implements IS this._emissionState.burstParticleCount = 0; this._emissionState.isBursting = false; this._emitEnded = false; - - // Ensure particles are visible when starting (they will be updated by setParticles) - // Note: New particles will be spawned and visible automatically } } @@ -907,6 +903,8 @@ export class EffectSolidParticleSystem extends SolidParticleSystem implements IS * Build per-particle behavior functions from configurations * Per-particle behaviors run each frame for each particle * "OverLife" behaviors are handled by gradients (system-level) + * + * IMPORTANT: Parse all values ONCE here, not every frame! */ private _buildPerParticleBehaviors(behaviors: Behavior[]): PerSolidParticleBehaviorFunction[] { const functions: PerSolidParticleBehaviorFunction[] = []; @@ -916,53 +914,174 @@ export class EffectSolidParticleSystem extends SolidParticleSystem implements IS case "ForceOverLife": case "ApplyForce": { const b = behavior as IForceOverLifeBehavior; - functions.push((particle: SolidParticle) => { - const particleWithSystem = particle as SolidParticleWithSystem; - const updateSpeed = particleWithSystem.system?.updateSpeed ?? 0.016; - - const forceX = b.x ?? b.force?.x; - const forceY = b.y ?? b.force?.y; - const forceZ = b.z ?? b.force?.z; - if (forceX !== undefined || forceY !== undefined || forceZ !== undefined) { - const fx = forceX !== undefined ? ValueUtils.parseConstantValue(forceX) : 0; - const fy = forceY !== undefined ? ValueUtils.parseConstantValue(forceY) : 0; - const fz = forceZ !== undefined ? ValueUtils.parseConstantValue(forceZ) : 0; - particle.velocity.x += fx * updateSpeed; - particle.velocity.y += fy * updateSpeed; - particle.velocity.z += fz * updateSpeed; - } - }); + // Pre-parse force values ONCE (not every frame!) + const forceX = b.x ?? b.force?.x; + const forceY = b.y ?? b.force?.y; + const forceZ = b.z ?? b.force?.z; + const fx = forceX !== undefined ? ValueUtils.parseConstantValue(forceX) : 0; + const fy = forceY !== undefined ? ValueUtils.parseConstantValue(forceY) : 0; + const fz = forceZ !== undefined ? ValueUtils.parseConstantValue(forceZ) : 0; + + if (fx !== 0 || fy !== 0 || fz !== 0) { + // Capture 'this' to access _scaledUpdateSpeed + const system = this; + functions.push((particle: SolidParticle) => { + // Use _scaledUpdateSpeed for FPS-independent force application + const deltaTime = system._scaledUpdateSpeed || system.updateSpeed; + particle.velocity.x += fx * deltaTime; + particle.velocity.y += fy * deltaTime; + particle.velocity.z += fz * deltaTime; + }); + } break; } case "ColorBySpeed": { const b = behavior as IColorBySpeedBehavior; - functions.push((particle: SolidParticle) => { - applyColorBySpeedSPS(particle, b); - }); + // Pre-parse min/max speed ONCE + const minSpeed = b.minSpeed !== undefined ? ValueUtils.parseConstantValue(b.minSpeed) : 0; + const maxSpeed = b.maxSpeed !== undefined ? ValueUtils.parseConstantValue(b.maxSpeed) : 1; + const colorKeys = b.color?.keys; + + if (colorKeys && colorKeys.length > 0) { + functions.push((particle: SolidParticle) => { + if (!particle.color) { + return; + } + const vel = particle.velocity; + const currentSpeed = Math.sqrt(vel.x * vel.x + vel.y * vel.y + vel.z * vel.z); + const speedRatio = Math.max(0, Math.min(1, (currentSpeed - minSpeed) / (maxSpeed - minSpeed || 1))); + const interpolatedColor = interpolateColorKeys(colorKeys, speedRatio); + const startColor = particle.props?.startColor; + + if (startColor) { + particle.color.r = interpolatedColor.r * startColor.r; + particle.color.g = interpolatedColor.g * startColor.g; + particle.color.b = interpolatedColor.b * startColor.b; + particle.color.a = startColor.a; + } else { + particle.color.r = interpolatedColor.r; + particle.color.g = interpolatedColor.g; + particle.color.b = interpolatedColor.b; + } + }); + } break; } case "SizeBySpeed": { const b = behavior as ISizeBySpeedBehavior; - functions.push((particle: SolidParticle) => { - applySizeBySpeedSPS(particle, b); - }); + // Pre-parse min/max speed ONCE + const minSpeed = b.minSpeed !== undefined ? ValueUtils.parseConstantValue(b.minSpeed) : 0; + const maxSpeed = b.maxSpeed !== undefined ? ValueUtils.parseConstantValue(b.maxSpeed) : 1; + const sizeKeys = b.size?.keys; + + if (sizeKeys && sizeKeys.length > 0) { + functions.push((particle: SolidParticle) => { + const vel = particle.velocity; + const currentSpeed = Math.sqrt(vel.x * vel.x + vel.y * vel.y + vel.z * vel.z); + const speedRatio = Math.max(0, Math.min(1, (currentSpeed - minSpeed) / (maxSpeed - minSpeed || 1))); + const sizeMultiplier = interpolateGradientKeys(sizeKeys, speedRatio, extractNumberFromValue); + const startSize = particle.props?.startSize ?? 1; + const newSize = startSize * sizeMultiplier; + particle.scaling.setAll(newSize); + }); + } break; } case "RotationBySpeed": { const b = behavior as IRotationBySpeedBehavior; + // Pre-parse values ONCE + const minSpeed = b.minSpeed !== undefined ? ValueUtils.parseConstantValue(b.minSpeed) : 0; + const maxSpeed = b.maxSpeed !== undefined ? ValueUtils.parseConstantValue(b.maxSpeed) : 1; + const angularVelocity = b.angularVelocity; + const hasKeys = + typeof angularVelocity === "object" && + angularVelocity !== null && + "keys" in angularVelocity && + Array.isArray(angularVelocity.keys) && + angularVelocity.keys.length > 0; + + // Pre-parse constant angular velocity if not using keys + let constantAngularSpeed = 0; + if (!hasKeys && angularVelocity) { + const parsed = ValueUtils.parseIntervalValue(angularVelocity); + constantAngularSpeed = (parsed.min + parsed.max) / 2; + } + + const system = this; functions.push((particle: SolidParticle) => { - applyRotationBySpeedSPS(particle, b); + const vel = particle.velocity; + const currentSpeed = Math.sqrt(vel.x * vel.x + vel.y * vel.y + vel.z * vel.z); + const deltaTime = system._scaledUpdateSpeed || system.updateSpeed; + + let angularSpeed = constantAngularSpeed; + if (hasKeys) { + const speedRatio = Math.max(0, Math.min(1, (currentSpeed - minSpeed) / (maxSpeed - minSpeed || 1))); + angularSpeed = interpolateGradientKeys((angularVelocity as any).keys, speedRatio, extractNumberFromValue); + } + + particle.rotation.z += angularSpeed * deltaTime; }); break; } case "OrbitOverLife": { const b = behavior as IOrbitOverLifeBehavior; + // Pre-parse constant values ONCE + const speed = b.speed !== undefined ? ValueUtils.parseConstantValue(b.speed) : 1; + const centerX = b.center?.x ?? 0; + const centerY = b.center?.y ?? 0; + const centerZ = b.center?.z ?? 0; + const hasRadiusKeys = + b.radius !== undefined && + b.radius !== null && + typeof b.radius === "object" && + "keys" in b.radius && + Array.isArray(b.radius.keys) && + b.radius.keys.length > 0; + + // Pre-parse constant radius if not using keys + let constantRadius = 1; + if (!hasRadiusKeys && b.radius !== undefined) { + const parsed = ValueUtils.parseIntervalValue(b.radius as Value); + constantRadius = (parsed.min + parsed.max) / 2; + } + functions.push((particle: SolidParticle) => { - applyOrbitOverLifeSPS(particle, b); + if (particle.lifeTime <= 0) { + return; + } + + const lifeRatio = particle.age / particle.lifeTime; + + // Get radius (from keys or constant) + let radius = constantRadius; + if (hasRadiusKeys) { + radius = interpolateGradientKeys((b.radius as any).keys, lifeRatio, extractNumberFromValue); + } + + const angle = lifeRatio * speed * Math.PI * 2; + + // Calculate orbit offset (NOT replacement!) + const orbitX = Math.cos(angle) * radius; + const orbitY = Math.sin(angle) * radius; + + // Store initial position if not stored yet + const props = (particle.props ||= {}) as any; + if (props.orbitInitialPos === undefined) { + props.orbitInitialPos = { + x: particle.position.x, + y: particle.position.y, + z: particle.position.z, + }; + } + + // Apply orbit as OFFSET from initial position (NOT replacement!) + particle.position.x = props.orbitInitialPos.x + centerX + orbitX; + particle.position.y = props.orbitInitialPos.y + centerY + orbitY; + particle.position.z = props.orbitInitialPos.z + centerZ; }); break; } @@ -1040,10 +1159,6 @@ export class EffectSolidParticleSystem extends SolidParticleSystem implements IS this._debugDeltaSum += deltaTime; const now = performance.now(); if (now - this._debugLastLog > 1000) { - const aliveCount = this.particles.filter((p) => p.alive).length; - console.log(`[SPS] emitRate=${this.emitRate}, updateSpeed=${this.updateSpeed}`); - console.log(` _scaledUpdateSpeed=${this._scaledUpdateSpeed?.toFixed(4)}, avgDelta=${(this._debugDeltaSum / this._debugFrameCount).toFixed(4)}`); - console.log(` waitEmiting=${this._emissionState.waitEmiting.toFixed(2)}, alive=${aliveCount}/${this.nbParticles}`); this._debugLastLog = now; this._debugFrameCount = 0; this._debugDeltaSum = 0; diff --git a/tools/src/effect/utils/valueParser.ts b/tools/src/effect/utils/valueParser.ts index 42281b849..b5b436636 100644 --- a/tools/src/effect/utils/valueParser.ts +++ b/tools/src/effect/utils/valueParser.ts @@ -34,18 +34,34 @@ export class ValueUtils { /** * Parse a constant color + * Supports formats: + * - { type: "ConstantColor", value: [r, g, b, a] } + * - { type: "ConstantColor", color: { r, g, b, a } } + * - [r, g, b, a] (array) */ public static parseConstantColor(value: Color): Color4 { if (value && typeof value === "object" && !Array.isArray(value)) { if ("type" in value && value.type === "ConstantColor") { + // Format: { type: "ConstantColor", value: [r, g, b, a] } if (value.value && Array.isArray(value.value)) { return new Color4(value.value[0] || 0, value.value[1] || 0, value.value[2] || 0, value.value[3] !== undefined ? value.value[3] : 1); } - } else if (Array.isArray(value) && value.length >= 3) { - // Array format [r, g, b, a?] - return new Color4(value[0] || 0, value[1] || 0, value[2] || 0, value[3] !== undefined ? value[3] : 1); + // Format: { type: "ConstantColor", color: { r, g, b, a } } + const anyValue = value as any; + if (anyValue.color && typeof anyValue.color === "object") { + return new Color4( + anyValue.color.r ?? 1, + anyValue.color.g ?? 1, + anyValue.color.b ?? 1, + anyValue.color.a !== undefined ? anyValue.color.a : 1 + ); + } } } + // Array format [r, g, b, a?] + if (Array.isArray(value) && value.length >= 3) { + return new Color4(value[0] || 0, value[1] || 0, value[2] || 0, value[3] !== undefined ? value[3] : 1); + } return new Color4(1, 1, 1, 1); } From f4a1bc238f31008a8e434e01d3fc73742132bdc1 Mon Sep 17 00:00:00 2001 From: Mikalai Lazitski Date: Fri, 19 Dec 2025 14:15:07 +0300 Subject: [PATCH 50/62] refactor: unify color function handling across behaviors by introducing a consistent IColorFunction structure, enhancing data conversion, and improving color parsing for better maintainability --- .../effect-editor/editors/color-function.tsx | 89 ++++++- .../effect-editor/properties/behaviors.tsx | 1 + tools/src/effect/behaviors/colorBySpeed.ts | 71 +++-- tools/src/effect/behaviors/colorOverLife.ts | 247 +++++++++++++----- tools/src/effect/factories/materialFactory.ts | 18 +- tools/src/effect/factories/systemFactory.ts | 2 + tools/src/effect/parsers/dataConverter.ts | 124 +++++++-- .../systems/effectSolidParticleSystem.ts | 7 +- tools/src/effect/types/behaviors.ts | 35 ++- tools/src/effect/types/quarksTypes.ts | 41 ++- 10 files changed, 487 insertions(+), 148 deletions(-) diff --git a/editor/src/editor/windows/effect-editor/editors/color-function.tsx b/editor/src/editor/windows/effect-editor/editors/color-function.tsx index dcfa09ecf..2fd6723cc 100644 --- a/editor/src/editor/windows/effect-editor/editors/color-function.tsx +++ b/editor/src/editor/windows/effect-editor/editors/color-function.tsx @@ -18,10 +18,91 @@ export interface IColorFunctionEditorProps { export function ColorFunctionEditor(props: IColorFunctionEditorProps): ReactNode { const { value, onChange, label } = props; - // Initialize color function type if not set - if (!value || !value.colorFunctionType) { - value.colorFunctionType = "ConstantColor"; - value.data = {}; + // Convert from Quarks format to UI format if needed + // Quarks format: { color: { type: "Gradient" | "ConstantColor" | "RandomColorBetweenGradient", ... } } + // UI format: { colorFunctionType: "Gradient" | "ConstantColor" | "RandomColorBetweenGradient", data: {...} } + if (value && !value.colorFunctionType) { + // Check if this is Quarks format + if (value.color && typeof value.color === "object" && "type" in value.color) { + const colorType = value.color.type; + + if (colorType === "Gradient") { + // Convert Gradient format + value.colorFunctionType = "Gradient"; + value.data = { + colorKeys: value.color.color?.keys || [], + alphaKeys: value.color.alpha?.keys || [], + }; + delete value.color; + } else if (colorType === "ConstantColor") { + // Convert ConstantColor format + value.colorFunctionType = "ConstantColor"; + const color = + value.color.color || + (value.color.value ? { r: value.color.value[0], g: value.color.value[1], b: value.color.value[2], a: value.color.value[3] } : { r: 1, g: 1, b: 1, a: 1 }); + value.data = { + color: { + r: color.r ?? 1, + g: color.g ?? 1, + b: color.b ?? 1, + a: color.a !== undefined ? color.a : 1, + }, + }; + delete value.color; + } else if (colorType === "RandomColorBetweenGradient") { + // Convert RandomColorBetweenGradient format + value.colorFunctionType = "RandomColorBetweenGradient"; + value.data = { + gradient1: { + colorKeys: value.color.gradient1?.color?.keys || [], + alphaKeys: value.color.gradient1?.alpha?.keys || [], + }, + gradient2: { + colorKeys: value.color.gradient2?.color?.keys || [], + alphaKeys: value.color.gradient2?.alpha?.keys || [], + }, + }; + delete value.color; + } else { + // Fallback: try old format + const hasColorKeys = value.color.color?.keys && value.color.color.keys.length > 0; + const hasAlphaKeys = value.color.alpha?.keys && value.color.alpha.keys.length > 0; + const hasKeys = value.color.keys && value.color.keys.length > 0; + + if (hasColorKeys || hasAlphaKeys || hasKeys) { + value.colorFunctionType = "Gradient"; + value.data = { + colorKeys: hasColorKeys ? value.color.color.keys : hasKeys ? value.color.keys : [], + alphaKeys: hasAlphaKeys ? value.color.alpha.keys : [], + }; + delete value.color; + } else { + value.colorFunctionType = "ConstantColor"; + value.data = {}; + } + } + } else if (value.color) { + // Old Quarks format without type + const hasColorKeys = value.color.color?.keys && value.color.color.keys.length > 0; + const hasAlphaKeys = value.color.alpha?.keys && value.color.alpha.keys.length > 0; + const hasKeys = value.color.keys && value.color.keys.length > 0; + + if (hasColorKeys || hasAlphaKeys || hasKeys) { + value.colorFunctionType = "Gradient"; + value.data = { + colorKeys: hasColorKeys ? value.color.color.keys : hasKeys ? value.color.keys : [], + alphaKeys: hasAlphaKeys ? value.color.alpha.keys : [], + }; + delete value.color; + } else { + value.colorFunctionType = "ConstantColor"; + value.data = {}; + } + } else { + // Initialize color function type if not set + value.colorFunctionType = "ConstantColor"; + value.data = {}; + } } const functionType = value.colorFunctionType as ColorFunctionType; diff --git a/editor/src/editor/windows/effect-editor/properties/behaviors.tsx b/editor/src/editor/windows/effect-editor/properties/behaviors.tsx index f19868874..fa8c4f4c5 100644 --- a/editor/src/editor/windows/effect-editor/properties/behaviors.tsx +++ b/editor/src/editor/windows/effect-editor/properties/behaviors.tsx @@ -432,6 +432,7 @@ function renderProperty(prop: IBehaviorProperty, behavior: any, onChange: () => return ; case "colorFunction": + // All color functions are now stored uniformly in behavior[prop.name] if (!behavior[prop.name]) { behavior[prop.name] = { colorFunctionType: prop.colorFunctionTypes?.[0] || "ConstantColor", diff --git a/tools/src/effect/behaviors/colorBySpeed.ts b/tools/src/effect/behaviors/colorBySpeed.ts index 885f57d3b..045d3755a 100644 --- a/tools/src/effect/behaviors/colorBySpeed.ts +++ b/tools/src/effect/behaviors/colorBySpeed.ts @@ -1,67 +1,66 @@ -import { SolidParticle, Particle, Vector3 } from "babylonjs"; import type { IColorBySpeedBehavior } from "../types/behaviors"; +import type { Particle } from "babylonjs"; +import type { EffectParticleSystem } from "../systems/effectParticleSystem"; import { interpolateColorKeys } from "./utils"; -import { ValueUtils } from "../utils/valueParser"; /** - * Apply ColorBySpeed behavior to Particle - * Gets currentSpeed from particle.velocity magnitude + * Apply ColorBySpeed behavior to ParticleSystem (per-particle) + * Uses unified IColorFunction structure: behavior.color = { colorFunctionType, data } */ -export function applyColorBySpeedPS(particle: Particle, behavior: IColorBySpeedBehavior): void { - if (!behavior.color || !behavior.color.keys || !particle.color || !particle.direction) { +export function applyColorBySpeedPS(particleSystem: EffectParticleSystem, behavior: IColorBySpeedBehavior, particle: Particle): void { + // New structure: behavior.color.data.colorKeys + if (!behavior.color || !behavior.color.data?.colorKeys || !particle.color || !particle.direction) { return; } - // Get current speed from particle velocity/direction - const currentSpeed = Vector3.Distance(Vector3.Zero(), particle.direction); + const minSpeed = behavior.minSpeed !== undefined ? (typeof behavior.minSpeed === "number" ? behavior.minSpeed : 0) : 0; + const maxSpeed = behavior.maxSpeed !== undefined ? (typeof behavior.maxSpeed === "number" ? behavior.maxSpeed : 1) : 1; + const colorKeys = behavior.color.data.colorKeys; - const colorKeys = behavior.color.keys; - const minSpeed = behavior.minSpeed !== undefined ? ValueUtils.parseConstantValue(behavior.minSpeed) : 0; - const maxSpeed = behavior.maxSpeed !== undefined ? ValueUtils.parseConstantValue(behavior.maxSpeed) : 1; - const speedRatio = Math.max(0, Math.min(1, (currentSpeed - minSpeed) / (maxSpeed - minSpeed || 1))); + if (!colorKeys || colorKeys.length === 0) { + return; + } + const vel = particle.direction; + const currentSpeed = Math.sqrt(vel.x * vel.x + vel.y * vel.y + vel.z * vel.z); + const speedRatio = Math.max(0, Math.min(1, (currentSpeed - minSpeed) / (maxSpeed - minSpeed || 1))); const interpolatedColor = interpolateColorKeys(colorKeys, speedRatio); - const startColor = particle.initialColor; - if (startColor) { - // Multiply with startColor (matching three.quarks behavior) - particle.color.r = interpolatedColor.r * startColor.r; - particle.color.g = interpolatedColor.g * startColor.g; - particle.color.b = interpolatedColor.b * startColor.b; - particle.color.a = startColor.a; // Keep original alpha - } else { - particle.color.r = interpolatedColor.r; - particle.color.g = interpolatedColor.g; - particle.color.b = interpolatedColor.b; - } + particle.color.r = interpolatedColor.r; + particle.color.g = interpolatedColor.g; + particle.color.b = interpolatedColor.b; + particle.color.a = interpolatedColor.a; } /** - * Apply ColorBySpeed behavior to SolidParticle - * Gets currentSpeed from particle.velocity magnitude + * Apply ColorBySpeed behavior to SolidParticleSystem (per-particle) + * Uses unified IColorFunction structure: behavior.color = { colorFunctionType, data } */ -export function applyColorBySpeedSPS(particle: SolidParticle, behavior: IColorBySpeedBehavior): void { - if (!behavior.color || !behavior.color.keys || !particle.color) { +export function applyColorBySpeedSPS(behavior: IColorBySpeedBehavior, particle: any): void { + // New structure: behavior.color.data.colorKeys + if (!behavior.color || !behavior.color.data?.colorKeys || !particle.color) { return; } - // Get current speed from particle velocity - const currentSpeed = Math.sqrt(particle.velocity.x * particle.velocity.x + particle.velocity.y * particle.velocity.y + particle.velocity.z * particle.velocity.z); + const minSpeed = behavior.minSpeed !== undefined ? (typeof behavior.minSpeed === "number" ? behavior.minSpeed : 0) : 0; + const maxSpeed = behavior.maxSpeed !== undefined ? (typeof behavior.maxSpeed === "number" ? behavior.maxSpeed : 1) : 1; + const colorKeys = behavior.color.data.colorKeys; - const colorKeys = behavior.color.keys; - const minSpeed = behavior.minSpeed !== undefined ? ValueUtils.parseConstantValue(behavior.minSpeed) : 0; - const maxSpeed = behavior.maxSpeed !== undefined ? ValueUtils.parseConstantValue(behavior.maxSpeed) : 1; - const speedRatio = Math.max(0, Math.min(1, (currentSpeed - minSpeed) / (maxSpeed - minSpeed || 1))); + if (!colorKeys || colorKeys.length === 0) { + return; + } + const vel = particle.velocity; + const currentSpeed = Math.sqrt(vel.x * vel.x + vel.y * vel.y + vel.z * vel.z); + const speedRatio = Math.max(0, Math.min(1, (currentSpeed - minSpeed) / (maxSpeed - minSpeed || 1))); const interpolatedColor = interpolateColorKeys(colorKeys, speedRatio); const startColor = particle.props?.startColor; if (startColor) { - // Multiply with startColor (matching three.quarks behavior) particle.color.r = interpolatedColor.r * startColor.r; particle.color.g = interpolatedColor.g * startColor.g; particle.color.b = interpolatedColor.b * startColor.b; - particle.color.a = startColor.a; // Keep original alpha + particle.color.a = startColor.a; } else { particle.color.r = interpolatedColor.r; particle.color.g = interpolatedColor.g; diff --git a/tools/src/effect/behaviors/colorOverLife.ts b/tools/src/effect/behaviors/colorOverLife.ts index 82113975f..fd2853c60 100644 --- a/tools/src/effect/behaviors/colorOverLife.ts +++ b/tools/src/effect/behaviors/colorOverLife.ts @@ -6,91 +6,183 @@ import type { EffectParticleSystem } from "../systems/effectParticleSystem"; /** * Apply ColorOverLife behavior to ParticleSystem + * Uses unified IColorFunction structure: behavior.color = { colorFunctionType, data } */ -export function applyColorOverLifePS(particleSystem: EffectParticleSystem, behavior: IColorOverLifeBehavior): void { - if (behavior.color && behavior.color.color && behavior.color.color.keys) { - const colorKeys = behavior.color.color.keys; +export function applyColorOverLifePS(particleSystem: EffectParticleSystem, behavior: IColorOverLifeBehavior | any): void { + // New unified structure: behavior.color is IColorFunction + const colorFunction = behavior.color; + if (!colorFunction) { + return; + } + + const colorFunctionType = colorFunction.colorFunctionType; + const data = colorFunction.data; + + // Handle ConstantColor + if (colorFunctionType === "ConstantColor" && data?.color) { + const color = data.color; + particleSystem.color1 = new Color4(color.r, color.g, color.b, color.a); + particleSystem.color2 = new Color4(color.r, color.g, color.b, color.a); + return; + } + + // Handle RandomColorBetweenGradient - apply first gradient (TODO: implement proper random selection per particle) + if (colorFunctionType === "RandomColorBetweenGradient" && data?.gradient1) { + const colorKeys = data.gradient1.colorKeys || []; + const alphaKeys = data.gradient1.alphaKeys || []; + + // Apply first gradient for (const key of colorKeys) { if (key.value !== undefined && key.pos !== undefined) { - const color = extractColorFromValue(key.value); - const alpha = extractAlphaFromValue(key.value); + let color: { r: number; g: number; b: number }; + let alpha: number; + + if (Array.isArray(key.value)) { + color = { r: key.value[0], g: key.value[1], b: key.value[2] }; + alpha = key.value[3] !== undefined ? key.value[3] : 1; + } else { + color = extractColorFromValue(key.value); + alpha = extractAlphaFromValue(key.value); + } + particleSystem.addColorGradient(key.pos, new Color4(color.r, color.g, color.b, alpha)); } } + + for (const key of alphaKeys) { + if (key.value !== undefined && key.pos !== undefined) { + const alpha = typeof key.value === "number" ? key.value : extractAlphaFromValue(key.value); + const existingGradients = particleSystem.getColorGradients(); + const existingGradient = existingGradients?.find((g) => Math.abs(g.gradient - key.pos) < 0.001); + if (existingGradient) { + existingGradient.color1.a = alpha; + if (existingGradient.color2) { + existingGradient.color2.a = alpha; + } + } else { + particleSystem.addColorGradient(key.pos, new Color4(1, 1, 1, alpha)); + } + } + } + return; } - if (behavior.color && behavior.color.alpha && behavior.color.alpha.keys) { - const alphaKeys = behavior.color.alpha.keys; + // Handle Gradient + if (colorFunctionType === "Gradient" && data) { + const colorKeys = data.colorKeys || []; + const alphaKeys = data.alphaKeys || []; + + // Apply color keys + for (const key of colorKeys) { + if (key.value !== undefined && key.pos !== undefined) { + let color: { r: number; g: number; b: number }; + let alpha: number; + + if (Array.isArray(key.value)) { + // UI format: [r, g, b, a] + color = { r: key.value[0], g: key.value[1], b: key.value[2] }; + alpha = key.value[3] !== undefined ? key.value[3] : 1; + } else { + // Quarks format: extract from value + color = extractColorFromValue(key.value); + alpha = extractAlphaFromValue(key.value); + } + + particleSystem.addColorGradient(key.pos, new Color4(color.r, color.g, color.b, alpha)); + } + } + + // Apply alpha keys (merge with existing color gradients) for (const key of alphaKeys) { if (key.value !== undefined && key.pos !== undefined) { - const alpha = extractAlphaFromValue(key.value); + const alpha = typeof key.value === "number" ? key.value : extractAlphaFromValue(key.value); const existingGradients = particleSystem.getColorGradients(); - const existingGradient = existingGradients?.find((g) => key.pos !== undefined && Math.abs(g.gradient - key.pos) < 0.001); + const existingGradient = existingGradients?.find((g) => Math.abs(g.gradient - key.pos) < 0.001); if (existingGradient) { existingGradient.color1.a = alpha; if (existingGradient.color2) { existingGradient.color2.a = alpha; } } else { - particleSystem.addColorGradient(key.pos ?? 0, new Color4(1, 1, 1, alpha)); + particleSystem.addColorGradient(key.pos, new Color4(1, 1, 1, alpha)); } } } + return; } } /** * Apply ColorOverLife behavior to SolidParticleSystem - * Adds color gradients to the system (similar to ParticleSystem native gradients) - * Properly combines color and alpha keys even when they have different positions + * Uses unified IColorFunction structure: behavior.color = { colorFunctionType, data } */ -export function applyColorOverLifeSPS(system: EffectSolidParticleSystem, behavior: IColorOverLifeBehavior): void { - if (!behavior.color) { +export function applyColorOverLifeSPS(system: EffectSolidParticleSystem, behavior: IColorOverLifeBehavior | any): void { + // New unified structure: behavior.color is IColorFunction + const colorFunction = behavior.color; + if (!colorFunction) { + return; + } + + const colorFunctionType = colorFunction.colorFunctionType; + const data = colorFunction.data; + let colorKeys: any[] = []; + let alphaKeys: any[] = []; + + // Handle ConstantColor + if (colorFunctionType === "ConstantColor" && data?.color) { + const color = data.color; + system.color1 = new Color4(color.r, color.g, color.b, color.a); + system.color2 = new Color4(color.r, color.g, color.b, color.a); + return; + } + + // Handle RandomColorBetweenGradient - apply first gradient (TODO: implement proper random selection per particle) + if (colorFunctionType === "RandomColorBetweenGradient" && data?.gradient1) { + colorKeys = data.gradient1.colorKeys || []; + alphaKeys = data.gradient1.alphaKeys || []; + } else if (colorFunctionType === "Gradient" && data) { + colorKeys = data.colorKeys || []; + alphaKeys = data.alphaKeys || []; + } else { return; } // Collect all unique positions from both color and alpha keys const allPositions = new Set(); - - // Get color keys - const colorKeys = behavior.color.color?.keys || behavior.color.keys || []; for (const key of colorKeys) { if (key.pos !== undefined) { allPositions.add(key.pos); } } - - // Get alpha keys - const alphaKeys = behavior.color.alpha?.keys || []; for (const key of alphaKeys) { const pos = key.pos ?? key.time ?? 0; allPositions.add(pos); } - // If no keys found, return if (allPositions.size === 0) { return; } - // Sort positions + // Sort positions and create gradients at each position const sortedPositions = Array.from(allPositions).sort((a, b) => a - b); - - // For each position, compute color and alpha separately for (const pos of sortedPositions) { - // Find color for this position (interpolate if needed) + // Get color at this position let color = { r: 1, g: 1, b: 1 }; if (colorKeys.length > 0) { - // Find the color key at this position or interpolate const exactColorKey = colorKeys.find((k) => k.pos !== undefined && Math.abs(k.pos - pos) < 0.001); if (exactColorKey && exactColorKey.value !== undefined) { - color = extractColorFromValue(exactColorKey.value); + if (Array.isArray(exactColorKey.value)) { + color = { r: exactColorKey.value[0], g: exactColorKey.value[1], b: exactColorKey.value[2] }; + } else { + color = extractColorFromValue(exactColorKey.value); + } } else { // Interpolate color from surrounding keys color = interpolateColorFromKeys(colorKeys, pos); } } - // Find alpha for this position (interpolate if needed) + // Get alpha at this position let alpha = 1; if (alphaKeys.length > 0) { const exactAlphaKey = alphaKeys.find((k) => { @@ -98,88 +190,111 @@ export function applyColorOverLifeSPS(system: EffectSolidParticleSystem, behavio return Math.abs(kPos - pos) < 0.001; }); if (exactAlphaKey && exactAlphaKey.value !== undefined) { - alpha = extractAlphaFromValue(exactAlphaKey.value); + if (typeof exactAlphaKey.value === "number") { + alpha = exactAlphaKey.value; + } else { + alpha = extractAlphaFromValue(exactAlphaKey.value); + } } else { // Interpolate alpha from surrounding keys alpha = interpolateAlphaFromKeys(alphaKeys, pos); } } else if (colorKeys.length > 0) { - // If no alpha keys, try to get alpha from color keys + // If no alpha keys, try to get alpha from color key const exactColorKey = colorKeys.find((k) => k.pos !== undefined && Math.abs(k.pos - pos) < 0.001); if (exactColorKey && exactColorKey.value !== undefined) { - alpha = extractAlphaFromValue(exactColorKey.value); + if (Array.isArray(exactColorKey.value)) { + alpha = exactColorKey.value[3] !== undefined ? exactColorKey.value[3] : 1; + } else { + alpha = extractAlphaFromValue(exactColorKey.value); + } } } - // Add gradient with combined color and alpha system.addColorGradient(pos, new Color4(color.r, color.g, color.b, alpha)); } } /** - * Interpolate color from gradient keys at given position + * Interpolate color from gradient keys at a given position */ function interpolateColorFromKeys(keys: any[], pos: number): { r: number; g: number; b: number } { if (keys.length === 0) { return { r: 1, g: 1, b: 1 }; } - if (keys.length === 1) { - return extractColorFromValue(keys[0].value); + const value = keys[0].value; + return Array.isArray(value) ? { r: value[0], g: value[1], b: value[2] } : extractColorFromValue(value); } // Find surrounding keys + let before = keys[0]; + let after = keys[keys.length - 1]; for (let i = 0; i < keys.length - 1; i++) { - const pos1 = keys[i].pos ?? 0; - const pos2 = keys[i + 1].pos ?? 1; - - if (pos >= pos1 && pos <= pos2) { - const t = pos2 - pos1 !== 0 ? (pos - pos1) / (pos2 - pos1) : 0; - const c1 = extractColorFromValue(keys[i].value); - const c2 = extractColorFromValue(keys[i + 1].value); - return { - r: c1.r + (c2.r - c1.r) * t, - g: c1.g + (c2.g - c1.g) * t, - b: c1.b + (c2.b - c1.b) * t, - }; + const k1 = keys[i]; + const k2 = keys[i + 1]; + if (k1.pos !== undefined && k2.pos !== undefined && k1.pos <= pos && k2.pos >= pos) { + before = k1; + after = k2; + break; } } - // Clamp to first or last - if (pos <= (keys[0].pos ?? 0)) { - return extractColorFromValue(keys[0].value); + if (before === after) { + const value = before.value; + return Array.isArray(value) ? { r: value[0], g: value[1], b: value[2] } : extractColorFromValue(value); } - return extractColorFromValue(keys[keys.length - 1].value); + + // Interpolate + const t = (pos - (before.pos ?? 0)) / ((after.pos ?? 1) - (before.pos ?? 0)); + const c1 = Array.isArray(before.value) ? { r: before.value[0], g: before.value[1], b: before.value[2] } : extractColorFromValue(before.value); + const c2 = Array.isArray(after.value) ? { r: after.value[0], g: after.value[1], b: after.value[2] } : extractColorFromValue(after.value); + + return { + r: c1.r + (c2.r - c1.r) * t, + g: c1.g + (c2.g - c1.g) * t, + b: c1.b + (c2.b - c1.b) * t, + }; } /** - * Interpolate alpha from gradient keys at given position + * Interpolate alpha from gradient keys at a given position */ function interpolateAlphaFromKeys(keys: any[], pos: number): number { if (keys.length === 0) { return 1; } - if (keys.length === 1) { - return extractAlphaFromValue(keys[0].value); + const value = keys[0].value; + return typeof value === "number" ? value : extractAlphaFromValue(value); } // Find surrounding keys + let before = keys[0]; + let after = keys[keys.length - 1]; for (let i = 0; i < keys.length - 1; i++) { - const pos1 = keys[i].pos ?? keys[i].time ?? 0; - const pos2 = keys[i + 1].pos ?? keys[i + 1].time ?? 1; - - if (pos >= pos1 && pos <= pos2) { - const t = pos2 - pos1 !== 0 ? (pos - pos1) / (pos2 - pos1) : 0; - const a1 = extractAlphaFromValue(keys[i].value); - const a2 = extractAlphaFromValue(keys[i + 1].value); - return a1 + (a2 - a1) * t; + const k1 = keys[i]; + const k2 = keys[i + 1]; + const k1Pos = k1.pos ?? k1.time ?? 0; + const k2Pos = k2.pos ?? k2.time ?? 1; + if (k1Pos <= pos && k2Pos >= pos) { + before = k1; + after = k2; + break; } } - // Clamp to first or last - if (pos <= (keys[0].pos ?? keys[0].time ?? 0)) { - return extractAlphaFromValue(keys[0].value); + if (before === after) { + const value = before.value; + return typeof value === "number" ? value : extractAlphaFromValue(value); } - return extractAlphaFromValue(keys[keys.length - 1].value); + + // Interpolate + const beforePos = before.pos ?? before.time ?? 0; + const afterPos = after.pos ?? after.time ?? 1; + const t = (pos - beforePos) / (afterPos - beforePos); + const a1 = typeof before.value === "number" ? before.value : extractAlphaFromValue(before.value); + const a2 = typeof after.value === "number" ? after.value : extractAlphaFromValue(after.value); + + return a1 + (a2 - a1) * t; } diff --git a/tools/src/effect/factories/materialFactory.ts b/tools/src/effect/factories/materialFactory.ts index aa470f4aa..dff791944 100644 --- a/tools/src/effect/factories/materialFactory.ts +++ b/tools/src/effect/factories/materialFactory.ts @@ -208,7 +208,20 @@ export class MaterialFactory implements IMaterialFactory { return this._createUnlitMaterial(name, material, babylonTexture, materialColor); } - return new PBRMaterial(name + "_material", this._scene); + // Create PBR material for other material types + // Note: Vertex colors are automatically used by PBR materials if mesh has vertex colors + // The VERTEXCOLOR define is set automatically based on mesh.isVerticesDataPresent(VertexBuffer.ColorKind) + const pbrMaterial = new PBRMaterial(name + "_material", this._scene); + pbrMaterial.albedoTexture = babylonTexture; + pbrMaterial.albedoColor = materialColor; + + this._applyTransparency(pbrMaterial, material, babylonTexture); + this._applyDepthWrite(pbrMaterial, material); + this._applySideSettings(pbrMaterial, material); + this._applyBlendMode(pbrMaterial, material); + + this._logger.log(`Created PBRMaterial with albedoTexture (vertex colors will be used automatically if mesh has them)`); + return pbrMaterial; } /** @@ -220,13 +233,14 @@ export class MaterialFactory implements IMaterialFactory { unlitMaterial.unlit = true; unlitMaterial.albedoColor = color; unlitMaterial.albedoTexture = texture; + // Note: Vertex colors are automatically used by PBR materials if mesh has vertex colors this._applyTransparency(unlitMaterial, material, texture); this._applyDepthWrite(unlitMaterial, material); this._applySideSettings(unlitMaterial, material); this._applyBlendMode(unlitMaterial, material); - this._logger.log(`Using MeshBasicMaterial: PBRMaterial with unlit=true, albedoTexture`); + this._logger.log(`Using MeshBasicMaterial: PBRMaterial with unlit=true, albedoTexture (vertex colors will be used automatically if mesh has them)`); this._logger.log(`Material created successfully: ${name}_material`); return unlitMaterial; diff --git a/tools/src/effect/factories/systemFactory.ts b/tools/src/effect/factories/systemFactory.ts index 94db6a31e..653d29b08 100644 --- a/tools/src/effect/factories/systemFactory.ts +++ b/tools/src/effect/factories/systemFactory.ts @@ -443,6 +443,8 @@ export class SystemFactory { } // Apply material if provided + // Note: Vertex colors are automatically used by PBR materials if mesh has vertex colors + // The SPS mesh will have vertex colors because _computeParticleColor is enabled if (emitter.materialId) { const material = this._materialFactory.createMaterial(emitter.materialId, name); if (material) { diff --git a/tools/src/effect/parsers/dataConverter.ts b/tools/src/effect/parsers/dataConverter.ts index 153730995..0a48afd7f 100644 --- a/tools/src/effect/parsers/dataConverter.ts +++ b/tools/src/effect/parsers/dataConverter.ts @@ -15,6 +15,9 @@ import type { IQuarksGradientKey, IQuarksShape, IQuarksColorOverLifeBehavior, + IQuarksGradientColor, + IQuarksConstantColorColor, + IQuarksRandomColorBetweenGradient, IQuarksSizeOverLifeBehavior, IQuarksRotationOverLifeBehavior, IQuarksForceOverLifeBehavior, @@ -32,12 +35,11 @@ import type { IMaterial, ITexture, IImage, IGeometry, IGeometryData } from "../t import type { IParticleSystemConfig } from "../types/emitter"; import type { Behavior, - IColorOverLifeBehavior, + IColorFunction, ISizeOverLifeBehavior, IForceOverLifeBehavior, ISpeedOverLifeBehavior, ILimitSpeedOverLifeBehavior, - IColorBySpeedBehavior, ISizeBySpeedBehavior, } from "../types/behaviors"; import type { Value } from "../types/values"; @@ -645,20 +647,94 @@ export class DataConverter { switch (IQuarksBehavior.type) { case "ColorOverLife": { const behavior = IQuarksBehavior as IQuarksColorOverLifeBehavior; - if (behavior.color) { - const Color: IColorOverLifeBehavior["color"] = {}; - if (behavior.color.color?.keys) { - Color.color = { keys: behavior.color.color.keys.map((k) => this._convertGradientKey(k)) }; - } - if (behavior.color.alpha?.keys) { - Color.alpha = { keys: behavior.color.alpha.keys.map((k) => this._convertGradientKey(k)) }; - } - if (behavior.color.keys) { - Color.keys = behavior.color.keys.map((k) => this._convertGradientKey(k)); + if (!behavior.color) { + return { + type: "ColorOverLife", + color: { + colorFunctionType: "ConstantColor", + data: {}, + }, + }; + } + + const colorType = behavior.color.type; + + // Convert color to unified IColorFunction structure + let colorFunction: IColorFunction; + + if (colorType === "Gradient") { + const gradientColor = behavior.color as IQuarksGradientColor; + colorFunction = { + colorFunctionType: "Gradient", + data: { + colorKeys: gradientColor.color?.keys ? gradientColor.color.keys.map((k) => this._convertGradientKey(k)) : [], + alphaKeys: gradientColor.alpha?.keys ? gradientColor.alpha.keys.map((k) => this._convertGradientKey(k)) : [], + }, + }; + } else if (colorType === "ConstantColor") { + const constantColor = behavior.color as IQuarksConstantColorColor; + const color = + constantColor.color || + (constantColor.value + ? { r: constantColor.value[0], g: constantColor.value[1], b: constantColor.value[2], a: constantColor.value[3] } + : { r: 1, g: 1, b: 1, a: 1 }); + colorFunction = { + colorFunctionType: "ConstantColor", + data: { + color: { + r: color.r ?? 1, + g: color.g ?? 1, + b: color.b ?? 1, + a: color.a !== undefined ? color.a : 1, + }, + }, + }; + } else if (colorType === "RandomColorBetweenGradient") { + const randomColor = behavior.color as IQuarksRandomColorBetweenGradient; + colorFunction = { + colorFunctionType: "RandomColorBetweenGradient", + data: { + gradient1: { + colorKeys: randomColor.gradient1?.color?.keys ? randomColor.gradient1.color.keys.map((k) => this._convertGradientKey(k)) : [], + alphaKeys: randomColor.gradient1?.alpha?.keys ? randomColor.gradient1.alpha.keys.map((k) => this._convertGradientKey(k)) : [], + }, + gradient2: { + colorKeys: randomColor.gradient2?.color?.keys ? randomColor.gradient2.color.keys.map((k) => this._convertGradientKey(k)) : [], + alphaKeys: randomColor.gradient2?.alpha?.keys ? randomColor.gradient2.alpha.keys.map((k) => this._convertGradientKey(k)) : [], + }, + }, + }; + } else { + // Fallback: try to detect format from keys + const hasColorKeys = (behavior.color as any).color?.keys && (behavior.color as any).color.keys.length > 0; + const hasAlphaKeys = (behavior.color as any).alpha?.keys && (behavior.color as any).alpha.keys.length > 0; + const hasKeys = (behavior.color as any).keys && (behavior.color as any).keys.length > 0; + + if (hasColorKeys || hasAlphaKeys || hasKeys) { + colorFunction = { + colorFunctionType: "Gradient", + data: { + colorKeys: hasColorKeys + ? (behavior.color as any).color.keys.map((k: any) => this._convertGradientKey(k)) + : hasKeys + ? (behavior.color as any).keys.map((k: any) => this._convertGradientKey(k)) + : [], + alphaKeys: hasAlphaKeys ? (behavior.color as any).alpha.keys.map((k: any) => this._convertGradientKey(k)) : [], + }, + }; + } else { + // Default to ConstantColor + colorFunction = { + colorFunctionType: "ConstantColor", + data: {}, + }; } - return { type: "ColorOverLife", color: Color }; } - return { type: "ColorOverLife" }; + + return { + type: "ColorOverLife", + color: colorFunction, + }; } case "SizeOverLife": { @@ -772,15 +848,25 @@ export class DataConverter { case "ColorBySpeed": { const behavior = IQuarksBehavior as IQuarksColorBySpeedBehavior; - const Behavior: IColorBySpeedBehavior = { + const colorFunction: IColorFunction = behavior.color?.keys + ? { + colorFunctionType: "Gradient", + data: { + colorKeys: behavior.color.keys.map((k: IQuarksGradientKey) => this._convertGradientKey(k)), + alphaKeys: [], + }, + } + : { + colorFunctionType: "ConstantColor", + data: {}, + }; + + return { type: "ColorBySpeed", + color: colorFunction, minSpeed: behavior.minSpeed !== undefined ? this._convertValue(behavior.minSpeed) : undefined, maxSpeed: behavior.maxSpeed !== undefined ? this._convertValue(behavior.maxSpeed) : undefined, }; - if (behavior.color?.keys) { - Behavior.color = { keys: behavior.color.keys.map((k: IQuarksGradientKey) => this._convertGradientKey(k)) }; - } - return Behavior; } case "SizeBySpeed": { diff --git a/tools/src/effect/systems/effectSolidParticleSystem.ts b/tools/src/effect/systems/effectSolidParticleSystem.ts index 5e6640b98..c691a39da 100644 --- a/tools/src/effect/systems/effectSolidParticleSystem.ts +++ b/tools/src/effect/systems/effectSolidParticleSystem.ts @@ -825,12 +825,16 @@ export class EffectSolidParticleSystem extends SolidParticleSystem implements IS /** * Override buildMesh to enable vertex colors and alpha * This is required for ColorOverLife behavior to work visually + * Note: PBR materials automatically use vertex colors if mesh has them + * The VERTEXCOLOR define is set automatically based on mesh.isVerticesDataPresent(VertexBuffer.ColorKind) */ public override buildMesh(): Mesh { const mesh = super.buildMesh(); if (mesh) { mesh.hasVertexAlpha = true; + // Vertex colors are already enabled via _computeParticleColor = true + // PBR materials will automatically use them if mesh has vertex color data } return mesh; @@ -941,7 +945,8 @@ export class EffectSolidParticleSystem extends SolidParticleSystem implements IS // Pre-parse min/max speed ONCE const minSpeed = b.minSpeed !== undefined ? ValueUtils.parseConstantValue(b.minSpeed) : 0; const maxSpeed = b.maxSpeed !== undefined ? ValueUtils.parseConstantValue(b.maxSpeed) : 1; - const colorKeys = b.color?.keys; + // New structure: b.color is IColorFunction with data.colorKeys + const colorKeys = b.color?.data?.colorKeys; if (colorKeys && colorKeys.length > 0) { functions.push((particle: SolidParticle) => { diff --git a/tools/src/effect/types/behaviors.ts b/tools/src/effect/types/behaviors.ts index 3c668751e..85326a5fa 100644 --- a/tools/src/effect/types/behaviors.ts +++ b/tools/src/effect/types/behaviors.ts @@ -23,19 +23,33 @@ export type SystemBehaviorFunction = (system: ParticleSystem | SolidParticleSyst /** * behavior types (converted from Quarks) */ -export interface IColorOverLifeBehavior { - type: "ColorOverLife"; - color?: { - color?: { - keys: IGradientKey[]; +/** + * Color function - unified structure for all color-related behaviors + */ +export interface IColorFunction { + colorFunctionType: "Gradient" | "ConstantColor" | "ColorRange" | "RandomColor" | "RandomColorBetweenGradient"; + data: { + color?: { r: number; g: number; b: number; a: number }; + colorA?: { r: number; g: number; b: number; a: number }; + colorB?: { r: number; g: number; b: number; a: number }; + colorKeys?: IGradientKey[]; + alphaKeys?: IGradientKey[]; + gradient1?: { + colorKeys?: IGradientKey[]; + alphaKeys?: IGradientKey[]; }; - alpha?: { - keys: IGradientKey[]; + gradient2?: { + colorKeys?: IGradientKey[]; + alphaKeys?: IGradientKey[]; }; - keys?: IGradientKey[]; }; } +export interface IColorOverLifeBehavior { + type: "ColorOverLife"; + color: IColorFunction; +} + export interface ISizeOverLifeBehavior { type: "SizeOverLife"; size?: { @@ -106,11 +120,10 @@ export interface ILimitSpeedOverLifeBehavior { export interface IColorBySpeedBehavior { type: "ColorBySpeed"; - color?: { - keys: IGradientKey[]; - }; + color: IColorFunction; minSpeed?: Value; maxSpeed?: Value; + speedRange?: { min: number; max: number }; } export interface ISizeBySpeedBehavior { diff --git a/tools/src/effect/types/quarksTypes.ts b/tools/src/effect/types/quarksTypes.ts index 308b8f8b4..01430d6a8 100644 --- a/tools/src/effect/types/quarksTypes.ts +++ b/tools/src/effect/types/quarksTypes.ts @@ -96,17 +96,40 @@ export interface IQuarksEmissionBurst { /** * Quarks/Three.js behavior types */ -export interface IQuarksColorOverLifeBehavior { - type: "ColorOverLife"; +export interface IQuarksCLinearFunction { + type: "CLinearFunction"; + subType: "Color" | "Number"; + keys: IQuarksGradientKey[]; +} + +export interface IQuarksGradientColor { + type: "Gradient"; + color?: IQuarksCLinearFunction; + alpha?: IQuarksCLinearFunction; +} + +export interface IQuarksConstantColorColor { + type: "ConstantColor"; color?: { - color?: { - keys: IQuarksGradientKey[]; - }; - alpha?: { - keys: IQuarksGradientKey[]; - }; - keys?: IQuarksGradientKey[]; + r: number; + g: number; + b: number; + a?: number; }; + value?: [number, number, number, number]; +} + +export interface IQuarksRandomColorBetweenGradient { + type: "RandomColorBetweenGradient"; + gradient1?: IQuarksGradientColor; + gradient2?: IQuarksGradientColor; +} + +export type IQuarksColorOverLifeColor = IQuarksGradientColor | IQuarksConstantColorColor | IQuarksRandomColorBetweenGradient; + +export interface IQuarksColorOverLifeBehavior { + type: "ColorOverLife"; + color?: IQuarksColorOverLifeColor; } export interface IQuarksSizeOverLifeBehavior { From d3ce2264395ebd22de55acc7442f452bba5e273c Mon Sep 17 00:00:00 2001 From: Mikalai Lazitski Date: Fri, 19 Dec 2025 14:18:38 +0300 Subject: [PATCH 51/62] refactor: enhance effect editor and solid particle system by implementing a new method to replace particle meshes, improving geometry handling, and ensuring proper resource management during updates --- .../effect-editor/properties/renderer.tsx | 33 ++++++----- .../systems/effectSolidParticleSystem.ts | 55 +++++++++++++++++++ 2 files changed, 73 insertions(+), 15 deletions(-) diff --git a/editor/src/editor/windows/effect-editor/properties/renderer.tsx b/editor/src/editor/windows/effect-editor/properties/renderer.tsx index a20fc097f..904216dfd 100644 --- a/editor/src/editor/windows/effect-editor/properties/renderer.tsx +++ b/editor/src/editor/windows/effect-editor/properties/renderer.tsx @@ -304,7 +304,6 @@ export class EffectEditorParticleRendererProperties extends Component Date: Wed, 24 Dec 2025 14:43:55 +0300 Subject: [PATCH 52/62] refactor: enhance effect editor and particle system functionality by implementing Unity prefab import, improving data handling, and introducing new converter methods for better integration and maintainability --- editor/package.json | 6 +- .../windows/effect-editor/converters/index.ts | 2 + .../converters/quarksConverter.ts | 1220 +++++++++++++++++ .../effect-editor/converters}/quarksTypes.ts | 256 ++-- .../converters/unityConverter.ts | 1175 ++++++++++++++++ .../editor/windows/effect-editor/graph.tsx | 80 +- .../editor/windows/effect-editor/index.tsx | 90 ++ .../modals/unity-import-modal.tsx | 1107 +++++++++++++++ .../editor/windows/effect-editor/toolbar.tsx | 64 +- editor/src/ui/shadcn/ui/scroll-area.tsx | 37 + tools/src/effect/behaviors/colorBySpeed.ts | 3 +- tools/src/effect/effect.ts | 254 +--- tools/src/effect/factories/geometryFactory.ts | 10 +- tools/src/effect/factories/index.ts | 2 +- tools/src/effect/factories/materialFactory.ts | 11 +- .../{systemFactory.ts => nodeFactory.ts} | 320 ++--- tools/src/effect/parsers/dataConverter.ts | 1118 --------------- tools/src/effect/parsers/index.ts | 2 - tools/src/effect/parsers/parser.ts | 122 -- .../effect/systems/effectParticleSystem.ts | 24 +- .../systems/effectSolidParticleSystem.ts | 13 +- tools/src/effect/types/factories.ts | 10 +- tools/src/effect/types/hierarchy.ts | 3 - tools/src/effect/types/loader.ts | 2 +- tools/src/effect/types/system.ts | 16 + yarn.lock | 42 + 26 files changed, 4169 insertions(+), 1820 deletions(-) create mode 100644 editor/src/editor/windows/effect-editor/converters/index.ts create mode 100644 editor/src/editor/windows/effect-editor/converters/quarksConverter.ts rename {tools/src/effect/types => editor/src/editor/windows/effect-editor/converters}/quarksTypes.ts (58%) create mode 100644 editor/src/editor/windows/effect-editor/converters/unityConverter.ts create mode 100644 editor/src/editor/windows/effect-editor/modals/unity-import-modal.tsx create mode 100644 editor/src/ui/shadcn/ui/scroll-area.tsx rename tools/src/effect/factories/{systemFactory.ts => nodeFactory.ts} (51%) delete mode 100644 tools/src/effect/parsers/dataConverter.ts delete mode 100644 tools/src/effect/parsers/index.ts delete mode 100644 tools/src/effect/parsers/parser.ts diff --git a/editor/package.json b/editor/package.json index 8cc11e670..a8f52ae0f 100644 --- a/editor/package.json +++ b/editor/package.json @@ -22,6 +22,7 @@ "license": "(Apache-2.0)", "devDependencies": { "@electron/rebuild": "3.7.1", + "@types/adm-zip": "^0.5.7", "@types/decompress": "4.2.7", "@types/fluent-ffmpeg": "^2.1.27", "@types/node": "^22", @@ -58,6 +59,8 @@ "@radix-ui/react-menubar": "^1.0.4", "@radix-ui/react-popover": "^1.0.7", "@radix-ui/react-progress": "^1.0.3", + "@radix-ui/react-radio-group": "^1.1.3", + "@radix-ui/react-scroll-area": "^1.2.10", "@radix-ui/react-select": "^2.0.0", "@radix-ui/react-separator": "^1.0.3", "@radix-ui/react-slider": "^1.2.0", @@ -67,11 +70,11 @@ "@radix-ui/react-toggle": "^1.1.1", "@radix-ui/react-toggle-group": "1.1.11", "@radix-ui/react-tooltip": "^1.0.7", - "@radix-ui/react-radio-group": "^1.1.3", "@recast-navigation/core": "^0.43.0", "@recast-navigation/generators": "^0.43.0", "@xterm/addon-fit": "^0.10.0", "@xterm/xterm": "^5.6.0-beta.119", + "adm-zip": "^0.5.16", "assimpjs": "0.0.10", "axios": "^1.12.0", "babylonjs": "8.41.0", @@ -99,6 +102,7 @@ "framer-motion": "12.23.24", "fs-extra": "11.2.0", "glob": "11.1.0", + "js-yaml": "^4.1.1", "markdown-to-jsx": "7.6.2", "math-expression-evaluator": "^2.0.6", "md5": "^2.3.0", diff --git a/editor/src/editor/windows/effect-editor/converters/index.ts b/editor/src/editor/windows/effect-editor/converters/index.ts new file mode 100644 index 000000000..76a222ac3 --- /dev/null +++ b/editor/src/editor/windows/effect-editor/converters/index.ts @@ -0,0 +1,2 @@ +export * from "./quarksConverter"; +export * from "./unityConverter"; diff --git a/editor/src/editor/windows/effect-editor/converters/quarksConverter.ts b/editor/src/editor/windows/effect-editor/converters/quarksConverter.ts new file mode 100644 index 000000000..7ae52c361 --- /dev/null +++ b/editor/src/editor/windows/effect-editor/converters/quarksConverter.ts @@ -0,0 +1,1220 @@ +import { Vector3, Matrix, Quaternion, Color3, Texture as BabylonTexture, ParticleSystem, Color4, Tools } from "babylonjs"; +import type { + IQuarksJSON, + IQuarksMaterial, + IQuarksTexture, + IQuarksImage, + IQuarksGeometry, + IQuarksObject, + IQuarksParticleEmitterConfig, + IQuarksBehavior, + IQuarksValue, + IQuarksColor, + IQuarksStartSize, + IQuarksStartColor, + IQuarksRotation, + IQuarksGradientKey, + IQuarksShape, + IQuarksColorOverLifeBehavior, + IQuarksGradientColor, + IQuarksConstantColorColor, + IQuarksRandomColorBetweenGradient, + IQuarksSizeOverLifeBehavior, + IQuarksRotationOverLifeBehavior, + IQuarksForceOverLifeBehavior, + IQuarksGravityForceBehavior, + IQuarksSpeedOverLifeBehavior, + IQuarksFrameOverLifeBehavior, + IQuarksLimitSpeedOverLifeBehavior, + IQuarksColorBySpeedBehavior, + IQuarksSizeBySpeedBehavior, + IQuarksRotationBySpeedBehavior, + IQuarksOrbitOverLifeBehavior, +} from "./quarksTypes"; +import type { + ITransform, + IGroup, + IEmitter, + IData, + IMaterial, + ITexture, + IImage, + IGeometry, + IGeometryData, + IParticleSystemConfig, + Behavior, + IColorFunction, + IForceOverLifeBehavior, + ISpeedOverLifeBehavior, + ILimitSpeedOverLifeBehavior, + ISizeBySpeedBehavior, + Value, + IGradientKey, + IShape, +} from "babylonjs-editor-tools"; + +/** + * Converts Quarks Effect to Babylon.js Effect format + * All coordinate system conversions happen here, once + */ +export class QuarksConverter { + // Constants + private static readonly DEFAULT_DURATION = 5; + private static readonly DEFAULT_COLOR = { r: 1, g: 1, b: 1, a: 1 }; + private static readonly DEFAULT_COLOR_HEX = 0xffffff; + private static readonly PREWARM_FPS = 60; + private static readonly DEFAULT_PREWARM_STEP_OFFSET = 1 / QuarksConverter.PREWARM_FPS; + + // Three.js constants + private static readonly THREE_REPEAT_WRAPPING = 1000; + private static readonly THREE_CLAMP_TO_EDGE_WRAPPING = 1001; + private static readonly THREE_MIRRORED_REPEAT_WRAPPING = 1002; + private static readonly THREE_LINEAR_FILTER = 1006; + private static readonly THREE_NEAREST_MIPMAP_NEAREST_FILTER = 1007; + private static readonly THREE_LINEAR_MIPMAP_NEAREST_FILTER = 1008; + private static readonly THREE_NEAREST_MIPMAP_LINEAR_FILTER = 1009; + /** + * Convert Quarks Effect to Babylon.js Effect format + * Handles errors gracefully and returns partial data if conversion fails + */ + public convert(data: IQuarksJSON): IData { + let root: IGroup | IEmitter | null = null; + + try { + root = this._convertObject(data.object, null); + } catch (error) { + console.error(`Failed to convert root object: ${error instanceof Error ? error.message : String(error)}`); + } + + // Convert all resources with error handling + const materials = this._convertResources(data.materials, (m) => this._convertMaterial(m), "materials"); + const textures = this._convertResources(data.textures, (t) => this._convertTexture(t), "textures"); + const images = this._convertResources(data.images, (i) => this._convertImage(i), "images"); + const geometries = this._convertResources(data.geometries, (g) => this._convertGeometry(g), "geometries"); + + return { + root, + materials, + textures, + images, + geometries, + }; + } + + /** + * Helper: Convert resources array with error handling + */ + private _convertResources(items: T[] | undefined, converter: (item: T) => R, resourceName: string): R[] { + try { + return (items || []).map(converter); + } catch (error) { + console.error(`Failed to convert ${resourceName}: ${error instanceof Error ? error.message : String(error)}`); + return []; + } + } + + /** + * Convert a IQuarks object to Babylon.js format + */ + private _convertObject(obj: IQuarksObject, parentUuid: string | null): IGroup | IEmitter | null { + if (!obj || typeof obj !== "object") { + return null; + } + + // Convert transform from right-handed to left-handed + const transform = this._convertTransform(obj.matrix, obj.position, obj.rotation, obj.scale); + + if (obj.type === "Group") { + const group: IGroup = { + uuid: obj.uuid || Tools.RandomId(), + name: obj.name || "Group", + transform, + children: [], + }; + + // Convert children + if (obj.children && Array.isArray(obj.children)) { + for (const child of obj.children) { + const convertedChild = this._convertObject(child, group.uuid); + if (convertedChild) { + group.children.push(convertedChild); + } + } + } + + return group; + } else if (obj.type === "ParticleEmitter" && obj.ps) { + // Convert emitter config from IQuarks to format + const config = this._convertEmitterConfig(obj.ps); + + const emitter: IEmitter = { + uuid: obj.uuid || Tools.RandomId(), + name: obj.name || "ParticleEmitter", + transform, + config, + materialId: obj.ps.material, + parentUuid: parentUuid ?? undefined, + systemType: config.systemType, // systemType is set in _convertEmitterConfig + matrix: obj.matrix, // Store original matrix for rotation extraction + }; + + return emitter; + } + + return null; + } + + /** + * Convert transform from IQuarks (right-handed) to Babylon.js (left-handed) + * This is the ONLY place where handedness conversion happens + */ + private _convertTransform(matrixArray?: number[], positionArray?: number[], rotationArray?: number[], scaleArray?: number[]): ITransform { + const position = Vector3.Zero(); + const rotation = Quaternion.Identity(); + const scale = Vector3.One(); + + if (matrixArray && Array.isArray(matrixArray) && matrixArray.length >= 16) { + // Use matrix (most accurate) + const matrix = Matrix.FromArray(matrixArray); + const tempPos = Vector3.Zero(); + const tempRot = Quaternion.Zero(); + const tempScale = Vector3.Zero(); + matrix.decompose(tempScale, tempRot, tempPos); + + // Convert from right-handed to left-handed + position.copyFrom(tempPos); + position.z = -position.z; // Negate Z position + + rotation.copyFrom(tempRot); + // Convert rotation quaternion: invert X component for proper X-axis rotation conversion + // This handles the case where X=-90° in RH looks like X=0° in LH + rotation.x *= -1; + + scale.copyFrom(tempScale); + } else { + // Use individual components + if (positionArray && Array.isArray(positionArray)) { + position.set(positionArray[0] || 0, positionArray[1] || 0, positionArray[2] || 0); + position.z = -position.z; // Convert to left-handed + } + + if (rotationArray && Array.isArray(rotationArray)) { + // If rotation is Euler angles, convert to quaternion + const eulerX = rotationArray[0] || 0; + const eulerY = rotationArray[1] || 0; + const eulerZ = rotationArray[2] || 0; + Quaternion.RotationYawPitchRollToRef(eulerY, eulerX, -eulerZ, rotation); // Negate Z for handedness + rotation.x *= -1; // Adjust X rotation component + } + + if (scaleArray && Array.isArray(scaleArray)) { + scale.set(scaleArray[0] || 1, scaleArray[1] || 1, scaleArray[2] || 1); + } + } + + return { + position, + rotation, + scale, + }; + } + + /** + * Convert emitter config from IQuarks to format + */ + private _convertEmitterConfig(config: IQuarksParticleEmitterConfig): IParticleSystemConfig { + const result = this._convertBasicEmitterConfig(config); + this._convertLifeProperties(config, result); + this._convertEmissionProperties(config, result); + this._convertVisualProperties(config, result); + this._convertBehaviorsAndShape(config, result); + this._convertBillboardConfig(config, result); + return result; + } + + /** + * Convert basic emitter configuration (system type, duration, prewarm, etc.) + */ + private _convertBasicEmitterConfig(config: IQuarksParticleEmitterConfig): IParticleSystemConfig { + const systemType: "solid" | "base" = config.renderMode === 2 ? "solid" : "base"; + const duration = config.duration ?? QuarksConverter.DEFAULT_DURATION; + const targetStopDuration = config.looping ? 0 : duration; + + // Convert prewarm to native preWarmCycles + let preWarmCycles = 0; + let preWarmStepOffset = QuarksConverter.DEFAULT_PREWARM_STEP_OFFSET; + if (config.prewarm) { + preWarmCycles = Math.ceil(duration * QuarksConverter.PREWARM_FPS); + preWarmStepOffset = QuarksConverter.DEFAULT_PREWARM_STEP_OFFSET; + } + + const isLocal = config.worldSpace === undefined ? false : !config.worldSpace; + const disposeOnStop = config.autoDestroy ?? false; + + return { + version: config.version, + systemType, + targetStopDuration, + preWarmCycles, + preWarmStepOffset, + isLocal, + disposeOnStop, + instancingGeometry: config.instancingGeometry, + renderOrder: config.renderOrder, + layers: config.layers, + uTileCount: config.uTileCount, + vTileCount: config.vTileCount, + }; + } + + /** + * Convert life-related properties (lifeTime, size, rotation, color) + */ + private _convertLifeProperties(config: IQuarksParticleEmitterConfig, result: IParticleSystemConfig): void { + if (config.startLife !== undefined) { + const lifeResult = this._convertValueToMinMax(config.startLife); + result.minLifeTime = lifeResult.min; + result.maxLifeTime = lifeResult.max; + if (lifeResult.gradients) { + result.lifeTimeGradients = lifeResult.gradients; + } + } + + if (config.startSize !== undefined) { + const sizeResult = this._convertStartSizeToMinMax(config.startSize); + result.minSize = sizeResult.min; + result.maxSize = sizeResult.max; + if (sizeResult.gradients) { + result.startSizeGradients = sizeResult.gradients; + } + } + + if (config.startRotation !== undefined) { + const rotResult = this._convertRotationToMinMax(config.startRotation); + result.minInitialRotation = rotResult.min; + result.maxInitialRotation = rotResult.max; + } + + if (config.startColor !== undefined) { + const colorResult = this._convertStartColorToColor4(config.startColor); + result.color1 = colorResult.color1; + result.color2 = colorResult.color2; + } + } + + /** + * Convert emission-related properties (speed, rate, bursts) + */ + private _convertEmissionProperties(config: IQuarksParticleEmitterConfig, result: IParticleSystemConfig): void { + if (config.startSpeed !== undefined) { + const speedResult = this._convertValueToMinMax(config.startSpeed); + result.minEmitPower = speedResult.min; + result.maxEmitPower = speedResult.max; + } + + if (config.emissionOverTime !== undefined) { + const emitResult = this._convertValueToMinMax(config.emissionOverTime); + result.emitRate = emitResult.min; + if (emitResult.gradients) { + result.emitRateGradients = emitResult.gradients; + } + } + + if (config.emissionOverDistance !== undefined) { + result.emissionOverDistance = this._convertValue(config.emissionOverDistance); + } + + if (config.emissionBursts !== undefined && Array.isArray(config.emissionBursts)) { + result.emissionBursts = config.emissionBursts.map((burst) => ({ + time: this._convertValue(burst.time), + count: this._convertValue(burst.count), + })); + } + } + + /** + * Convert visual properties (sprite animation, shape) + */ + private _convertVisualProperties(config: IQuarksParticleEmitterConfig, result: IParticleSystemConfig): void { + if (config.startTileIndex !== undefined) { + result.startTileIndex = this._convertValue(config.startTileIndex); + } + + if (config.shape !== undefined) { + result.shape = this._convertShape(config.shape); + } + } + + /** + * Convert behaviors and shape + */ + private _convertBehaviorsAndShape(config: IQuarksParticleEmitterConfig, result: IParticleSystemConfig): void { + if (config.behaviors !== undefined && Array.isArray(config.behaviors)) { + result.behaviors = config.behaviors.map((behavior) => this._convertBehavior(behavior)); + } + } + + /** + * Convert billboard configuration from renderMode + */ + private _convertBillboardConfig(config: IQuarksParticleEmitterConfig, result: IParticleSystemConfig): void { + const billboardConfig = this._convertRenderMode(config.renderMode); + result.isBillboardBased = billboardConfig.isBillboardBased; + if (billboardConfig.billboardMode !== undefined) { + result.billboardMode = billboardConfig.billboardMode; + } + } + + /** + * Helper: Convert optional IQuarksValue to optional Value + */ + private _convertOptionalValue(value: IQuarksValue | undefined): Value | undefined { + return value !== undefined ? this._convertValue(value) : undefined; + } + + /** + * Helper: Convert array of gradient keys + */ + private _convertGradientKeys(keys: IQuarksGradientKey[] | undefined): IGradientKey[] { + return keys ? keys.map((k) => this._convertGradientKey(k)) : []; + } + + /** + * Helper: Convert speed/frame value (can be Value or object with keys) + */ + private _convertSpeedOrFrameValue( + value: IQuarksValue | { keys?: IQuarksGradientKey[]; functions?: unknown[] } | undefined + ): Value | { keys?: IGradientKey[]; functions?: unknown[] } | undefined { + if (value === undefined) { + return undefined; + } + if (typeof value === "object" && value !== null && "keys" in value) { + const result: { keys?: IGradientKey[]; functions?: unknown[] } = {}; + if (value.keys) { + result.keys = this._convertGradientKeys(value.keys); + } + if ("functions" in value && value.functions) { + result.functions = value.functions; + } + return result; + } + if (typeof value === "number" || (typeof value === "object" && value !== null && "type" in value)) { + return this._convertValue(value as IQuarksValue); + } + return undefined; + } + + /** + * Helper: Create Color4 from RGBA with fallbacks + */ + private _createColor4(r: number | undefined, g: number | undefined, b: number | undefined, a: number | undefined = 1): Color4 { + return new Color4(r ?? 1, g ?? 1, b ?? 1, a ?? 1); + } + + /** + * Helper: Create Color4 from array + */ + private _createColor4FromArray(arr: [number, number, number, number] | undefined): Color4 { + return this._createColor4(arr?.[0], arr?.[1], arr?.[2], arr?.[3]); + } + + /** + * Helper: Create Color4 from RGBA object + */ + private _createColor4FromRGBA(rgba: { r: number; g: number; b: number; a?: number } | undefined): Color4 { + return rgba ? this._createColor4(rgba.r, rgba.g, rgba.b, rgba.a) : this._createColor4(1, 1, 1, 1); + } + + /** + * Helper: Convert renderMode to billboard config + */ + private _convertRenderMode(renderMode: number | undefined): { isBillboardBased: boolean; billboardMode?: number } { + const renderModeMap: Record = { + 0: { isBillboardBased: true, billboardMode: ParticleSystem.BILLBOARDMODE_ALL }, + 1: { isBillboardBased: true, billboardMode: ParticleSystem.BILLBOARDMODE_STRETCHED }, + 2: { isBillboardBased: false, billboardMode: ParticleSystem.BILLBOARDMODE_ALL }, + 3: { isBillboardBased: true, billboardMode: ParticleSystem.BILLBOARDMODE_ALL }, + 4: { isBillboardBased: true, billboardMode: ParticleSystem.BILLBOARDMODE_Y }, + 5: { isBillboardBased: true, billboardMode: ParticleSystem.BILLBOARDMODE_Y }, + }; + + if (renderMode !== undefined && renderMode in renderModeMap) { + return renderModeMap[renderMode]; + } + return { isBillboardBased: true, billboardMode: ParticleSystem.BILLBOARDMODE_ALL }; + } + + /** + * Helper: Flip Z coordinate in array (for left-handed conversion) + */ + private _flipZCoordinate(array: number[], itemSize: number = 3): number[] { + const result = Array.from(array); + for (let i = itemSize - 1; i < result.length; i += itemSize) { + result[i] = -result[i]; + } + return result; + } + + /** + * Helper: Convert attribute array + */ + private _convertAttribute(attr: { array: number[]; itemSize: number } | undefined, flipZ: boolean = false): { array: number[]; itemSize: number } | undefined { + if (!attr) { + return undefined; + } + return { + array: flipZ ? this._flipZCoordinate(attr.array, attr.itemSize) : Array.from(attr.array), + itemSize: attr.itemSize, + }; + } + + /** + * Convert IQuarks value to value + */ + private _convertValue(value: IQuarksValue): Value { + if (typeof value === "number") { + return value; + } + if (value.type === "ConstantValue") { + return { + type: "ConstantValue", + value: value.value, + }; + } + if (value.type === "IntervalValue") { + return { + type: "IntervalValue", + min: value.a ?? 0, + max: value.b ?? 0, + }; + } + if (value.type === "PiecewiseBezier") { + return { + type: "PiecewiseBezier", + functions: value.functions.map((f) => ({ + function: f.function, + start: f.start, + })), + }; + } + // Fallback: return as Value (should not happen with proper types) + return value as Value; + } + + /** + * Convert IQuarksStartSize to min/max (handles Vector3Function) + * - ConstantValue → min = max = value + * - IntervalValue → min = a, max = b + * - PiecewiseBezier → gradients array + */ + private _convertStartSizeToMinMax(startSize: IQuarksStartSize): { min: number; max: number; gradients?: Array<{ gradient: number; factor: number; factor2?: number }> } { + // Handle Vector3Function type + if (typeof startSize === "object" && startSize !== null && "type" in startSize && startSize.type === "Vector3Function") { + // For Vector3Function, use the main value or average of x, y, z + if (startSize.value !== undefined) { + return this._convertValueToMinMax(startSize.value); + } + // Fallback: use x value if available + if (startSize.x !== undefined) { + return this._convertValueToMinMax(startSize.x); + } + return { min: 1, max: 1 }; + } + // Otherwise treat as IQuarksValue + return this._convertValueToMinMax(startSize as IQuarksValue); + } + + private _convertValueToMinMax(value: IQuarksValue): { min: number; max: number; gradients?: Array<{ gradient: number; factor: number; factor2?: number }> } { + if (typeof value === "number") { + return { min: value, max: value }; + } + if (value.type === "ConstantValue") { + return { min: value.value, max: value.value }; + } + if (value.type === "IntervalValue") { + return { min: value.a ?? 0, max: value.b ?? 0 }; + } + if (value.type === "PiecewiseBezier" && value.functions) { + // Convert PiecewiseBezier to gradients + const gradients: Array<{ gradient: number; factor: number; factor2?: number }> = []; + let minVal = Infinity; + let maxVal = -Infinity; + + for (const func of value.functions) { + const startTime = func.start; + // Evaluate bezier at start and end points + const startValue = this._evaluateBezierAt(func.function, 0); + const endValue = this._evaluateBezierAt(func.function, 1); + + gradients.push({ gradient: startTime, factor: startValue }); + + // Track min/max for fallback + minVal = Math.min(minVal, startValue, endValue); + maxVal = Math.max(maxVal, startValue, endValue); + } + + // Add final point at gradient 1.0 if not present + if (gradients.length > 0 && gradients[gradients.length - 1].gradient < 1) { + const lastFunc = value.functions[value.functions.length - 1]; + const endValue = this._evaluateBezierAt(lastFunc.function, 1); + gradients.push({ gradient: 1, factor: endValue }); + } + + return { + min: minVal === Infinity ? 1 : minVal, + max: maxVal === -Infinity ? 1 : maxVal, + gradients: gradients.length > 0 ? gradients : undefined, + }; + } + return { min: 1, max: 1 }; + } + + /** + * Evaluate bezier curve at time t + * Bezier format: { p0, p1, p2, p3 } for cubic bezier + */ + private _evaluateBezierAt(bezier: { p0: number; p1: number; p2: number; p3: number }, t: number): number { + const { p0, p1, p2, p3 } = bezier; + const t2 = t * t; + const t3 = t2 * t; + const mt = 1 - t; + const mt2 = mt * mt; + const mt3 = mt2 * mt; + return mt3 * p0 + 3 * mt2 * t * p1 + 3 * mt * t2 * p2 + t3 * p3; + } + + /** + * Helper: Extract min/max from IQuarksValue + */ + private _extractMinMaxFromValue(value: IQuarksValue | undefined): { min: number; max: number } { + if (value === undefined) { + return { min: 0, max: 0 }; + } + if (typeof value === "number") { + return { min: value, max: value }; + } + if (value.type === "ConstantValue") { + return { min: value.value, max: value.value }; + } + if (value.type === "IntervalValue") { + return { min: value.a ?? 0, max: value.b ?? 0 }; + } + return { min: 0, max: 0 }; + } + + /** + * Convert IQuarks rotation to native min/max radians + * Supports: number, ConstantValue, IntervalValue, Euler, AxisAngle, RandomQuat + */ + private _convertRotationToMinMax(rotation: IQuarksRotation): { min: number; max: number } { + if (typeof rotation === "number") { + return { min: rotation, max: rotation }; + } + + if (typeof rotation === "object" && rotation !== null && "type" in rotation) { + const rotationType = rotation.type; + + if (rotationType === "ConstantValue") { + return this._extractMinMaxFromValue(rotation as IQuarksValue); + } + + if (rotationType === "IntervalValue") { + return this._extractMinMaxFromValue(rotation as IQuarksValue); + } + + // Handle Euler type - for 2D/billboard particles we use angleZ, fallback to angleX + if (rotationType === "Euler") { + const euler = rotation as { type: string; angleZ?: IQuarksValue; angleX?: IQuarksValue }; + if (euler.angleZ !== undefined) { + return this._extractMinMaxFromValue(euler.angleZ); + } + if (euler.angleX !== undefined) { + return this._extractMinMaxFromValue(euler.angleX); + } + } + } + + return { min: 0, max: 0 }; + } + + /** + * Convert IQuarksStartColor to native Babylon.js Color4 (color1, color2) + */ + private _convertStartColorToColor4(startColor: IQuarksStartColor): { color1: Color4; color2: Color4 } { + // Handle Gradient type + if (typeof startColor === "object" && startColor !== null && "type" in startColor) { + if (startColor.type === "Gradient") { + // For Gradient, extract color from CLinearFunction if available + const gradientColor = startColor as IQuarksGradientColor; + if (gradientColor.color?.keys && gradientColor.color.keys.length > 0) { + const firstKey = gradientColor.color.keys[0]; + const lastKey = gradientColor.color.keys[gradientColor.color.keys.length - 1]; + const color1 = this._extractColorFromGradientKey(firstKey); + const color2 = this._extractColorFromGradientKey(lastKey); + return { color1, color2 }; + } + } + if (startColor.type === "ColorRange") { + // For ColorRange, use a and b colors + const colorRange = startColor as { type: string; a?: { r: number; g: number; b: number; a?: number }; b?: { r: number; g: number; b: number; a?: number } }; + const color1 = this._createColor4FromRGBA(colorRange.a); + const color2 = this._createColor4FromRGBA(colorRange.b); + return { color1, color2 }; + } + } + // Otherwise treat as IQuarksColor + return this._convertColorToColor4(startColor as IQuarksColor); + } + + /** + * Extract Color4 from gradient key value + */ + private _extractColorFromGradientKey(key: IQuarksGradientKey): Color4 { + if (Array.isArray(key.value)) { + return this._createColor4FromArray(key.value as [number, number, number, number]); + } + if (typeof key.value === "object" && key.value !== null && "r" in key.value) { + return this._createColor4FromRGBA(key.value as { r: number; g: number; b: number; a?: number }); + } + return this._createColor4(1, 1, 1, 1); + } + + /** + * Convert IQuarks color to native Babylon.js Color4 (color1, color2) + */ + private _convertColorToColor4(color: IQuarksColor): { color1: Color4; color2: Color4 } { + if (Array.isArray(color)) { + const c = this._createColor4FromArray(color as [number, number, number, number]); + return { color1: c, color2: c }; + } + + if (typeof color === "object" && color !== null && "type" in color) { + if (color.type === "ConstantColor") { + const constColor = color as IQuarksConstantColorColor; + if (constColor.value && Array.isArray(constColor.value)) { + const c = this._createColor4FromArray(constColor.value); + return { color1: c, color2: c }; + } + if (constColor.color) { + const c = this._createColor4FromRGBA(constColor.color); + return { color1: c, color2: c }; + } + } + // Handle RandomColor (interpolation between two colors) + const randomColor = color as { type: string; a?: [number, number, number, number]; b?: [number, number, number, number] }; + if (randomColor.type === "RandomColor" && randomColor.a && randomColor.b) { + const color1 = this._createColor4FromArray(randomColor.a); + const color2 = this._createColor4FromArray(randomColor.b); + return { color1, color2 }; + } + } + + const white = this._createColor4(1, 1, 1, 1); + return { color1: white, color2: white }; + } + + /** + * Convert IQuarks gradient key to gradient key + */ + private _convertGradientKey(key: IQuarksGradientKey): IGradientKey { + return { + time: key.time, + value: key.value, + pos: key.pos, + }; + } + + /** + * Convert IQuarks shape to shape + */ + private _convertShape(shape: IQuarksShape): IShape { + const result: IShape = { + type: shape.type, + radius: shape.radius, + arc: shape.arc, + thickness: shape.thickness, + angle: shape.angle, + mode: shape.mode, + spread: shape.spread, + size: shape.size, + height: shape.height, + }; + if (shape.speed !== undefined) { + result.speed = this._convertValue(shape.speed); + } + return result; + } + + /** + * Convert IQuarks behavior to behavior + */ + private _convertBehavior(behavior: IQuarksBehavior): Behavior { + switch (behavior.type) { + case "ColorOverLife": + return this._convertColorOverLifeBehavior(behavior as IQuarksColorOverLifeBehavior); + case "SizeOverLife": + return this._convertSizeOverLifeBehavior(behavior as IQuarksSizeOverLifeBehavior); + case "RotationOverLife": + case "Rotation3DOverLife": + return this._convertRotationOverLifeBehavior(behavior as IQuarksRotationOverLifeBehavior); + case "ForceOverLife": + case "ApplyForce": + return this._convertForceOverLifeBehavior(behavior as IQuarksForceOverLifeBehavior); + case "GravityForce": + return this._convertGravityForceBehavior(behavior as IQuarksGravityForceBehavior); + case "SpeedOverLife": + return this._convertSpeedOverLifeBehavior(behavior as IQuarksSpeedOverLifeBehavior); + case "FrameOverLife": + return this._convertFrameOverLifeBehavior(behavior as IQuarksFrameOverLifeBehavior); + case "LimitSpeedOverLife": + return this._convertLimitSpeedOverLifeBehavior(behavior as IQuarksLimitSpeedOverLifeBehavior); + case "ColorBySpeed": + return this._convertColorBySpeedBehavior(behavior as IQuarksColorBySpeedBehavior); + case "SizeBySpeed": + return this._convertSizeBySpeedBehavior(behavior as IQuarksSizeBySpeedBehavior); + case "RotationBySpeed": + return this._convertRotationBySpeedBehavior(behavior as IQuarksRotationBySpeedBehavior); + case "OrbitOverLife": + return this._convertOrbitOverLifeBehavior(behavior as IQuarksOrbitOverLifeBehavior); + default: + // Fallback for unknown behaviors - copy as-is + return behavior as Behavior; + } + } + + /** + * Extract color from ConstantColor behavior + */ + private _extractConstantColor(constantColor: IQuarksConstantColorColor): { r: number; g: number; b: number; a: number } { + if (constantColor.color) { + return { + r: constantColor.color.r, + g: constantColor.color.g, + b: constantColor.color.b, + a: constantColor.color.a ?? 1, + }; + } + if (constantColor.value && Array.isArray(constantColor.value) && constantColor.value.length >= 4) { + return { + r: constantColor.value[0], + g: constantColor.value[1], + b: constantColor.value[2], + a: constantColor.value[3], + }; + } + return QuarksConverter.DEFAULT_COLOR; + } + + /** + * Convert ColorOverLife behavior + */ + private _convertColorOverLifeBehavior(behavior: IQuarksColorOverLifeBehavior): Behavior { + if (!behavior.color) { + return { + type: "ColorOverLife", + color: { + colorFunctionType: "ConstantColor", + data: {}, + }, + }; + } + + const colorType = behavior.color.type; + let colorFunction: IColorFunction; + + if (colorType === "Gradient") { + const gradientColor = behavior.color as IQuarksGradientColor; + colorFunction = { + colorFunctionType: "Gradient", + data: { + colorKeys: this._convertGradientKeys(gradientColor.color?.keys), + alphaKeys: this._convertGradientKeys(gradientColor.alpha?.keys), + }, + }; + } else if (colorType === "ConstantColor") { + const constantColor = behavior.color as IQuarksConstantColorColor; + const color = this._extractConstantColor(constantColor); + colorFunction = { + colorFunctionType: "ConstantColor", + data: { + color: { + r: color.r ?? 1, + g: color.g ?? 1, + b: color.b ?? 1, + a: color.a !== undefined ? color.a : 1, + }, + }, + }; + } else if (colorType === "RandomColorBetweenGradient") { + const randomColor = behavior.color as IQuarksRandomColorBetweenGradient; + colorFunction = { + colorFunctionType: "RandomColorBetweenGradient", + data: { + gradient1: { + colorKeys: this._convertGradientKeys(randomColor.gradient1?.color?.keys), + alphaKeys: this._convertGradientKeys(randomColor.gradient1?.alpha?.keys), + }, + gradient2: { + colorKeys: this._convertGradientKeys(randomColor.gradient2?.color?.keys), + alphaKeys: this._convertGradientKeys(randomColor.gradient2?.alpha?.keys), + }, + }, + }; + } else { + // Fallback: try to detect format from keys + const colorData = behavior.color as { color?: { keys?: IQuarksGradientKey[] }; alpha?: { keys?: IQuarksGradientKey[] }; keys?: IQuarksGradientKey[] }; + const hasColorKeys = colorData.color?.keys && colorData.color.keys.length > 0; + const hasAlphaKeys = colorData.alpha?.keys && colorData.alpha.keys.length > 0; + const hasKeys = colorData.keys && colorData.keys.length > 0; + + if (hasColorKeys || hasAlphaKeys || hasKeys) { + const colorKeys = hasColorKeys ? this._convertGradientKeys(colorData.color?.keys) : hasKeys ? this._convertGradientKeys(colorData.keys) : []; + const alphaKeys = hasAlphaKeys ? this._convertGradientKeys(colorData.alpha?.keys) : []; + colorFunction = { + colorFunctionType: "Gradient", + data: { + colorKeys, + alphaKeys, + }, + }; + } else { + // Default to ConstantColor + colorFunction = { + colorFunctionType: "ConstantColor", + data: {}, + }; + } + } + + return { + type: "ColorOverLife", + color: colorFunction, + }; + } + + /** + * Convert SizeOverLife behavior + */ + private _convertSizeOverLifeBehavior(behavior: IQuarksSizeOverLifeBehavior): Behavior { + if (!behavior.size) { + return { type: "SizeOverLife" }; + } + return { + type: "SizeOverLife", + size: { + ...(behavior.size.keys && { keys: this._convertGradientKeys(behavior.size.keys) }), + ...(behavior.size.functions && { functions: behavior.size.functions }), + }, + }; + } + + /** + * Convert RotationOverLife behavior + */ + private _convertRotationOverLifeBehavior(behavior: IQuarksRotationOverLifeBehavior): Behavior { + return { + type: behavior.type, + angularVelocity: this._convertOptionalValue(behavior.angularVelocity), + }; + } + + /** + * Convert ForceOverLife behavior + */ + private _convertForceOverLifeBehavior(behavior: IQuarksForceOverLifeBehavior): Behavior { + const result: IForceOverLifeBehavior = { type: behavior.type }; + if (behavior.force) { + result.force = { + x: this._convertOptionalValue(behavior.force.x), + y: this._convertOptionalValue(behavior.force.y), + z: this._convertOptionalValue(behavior.force.z), + }; + } + result.x = this._convertOptionalValue(behavior.x); + result.y = this._convertOptionalValue(behavior.y); + result.z = this._convertOptionalValue(behavior.z); + return result; + } + + /** + * Convert GravityForce behavior + */ + private _convertGravityForceBehavior(behavior: IQuarksGravityForceBehavior): Behavior { + return { + type: "GravityForce", + gravity: this._convertOptionalValue(behavior.gravity), + } as Behavior; + } + + /** + * Convert SpeedOverLife behavior + */ + private _convertSpeedOverLifeBehavior(behavior: IQuarksSpeedOverLifeBehavior): Behavior { + const speed = this._convertSpeedOrFrameValue(behavior.speed); + return { type: "SpeedOverLife", ...(speed !== undefined && { speed }) } as ISpeedOverLifeBehavior; + } + + /** + * Convert FrameOverLife behavior + */ + private _convertFrameOverLifeBehavior(behavior: IQuarksFrameOverLifeBehavior): Behavior { + const frame = this._convertSpeedOrFrameValue(behavior.frame); + return { type: "FrameOverLife", ...(frame !== undefined && { frame }) } as Behavior; + } + + /** + * Convert LimitSpeedOverLife behavior + */ + private _convertLimitSpeedOverLifeBehavior(behavior: IQuarksLimitSpeedOverLifeBehavior): Behavior { + const speed = this._convertSpeedOrFrameValue(behavior.speed); + return { + type: "LimitSpeedOverLife", + maxSpeed: this._convertOptionalValue(behavior.maxSpeed), + ...(speed !== undefined && { speed }), + dampen: this._convertOptionalValue(behavior.dampen), + } as ILimitSpeedOverLifeBehavior; + } + + /** + * Convert ColorBySpeed behavior + */ + private _convertColorBySpeedBehavior(behavior: IQuarksColorBySpeedBehavior): Behavior { + const colorFunction: IColorFunction = behavior.color?.keys + ? { + colorFunctionType: "Gradient", + data: { + colorKeys: this._convertGradientKeys(behavior.color.keys), + alphaKeys: [], + }, + } + : { + colorFunctionType: "ConstantColor", + data: {}, + }; + + return { + type: "ColorBySpeed", + color: colorFunction, + minSpeed: this._convertOptionalValue(behavior.minSpeed), + maxSpeed: this._convertOptionalValue(behavior.maxSpeed), + }; + } + + /** + * Convert SizeBySpeed behavior + */ + private _convertSizeBySpeedBehavior(behavior: IQuarksSizeBySpeedBehavior): Behavior { + return { + type: "SizeBySpeed", + minSpeed: this._convertOptionalValue(behavior.minSpeed), + maxSpeed: this._convertOptionalValue(behavior.maxSpeed), + ...(behavior.size?.keys && { size: { keys: this._convertGradientKeys(behavior.size.keys) } }), + } as ISizeBySpeedBehavior; + } + + /** + * Convert RotationBySpeed behavior + */ + private _convertRotationBySpeedBehavior(behavior: IQuarksRotationBySpeedBehavior): Behavior { + return { + type: "RotationBySpeed", + angularVelocity: this._convertOptionalValue(behavior.angularVelocity), + minSpeed: this._convertOptionalValue(behavior.minSpeed), + maxSpeed: this._convertOptionalValue(behavior.maxSpeed), + } as Behavior; + } + + /** + * Convert OrbitOverLife behavior + */ + private _convertOrbitOverLifeBehavior(behavior: IQuarksOrbitOverLifeBehavior): Behavior { + return { + type: "OrbitOverLife", + center: behavior.center, + radius: this._convertOptionalValue(behavior.radius), + speed: this._convertOptionalValue(behavior.speed), + } as Behavior; + } + + /** + * Convert IQuarks materials to materials + */ + private _convertMaterial(material: IQuarksMaterial): IMaterial { + const babylonMaterial: IMaterial = { + uuid: material.uuid, + type: material.type, + transparent: material.transparent, + depthWrite: material.depthWrite, + side: material.side, + map: material.map, + }; + + // Convert color from hex to Color3 + if (material.color !== undefined) { + const colorHex = typeof material.color === "number" ? material.color : parseInt(String(material.color).replace("#", ""), 16) || QuarksConverter.DEFAULT_COLOR_HEX; + const r = ((colorHex >> 16) & 0xff) / 255; + const g = ((colorHex >> 8) & 0xff) / 255; + const b = (colorHex & 0xff) / 255; + babylonMaterial.color = new Color3(r, g, b); + } + + // Convert blending mode (Three.js → Babylon.js) + if (material.blending !== undefined) { + const blendModeMap: Record = { + 0: 0, // NoBlending → ALPHA_DISABLE + 1: 1, // NormalBlending → ALPHA_COMBINE + 2: 2, // AdditiveBlending → ALPHA_ADD + }; + babylonMaterial.blending = blendModeMap[material.blending] ?? material.blending; + } + + return babylonMaterial; + } + + /** + * Convert IQuarks textures to textures + */ + private _convertTexture(texture: IQuarksTexture): ITexture { + const babylonTexture: ITexture = { + uuid: texture.uuid, + image: texture.image, + generateMipmaps: texture.generateMipmaps, + flipY: texture.flipY, + }; + + // Convert wrap mode (Three.js → Babylon.js) + if (texture.wrap && Array.isArray(texture.wrap)) { + const wrapModeMap: Record = { + [QuarksConverter.THREE_REPEAT_WRAPPING]: BabylonTexture.WRAP_ADDRESSMODE, + [QuarksConverter.THREE_CLAMP_TO_EDGE_WRAPPING]: BabylonTexture.CLAMP_ADDRESSMODE, + [QuarksConverter.THREE_MIRRORED_REPEAT_WRAPPING]: BabylonTexture.MIRROR_ADDRESSMODE, + }; + babylonTexture.wrapU = wrapModeMap[texture.wrap[0]] ?? BabylonTexture.WRAP_ADDRESSMODE; + babylonTexture.wrapV = wrapModeMap[texture.wrap[1]] ?? BabylonTexture.WRAP_ADDRESSMODE; + } + + // Convert repeat to scale + if (texture.repeat && Array.isArray(texture.repeat)) { + babylonTexture.uScale = texture.repeat[0] || 1; + babylonTexture.vScale = texture.repeat[1] || 1; + } + + // Convert offset + if (texture.offset && Array.isArray(texture.offset)) { + babylonTexture.uOffset = texture.offset[0] || 0; + babylonTexture.vOffset = texture.offset[1] || 0; + } + + // Convert rotation + if (texture.rotation !== undefined) { + babylonTexture.uAng = texture.rotation; + } + + // Convert channel + if (typeof texture.channel === "number") { + babylonTexture.coordinatesIndex = texture.channel; + } + + // Convert sampling mode (Three.js filters → Babylon.js sampling mode) + if (texture.minFilter !== undefined) { + if (texture.minFilter === QuarksConverter.THREE_LINEAR_MIPMAP_NEAREST_FILTER || texture.minFilter === QuarksConverter.THREE_NEAREST_MIPMAP_LINEAR_FILTER) { + babylonTexture.samplingMode = BabylonTexture.TRILINEAR_SAMPLINGMODE; + } else if (texture.minFilter === QuarksConverter.THREE_NEAREST_MIPMAP_NEAREST_FILTER || texture.minFilter === QuarksConverter.THREE_LINEAR_FILTER) { + babylonTexture.samplingMode = BabylonTexture.BILINEAR_SAMPLINGMODE; + } else { + babylonTexture.samplingMode = BabylonTexture.NEAREST_SAMPLINGMODE; + } + } else if (texture.magFilter !== undefined) { + babylonTexture.samplingMode = texture.magFilter === QuarksConverter.THREE_LINEAR_FILTER ? BabylonTexture.BILINEAR_SAMPLINGMODE : BabylonTexture.NEAREST_SAMPLINGMODE; + } else { + babylonTexture.samplingMode = BabylonTexture.TRILINEAR_SAMPLINGMODE; + } + + return babylonTexture; + } + + /** + * Convert IQuarks images to images (normalize URLs) + */ + private _convertImage(image: IQuarksImage): IImage { + return { + uuid: image.uuid, + url: image.url || "", + }; + } + + /** + * Convert IQuarks geometries to geometries (convert to left-handed) + */ + private _convertGeometry(geometry: IQuarksGeometry): IGeometry { + if (geometry.type === "PlaneGeometry") { + // PlaneGeometry - simple properties + const planeGeometry = geometry as IQuarksGeometry & { width?: number; height?: number }; + return { + uuid: geometry.uuid, + type: "PlaneGeometry" as const, + width: planeGeometry.width ?? 1, + height: planeGeometry.height ?? 1, + }; + } else if (geometry.type === "BufferGeometry") { + // BufferGeometry - convert attributes to left-handed + const result: IGeometry = { + uuid: geometry.uuid, + type: "BufferGeometry", + }; + + if (geometry.data?.attributes) { + const attributes: IGeometryData["attributes"] = {}; + const sourceAttrs = geometry.data.attributes; + + // Convert position and normal (right-hand → left-hand: flip Z) + const positionAttr = this._convertAttribute(sourceAttrs.position, true); + if (positionAttr) { + attributes.position = positionAttr; + } + + const normalAttr = this._convertAttribute(sourceAttrs.normal, true); + if (normalAttr) { + attributes.normal = normalAttr; + } + + // UV and color - no conversion needed + const uvAttr = this._convertAttribute(sourceAttrs.uv, false); + if (uvAttr) { + attributes.uv = uvAttr; + } + + const colorAttr = this._convertAttribute(sourceAttrs.color, false); + if (colorAttr) { + attributes.color = colorAttr; + } + + result.data = { + attributes, + }; + + // Convert indices (reverse winding order for left-handed) + if (geometry.data.index) { + const indices = Array.from(geometry.data.index.array); + // Reverse winding: swap every 2nd and 3rd index in each triangle + for (let i = 0; i < indices.length; i += 3) { + const temp = indices[i + 1]; + indices[i + 1] = indices[i + 2]; + indices[i + 2] = temp; + } + result.data.index = { + array: indices, + }; + } + } + + return result; + } + + // Unknown geometry type - return as-is + return { + uuid: geometry.uuid, + type: geometry.type as "PlaneGeometry" | "BufferGeometry", + }; + } +} diff --git a/tools/src/effect/types/quarksTypes.ts b/editor/src/editor/windows/effect-editor/converters/quarksTypes.ts similarity index 58% rename from tools/src/effect/types/quarksTypes.ts rename to editor/src/editor/windows/effect-editor/converters/quarksTypes.ts index 01430d6a8..0ebc564f1 100644 --- a/tools/src/effect/types/quarksTypes.ts +++ b/editor/src/editor/windows/effect-editor/converters/quarksTypes.ts @@ -1,10 +1,35 @@ /** - * Type definitions for Quarks/Three.js JSON structures - * These represent the incoming format from Quarks/Three.js + * Type definitions for Quarks JSON structures + * These represent the incoming format from Quarks */ /** - * Quarks/Three.js value types + * Common Bezier function structure used across multiple types + */ +export interface IQuarksBezierFunction { + p0: number; + p1: number; + p2: number; + p3: number; +} + +export interface IQuarksBezierFunctionSegment { + function: IQuarksBezierFunction; + start: number; +} + +/** + * Common RGBA color structure + */ +export interface IQuarksRGBA { + r: number; + g: number; + b: number; + a?: number; +} + +/** + * Quarks value types */ export interface IQuarksConstantValue { type: "ConstantValue"; @@ -19,58 +44,50 @@ export interface IQuarksIntervalValue { export interface IQuarksPiecewiseBezier { type: "PiecewiseBezier"; - functions: Array<{ - function: { - p0: number; - p1: number; - p2: number; - p3: number; - }; - start: number; - }>; + functions: IQuarksBezierFunctionSegment[]; } export type IQuarksValue = IQuarksConstantValue | IQuarksIntervalValue | IQuarksPiecewiseBezier | number; /** - * Quarks/Three.js color types + * Quarks color types */ export interface IQuarksConstantColor { type: "ConstantColor"; - color?: { - r: number; - g: number; - b: number; - a?: number; - }; + color?: IQuarksRGBA; value?: [number, number, number, number]; // RGBA array alternative } export type IQuarksColor = IQuarksConstantColor | [number, number, number, number] | string; /** - * Quarks/Three.js rotation types + * Quarks rotation types */ export interface IQuarksEulerRotation { type: "Euler"; angleX?: IQuarksValue; angleY?: IQuarksValue; angleZ?: IQuarksValue; + eulerOrder?: string; + functions?: IQuarksBezierFunctionSegment[]; + a?: number; + b?: number; + value?: number; } export type IQuarksRotation = IQuarksEulerRotation | IQuarksValue; /** - * Quarks/Three.js gradient key + * Quarks gradient key */ export interface IQuarksGradientKey { time?: number; - value: number | [number, number, number, number] | { r: number; g: number; b: number; a?: number }; + value: number | [number, number, number, number] | IQuarksRGBA; pos?: number; } /** - * Quarks/Three.js shape configuration + * Quarks shape configuration */ export interface IQuarksShape { type: string; @@ -86,15 +103,18 @@ export interface IQuarksShape { } /** - * Quarks/Three.js emission burst + * Quarks emission burst */ export interface IQuarksEmissionBurst { time: IQuarksValue; count: IQuarksValue; + cycle?: number; + interval?: number; + probability?: number; } /** - * Quarks/Three.js behavior types + * Quarks behavior types */ export interface IQuarksCLinearFunction { type: "CLinearFunction"; @@ -110,12 +130,7 @@ export interface IQuarksGradientColor { export interface IQuarksConstantColorColor { type: "ConstantColor"; - color?: { - r: number; - g: number; - b: number; - a?: number; - }; + color?: IQuarksRGBA; value?: [number, number, number, number]; } @@ -136,13 +151,8 @@ export interface IQuarksSizeOverLifeBehavior { type: "SizeOverLife"; size?: { keys?: IQuarksGradientKey[]; - functions?: Array<{ - start: number; - function: { - p0?: number; - p3?: number; - }; - }>; + functions?: IQuarksBezierFunctionSegment[]; + type?: string; }; } @@ -200,29 +210,31 @@ export interface IQuarksLimitSpeedOverLifeBehavior { dampen?: IQuarksValue; } -export interface IQuarksColorBySpeedBehavior { +/** + * Base interface for speed-based behaviors + */ +interface IQuarksSpeedBasedBehavior { + minSpeed?: IQuarksValue; + maxSpeed?: IQuarksValue; +} + +export interface IQuarksColorBySpeedBehavior extends IQuarksSpeedBasedBehavior { type: "ColorBySpeed"; color?: { keys: IQuarksGradientKey[]; }; - minSpeed?: IQuarksValue; - maxSpeed?: IQuarksValue; } -export interface IQuarksSizeBySpeedBehavior { +export interface IQuarksSizeBySpeedBehavior extends IQuarksSpeedBasedBehavior { type: "SizeBySpeed"; size?: { keys: IQuarksGradientKey[]; }; - minSpeed?: IQuarksValue; - maxSpeed?: IQuarksValue; } -export interface IQuarksRotationBySpeedBehavior { +export interface IQuarksRotationBySpeedBehavior extends IQuarksSpeedBasedBehavior { type: "RotationBySpeed"; angularVelocity?: IQuarksValue; - minSpeed?: IQuarksValue; - maxSpeed?: IQuarksValue; } export interface IQuarksOrbitOverLifeBehavior { @@ -236,6 +248,17 @@ export interface IQuarksOrbitOverLifeBehavior { speed?: IQuarksValue; } +export interface IQuarksNoiseBehavior { + type: "Noise"; + frequency?: IQuarksValue; + power?: IQuarksValue; + positionAmount?: IQuarksValue; + rotationAmount?: IQuarksValue; + x?: IQuarksValue; + y?: IQuarksValue; + z?: IQuarksValue; +} + export type IQuarksBehavior = | IQuarksColorOverLifeBehavior | IQuarksSizeOverLifeBehavior @@ -249,10 +272,46 @@ export type IQuarksBehavior = | IQuarksSizeBySpeedBehavior | IQuarksRotationBySpeedBehavior | IQuarksOrbitOverLifeBehavior + | IQuarksNoiseBehavior | { type: string; [key: string]: unknown }; // Fallback for unknown behaviors /** - * Quarks/Three.js particle emitter configuration + * Quarks start size with Vector3Function support + */ +export interface IQuarksVector3FunctionSize { + type: "Vector3Function"; + x?: IQuarksValue; + y?: IQuarksValue; + z?: IQuarksValue; + functions?: IQuarksBezierFunctionSegment[]; + a?: number; + b?: number; + value?: number; +} + +export type IQuarksStartSize = IQuarksValue | IQuarksVector3FunctionSize; + +/** + * Quarks start color with Gradient and ColorRange support + */ +export interface IQuarksGradientStartColor { + type: "Gradient"; + alpha?: IQuarksCLinearFunction; + color?: IQuarksCLinearFunction; +} + +export interface IQuarksColorRangeStartColor { + type: "ColorRange"; + a?: IQuarksRGBA; + b?: IQuarksRGBA; + color?: IQuarksCLinearFunction; + alpha?: IQuarksCLinearFunction; +} + +export type IQuarksStartColor = IQuarksColor | IQuarksGradientStartColor | IQuarksColorRangeStartColor; + +/** + * Quarks particle emitter configuration */ export interface IQuarksParticleEmitterConfig { version?: string; @@ -264,8 +323,8 @@ export interface IQuarksParticleEmitterConfig { startLife?: IQuarksValue; startSpeed?: IQuarksValue; startRotation?: IQuarksRotation; - startSize?: IQuarksValue; - startColor?: IQuarksColor; + startSize?: IQuarksStartSize; + startColor?: IQuarksStartColor; emissionOverTime?: IQuarksValue; emissionOverDistance?: IQuarksValue; emissionBursts?: IQuarksEmissionBurst[]; @@ -288,81 +347,95 @@ export interface IQuarksParticleEmitterConfig { } /** - * Quarks/Three.js object types + * Base interface for Quarks objects with common transform properties */ -export interface IQuarksGroup { +interface IQuarksObjectBase { uuid: string; - type: "Group"; name: string; - matrix?: number[]; + matrix: number[]; + layers: number; + up: number[]; + children: IQuarksObject[]; position?: number[]; rotation?: number[]; scale?: number[]; - children?: IQuarksObject[]; } -export interface IQuarksParticleEmitter { - uuid: string; +/** + * Quarks object types + */ +export interface IQuarksGroup extends IQuarksObjectBase { + type: "Group"; +} + +export interface IQuarksParticleEmitter extends IQuarksObjectBase { type: "ParticleEmitter"; - name: string; - matrix?: number[]; - position?: number[]; - rotation?: number[]; - scale?: number[]; ps: IQuarksParticleEmitterConfig; - children?: IQuarksObject[]; } export type IQuarksObject = IQuarksGroup | IQuarksParticleEmitter; /** - * Quarks/Three.js material + * Base interface for Quarks resources (materials, textures, images, geometries) */ -export interface IQuarksMaterial { +interface IQuarksResource { uuid: string; - type: string; name?: string; +} + +/** + * Quarks material + */ +export interface IQuarksMaterial extends IQuarksResource { + type: string; color?: number; map?: string; blending?: number; + blendColor?: number; side?: number; transparent?: boolean; depthWrite?: boolean; + envMapRotation?: number[]; + reflectivity?: number; + refractionRatio?: number; } /** - * Quarks/Three.js texture + * Quarks texture */ -export interface IQuarksTexture { - uuid: string; - name?: string; +export interface IQuarksTexture extends IQuarksResource { image?: string; mapping?: number; wrap?: number[]; repeat?: number[]; offset?: number[]; + center?: number[]; rotation?: number; minFilter?: number; magFilter?: number; flipY?: boolean; generateMipmaps?: boolean; format?: number; + internalFormat?: number | null; + type?: number; channel?: number; + anisotropy?: number; + colorSpace?: string; + premultiplyAlpha?: boolean; + unpackAlignment?: number; } /** - * Quarks/Three.js image + * Quarks image */ -export interface IQuarksImage { - uuid: string; +export interface IQuarksImage extends IQuarksResource { url?: string; } /** - * Quarks/Three.js geometry + * Quarks geometry */ -export interface IQuarksGeometry { - uuid: string; +export interface IQuarksGeometry extends IQuarksResource { type: string; data?: { attributes?: Record< @@ -371,6 +444,7 @@ export interface IQuarksGeometry { itemSize: number; type: string; array: number[]; + normalized?: boolean; } >; index?: { @@ -378,20 +452,30 @@ export interface IQuarksGeometry { array: number[]; }; }; + // Geometry-specific properties (for different geometry types) + height?: number; + heightSegments?: number; + width?: number; + widthSegments?: number; + radius?: number; + phiLength?: number; + phiStart?: number; + thetaLength?: number; + thetaStart?: number; } /** - * Quarks/Three.js JSON structure + * Quarks JSON structure */ export interface IQuarksJSON { - metadata?: { - version?: number; - type?: string; - generator?: string; + metadata: { + version: number; + type: string; + generator: string; }; - geometries?: IQuarksGeometry[]; - materials?: IQuarksMaterial[]; - textures?: IQuarksTexture[]; - images?: IQuarksImage[]; - object?: IQuarksObject; + geometries: IQuarksGeometry[]; + materials: IQuarksMaterial[]; + textures: IQuarksTexture[]; + images: IQuarksImage[]; + object: IQuarksObject; } diff --git a/editor/src/editor/windows/effect-editor/converters/unityConverter.ts b/editor/src/editor/windows/effect-editor/converters/unityConverter.ts new file mode 100644 index 000000000..1b6b29614 --- /dev/null +++ b/editor/src/editor/windows/effect-editor/converters/unityConverter.ts @@ -0,0 +1,1175 @@ +/** + * Unity Prefab → Babylon.js Effect Converter + * + * Converts Unity particle system prefabs directly to our Babylon.js effect format, + * bypassing the Quarks JSON intermediate step. + * + * Based on extracted Unity → Quarks converter logic, but outputs IData format. + */ + +import { Vector3, Color4, Quaternion, Color3, Scene, Mesh, VertexData, SceneLoader, Tools } from "babylonjs"; +import type { + IData, + IEmitter, + IGroup, + IParticleSystemConfig, + Behavior, + Value, + IConstantColor, + IGradientColor, + IRandomColor, + IRandomColorBetweenGradient, +} from "babylonjs-editor-tools/src/effect/types"; +import type { IMaterial, ITexture, IImage, IGeometry } from "babylonjs-editor-tools/src/effect/types/resources"; +import * as yaml from "js-yaml"; + +// Note: Babylon.js loaders (FBXFileLoader, OBJFileLoader) are imported in toolbar.tsx +// via "babylonjs-loaders" to register them with SceneLoader globally. +// This allows SceneLoader.ImportMeshAsync to work with FBX/OBJ files. + +/** + * Helper to get component by type from GameObject + */ +function getComponentByType(gameObject: any, componentType: string, components: Map): any | null { + if (!gameObject.m_Component) return null; + + for (const compRef of gameObject.m_Component) { + const compId = compRef.component?.fileID || compRef.component; + const comp = components.get(compId); + if (comp && comp[componentType]) { + return comp[componentType]; + } + } + + return null; +} + +/** + * Find root GameObject in hierarchy (Transform with no parent) + * Based on original Unity converter logic + */ +function findRootGameObject(components: Map): string | null { + console.log(`[findRootGameObject] Searching in ${components.size} components`); + + // Look for Transform component with m_Father.fileID === "0" + let transformCount = 0; + let gameObjectCount = 0; + + for (const [_id, comp] of components) { + if (comp.Transform) transformCount++; + if (comp.GameObject) gameObjectCount++; + + // Check if this component is a Transform + if (comp.Transform) { + // Check if Transform has m_Father with fileID === "0" (no parent = root) + if (comp.Transform.m_Father !== undefined && comp.Transform.m_Father !== null) { + const fatherFileID = typeof comp.Transform.m_Father === "object" ? comp.Transform.m_Father.fileID : comp.Transform.m_Father; + const fatherFileIDStr = String(fatherFileID); + + if (fatherFileIDStr === "0") { + // Found root Transform, get the GameObject it belongs to + const gameObjectRef = comp.Transform.m_GameObject; + if (gameObjectRef) { + const gameObjectFileID = typeof gameObjectRef === "object" ? gameObjectRef.fileID : gameObjectRef; + const gameObjectFileIDStr = String(gameObjectFileID); + + // IMPORTANT: Return the component ID (key in Map) that contains this GameObject + // The gameObjectFileIDStr is the fileID reference, but we need to find the component with that ID + // Components are stored with their YAML anchor ID as the key (e.g., "195608") + const gameObjectComponent = components.get(gameObjectFileIDStr); + if (gameObjectComponent && gameObjectComponent.GameObject) { + console.log( + `[findRootGameObject] Found root Transform with m_Father === "0", GameObject fileID: ${gameObjectFileIDStr}, component ID: ${gameObjectFileIDStr}` + ); + return gameObjectFileIDStr; // This is the component ID/key + } else { + console.warn(`[findRootGameObject] GameObject ${gameObjectFileIDStr} not found in components map`); + } + } + } + } else if (comp.Transform.m_GameObject) { + // If no m_Father, it might be root (check if it's the only Transform) + const gameObjectRef = comp.Transform.m_GameObject; + const gameObjectFileID = typeof gameObjectRef === "object" ? gameObjectRef.fileID : gameObjectRef; + // Try this as root if we don't find one with m_Father === "0" + const candidate = String(gameObjectFileID); + // But first check if there's a Transform with explicit m_Father === "0" + let hasExplicitRoot = false; + for (const [_id2, comp2] of components) { + if (comp2.Transform && comp2.Transform.m_Father !== undefined && comp2.Transform.m_Father !== null) { + const fatherFileID2 = typeof comp2.Transform.m_Father === "object" ? comp2.Transform.m_Father.fileID : comp2.Transform.m_Father; + if (String(fatherFileID2) === "0") { + hasExplicitRoot = true; + break; + } + } + } + if (!hasExplicitRoot) { + console.log(`[findRootGameObject] Using Transform without m_Father as root, GameObject fileID: ${candidate}`); + return candidate; + } + } + } + } + + console.log(`[findRootGameObject] No Transform with m_Father === "0" found. Transform count: ${transformCount}, GameObject count: ${gameObjectCount}`); + + // Fallback: find first GameObject if no root Transform found + for (const [_id, comp] of components) { + if (comp.GameObject) { + console.log(`[findRootGameObject] Fallback: Using first GameObject found, component ID: ${_id}`); + return _id; // Use component ID as GameObject ID + } + } + + console.warn(`[findRootGameObject] No GameObject found at all! Component keys:`, Array.from(components.keys()).slice(0, 10)); + console.log(`[findRootGameObject] Sample component structure:`, Array.from(components.entries())[0]); + return null; +} + +/** + * Unity to Babylon.js coordinate system conversion + * Unity: Y-up, left-handed → Babylon.js: Y-up, left-handed (same!) + * But Quarks was Three.js (right-handed), so no conversion needed for us + */ +function convertVector3(unityVec: { x: string; y: string; z: string }): [number, number, number] { + return [parseFloat(unityVec.x), parseFloat(unityVec.y), parseFloat(unityVec.z)]; +} + +/** + * Convert Unity Color to our Color4 + */ +function convertColor(unityColor: { r: string; g: string; b: string; a: string }): Color4 { + return new Color4(parseFloat(unityColor.r), parseFloat(unityColor.g), parseFloat(unityColor.b), parseFloat(unityColor.a)); +} + +/** + * Convert Unity AnimationCurve to our PiecewiseBezier Value + */ +function convertAnimationCurve(curve: any, scalar: number = 1): Value { + const m_Curve = curve.m_Curve; + if (!m_Curve || m_Curve.length === 0) { + return { type: "ConstantValue", value: 0 }; + } + + // If only one key, return constant + if (m_Curve.length === 1) { + return { type: "ConstantValue", value: parseFloat(m_Curve[0].value) * scalar }; + } + + // Convert to PiecewiseBezier + const functions: Array<{ + function: { + p0: number; + p1: number; + p2: number; + p3: number; + }; + start: number; + }> = []; + + // Add initial key if curve doesn't start at 0 + if (m_Curve.length >= 1 && parseFloat(m_Curve[0].time) > 0) { + const val = parseFloat(m_Curve[0].value) * scalar; + functions.push({ + function: { + p0: val, + p1: val, + p2: val, + p3: val, + }, + start: 0, + }); + } + + // Convert each segment + for (let i = 0; i < m_Curve.length - 1; i++) { + const curr = m_Curve[i]; + const next = m_Curve[i + 1]; + const segmentDuration = parseFloat(next.time) - parseFloat(curr.time); + + const p0 = parseFloat(curr.value) * scalar; + const p1 = (parseFloat(curr.value) + (parseFloat(curr.outSlope) * segmentDuration) / 3) * scalar; + const p2 = (parseFloat(next.value) - (parseFloat(next.inSlope) * segmentDuration) / 3) * scalar; + const p3 = parseFloat(next.value) * scalar; + + functions.push({ + function: { + p0, + p1, + p2, + p3, + }, + start: parseFloat(curr.time), + }); + } + + // Add final key if curve doesn't end at 1 + if (m_Curve.length >= 2 && parseFloat(m_Curve[m_Curve.length - 1].time) < 1) { + const val = parseFloat(m_Curve[m_Curve.length - 1].value) * scalar; + functions.push({ + function: { + p0: val, + p1: val, + p2: val, + p3: val, + }, + start: parseFloat(m_Curve[m_Curve.length - 1].time), + }); + } + + return { + type: "PiecewiseBezier", + functions, + }; +} + +/** + * Convert Unity MinMaxCurve to our Value + */ +function convertMinMaxCurve(minMaxCurve: any): Value { + const minMaxState = minMaxCurve.minMaxState; + const scalar = parseFloat(minMaxCurve.scalar || "1"); + + switch (minMaxState) { + case "0": // Constant + return { type: "ConstantValue", value: scalar }; + case "1": // Curve + return convertAnimationCurve(minMaxCurve.maxCurve, scalar); + case "2": // Random between two constants + return { + type: "IntervalValue", + min: parseFloat(minMaxCurve.minScalar || "0") * scalar, + max: scalar, + }; + case "3": // Random between two curves + // For now, just use max curve (proper implementation would need RandomColor equivalent for Value) + return convertAnimationCurve(minMaxCurve.maxCurve, scalar); + default: + return { type: "ConstantValue", value: scalar }; + } +} + +/** + * Convert Unity Gradient to our Color + */ +function convertGradient(gradient: any): IConstantColor | IGradientColor { + const colorKeys: Array<{ time: number; value: [number, number, number, number] }> = []; + + // Parse color keys + for (let i = 0; i < gradient.m_NumColorKeys; i++) { + const key = gradient[`key${i}`]; + const time = parseFloat(gradient[`ctime${i}`]) / 65535; // Unity stores time as 0-65535 + colorKeys.push({ + time, + value: [parseFloat(key.r), parseFloat(key.g), parseFloat(key.b), 1], + }); + } + + // Parse alpha keys + const alphaKeys: Array<{ time: number; value: number }> = []; + for (let i = 0; i < gradient.m_NumAlphaKeys; i++) { + const key = gradient[`key${i}`]; + const time = parseFloat(gradient[`atime${i}`]) / 65535; + alphaKeys.push({ + time, + value: parseFloat(key.a), + }); + } + + // If only one color key and one alpha key, return constant color + if (colorKeys.length === 1 && alphaKeys.length === 1) { + return { + type: "ConstantColor", + value: [...colorKeys[0].value.slice(0, 3), alphaKeys[0].value] as [number, number, number, number], + }; + } + + // Return gradient + return { + type: "Gradient", + colorKeys: colorKeys.map((k) => ({ time: k.time, value: k.value as [number, number, number, number] })), + alphaKeys: alphaKeys.map((k) => ({ time: k.time, value: k.value })), + }; +} + +/** + * Convert Unity MinMaxGradient to our Color + */ +function convertMinMaxGradient(minMaxGradient: any): IConstantColor | IGradientColor | IRandomColor | IRandomColorBetweenGradient { + const minMaxState = minMaxGradient.minMaxState; + + switch (minMaxState) { + case "0": // Constant color + return { + type: "ConstantColor", + value: [ + parseFloat(minMaxGradient.maxColor.r), + parseFloat(minMaxGradient.maxColor.g), + parseFloat(minMaxGradient.maxColor.b), + parseFloat(minMaxGradient.maxColor.a), + ] as [number, number, number, number], + }; + case "1": // Gradient + return convertGradient(minMaxGradient.maxGradient); + case "2": // Random between two colors + return { + type: "RandomColor", + colorA: [ + parseFloat(minMaxGradient.minColor.r), + parseFloat(minMaxGradient.minColor.g), + parseFloat(minMaxGradient.minColor.b), + parseFloat(minMaxGradient.minColor.a), + ] as [number, number, number, number], + colorB: [ + parseFloat(minMaxGradient.maxColor.r), + parseFloat(minMaxGradient.maxColor.g), + parseFloat(minMaxGradient.maxColor.b), + parseFloat(minMaxGradient.maxColor.a), + ] as [number, number, number, number], + }; + case "3": // Random between two gradients + const grad1 = convertGradient(minMaxGradient.minGradient); + const grad2 = convertGradient(minMaxGradient.maxGradient); + if (grad1.type === "Gradient" && grad2.type === "Gradient") { + return { + type: "RandomColorBetweenGradient", + gradient1: { + colorKeys: grad1.colorKeys, + alphaKeys: grad1.alphaKeys, + }, + gradient2: { + colorKeys: grad2.colorKeys, + alphaKeys: grad2.alphaKeys, + }, + }; + } + // Fallback to constant color if conversion failed + return { type: "ConstantColor", value: [1, 1, 1, 1] }; + default: + return { type: "ConstantColor", value: [1, 1, 1, 1] }; + } +} + +/** + * Convert Unity ParticleSystem shape to our emitter shape + */ +function convertShape(shapeModule: any): any { + if (!shapeModule || shapeModule.enabled !== "1") { + return { type: "point" }; // Default to point emitter + } + + const shapeType = shapeModule.type; + + switch (shapeType) { + case "0": // Sphere + return { + type: "sphere", + radius: parseFloat(shapeModule.radius?.value || "1"), + arc: (parseFloat(shapeModule.arc?.value || "360") / 180) * Math.PI, + thickness: parseFloat(shapeModule.radiusThickness || "1"), + }; + case "4": // Cone + return { + type: "cone", + radius: parseFloat(shapeModule.radius?.value || "1"), + arc: (parseFloat(shapeModule.arc?.value || "360") / 180) * Math.PI, + thickness: parseFloat(shapeModule.radiusThickness || "1"), + angle: (parseFloat(shapeModule.angle?.value || "25") / 180) * Math.PI, + }; + case "5": // Box + return { + type: "box", + width: parseFloat(shapeModule.boxThickness?.x || "1"), + height: parseFloat(shapeModule.boxThickness?.y || "1"), + depth: parseFloat(shapeModule.boxThickness?.z || "1"), + }; + case "10": // Circle + return { + type: "sphere", // Use sphere with arc for circle + radius: parseFloat(shapeModule.radius?.value || "1"), + arc: (parseFloat(shapeModule.arc?.value || "360") / 180) * Math.PI, + }; + default: + return { type: "point" }; + } +} + +/** + * Convert Unity ParticleSystem to our IParticleSystemConfig + */ +function convertParticleSystem(unityPS: any, _renderer: any): IParticleSystemConfig { + const main = unityPS.InitialModule; + + const config: IParticleSystemConfig = { + version: "2.0", + systemType: "base", // Unity uses GPU particles, similar to our base system + + // Basic properties + minLifeTime: parseFloat(main.startLifetime?.minScalar || main.startLifetime?.scalar || "5"), + maxLifeTime: parseFloat(main.startLifetime?.scalar || "5"), + minSize: parseFloat(main.startSize?.minScalar || main.startSize?.scalar || "1"), + maxSize: parseFloat(main.startSize?.scalar || "1"), + minEmitPower: parseFloat(main.startSpeed?.minScalar || main.startSpeed?.scalar || "5"), + maxEmitPower: parseFloat(main.startSpeed?.scalar || "5"), + emitRate: parseFloat(unityPS.EmissionModule?.rateOverTime?.scalar || "10"), + + // Duration and looping + targetStopDuration: main.looping === "1" ? 0 : parseFloat(main.duration?.scalar || "5"), + preWarmCycles: main.prewarm === "1" ? 100 : 0, + isLocal: main.simulationSpace === "0", // 0 = Local, 1 = World + + // Color + color1: convertColor({ + r: main.startColor?.maxColor?.r || "1", + g: main.startColor?.maxColor?.g || "1", + b: main.startColor?.maxColor?.b || "1", + a: main.startColor?.maxColor?.a || "1", + }), + color2: convertColor({ + r: main.startColor?.maxColor?.r || "1", + g: main.startColor?.maxColor?.g || "1", + b: main.startColor?.maxColor?.b || "1", + a: main.startColor?.maxColor?.a || "1", + }), + + // Rotation + minInitialRotation: parseFloat(main.startRotation?.minScalar || main.startRotation?.scalar || "0"), + maxInitialRotation: parseFloat(main.startRotation?.scalar || "0"), + + // Gravity (if enabled) + gravity: main.gravityModifier?.scalar ? new Vector3(0, parseFloat(main.gravityModifier.scalar) * -9.81, 0) : undefined, + + // Shape/Emitter + shape: convertShape(unityPS.ShapeModule), + + // Behaviors + behaviors: [], + }; + + // Convert modules to behaviors + const behaviors: Behavior[] = []; + + // ColorOverLife + if (unityPS.ColorModule && unityPS.ColorModule.enabled === "1") { + const colorGradient = convertMinMaxGradient(unityPS.ColorModule.gradient); + + // Convert Color type to IColorFunction + let colorFunction: { colorFunctionType: string; data: any }; + if (colorGradient.type === "ConstantColor") { + colorFunction = { + colorFunctionType: "ConstantColor", + data: { + color: { + r: colorGradient.value[0], + g: colorGradient.value[1], + b: colorGradient.value[2], + a: colorGradient.value[3], + }, + }, + }; + } else if (colorGradient.type === "Gradient") { + colorFunction = { + colorFunctionType: "Gradient", + data: { + colorKeys: colorGradient.colorKeys, + alphaKeys: colorGradient.alphaKeys || [], + }, + }; + } else if (colorGradient.type === "RandomColor") { + colorFunction = { + colorFunctionType: "ColorRange", + data: { + colorA: colorGradient.colorA, + colorB: colorGradient.colorB, + }, + }; + } else if (colorGradient.type === "RandomColorBetweenGradient") { + colorFunction = { + colorFunctionType: "RandomColorBetweenGradient", + data: { + gradient1: { + colorKeys: colorGradient.gradient1.colorKeys, + alphaKeys: colorGradient.gradient1.alphaKeys || [], + }, + gradient2: { + colorKeys: colorGradient.gradient2.colorKeys, + alphaKeys: colorGradient.gradient2.alphaKeys || [], + }, + }, + }; + } else { + colorFunction = { + colorFunctionType: "ConstantColor", + data: { color: { r: 1, g: 1, b: 1, a: 1 } }, + }; + } + + behaviors.push({ + type: "ColorOverLife", + color: colorFunction, + }); + } + + // SizeOverLife + if (unityPS.SizeModule && unityPS.SizeModule.enabled === "1") { + const sizeValue = convertMinMaxCurve(unityPS.SizeModule.curve); + behaviors.push({ + type: "SizeOverLife", + size: sizeValue, + }); + } + + // RotationOverLife + if (unityPS.RotationOverLifetimeModule && unityPS.RotationOverLifetimeModule.enabled === "1") { + const rotationZ = convertMinMaxCurve(unityPS.RotationOverLifetimeModule.z || unityPS.RotationOverLifetimeModule.curve); + behaviors.push({ + type: "RotationOverLife", + angularVelocity: rotationZ, + }); + } + + // Rotation3DOverLife (if separate X, Y, Z) + if (unityPS.RotationOverLifetimeModule && unityPS.RotationOverLifetimeModule.enabled === "1" && unityPS.RotationOverLifetimeModule.separateAxes === "1") { + behaviors.push({ + type: "Rotation3DOverLife", + angularVelocityX: convertMinMaxCurve(unityPS.RotationOverLifetimeModule.x), + angularVelocityY: convertMinMaxCurve(unityPS.RotationOverLifetimeModule.y), + angularVelocityZ: convertMinMaxCurve(unityPS.RotationOverLifetimeModule.z), + }); + } + + // VelocityOverLife (SpeedOverLife) + if (unityPS.VelocityModule && unityPS.VelocityModule.enabled === "1") { + const speedModifier = unityPS.VelocityModule.speedModifier || { minMaxState: "0", scalar: "1" }; + behaviors.push({ + type: "SpeedOverLife", + speed: convertMinMaxCurve(speedModifier), + }); + } + + // LimitVelocityOverLife + if (unityPS.ClampVelocityModule && unityPS.ClampVelocityModule.enabled === "1") { + behaviors.push({ + type: "LimitSpeedOverLife", + limitVelocity: convertMinMaxCurve(unityPS.ClampVelocityModule.magnitude), + dampen: parseFloat(unityPS.ClampVelocityModule.dampen || "0.1"), + }); + } + + // ForceOverLife (from Unity's Force module or gravity) + if (unityPS.ForceModule && unityPS.ForceModule.enabled === "1") { + behaviors.push({ + type: "ForceOverLife", + force: { + x: parseFloat(unityPS.ForceModule.x?.scalar || "0"), + y: parseFloat(unityPS.ForceModule.y?.scalar || "0"), + z: parseFloat(unityPS.ForceModule.z?.scalar || "0"), + }, + }); + } + + // ColorBySpeed + if (unityPS.ColorBySpeedModule && unityPS.ColorBySpeedModule.enabled === "1") { + const range = unityPS.ColorBySpeedModule.range; + const colorGradient = convertMinMaxGradient(unityPS.ColorBySpeedModule.gradient); + + let colorFunction: { colorFunctionType: string; data: any }; + if (colorGradient.type === "Gradient") { + colorFunction = { + colorFunctionType: "Gradient", + data: { + colorKeys: colorGradient.colorKeys, + alphaKeys: colorGradient.alphaKeys || [], + }, + }; + } else { + colorFunction = { + colorFunctionType: "ConstantColor", + data: { color: { r: 1, g: 1, b: 1, a: 1 } }, + }; + } + + behaviors.push({ + type: "ColorBySpeed", + color: colorFunction, + minSpeed: { type: "ConstantValue", value: parseFloat(range?.x || "0") }, + maxSpeed: { type: "ConstantValue", value: parseFloat(range?.y || "1") }, + }); + } + + // SizeBySpeed + if (unityPS.SizeBySpeedModule && unityPS.SizeBySpeedModule.enabled === "1") { + const range = unityPS.SizeBySpeedModule.range; + behaviors.push({ + type: "SizeBySpeed", + size: convertMinMaxCurve(unityPS.SizeBySpeedModule.curve), + minSpeed: { type: "ConstantValue", value: parseFloat(range?.x || "0") }, + maxSpeed: { type: "ConstantValue", value: parseFloat(range?.y || "1") }, + }); + } + + // RotationBySpeed + if (unityPS.RotationBySpeedModule && unityPS.RotationBySpeedModule.enabled === "1") { + const range = unityPS.RotationBySpeedModule.range; + behaviors.push({ + type: "RotationBySpeed", + angularVelocity: convertMinMaxCurve(unityPS.RotationBySpeedModule.curve), + minSpeed: { type: "ConstantValue", value: parseFloat(range?.x || "0") }, + maxSpeed: { type: "ConstantValue", value: parseFloat(range?.y || "1") }, + }); + } + + // NoiseModule (approximation) + if (unityPS.NoiseModule && unityPS.NoiseModule.enabled === "1") { + config.noiseStrength = new Vector3( + parseFloat(unityPS.NoiseModule.strengthX?.scalar || "0"), + parseFloat(unityPS.NoiseModule.strengthY?.scalar || "0"), + parseFloat(unityPS.NoiseModule.strengthZ?.scalar || "0") + ); + } + + config.behaviors = behaviors; + + return config; +} + +/** + * Intermediate format for convertGameObject (before conversion to IData format) + */ +interface IIntermediateGameObject { + type: "emitter" | "group"; + name: string; + position: [number, number, number]; + scale: [number, number, number]; + rotation: [number, number, number, number]; + emitter?: IParticleSystemConfig; + renderMode?: number; + materialId?: string; // GUID of material from ParticleSystemRenderer + children?: IIntermediateGameObject[]; +} + +/** + * Convert Unity GameObject hierarchy to our IGroup/IEmitter structure + */ +function convertGameObject(gameObject: any, components: Map): IIntermediateGameObject { + // Get Transform component + const transform = getComponentByType(gameObject, "Transform", components); + + const position = transform ? convertVector3(transform.m_LocalPosition) : ([0, 0, 0] as [number, number, number]); + const scale = transform ? convertVector3(transform.m_LocalScale) : ([1, 1, 1] as [number, number, number]); + const rotation = transform + ? ([parseFloat(transform.m_LocalRotation.x), parseFloat(transform.m_LocalRotation.y), parseFloat(transform.m_LocalRotation.z), parseFloat(transform.m_LocalRotation.w)] as [ + number, + number, + number, + number, + ]) + : ([0, 0, 0, 1] as [number, number, number, number]); + + // Check if this GameObject has a ParticleSystem component + const ps = getComponentByType(gameObject, "ParticleSystem", components); + + if (ps) { + // It's a particle emitter + const renderer = getComponentByType(gameObject, "ParticleSystemRenderer", components); + const emitterConfig = convertParticleSystem(ps, renderer); + + // Determine render mode from renderer + let renderMode = 0; // Default: BillBoard + let materialId: string | undefined; + if (renderer) { + const m_RenderMode = parseInt(renderer.m_RenderMode || "0"); + switch (m_RenderMode) { + case 0: + renderMode = 0; // BillBoard + break; + case 1: + renderMode = 1; // StretchedBillBoard + break; + case 2: + renderMode = 2; // HorizontalBillBoard + break; + case 3: + renderMode = 3; // VerticalBillBoard + break; + case 4: + renderMode = 4; // Mesh + break; + } + + // Extract material GUID from renderer + if (renderer.m_Materials && Array.isArray(renderer.m_Materials) && renderer.m_Materials.length > 0) { + const materialRef = renderer.m_Materials[0]; + if (materialRef && materialRef.guid) { + materialId = materialRef.guid; + } + } + } + + const emitter: IIntermediateGameObject = { + type: "emitter", + name: gameObject.m_Name || "ParticleSystem", + position, + scale, + rotation, + emitter: emitterConfig, + renderMode, + materialId, + }; + + return emitter; + } else { + // It's a group (container) + const group: IIntermediateGameObject = { + type: "group", + name: gameObject.m_Name || "Group", + position, + scale, + rotation, + children: [], + }; + + // Recursively convert children + if (transform && transform.m_Children) { + for (const childRef of transform.m_Children) { + const childTransform = components.get(childRef.fileID); + if (childTransform && childTransform.Transform) { + const childGORef = childTransform.Transform.m_GameObject; + const childGOId = childGORef?.fileID || childGORef; + const childGO = components.get(childGOId); + + if (childGO && childGO.GameObject) { + if (!group.children) { + group.children = []; + } + group.children.push(convertGameObject(childGO.GameObject, components)); + } + } + } + } + + return group; + } +} + +/** + * Convert convertGameObject result to IGroup or IEmitter format + * Recursively processes children + */ +function _convertToIDataFormat(converted: IIntermediateGameObject): IGroup | IEmitter | null { + if (!converted) { + return null; + } + + const uuid = Tools.RandomId(); + + if (converted.type === "group") { + // Convert children recursively + const children: (IGroup | IEmitter)[] = []; + if (converted.children && Array.isArray(converted.children)) { + for (const child of converted.children) { + const childConverted = _convertToIDataFormat(child); + if (childConverted) { + children.push(childConverted); + } + } + } + + const group: IGroup = { + uuid, + name: converted.name, + transform: { + position: new Vector3(converted.position[0], converted.position[1], converted.position[2]), + rotation: new Quaternion(converted.rotation[0], converted.rotation[1], converted.rotation[2], converted.rotation[3]), + scale: new Vector3(converted.scale[0], converted.scale[1], converted.scale[2]), + }, + children: children, + }; + return group; + } else { + if (!converted.emitter) { + console.warn("Emitter config is missing for", converted.name); + return null; + } + const emitter: IEmitter = { + uuid, + name: converted.name, + transform: { + position: new Vector3(converted.position[0], converted.position[1], converted.position[2]), + rotation: new Quaternion(converted.rotation[0], converted.rotation[1], converted.rotation[2], converted.rotation[3]), + scale: new Vector3(converted.scale[0], converted.scale[1], converted.scale[2]), + }, + config: converted.emitter, + systemType: converted.renderMode === 4 ? "solid" : "base", // Mesh = solid, others = base + materialId: converted.materialId, // Link material to emitter + }; + return emitter; + } +} + +/** + * Convert Unity model buffer to IGeometry using Babylon.js loaders + */ +async function convertUnityModel(guid: string, buffer: Buffer, extension: string, scene: Scene): Promise { + try { + // Determine MIME type based on extension + let mimeType = "application/octet-stream"; + if (extension === "obj") { + mimeType = "text/plain"; + } else if (extension === "fbx") { + mimeType = "application/octet-stream"; + } + + // Create data URL from buffer + const dataUrl = `data:${mimeType};base64,${buffer.toString("base64")}`; + + // Import mesh using Babylon.js SceneLoader + const result = await SceneLoader.ImportMeshAsync("", dataUrl, "", scene); + + if (!result || !result.meshes || result.meshes.length === 0) { + return null; + } + + // Find the first mesh + const mesh = result.meshes.find((m) => m instanceof Mesh) as Mesh | undefined; + if (!mesh || !mesh.geometry) { + return null; + } + + // Extract vertex data + const vertexData = VertexData.ExtractFromMesh(mesh); + if (!vertexData) { + return null; + } + + // Convert to IGeometry format + const geometry: IGeometry = { + uuid: guid, + type: "BufferGeometry", + data: { + attributes: {}, + }, + }; + + // Convert positions + if (vertexData.positions) { + geometry.data!.attributes.position = { + array: Array.from(vertexData.positions), + itemSize: 3, + }; + } + + // Convert normals + if (vertexData.normals) { + geometry.data!.attributes.normal = { + array: Array.from(vertexData.normals), + itemSize: 3, + }; + } + + // Convert UVs + if (vertexData.uvs) { + geometry.data!.attributes.uv = { + array: Array.from(vertexData.uvs), + itemSize: 2, + }; + } + + // Convert indices + if (vertexData.indices) { + geometry.data!.index = { + array: Array.from(vertexData.indices), + }; + } + + // Cleanup: dispose imported meshes + for (const m of result.meshes) { + m.dispose(); + } + + return geometry; + } catch (error) { + console.warn(`Failed to convert Unity model ${guid}:`, error); + return null; + } +} + +/** + * Convert Unity prefab components to IData + * + * @param components - Already parsed Unity components Map (parsed in modal) + * @param dependencies - Optional dependencies (textures, materials, models, sounds) + * @param scene - Babylon.js Scene for loading models (required for model parsing) + * @returns IData structure ready for our Effect system + */ +export async function convertUnityPrefabToData( + components: Map, + dependencies?: { + textures?: Map; + materials?: Map; + models?: Map; + sounds?: Map; + meta?: Map; + }, + scene?: Scene +): Promise { + // Validate components is a Map + if (!(components instanceof Map)) { + console.error("convertUnityPrefabToData: components must be a Map, got:", typeof components, components); + throw new Error("components must be a Map"); + } + + let root: IGroup | IEmitter | null = null; + + // Find root GameObject + const rootGameObjectId = findRootGameObject(components); + if (!rootGameObjectId) { + console.warn("No root GameObject found in Unity prefab"); + return { + root: null, + materials: [], + textures: [], + images: [], + geometries: [], + }; + } + + const rootComponent = components.get(rootGameObjectId); + if (!rootComponent) { + console.warn(`[convertUnityPrefabToData] Root GameObject component ${rootGameObjectId} not found in components map`); + console.log("[convertUnityPrefabToData] Available component IDs (first 20):", Array.from(components.keys()).slice(0, 20)); + console.log("[convertUnityPrefabToData] Component structure samples:"); + for (const [id, comp] of Array.from(components.entries()).slice(0, 5)) { + console.log(` Component ${id}:`, { + keys: Object.keys(comp), + hasTransform: !!comp.Transform, + hasGameObject: !!comp.GameObject, + __type: comp.__type, + }); + } + return { + root: null, + materials: [], + textures: [], + images: [], + geometries: [], + }; + } + + console.log(`[convertUnityPrefabToData] Found root component ${rootGameObjectId}:`, { + keys: Object.keys(rootComponent), + hasGameObject: !!rootComponent.GameObject, + hasTransform: !!rootComponent.Transform, + __type: rootComponent.__type, + }); + + // Get GameObject from component (could be rootComponent.GameObject or rootComponent itself) + const gameObject = rootComponent.GameObject || rootComponent; + if (!gameObject || (typeof gameObject === "object" && !gameObject.m_Name && !gameObject.m_Component)) { + console.warn(`[convertUnityPrefabToData] Root GameObject ${rootGameObjectId} structure invalid:`, rootComponent); + console.log("[convertUnityPrefabToData] Available keys in rootComponent:", Object.keys(rootComponent)); + console.log("[convertUnityPrefabToData] gameObject:", gameObject); + + // Try to find GameObject component directly + for (const [_id, comp] of components) { + if (comp.GameObject && comp.GameObject.m_Name) { + console.log(`[convertUnityPrefabToData] Found GameObject component ${_id} with name:`, comp.GameObject.m_Name); + const foundGameObject = comp.GameObject; + if (foundGameObject.m_Component) { + console.log(`[convertUnityPrefabToData] Using GameObject from component ${_id}`); + const converted = convertGameObject(foundGameObject, components); + root = _convertToIDataFormat(converted); + break; + } + } + } + + if (!root) { + return { + root: null, + materials: [], + textures: [], + images: [], + geometries: [], + }; + } + } else { + // Convert root GameObject and its hierarchy recursively + console.log(`[convertUnityPrefabToData] Converting GameObject:`, gameObject.m_Name); + const converted = convertGameObject(gameObject, components); + root = _convertToIDataFormat(converted); + } + + // Process dependencies if provided + const materials: IMaterial[] = []; + const textures: ITexture[] = []; + const images: IImage[] = []; + const geometries: IGeometry[] = []; + + if (dependencies) { + // Convert materials from YAML to IData format + if (dependencies.materials) { + for (const [guid, yamlContent] of dependencies.materials) { + try { + const material = convertUnityMaterial(guid, yamlContent, dependencies); + if (material) { + materials.push(material); + } + } catch (error) { + console.warn(`Failed to convert material ${guid}:`, error); + } + } + } + + // Convert textures to IData format + if (dependencies.textures) { + for (const [guid, buffer] of dependencies.textures) { + // Create image entry for texture + const imageId = `image-${guid}`; + images.push({ + uuid: imageId, + url: `data:image/png;base64,${buffer.toString("base64")}`, // Convert buffer to data URL + }); + + // Create texture entry + textures.push({ + uuid: guid, + image: imageId, + wrapU: 0, // Repeat + wrapV: 0, // Repeat + generateMipmaps: true, + flipY: false, + }); + } + } + + // Convert models to IData format (for mesh particles) + // Parse models using Babylon.js loaders if Scene is provided + if (dependencies.models && scene) { + for (const [guid, buffer] of dependencies.models) { + // Determine file extension from meta + const meta = dependencies.meta?.get(guid); + const path = meta?.path || ""; + const ext = path.split(".").pop()?.toLowerCase() || "fbx"; + + try { + const geometry = await convertUnityModel(guid, buffer, ext, scene); + if (geometry) { + geometries.push(geometry); + } else { + // Fallback: store placeholder if parsing failed + geometries.push({ + uuid: guid, + type: "BufferGeometry", + }); + } + } catch (error) { + console.warn(`Failed to parse model ${guid}:`, error); + // Fallback: store placeholder + geometries.push({ + uuid: guid, + type: "BufferGeometry", + }); + } + } + } else if (dependencies.models) { + // If no Scene provided, store placeholders (models will be loaded later) + for (const [guid] of dependencies.models) { + geometries.push({ + uuid: guid, + type: "BufferGeometry", + }); + } + } + } + + return { + root, + materials, + textures, + images, + geometries, + }; +} + +/** + * Convert Unity Material YAML to IMaterial + */ +function convertUnityMaterial(guid: string, yamlContent: string, dependencies: any): IMaterial | null { + try { + // Parse Unity material YAML + const parsed = yaml.load(yamlContent) as any; + if (!parsed || !parsed.Material) { + return null; + } + + const unityMat = parsed.Material; + const material: IMaterial = { + uuid: guid, + }; + + // Extract color + if (unityMat.m_SavedProperties?.m_Colors) { + const colorProps = unityMat.m_SavedProperties.m_Colors; + for (const colorProp of colorProps) { + if (colorProp._Color) { + const r = parseFloat(colorProp._Color.r || "1"); + const g = parseFloat(colorProp._Color.g || "1"); + const b = parseFloat(colorProp._Color.b || "1"); + material.color = new Color3(r, g, b); + break; + } + } + } + + // Extract texture (MainTex) + if (unityMat.m_SavedProperties?.m_TexEnvs) { + for (const texEnv of unityMat.m_SavedProperties.m_TexEnvs) { + if (texEnv._MainTex && texEnv._MainTex.m_Texture) { + const texRef = texEnv._MainTex.m_Texture; + const textureGuid = texRef.guid || texRef.fileID; + if (textureGuid && dependencies.textures?.has(textureGuid)) { + material.map = textureGuid; // Reference to texture UUID + } + break; + } + } + } + + // Extract transparency + if (unityMat.stringTagMap?.RenderType === "Transparent") { + material.transparent = true; + } + + // Extract opacity + if (unityMat.m_SavedProperties?.m_Colors) { + for (const colorProp of unityMat.m_SavedProperties.m_Colors) { + if (colorProp._Color && colorProp._Color.a !== undefined) { + material.opacity = parseFloat(colorProp._Color.a || "1"); + break; + } + } + } + + // Extract blending mode from shader + const shaderFileID = unityMat.m_Shader?.fileID; + if (shaderFileID) { + // Unity shader IDs: 200 = Standard, 203 = Unlit, etc. + // For now, use default blending + material.blending = 0; // Normal blending + } + + return material; + } catch (error) { + console.warn(`Failed to parse Unity material ${guid}:`, error); + return null; + } +} + +/** + * Convert Unity prefab ZIP to IData + * + * @param zipBuffer - Unity prefab ZIP file buffer + * @returns Array of IData structures + */ diff --git a/editor/src/editor/windows/effect-editor/graph.tsx b/editor/src/editor/windows/effect-editor/graph.tsx index 694a8ffd4..177423d61 100644 --- a/editor/src/editor/windows/effect-editor/graph.tsx +++ b/editor/src/editor/windows/effect-editor/graph.tsx @@ -19,7 +19,7 @@ import { IEffectEditor } from "."; import { saveSingleFileDialog } from "../../../tools/dialog"; import { writeJSON } from "fs-extra"; import { toast } from "sonner"; -import { Effect, type IEffectNode, EffectSolidParticleSystem } from "babylonjs-editor-tools"; +import { Effect, type IEffectNode, EffectSolidParticleSystem, type IData } from "babylonjs-editor-tools"; export interface IEffectEditorGraphProps { filePath: string | null; @@ -116,11 +116,19 @@ export class EffectEditorGraph extends Component { + try { + if (!this.props.editor.preview?.scene) { + console.error("Scene is not available"); + return; + } + + // Create effect from IData + const effectName = prefabName.replace(".prefab", "").split("/").pop() || "Unity Effect"; + const effect = new Effect(data, this.props.editor.preview.scene); + + // Generate unique ID for effect + const effectId = `unity-effect-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; + + // Store effect with data for export + this._effects.set(effectId, { + id: effectId, + name: effectName, + effect: effect, + originalJsonData: data, + }); + + // Rebuild tree with all effects + this._rebuildTree(); + + // Apply prewarm before starting (if any systems have prewarm enabled) + effect.applyPrewarm(); + + // Start systems + effect.start(); + + // Notify preview to sync playing state + setTimeout(() => { + if (this.props.editor?.preview) { + (this.props.editor.preview as any).forceUpdate?.(); + } + }, 100); + } catch (error) { + console.error("Failed to load Unity prefab:", error); + throw error; + } + } + /** * Rebuild tree from all effects */ @@ -215,11 +269,10 @@ export class EffectEditorGraph extends Component - {isSolid ? "Solid" : "Base"} - - ) : undefined; + const secondaryLabel = + Node.type === "particle" ? ( + {isSolid ? "Solid" : "Base"} + ) : undefined; return { id: nodeId, @@ -500,8 +553,15 @@ export class EffectEditorGraph extends Component
+ {/* Unity Import Modal */} + this.setState({ isUnityImportModalOpen: false })} + onImport={(contexts, prefabNames) => this.importUnityData(contexts, prefabNames)} + /> + ); @@ -120,4 +130,84 @@ export default class EffectEditorWindow extends Component { + try { + // Import Unity converter + const { convertUnityPrefabToData } = await import("babylonjs-editor-tools"); + + // Get Scene from preview for model loading + let scene = this.editor.preview?.scene || null; + if (!scene) { + // Try waiting a bit for preview to initialize + await new Promise((resolve) => setTimeout(resolve, 100)); + scene = this.editor.preview?.scene || null; + } + if (!scene) { + console.warn("Scene not available for model loading, models will be placeholders"); + } + + // Convert each prefab with its dependencies + let successCount = 0; + for (let i = 0; i < contexts.length; i++) { + try { + const context = contexts[i]; + const prefabName = prefabNames[i]; + + // Validate context structure + if (!context) { + console.error("Context is null/undefined:", context); + toast.error(`Invalid prefab data for ${prefabName}`); + continue; + } + + if (!context.prefabComponents) { + console.error("prefabComponents is missing in context:", context); + toast.error(`Missing prefab components for ${prefabName}`); + continue; + } + + if (!(context.prefabComponents instanceof Map)) { + console.error("prefabComponents is not a Map:", typeof context.prefabComponents, context.prefabComponents); + toast.error(`Invalid prefab components type for ${prefabName}`); + continue; + } + + // Convert to IData (pass already parsed components, dependencies, and Scene for model parsing) + const data = await convertUnityPrefabToData(context.prefabComponents, context.dependencies, scene); + + // Import into graph + if (this.editor.graph) { + await this.editor.graph.loadFromUnityData(data, prefabName); + successCount++; + } else { + toast.error(`Failed to import ${prefabName}: Graph not available`); + } + } catch (error) { + console.error(`Failed to import prefab ${prefabNames[i]}:`, error); + toast.error(`Failed to import ${prefabNames[i]}`); + } + } + + if (successCount > 0) { + toast.success(`Successfully imported ${successCount} prefab${successCount > 1 ? "s" : ""}`); + } + } catch (error) { + console.error("Failed to import Unity prefabs:", error); + toast.error("Failed to import Unity prefabs"); + throw error; + } + } + + /** + * Open Unity import modal + */ + public openUnityImportModal(): void { + this.setState({ isUnityImportModalOpen: true }); + } } diff --git a/editor/src/editor/windows/effect-editor/modals/unity-import-modal.tsx b/editor/src/editor/windows/effect-editor/modals/unity-import-modal.tsx new file mode 100644 index 000000000..9ca2f662b --- /dev/null +++ b/editor/src/editor/windows/effect-editor/modals/unity-import-modal.tsx @@ -0,0 +1,1107 @@ +/** + * Unity Asset Import Modal + * Allows importing Unity Particle System prefabs from ZIP archives + */ + +import { Component, ReactNode } from "react"; +import { Tree, TreeNodeInfo } from "@blueprintjs/core"; +import { Button } from "../../../../ui/shadcn/ui/button"; +import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "../../../../ui/shadcn/ui/dialog"; +import { Checkbox } from "../../../../ui/shadcn/ui/checkbox"; +import { Upload, FileArchive, Search } from "lucide-react"; +import { HiOutlineFolder } from "react-icons/hi2"; +import { toast } from "sonner"; +import * as yaml from "js-yaml"; +import { Input } from "../../../../ui/shadcn/ui/input"; +import { ScrollArea } from "../../../../ui/shadcn/ui/scroll-area"; + +/** + * Recursively process Unity inline objects like {fileID: 123} + */ +function processUnityInlineObjects(obj: any): any { + if (typeof obj === "string") { + const inlineMatch = obj.match(/^\s*\{([^}]+)\}\s*$/); + if (inlineMatch) { + const pairs = inlineMatch[1].split(","); + const result: any = {}; + for (const pair of pairs) { + const [key, value] = pair.split(":").map((s) => s.trim()); + if (key && value !== undefined) { + if (value === "true") result[key] = true; + else if (value === "false") result[key] = false; + else if (/^-?\d+$/.test(value)) result[key] = parseInt(value, 10); + else if (/^-?\d*\.\d+$/.test(value)) result[key] = parseFloat(value); + else result[key] = value.replace(/^["']|["']$/g, ""); + } + } + return result; + } + return obj; + } + + if (Array.isArray(obj)) { + return obj.map((item) => processUnityInlineObjects(item)); + } + + if (obj && typeof obj === "object") { + const result: any = {}; + for (const [key, value] of Object.entries(obj)) { + result[key] = processUnityInlineObjects(value); + } + return result; + } + + return obj; +} + +/** + * Parse Unity YAML string into component map (same logic as in converter) + */ +function parseUnityYAML(yamlContent: string): Map { + // Validate input + if (typeof yamlContent !== "string") { + console.error("parseUnityYAML: yamlContent must be a string, got:", typeof yamlContent, yamlContent); + throw new Error("parseUnityYAML: yamlContent must be a string"); + } + + const components = new Map(); + const documents = yamlContent.split(/^---\s+/gm).filter(Boolean); + + for (const doc of documents) { + const match = doc.match(/^!u!(\d+)\s+&(\d+)/); + if (!match) continue; + + const [, componentType, componentId] = match; + const yamlWithoutTag = doc.replace(/^!u!(\d+)\s+&(\d+)\s*\n/, ""); + + try { + const parsed = yaml.load(yamlWithoutTag, { + schema: yaml.DEFAULT_SCHEMA, + }) as any; + + if (!parsed || typeof parsed !== "object") { + continue; + } + + const processed = processUnityInlineObjects(parsed); + + const component: any = { + id: componentId, + __type: componentType, + ...processed, + }; + + components.set(componentId, component); + + const mainKey = Object.keys(processed).find((k) => k !== "id" && k !== "__type"); + if (mainKey && processed[mainKey] && typeof processed[mainKey] === "object") { + processed[mainKey].__id = componentId; + } + } catch (error) { + console.warn(`Failed to parse Unity YAML component ${componentId}:`, error); + continue; + } + } + + return components; +} + +export interface IUnityPrefabNode { + name: string; + path: string; + type: "prefab" | "folder"; + children?: IUnityPrefabNode[]; +} + +export interface IUnityAssetContext { + prefabComponents: Map; // Already parsed Unity components + dependencies: { + textures: Map; // GUID -> file data + materials: Map; // GUID -> YAML content + models: Map; // GUID -> file data + sounds: Map; // GUID -> file data + meta: Map; // GUID -> meta data + }; +} + +export interface IUnityImportModalProps { + isOpen: boolean; + onClose: () => void; + onImport: (contexts: IUnityAssetContext[], prefabNames: string[]) => void; +} + +export interface IUnityImportModalState { + isDragging: boolean; + zipFile: File | null; + treeNodes: TreeNodeInfo[]; + selectedPrefabs: Set; + isProcessing: boolean; + searchQuery: string; +} + +/** + * Modal for importing Unity prefabs from ZIP archives + */ +export class UnityImportModal extends Component { + private _dropZoneRef: HTMLDivElement | null = null; + + constructor(props: IUnityImportModalProps) { + super(props); + + this.state = { + isDragging: false, + zipFile: null, + treeNodes: [], + selectedPrefabs: new Set(), + isProcessing: false, + searchQuery: "", + }; + } + + public render(): ReactNode { + const { isOpen } = this.props; + const { isDragging, zipFile, treeNodes, selectedPrefabs, isProcessing } = this.state; + + const { searchQuery } = this.state; + const filteredTreeNodes = this._filterTreeNodes(treeNodes, searchQuery); + + return ( + !open && this._handleClose()}> + + + + + Import Unity Assets + + + {/* Search and Controls - only show when tree is loaded */} + {treeNodes.length > 0 && ( +
+
+
+ + + {selectedPrefabs.size} of {this._countPrefabs(treeNodes)} prefab{this._countPrefabs(treeNodes) !== 1 ? "s" : ""} selected + +
+
+ + +
+
+ + {/* Search Input */} +
+ + this.setState({ searchQuery: e.target.value })} + className="pl-9 h-9" + /> +
+
+ )} +
+ +
+ {/* Drop Zone */} + {!zipFile && ( +
(this._dropZoneRef = ref)} + className={` + border-2 border-dashed rounded-lg p-12 + flex flex-col items-center justify-center gap-4 + transition-all duration-200 cursor-pointer + ${isDragging ? "border-primary bg-primary/5 scale-[1.02]" : "border-border hover:border-primary/50 hover:bg-muted/30"} + `} + onDragOver={(e) => this._handleDragOver(e)} + onDragLeave={(e) => this._handleDragLeave(e)} + onDrop={(e) => this._handleDrop(e)} + onClick={() => this._handleClickUpload()} + > +
+ +
+
+

{isDragging ? "Drop ZIP archive here" : "Drag & drop Unity ZIP archive"}

+

or click to browse

+
+

Supported: .zip (Unity Prefab + Meta files)

+
+ )} + + {/* File Info */} + {zipFile && treeNodes.length === 0 && ( +
+
+ +
+
+

{zipFile.name}

+

{this._formatFileSize(zipFile.size)}

+
+ +
+ )} + + {/* Prefab Tree */} + {treeNodes.length > 0 && ( + +
+ +
+
+ )} + + {/* Processing State */} + {isProcessing && ( +
+
+
+
+
+
+

Processing ZIP archive...

+

Please wait

+
+
+
+ )} +
+ + + + + +
+
+ ); + } + + /** + * Build tree structure from ZIP file paths + */ + private _buildTreeFromPaths(prefabPaths: string[]): IUnityPrefabNode[] { + // Build folder tree structure + const folderMap = new Map(); + + // Helper to get or create folder node + const getOrCreateFolder = (folderPath: string, folderName: string): IUnityPrefabNode => { + if (!folderMap.has(folderPath)) { + folderMap.set(folderPath, { + name: folderName, + path: folderPath, + type: "folder", + children: [], + }); + } + return folderMap.get(folderPath)!; + }; + + // Process each prefab path + for (const path of prefabPaths) { + const parts = path.split("/").filter(Boolean); + const fileName = parts.pop() || path; + const folderParts = parts; + + // Build folder hierarchy + let currentPath = ""; + let parentNode: IUnityPrefabNode | null = null; + + for (let i = 0; i < folderParts.length; i++) { + const folderName = folderParts[i]; + currentPath = currentPath ? `${currentPath}/${folderName}` : folderName; + + const folderNode = getOrCreateFolder(currentPath, folderName); + if (parentNode && parentNode.children) { + // Check if already added + if (!parentNode.children.some((c) => c.path === currentPath)) { + parentNode.children.push(folderNode); + } + } + parentNode = folderNode; + } + + // Add prefab to parent folder (or root) + const prefabNode: IUnityPrefabNode = { + name: fileName.replace(".prefab", ""), + path: path, + type: "prefab", + }; + + if (parentNode) { + if (!parentNode.children) { + parentNode.children = []; + } + parentNode.children.push(prefabNode); + } else { + // Root level prefab - add to root + if (!folderMap.has("")) { + folderMap.set("", { + name: "", + path: "", + type: "folder", + children: [], + }); + } + folderMap.get("")!.children!.push(prefabNode); + } + } + + // Get root folders (those without parent in map) + const rootNodes: IUnityPrefabNode[] = []; + for (const [path, node] of folderMap) { + if (path === "" || !folderMap.has(path.split("/").slice(0, -1).join("/"))) { + rootNodes.push(node); + } + } + + // Sort: folders first, then prefabs, alphabetically + const sortNode = (node: IUnityPrefabNode): void => { + if (node.children) { + node.children.sort((a, b) => { + if (a.type === "folder" && b.type === "prefab") return -1; + if (a.type === "prefab" && b.type === "folder") return 1; + return a.name.localeCompare(b.name); + }); + node.children.forEach(sortNode); + } + }; + + rootNodes.forEach(sortNode); + rootNodes.sort((a, b) => a.name.localeCompare(b.name)); + + return rootNodes; + } + + /** + * Convert IUnityPrefabNode to TreeNodeInfo + */ + private _convertToTreeNode(node: IUnityPrefabNode): TreeNodeInfo { + const isSelected = this.state.selectedPrefabs.has(node.path); + const childNodes = node.children ? node.children.map((child) => this._convertToTreeNode(child)) : undefined; + + const label = ( +
+ {node.type === "prefab" && ( + this._handleTogglePrefab(node.path, checked as boolean)} onClick={(e) => e.stopPropagation()} /> + )} + {node.name || "Root"} +
+ ); + + return { + id: node.path, + label, + icon: node.type === "folder" ? : undefined, + isExpanded: node.type === "folder", + hasCaret: node.type === "folder" && childNodes && childNodes.length > 0, + childNodes, + isSelected: false, // Tree selection is handled by checkbox + nodeData: node, + }; + } + + /** + * Handle node click + */ + private _handleNodeClick = (nodeData: TreeNodeInfo): void => { + // Toggle checkbox for prefab nodes + if (nodeData.nodeData?.type === "prefab") { + const isSelected = this.state.selectedPrefabs.has(nodeData.nodeData.path); + this._handleTogglePrefab(nodeData.nodeData.path, !isSelected); + } + }; + + /** + * Handle node expand + */ + private _handleNodeExpand = (nodeData: TreeNodeInfo): void => { + // Update tree to reflect expansion + const updateNodes = (nodes: TreeNodeInfo[]): TreeNodeInfo[] => { + return nodes.map((n) => { + if (n.id === nodeData.id) { + return { ...n, isExpanded: true }; + } + if (n.childNodes) { + return { ...n, childNodes: updateNodes(n.childNodes) }; + } + return n; + }); + }; + this.setState({ treeNodes: updateNodes(this.state.treeNodes) }); + }; + + /** + * Handle node collapse + */ + private _handleNodeCollapse = (nodeData: TreeNodeInfo): void => { + // Update tree to reflect collapse + const updateNodes = (nodes: TreeNodeInfo[]): TreeNodeInfo[] => { + return nodes.map((n) => { + if (n.id === nodeData.id) { + return { ...n, isExpanded: false }; + } + if (n.childNodes) { + return { ...n, childNodes: updateNodes(n.childNodes) }; + } + return n; + }); + }; + this.setState({ treeNodes: updateNodes(this.state.treeNodes) }); + }; + + /** + * Handle drag over + */ + private _handleDragOver(e: React.DragEvent): void { + e.preventDefault(); + e.stopPropagation(); + this.setState({ isDragging: true }); + } + + /** + * Handle drag leave + */ + private _handleDragLeave(e: React.DragEvent): void { + e.preventDefault(); + e.stopPropagation(); + if (e.currentTarget === this._dropZoneRef) { + this.setState({ isDragging: false }); + } + } + + /** + * Handle drop + */ + private async _handleDrop(e: React.DragEvent): Promise { + e.preventDefault(); + e.stopPropagation(); + this.setState({ isDragging: false }); + + const files = Array.from(e.dataTransfer.files); + const zipFile = files.find((f) => f.name.endsWith(".zip")); + + if (!zipFile) { + toast.error("Please drop a ZIP archive"); + return; + } + + await this._processZipFile(zipFile); + } + + /** + * Handle click upload + */ + private _handleClickUpload(): void { + const input = document.createElement("input"); + input.type = "file"; + input.accept = ".zip"; + input.onchange = async (e) => { + const file = (e.target as HTMLInputElement).files?.[0]; + if (file) { + await this._processZipFile(file); + } + }; + input.click(); + } + + /** + * Process ZIP file and extract prefab list with folder structure + */ + private async _processZipFile(file: File): Promise { + this.setState({ zipFile: file, isProcessing: true }); + + try { + // Convert File to Buffer for adm-zip (Electron-compatible) + const arrayBuffer = await file.arrayBuffer(); + const buffer = Buffer.from(arrayBuffer); + + // Use adm-zip for Electron (better than jszip for Node.js/Electron) + const AdmZip = (await import("adm-zip")).default; + const zip = new AdmZip(buffer); + + const prefabPaths: string[] = []; + + // Get all entries from ZIP + const zipEntries = zip.getEntries(); + + // Find all .prefab files (ignore .meta files and directories) + for (const entry of zipEntries) { + if (entry.entryName.endsWith(".prefab") && !entry.isDirectory && !entry.entryName.endsWith(".meta")) { + prefabPaths.push(entry.entryName); + } + } + + if (prefabPaths.length === 0) { + toast.error("No .prefab files found in ZIP archive"); + this.setState({ isProcessing: false, zipFile: null }); + return; + } + + // Build tree structure from paths + const treeStructure = this._buildTreeFromPaths(prefabPaths); + + // Convert to TreeNodeInfo + const treeNodes = treeStructure.map((node) => this._convertToTreeNode(node)); + + this.setState({ + treeNodes, + isProcessing: false, + }); + + toast.success(`Found ${prefabPaths.length} prefab${prefabPaths.length > 1 ? "s" : ""} in archive`); + } catch (error) { + console.error("Failed to process ZIP:", error); + toast.error("Failed to process ZIP archive"); + this.setState({ isProcessing: false, zipFile: null }); + } + } + + /** + * Handle toggle prefab selection + */ + private _handleTogglePrefab(path: string, checked: boolean): void { + const selectedPrefabs = new Set(this.state.selectedPrefabs); + if (checked) { + selectedPrefabs.add(path); + } else { + selectedPrefabs.delete(path); + } + + // Update tree nodes to reflect checkbox state + const updateNodes = (nodes: TreeNodeInfo[]): TreeNodeInfo[] => { + return nodes.map((n) => { + if (n.nodeData?.path === path) { + // Update label with new checkbox state + const node = n.nodeData; + const label = ( +
+ {node.type === "prefab" && ( + this._handleTogglePrefab(path, c as boolean)} onClick={(e) => e.stopPropagation()} /> + )} + {node.name || "Root"} +
+ ); + return { ...n, label }; + } + if (n.childNodes) { + return { ...n, childNodes: updateNodes(n.childNodes) }; + } + return n; + }); + }; + + this.setState({ + selectedPrefabs, + treeNodes: updateNodes(this.state.treeNodes), + }); + } + + /** + * Handle select all prefabs + */ + private _handleSelectAll(): void { + const allPaths = this._collectAllPaths(this.state.treeNodes); + this.setState({ selectedPrefabs: new Set(allPaths) }); + + // Update tree to reflect all selected + const updateNodes = (nodes: TreeNodeInfo[]): TreeNodeInfo[] => { + return nodes.map((n) => { + if (n.nodeData?.type === "prefab") { + const node = n.nodeData; + const label = ( +
+ this._handleTogglePrefab(node.path, c as boolean)} onClick={(e) => e.stopPropagation()} /> + {node.name} +
+ ); + return { ...n, label }; + } + if (n.childNodes) { + return { ...n, childNodes: updateNodes(n.childNodes) }; + } + return n; + }); + }; + this.setState({ treeNodes: updateNodes(this.state.treeNodes) }); + } + + /** + * Handle select none + */ + private _handleSelectNone(): void { + this.setState({ selectedPrefabs: new Set() }); + + // Update tree to reflect none selected + const updateNodes = (nodes: TreeNodeInfo[]): TreeNodeInfo[] => { + return nodes.map((n) => { + if (n.nodeData?.type === "prefab") { + const node = n.nodeData; + const label = ( +
+ this._handleTogglePrefab(node.path, c as boolean)} onClick={(e) => e.stopPropagation()} /> + {node.name} +
+ ); + return { ...n, label }; + } + if (n.childNodes) { + return { ...n, childNodes: updateNodes(n.childNodes) }; + } + return n; + }); + }; + this.setState({ treeNodes: updateNodes(this.state.treeNodes) }); + } + + /** + * Collect all prefab paths recursively from tree nodes + */ + private _collectAllPaths(nodes: TreeNodeInfo[]): string[] { + const paths: string[] = []; + for (const node of nodes) { + if (node.nodeData?.type === "prefab") { + paths.push(node.nodeData.path); + } + if (node.childNodes) { + paths.push(...this._collectAllPaths(node.childNodes)); + } + } + return paths; + } + + /** + * Count total prefabs in tree + */ + private _countPrefabs(nodes: TreeNodeInfo[]): number { + let count = 0; + for (const node of nodes) { + if (node.nodeData?.type === "prefab") { + count++; + } + if (node.childNodes) { + count += this._countPrefabs(node.childNodes); + } + } + return count; + } + + /** + * Filter tree nodes by search query + */ + private _filterTreeNodes(nodes: TreeNodeInfo[], query: string): TreeNodeInfo[] { + if (!query.trim()) { + return nodes; + } + + const lowerQuery = query.toLowerCase(); + + const filterNode = (node: TreeNodeInfo): TreeNodeInfo | null => { + const nodeName = node.nodeData?.name?.toLowerCase() || ""; + const matchesQuery = nodeName.includes(lowerQuery); + + // Filter children first + let filteredChildren: TreeNodeInfo[] | undefined; + if (node.childNodes) { + filteredChildren = node.childNodes.map(filterNode).filter((n) => n !== null) as TreeNodeInfo[]; + } + + // If this node matches or has matching children, include it + if (matchesQuery || (filteredChildren && filteredChildren.length > 0)) { + return { + ...node, + childNodes: filteredChildren && filteredChildren.length > 0 ? filteredChildren : undefined, + isExpanded: matchesQuery || (filteredChildren && filteredChildren.length > 0) ? true : node.isExpanded, + }; + } + + return null; + }; + + return nodes.map(filterNode).filter((n) => n !== null) as TreeNodeInfo[]; + } + + /** + * Handle remove file + */ + private _handleRemoveFile(): void { + this.setState({ + zipFile: null, + treeNodes: [], + selectedPrefabs: new Set(), + searchQuery: "", + }); + } + + /** + * Collect all fileID references from parsed YAML object + */ + private _collectFileIDs(obj: any, fileIDs: Set): void { + if (typeof obj !== "object" || obj === null) { + return; + } + + if (Array.isArray(obj)) { + for (const item of obj) { + this._collectFileIDs(item, fileIDs); + } + return; + } + + // Check for Unity fileID references: {fileID: "123"} or {fileID: 123} + if (obj.fileID !== undefined) { + const fileID = String(obj.fileID); + if (fileID !== "0" && fileID !== "4294967295") { + // 0 = null, 4294967295 = missing + fileIDs.add(fileID); + } + } + + // Recursively check all properties + for (const value of Object.values(obj)) { + this._collectFileIDs(value, fileIDs); + } + } + + /** + * Parse Unity .meta file to extract GUID and fileID mappings + */ + private _parseMetaFile(metaContent: string): { guid: string; fileIDToGUID: Map } | null { + try { + const parsed = yaml.load(metaContent) as any; + if (!parsed || !parsed.guid) { + return null; + } + + const fileIDToGUID = new Map(); + const guid = parsed.guid; + + // Unity stores fileID -> GUID mappings in the meta file + // Look for external references in the meta file + if (parsed.ExternalObjects) { + for (const [key, value] of Object.entries(parsed.ExternalObjects)) { + if (value && typeof value === "object" && (value as any).guid) { + fileIDToGUID.set(key, (value as any).guid); + } + } + } + + // Also check for fileIDToRecycleName which maps fileID to GUID + if (parsed.fileIDToRecycleName) { + for (const [fileID, guidOrName] of Object.entries(parsed.fileIDToRecycleName)) { + // Sometimes it's a GUID directly, sometimes it needs lookup + if (typeof guidOrName === "string") { + fileIDToGUID.set(fileID, guidOrName); + } + } + } + + return { + guid, + fileIDToGUID, + }; + } catch (error) { + console.warn("Failed to parse meta file:", error); + return null; + } + } + + /** + * Collect all dependencies for a prefab + */ + private async _collectDependencies(zip: any, prefabPath: string, allEntries: any[]): Promise { + const dependencies: IUnityAssetContext["dependencies"] = { + textures: new Map(), + materials: new Map(), + models: new Map(), + sounds: new Map(), + meta: new Map(), + }; + + // Read prefab YAML + const prefabEntry = zip.getEntry(prefabPath); + if (!prefabEntry) { + return dependencies; + } + + const prefabYaml = prefabEntry.getData().toString("utf8"); + + // Parse Unity YAML to find fileID references + const components = parseUnityYAML(prefabYaml); + + // Build fileID -> GUID mapping from all .meta files FIRST + const fileIDToGUID = new Map(); + const guidToPath = new Map(); + const guidToMeta = new Map(); + + // First pass: collect all meta files and build GUID -> path mapping + for (const entry of allEntries) { + if (entry.entryName.endsWith(".meta") && !entry.isDirectory) { + try { + const metaContent = entry.getData().toString("utf8"); + const meta = this._parseMetaFile(metaContent); + if (meta) { + const assetPath = entry.entryName.replace(/\.meta$/, ""); + guidToPath.set(meta.guid, assetPath); + guidToMeta.set(meta.guid, meta); + + // Map fileID -> GUID from this meta file + for (const [fileID, guid] of meta.fileIDToGUID) { + fileIDToGUID.set(fileID, guid); + } + } + } catch (error) { + console.warn(`Failed to parse meta file ${entry.entryName}:`, error); + } + } + } + + // Collect component IDs (internal references within prefab) + const componentIDs = new Set(components.keys()); + + // Collect all fileID references from components + const allFileIDs = new Set(); + for (const [_id, component] of components) { + this._collectFileIDs(component, allFileIDs); + } + + // Find external fileID references (those that are NOT component IDs but have GUID mappings) + // These are references to external assets (textures, materials, models, etc.) + const externalFileIDs = new Set(); + for (const fileID of allFileIDs) { + // If fileID is not a component ID (internal) and has a GUID mapping, it's an external asset reference + if (!componentIDs.has(fileID) && fileIDToGUID.has(fileID)) { + externalFileIDs.add(fileID); + } + } + + // Also check for direct GUID references in components (m_Texture, m_Material, etc.) + const referencedGUIDs = new Set(); + for (const [_id, component] of components) { + this._collectGUIDReferences(component, referencedGUIDs); + } + + // Collect all assets that are referenced + const collectedGUIDs = new Set(); + for (const fileID of externalFileIDs) { + const guid = fileIDToGUID.get(fileID); + if (guid) { + collectedGUIDs.add(guid); + } + } + for (const guid of referencedGUIDs) { + collectedGUIDs.add(guid); + } + + // Collect dependencies by GUID (ONLY those that are actually referenced) + for (const guid of collectedGUIDs) { + const assetPath = guidToPath.get(guid); + if (!assetPath) { + continue; + } + + const assetEntry = zip.getEntry(assetPath); + if (!assetEntry || assetEntry.isDirectory) { + continue; + } + + const ext = assetPath.split(".").pop()?.toLowerCase(); + const meta = guidToMeta.get(guid); + + if (meta) { + dependencies.meta.set(guid, { path: assetPath, guid }); + + // Categorize by extension + if (ext === "png" || ext === "jpg" || ext === "jpeg" || ext === "tga" || ext === "tiff" || ext === "dds") { + dependencies.textures.set(guid, assetEntry.getData()); + } else if (ext === "mat") { + const matContent = assetEntry.getData().toString("utf8"); + dependencies.materials.set(guid, matContent); + } else if (ext === "fbx" || ext === "obj" || ext === "dae" || ext === "mesh") { + dependencies.models.set(guid, assetEntry.getData()); + } else if (ext === "wav" || ext === "mp3" || ext === "ogg" || ext === "aac") { + dependencies.sounds.set(guid, assetEntry.getData()); + } + } + } + + console.log(`Collected dependencies for ${prefabPath}:`, { + textures: dependencies.textures.size, + materials: dependencies.materials.size, + models: dependencies.models.size, + sounds: dependencies.sounds.size, + meta: dependencies.meta.size, + }); + + return dependencies; + } + + /** + * Collect GUID references from Unity component (m_Texture, m_Material, etc.) + */ + private _collectGUIDReferences(obj: any, guids: Set): void { + if (typeof obj !== "object" || obj === null) { + return; + } + + if (Array.isArray(obj)) { + for (const item of obj) { + this._collectGUIDReferences(item, guids); + } + return; + } + + // Check for GUID fields (Unity uses guid field in some references) + if (obj.guid && typeof obj.guid === "string") { + guids.add(obj.guid); + } + + // Check for common Unity asset reference patterns + if (obj.m_Texture && obj.m_Texture.guid) { + guids.add(obj.m_Texture.guid); + } + if (obj.m_Material && obj.m_Material.guid) { + guids.add(obj.m_Material.guid); + } + if (obj.m_Mesh && obj.m_Mesh.guid) { + guids.add(obj.m_Mesh.guid); + } + + // Recursively check all properties + for (const value of Object.values(obj)) { + this._collectGUIDReferences(value, guids); + } + } + + /** + * Handle import - collect all dependencies and pass to converter + */ + private async _handleImport(): Promise { + const { zipFile, selectedPrefabs } = this.state; + if (!zipFile || selectedPrefabs.size === 0) { + toast.error("No prefabs selected"); + return; + } + + this.setState({ isProcessing: true }); + + try { + // Convert File to Buffer + const arrayBuffer = await zipFile.arrayBuffer(); + const buffer = Buffer.from(arrayBuffer); + + // Load ZIP + const AdmZip = (await import("adm-zip")).default; + const zip = new AdmZip(buffer); + const allEntries = zip.getEntries(); + + // Process each selected prefab + const contexts: IUnityAssetContext[] = []; + const prefabNames: string[] = []; + + for (const prefabPath of Array.from(selectedPrefabs)) { + try { + // Read prefab YAML + const prefabEntry = zip.getEntry(prefabPath); + if (!prefabEntry || prefabEntry.isDirectory) { + continue; + } + + const prefabYaml = prefabEntry.getData().toString("utf8"); + + // Validate YAML content + if (typeof prefabYaml !== "string" || !prefabYaml.trim()) { + console.error(`Invalid prefab YAML for ${prefabPath}`); + toast.error(`Invalid prefab file: ${prefabPath.split("/").pop()}`); + continue; + } + + // Parse Unity YAML here (already done in _collectDependencies, but we need it here too) + const prefabComponents = parseUnityYAML(prefabYaml); + + // Validate parsed components + if (!(prefabComponents instanceof Map)) { + console.error(`Failed to parse prefab components for ${prefabPath}, got:`, typeof prefabComponents); + toast.error(`Failed to parse prefab: ${prefabPath.split("/").pop()}`); + continue; + } + + // Collect all dependencies + const dependencies = await this._collectDependencies(zip, prefabPath, allEntries); + + contexts.push({ + prefabComponents, + dependencies, + }); + + prefabNames.push(prefabPath.split("/").pop()?.replace(".prefab", "") || prefabPath); + } catch (error) { + console.error(`Failed to process prefab ${prefabPath}:`, error); + toast.error(`Failed to process ${prefabPath.split("/").pop()}`); + } + } + + if (contexts.length > 0) { + this.props.onImport(contexts, prefabNames); + this._handleClose(); + } else { + toast.error("No valid prefabs to import"); + } + } catch (error) { + console.error("Failed to import Unity prefabs:", error); + toast.error("Failed to import Unity prefabs"); + } finally { + this.setState({ isProcessing: false }); + } + } + + /** + * Handle close + */ + private _handleClose(): void { + this.setState({ + isDragging: false, + zipFile: null, + treeNodes: [], + selectedPrefabs: new Set(), + isProcessing: false, + searchQuery: "", + }); + this.props.onClose(); + } + + /** + * Format file size + */ + private _formatFileSize(bytes: number): string { + if (bytes < 1024) return `${bytes} B`; + if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; + return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; + } +} diff --git a/editor/src/editor/windows/effect-editor/toolbar.tsx b/editor/src/editor/windows/effect-editor/toolbar.tsx index 085355b24..89b96e466 100644 --- a/editor/src/editor/windows/effect-editor/toolbar.tsx +++ b/editor/src/editor/windows/effect-editor/toolbar.tsx @@ -1,6 +1,17 @@ import { Component, ReactNode } from "react"; -import { Menubar, MenubarContent, MenubarItem, MenubarMenu, MenubarSeparator, MenubarShortcut, MenubarTrigger } from "../../../ui/shadcn/ui/menubar"; +import { + Menubar, + MenubarContent, + MenubarItem, + MenubarMenu, + MenubarSeparator, + MenubarShortcut, + MenubarSub, + MenubarSubContent, + MenubarSubTrigger, + MenubarTrigger, +} from "../../../ui/shadcn/ui/menubar"; import { openSingleFileDialog, saveSingleFileDialog } from "../../../tools/dialog"; import { ToolbarComponent } from "../../../ui/toolbar"; @@ -11,7 +22,15 @@ export interface IEffectEditorToolbarProps { editor: IEffectEditor; } -export class EffectEditorToolbar extends Component { +export interface IEffectEditorToolbarState { + // No state needed - modal is managed by editor +} + +export class EffectEditorToolbar extends Component { + constructor(props: IEffectEditorToolbarProps) { + super(props); + this.state = {}; + } public render(): ReactNode { return ( @@ -38,7 +57,16 @@ export class EffectEditorToolbar extends Component { - this._handleImport()}>Import... + {/* Import Submenu */} + + Import... + + this._handleImportBabylonEffect()}>Babylon Effect JSON + this._handleImportQuarks()}>Quarks JSON + + this._handleImportUnity()}>Unity Assets + + @@ -91,9 +119,12 @@ export class EffectEditorToolbar extends Component { this.props.editor.saveAs(file); } - private _handleImport(): void { + /** + * Handle import Babylon Effect JSON + */ + private _handleImportBabylonEffect(): void { const file = openSingleFileDialog({ - title: "Import Effect File", + title: "Import Babylon Effect JSON", filters: [{ name: "Effect Files", extensions: ["Effect", "json"] }], }); @@ -103,4 +134,27 @@ export class EffectEditorToolbar extends Component { this.props.editor.importFile(file); } + + /** + * Handle import Quarks JSON + */ + private _handleImportQuarks(): void { + const file = openSingleFileDialog({ + title: "Import Quarks JSON", + filters: [{ name: "Quarks Files", extensions: ["json"] }], + }); + + if (!file) { + return; + } + + this.props.editor.importFile(file); + } + + /** + * Handle import Unity assets (open modal) + */ + private _handleImportUnity(): void { + this.props.editor.openUnityImportModal(); + } } diff --git a/editor/src/ui/shadcn/ui/scroll-area.tsx b/editor/src/ui/shadcn/ui/scroll-area.tsx new file mode 100644 index 000000000..541bed078 --- /dev/null +++ b/editor/src/ui/shadcn/ui/scroll-area.tsx @@ -0,0 +1,37 @@ +import * as React from "react"; +import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"; + +import { cn } from "../../utils"; + +const ScrollArea = React.forwardRef, React.ComponentPropsWithoutRef>( + ({ className, children, ...props }, ref) => ( + + {children} + + + + ) +); +ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName; + +const ScrollBar = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, orientation = "vertical", ...props }, ref) => ( + + + +)); +ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName; + +export { ScrollArea, ScrollBar }; diff --git a/tools/src/effect/behaviors/colorBySpeed.ts b/tools/src/effect/behaviors/colorBySpeed.ts index 045d3755a..e737a51f7 100644 --- a/tools/src/effect/behaviors/colorBySpeed.ts +++ b/tools/src/effect/behaviors/colorBySpeed.ts @@ -1,13 +1,12 @@ import type { IColorBySpeedBehavior } from "../types/behaviors"; import type { Particle } from "babylonjs"; -import type { EffectParticleSystem } from "../systems/effectParticleSystem"; import { interpolateColorKeys } from "./utils"; /** * Apply ColorBySpeed behavior to ParticleSystem (per-particle) * Uses unified IColorFunction structure: behavior.color = { colorFunctionType, data } */ -export function applyColorBySpeedPS(particleSystem: EffectParticleSystem, behavior: IColorBySpeedBehavior, particle: Particle): void { +export function applyColorBySpeedPS(behavior: IColorBySpeedBehavior, particle: Particle): void { // New structure: behavior.color.data.colorKeys if (!behavior.color || !behavior.color.data?.colorKeys || !particle.color || !particle.direction) { return; diff --git a/tools/src/effect/effect.ts b/tools/src/effect/effect.ts index 5f2eea98c..d86f826b8 100644 --- a/tools/src/effect/effect.ts +++ b/tools/src/effect/effect.ts @@ -1,51 +1,17 @@ -import { Scene, Tools, IDisposable, TransformNode, MeshBuilder, Texture, Color4, AbstractMesh } from "babylonjs"; -import type { IQuarksJSON } from "./types/quarksTypes"; -import type { ILoaderOptions } from "./types/loader"; -import { Parser } from "./parsers/parser"; +import { Scene, IDisposable, TransformNode, MeshBuilder, Texture, Color4, AbstractMesh, Tools } from "babylonjs"; import { EffectParticleSystem } from "./systems/effectParticleSystem"; import { EffectSolidParticleSystem } from "./systems/effectSolidParticleSystem"; -import type { IGroup, IEmitter, IData } from "./types/hierarchy"; -import type { IParticleSystemConfig } from "./types/emitter"; -import { isSystem } from "./types/system"; - -/** - * Effect Node - represents either a particle system or a group - */ -export interface IEffectNode { - /** Node name */ - name: string; - /** Node UUID from original JSON */ - uuid?: string; - /** Particle system (if this is a particle emitter) */ - system?: EffectParticleSystem | EffectSolidParticleSystem; - /** Transform node (if this is a group) */ - group?: TransformNode; - /** Parent node */ - parent?: IEffectNode; - /** Child nodes */ - children: IEffectNode[]; - /** Node type */ - type: "particle" | "group"; -} +import { IGroup, IEmitter, IData, isSystem, IEffectNode, ILoaderOptions, IParticleSystemConfig } from "./types"; +import { NodeFactory } from "./factories"; /** * Effect containing multiple particle systems with hierarchy support * Main entry point for loading and creating from Three.js particle JSON files */ export class Effect implements IDisposable { - /** All particle systems in this effect */ - private _systems: (EffectParticleSystem | EffectSolidParticleSystem)[] = []; - /** Root node of the effect hierarchy */ private _root: IEffectNode | null = null; - /** - * Get all particle systems in this effect - */ - public get systems(): ReadonlyArray { - return this._systems; - } - /** * Get root node of the effect hierarchy */ @@ -65,170 +31,26 @@ export class Effect implements IDisposable { /** Map of groups by UUID */ private readonly _groupsByUuid = new Map(); - /** All nodes in the hierarchy */ - private readonly _nodes = new Map(); - /** Scene reference for creating new systems */ private _scene: Scene | null = null; /** - * Load a Three.js particle JSON file and create particle systems - * @param url URL to the JSON file - * @param scene The Babylon.js scene - * @param rootUrl Root URL for loading textures - * @param options Optional parsing options - * @returns Promise that resolves to a Effect - */ - public static async LoadAsync(url: string, scene: Scene, rootUrl: string = "", options?: ILoaderOptions): Promise { - return new Promise((resolve, reject) => { - Tools.LoadFile( - url, - (data) => { - try { - const jsonData = JSON.parse(data.toString()); - const effect = Effect.Parse(jsonData, scene, rootUrl, options); - resolve(effect); - } catch (error) { - reject(error); - } - }, - undefined, - undefined, - undefined, - (error) => { - reject(error); - } - ); - }); - } - - /** - * Parse a Three.js particle JSON file and create Babylon.js particle systems - * @param jsonData The Three.js JSON data - * @param scene The Babylon.js scene - * @param rootUrl Root URL for loading textures + * Create Effect from IData + * + * + * @param data IData structure (required) + * @param scene Babylon.js scene (required) + * @param rootUrl Root URL for loading textures (optional) * @param options Optional parsing options - * @returns A Effect containing all particle systems */ - public static Parse(jsonData: IQuarksJSON, scene: Scene, rootUrl: string = "", options?: ILoaderOptions): Effect { - return new Effect(jsonData, scene, rootUrl, options); - } - - /** - * Create a Effect directly from JSON data - * @param jsonData The Three.js JSON data - * @param scene The Babylon.js scene - * @param rootUrl Root URL for loading textures - * @param options Optional parsing options - */ - constructor(jsonData?: IQuarksJSON, scene?: Scene, rootUrl: string = "", options?: ILoaderOptions) { - this._scene = scene || null; - if (jsonData && scene) { - const parser = new Parser(scene, rootUrl, jsonData, options); - const parseResult = parser.parse(); - - this._systems.push(...parseResult.systems); - if (parseResult.data && parseResult.groupNodesMap) { - this._buildHierarchy(parseResult.data, parseResult.groupNodesMap, parseResult.systems); - } - } else if (scene) { - // Create empty effect with root group - this._scene = scene; - this._createEmptyEffect(); - } - } - - /** - * Build hierarchy from data and group nodes map - * Handles errors gracefully and continues building partial hierarchy if errors occur - */ - private _buildHierarchy(Data: IData, groupNodesMap: Map, systems: (EffectParticleSystem | EffectSolidParticleSystem)[]): void { - if (!Data || !Data.root) { - return; + constructor(data: IData, scene: Scene, rootUrl: string = "", options: ILoaderOptions) { + if (!data || !scene) { + throw new Error("Effect constructor requires IData and Scene"); } - try { - // Create nodes from hierarchy - this._root = this._buildNodeFromHierarchy(Data.root, null, groupNodesMap, systems); - } catch (error) { - // Log error but don't throw - effect can still work with partial hierarchy - console.error(`Failed to build hierarchy: ${error instanceof Error ? error.message : String(error)}`); - } - } - - /** - * Recursively build nodes from hierarchy - */ - private _buildNodeFromHierarchy( - obj: IGroup | IEmitter, - parent: IEffectNode | null, - groupNodesMap: Map, - systems: (EffectParticleSystem | EffectSolidParticleSystem)[] - ): IEffectNode | null { - if (!obj) { - return null; - } - - try { - const node: IEffectNode = { - name: obj.name, - uuid: obj.uuid, - parent: parent || undefined, - children: [], - type: "config" in obj ? "particle" : "group", - }; - - if (node.type === "particle") { - // Find system by name - const emitter = obj as IEmitter; - const system = systems.find((s) => s.name === emitter.name); - if (system) { - node.system = system; - this._systemsByName.set(emitter.name, system); - if (emitter.uuid) { - this._systemsByUuid.set(emitter.uuid, system); - } - } - } else { - // Find group TransformNode - const group = obj as IGroup; - const groupNode = group.uuid ? groupNodesMap.get(group.uuid) : null; - if (groupNode) { - node.group = groupNode; - this._groupsByName.set(group.name, groupNode); - if (group.uuid) { - this._groupsByUuid.set(group.uuid, groupNode); - } - } - } - - // Process children with error handling - if ("children" in obj && obj.children) { - for (const child of obj.children) { - try { - const childNode = this._buildNodeFromHierarchy(child, node, groupNodesMap, systems); - if (childNode) { - node.children.push(childNode); - } - } catch (error) { - // Log error but continue processing other children - console.warn(`Failed to build child node ${child.name}: ${error instanceof Error ? error.message : String(error)}`); - } - } - } - - // Store node - if (obj.uuid) { - this._nodes.set(obj.uuid, node); - } - this._nodes.set(obj.name, node); - - return node; - } catch (error) { - // Log error but return null to continue building other parts of hierarchy - console.error(`Failed to build node ${obj.name}: ${error instanceof Error ? error.message : String(error)}`); - return null; - } + this._scene = scene; + const nodeFactory = new NodeFactory(scene, data, rootUrl, options); + this._root = nodeFactory.create(); } /** @@ -477,25 +299,6 @@ export class Effect implements IDisposable { } } - /** - * Apply prewarm to systems that have it enabled - * Should be called after hierarchy is built and all systems are created - * Uses Babylon.js built-in prewarm properties for ParticleSystem - */ - public applyPrewarm(): void { - for (const system of this._systems) { - if (system instanceof EffectParticleSystem && system.preWarmCycles > 0) { - // ParticleSystem uses native preWarmCycles/preWarmStepOffset - // Already configured via config.preWarmCycles, nothing more needed - } else if (system instanceof EffectSolidParticleSystem && system.preWarmCycles > 0) { - // For SolidParticleSystem, we need to manually simulate prewarm - // Start the system and let it run for duration - // Note: SPS doesn't have built-in prewarm, so we'll start it normally - // The prewarm effect will be visible when system starts - } - } - } - /** * Check if any system is started */ @@ -515,33 +318,6 @@ export class Effect implements IDisposable { return false; } - /** - * Create empty effect with root group - */ - private _createEmptyEffect(): void { - if (!this._scene) { - return; - } - - const rootGroup = new TransformNode("Root", this._scene); - const rootUuid = Tools.RandomId(); - rootGroup.id = rootUuid; - - const rootNode: IEffectNode = { - name: "Root", - uuid: rootUuid, - group: rootGroup, - children: [], - type: "group", - }; - - this._root = rootNode; - this._groupsByName.set("Root", rootGroup); - this._groupsByUuid.set(rootUuid, rootGroup); - this._nodes.set(rootUuid, rootNode); - this._nodes.set("Root", rootNode); - } - /** * Create a new group node * @param parentNode Parent node (if null, adds to root) diff --git a/tools/src/effect/factories/geometryFactory.ts b/tools/src/effect/factories/geometryFactory.ts index a86a6b9f3..e50e5c456 100644 --- a/tools/src/effect/factories/geometryFactory.ts +++ b/tools/src/effect/factories/geometryFactory.ts @@ -12,7 +12,7 @@ export class GeometryFactory implements IGeometryFactory { private _logger: Logger; private _data: IData; - constructor(data: IData, options: ILoaderOptions) { + constructor(data: IData, options?: ILoaderOptions) { this._data = data; this._logger = new Logger("[GeometryFactory]", options); } @@ -20,12 +20,12 @@ export class GeometryFactory implements IGeometryFactory { /** * Create a mesh from geometry ID */ - public createMesh(geometryId: string, name: string, scene: Scene): Nullable { + public createMesh(geometryId: string, name: string, scene: Scene): Mesh { this._logger.log(`Creating mesh from geometry ID: ${geometryId}, name: ${name}`); const geometryData = this._findGeometry(geometryId); if (!geometryData) { - return null; + return new Mesh(name, scene); } const geometryName = geometryData.type || geometryId; @@ -34,7 +34,7 @@ export class GeometryFactory implements IGeometryFactory { const mesh = this._createMeshFromGeometry(geometryData, name, scene); if (!mesh) { this._logger.warn(`Failed to create mesh from geometry ${geometryId}`); - return null; + return new Mesh(name, scene); } return mesh; @@ -44,7 +44,7 @@ export class GeometryFactory implements IGeometryFactory { * Create or load particle mesh for SPS * Tries to load geometry if specified, otherwise creates default plane */ - public createParticleMesh(config: { instancingGeometry?: string }, name: string, scene: Scene): Nullable { + public createParticleMesh(config: { instancingGeometry?: string }, name: string, scene: Scene): Mesh { let particleMesh = this._loadParticleGeometry(config, name, scene); if (!particleMesh) { diff --git a/tools/src/effect/factories/index.ts b/tools/src/effect/factories/index.ts index 91f5e73be..974bb9ae1 100644 --- a/tools/src/effect/factories/index.ts +++ b/tools/src/effect/factories/index.ts @@ -1,3 +1,3 @@ export { MaterialFactory } from "./materialFactory"; export { GeometryFactory } from "./geometryFactory"; -export { SystemFactory } from "./systemFactory"; +export { NodeFactory } from "./nodeFactory"; diff --git a/tools/src/effect/factories/materialFactory.ts b/tools/src/effect/factories/materialFactory.ts index dff791944..43b41af9a 100644 --- a/tools/src/effect/factories/materialFactory.ts +++ b/tools/src/effect/factories/materialFactory.ts @@ -13,8 +13,7 @@ export class MaterialFactory implements IMaterialFactory { private _scene: Scene; private _data: IData; private _rootUrl: string; - - constructor(scene: Scene, data: IData, rootUrl: string, options: ILoaderOptions) { + constructor(scene: Scene, data: IData, rootUrl: string, options?: ILoaderOptions) { this._scene = scene; this._data = data; this._rootUrl = rootUrl; @@ -24,10 +23,10 @@ export class MaterialFactory implements IMaterialFactory { /** * Create a texture from material ID (for ParticleSystem - no material needed) */ - public createTexture(materialId: string): Nullable { + public createTexture(materialId: string): BabylonTexture { const textureData = this._resolveTextureData(materialId); if (!textureData) { - return null; + return new BabylonTexture(materialId, this._scene); } const { texture, image } = textureData; @@ -184,12 +183,12 @@ export class MaterialFactory implements IMaterialFactory { /** * Create a material with texture from material ID */ - public createMaterial(materialId: string, name: string): Nullable { + public createMaterial(materialId: string, name: string): PBRMaterial { this._logger.log(`Creating material for ID: ${materialId}, name: ${name}`); const textureData = this._resolveTextureData(materialId); if (!textureData) { - return null; + return new PBRMaterial(name + "_material", this._scene); } const { material, texture, image } = textureData; diff --git a/tools/src/effect/factories/systemFactory.ts b/tools/src/effect/factories/nodeFactory.ts similarity index 51% rename from tools/src/effect/factories/systemFactory.ts rename to tools/src/effect/factories/nodeFactory.ts index 653d29b08..7e17e1b90 100644 --- a/tools/src/effect/factories/systemFactory.ts +++ b/tools/src/effect/factories/nodeFactory.ts @@ -1,201 +1,136 @@ -import { Nullable, Vector3, TransformNode, Scene, AbstractMesh } from "babylonjs"; -import { EffectParticleSystem } from "../systems/effectParticleSystem"; -import { EffectSolidParticleSystem } from "../systems/effectSolidParticleSystem"; -import type { IData, IGroup, IEmitter, ITransform } from "../types/hierarchy"; -import type { IParticleSystemConfig } from "../types/emitter"; +import { Nullable, Vector3, TransformNode, Scene, AbstractMesh, Tools } from "babylonjs"; +import { EffectParticleSystem, EffectSolidParticleSystem } from "../systems"; +import { IData, IGroup, IEmitter, ITransform, IParticleSystemConfig, ILoaderOptions, IMaterialFactory, IGeometryFactory, IEffectNode, isSystem } from "../types"; import { Logger } from "../loggers/logger"; -import { MatrixUtils } from "../utils/matrixUtils"; -import type { IMaterialFactory, IGeometryFactory } from "../types/factories"; -import type { ILoaderOptions } from "../types/loader"; -import { CapacityCalculator } from "../utils/capacityCalculator"; -import { ValueUtils } from "../utils/valueParser"; - +import { CapacityCalculator, ValueUtils, MatrixUtils } from "../utils"; +import { MaterialFactory } from "./materialFactory"; +import { GeometryFactory } from "./geometryFactory"; /** * Factory for creating particle systems from data * Creates all nodes, sets parents, and applies transformations in a single pass */ -export class SystemFactory { +export class NodeFactory { private _logger: Logger; private _scene: Scene; - private _groupNodesMap: Map; + private _data: IData; + private _materialFactory: IMaterialFactory; private _geometryFactory: IGeometryFactory; - constructor(scene: Scene, options: ILoaderOptions, groupNodesMap: Map, materialFactory: IMaterialFactory, geometryFactory: IGeometryFactory) { + constructor(scene: Scene, data: IData, rootUrl: string, options?: ILoaderOptions) { this._scene = scene; - this._groupNodesMap = groupNodesMap; + this._data = data; this._logger = new Logger("[SystemFactory]", options); - this._materialFactory = materialFactory; - this._geometryFactory = geometryFactory; + this._materialFactory = new MaterialFactory(scene, data, rootUrl, options); + this._geometryFactory = new GeometryFactory(data, options); } /** * Create particle systems from data * Creates all nodes, sets parents, and applies transformations in one pass */ - public createSystems(data: IData): (EffectParticleSystem | EffectSolidParticleSystem)[] { - if (!data.root) { + public create(): IEffectNode { + if (!this._data.root) { this._logger.warn("No root object found in data"); - return []; - } - - this._logger.log("Processing hierarchy: creating nodes, setting parents, and applying transformations"); - const particleSystems: (EffectParticleSystem | EffectSolidParticleSystem)[] = []; - this._processObject(data.root, null, 0, particleSystems, data); - return particleSystems; + const rootGroup = new TransformNode("Root", this._scene); + const rootUuid = Tools.RandomId(); + rootGroup.id = rootUuid; + + const rootNode: IEffectNode = { + name: "Root", + uuid: rootUuid, + data: rootGroup, + children: [], + type: "group", + }; + return rootNode; + } + return this._createNode(this._data.root, null); } - /** * Recursively process object hierarchy * Creates nodes, sets parents, and applies transformations in one pass */ - private _processObject( - obj: IGroup | IEmitter, - parentGroup: Nullable, - depth: number, - particleSystems: (EffectParticleSystem | EffectSolidParticleSystem)[], - data: IData - ): void { - this._logger.log(`${" ".repeat(depth)}Processing object: ${obj.name}`); - - if (this._isGroup(obj)) { - this._processGroup(obj, parentGroup, depth, particleSystems, data); - } else { - this._processEmitter(obj, parentGroup, depth, particleSystems); - } - } - - /** - * Process a Group object - */ - private _processGroup( - group: IGroup, - parentGroup: Nullable, - depth: number, - particleSystems: (EffectParticleSystem | EffectSolidParticleSystem)[], - data: IData - ): void { - const groupNode = this._createGroupNode(group, parentGroup, depth); - this._processChildren(group.children, groupNode, depth, particleSystems, data); - } + private _createNode(obj: IGroup | IEmitter, parentNode: IEffectNode | null): IEffectNode { + this._logger.log(`Processing object: ${obj.name}`); - /** - * Process a Emitter object - */ - private _processEmitter(emitter: IEmitter, parentGroup: Nullable, depth: number, particleSystems: (EffectParticleSystem | EffectSolidParticleSystem)[]): void { - const particleSystem = this._createEffectSystem(emitter, parentGroup, depth); - if (particleSystem) { - particleSystems.push(particleSystem); + if ("children" in obj && obj.children) { + const groupNode = this._createGroupNode(obj as IGroup, parentNode); + groupNode.children = this._createChildrenNodes(obj.children, groupNode); + return groupNode; + } else { + const emitterNode = this._createParticleNode(obj as IEmitter, parentNode); + return emitterNode; } } /** * Process children of a group recursively */ - private _processChildren( - children: (IGroup | IEmitter)[] | undefined, - parentGroup: TransformNode, - depth: number, - particleSystems: (EffectParticleSystem | EffectSolidParticleSystem)[], - data: IData - ): void { + private _createChildrenNodes(children: (IGroup | IEmitter)[] | undefined, parentNode: IEffectNode | null): IEffectNode[] { if (!children || children.length === 0) { - return; + return []; } - this._logger.log(`${" ".repeat(depth)}Processing ${children.length} children`); - children.forEach((child) => { - this._processObject(child, parentGroup, depth + 1, particleSystems, data); + this._logger.log(`Processing ${children.length} children for parent node: ${parentNode?.name || "none"}`); + return children.map((child) => { + return this._createNode(child, parentNode); }); } /** * Create a TransformNode for a Group */ - private _createGroupNode(group: IGroup, parentGroup: Nullable, depth: number): TransformNode { - const groupNode = new TransformNode(group.name, this._scene); - groupNode.id = group.uuid; + private _createGroupNode(group: IGroup, parentNode: IEffectNode | null): IEffectNode { + const transformNode = new TransformNode(group.name, this._scene); + transformNode.id = group.uuid; + const node: IEffectNode = { + name: group.name, + uuid: group.uuid, + children: [], + data: transformNode, + type: "group", + }; - this._applyTransform(groupNode, group.transform, depth); - this._setParent(groupNode, parentGroup, depth); + this._applyTransform(node, group.transform); - // Store in map for potential future reference - this._groupNodesMap.set(group.uuid, groupNode); + if (parentNode) { + this._setParent(node, parentNode); + } - this._logger.log(`${" ".repeat(depth)}Created group node: ${group.name}`); - return groupNode; + this._logger.log(`Created group node: ${group.name}`); + return node; } /** * Create a particle system from a Emitter */ - private _createEffectSystem(emitter: IEmitter, parentGroup: Nullable, depth: number): Nullable { - const indent = " ".repeat(depth); - const parentName = parentGroup ? parentGroup.name : "none"; - this._logger.log(`${indent}Processing emitter: ${emitter.name} (parent: ${parentName})`); - - try { - const config = emitter.config; - if (!config) { - this._logger.warn(`${indent}Emitter ${emitter.name} has no config, skipping`); - return null; - } + private _createParticleNode(emitter: IEmitter, parentNode: IEffectNode | null): IEffectNode { + const parentName = parentNode ? parentNode.name : "none"; + const systemType = emitter.systemType; + this._logger.log(`Processing emitter: ${emitter.name} (parent: ${parentName})`); - const isLooping = config.targetStopDuration === 0; - this._logger.log(`${indent} Config: targetStopDuration=${config.targetStopDuration}, looping=${isLooping}, systemType=${emitter.systemType}`); + // const cumulativeScale = this._calculateCumulativeScale(parentGroup); - const cumulativeScale = this._calculateCumulativeScale(parentGroup); - this._logger.log(`${indent}Cumulative scale: (${cumulativeScale.x.toFixed(2)}, ${cumulativeScale.y.toFixed(2)}, ${cumulativeScale.z.toFixed(2)})`); + let particleSystem: EffectParticleSystem | EffectSolidParticleSystem; - // Use systemType from emitter (determined during conversion) - const systemType = emitter.systemType || "base"; - this._logger.log(`Using ${systemType === "solid" ? "SolidParticleSystem" : "ParticleSystem"}`); - - let particleSystem: EffectParticleSystem | EffectSolidParticleSystem | null = null; + if (systemType === "solid") { + particleSystem = this._createEffectSolidParticleSystem(emitter, parentNode); + } else { + particleSystem = this._createEffectParticleSystem(emitter, parentNode); + } - try { - if (systemType === "solid") { - particleSystem = this._createEffectSolidParticleSystem(emitter, parentGroup); - } else { - particleSystem = this._createEffectParticleSystem(emitter, parentGroup, cumulativeScale, depth); - } - } catch (error) { - this._logger.error(`${indent}Failed to create ${systemType} system for emitter ${emitter.name}: ${error instanceof Error ? error.message : String(error)}`); - return null; - } + const node: IEffectNode = { + name: emitter.name, + uuid: emitter.uuid, + children: [], + data: particleSystem, + type: "particle", + }; - if (!particleSystem) { - this._logger.warn(`${indent}Failed to create particle system for emitter: ${emitter.name}`); - return null; - } + this._logger.log(`Created particle system: ${emitter.name}`); - // Apply transform to particle system - try { - if (particleSystem instanceof EffectSolidParticleSystem) { - // For SPS, transform is applied to the mesh - if (particleSystem.mesh) { - this._applyTransform(particleSystem.mesh, emitter.transform, depth); - this._setParent(particleSystem.mesh, parentGroup, depth); - } - } else if (particleSystem instanceof EffectParticleSystem) { - // For PS, transform is applied to the emitter mesh - const emitterNode = particleSystem.getParentNode(); - if (emitterNode) { - this._applyTransform(emitterNode, emitter.transform, depth); - this._setParent(emitterNode, parentGroup, depth); - } - } - } catch (error) { - this._logger.warn(`${indent}Failed to apply transform to system ${emitter.name}: ${error instanceof Error ? error.message : String(error)}`); - // Continue - system is created, just transform failed - } - - this._logger.log(`${indent}Created particle system: ${emitter.name}`); - return particleSystem; - } catch (error) { - this._logger.error(`${indent}Unexpected error creating particle system ${emitter.name}: ${error instanceof Error ? error.message : String(error)}`); - return null; - } + return node; } /** @@ -341,11 +276,12 @@ export class SystemFactory { * Apply emission bursts by converting them to emit rate gradients * Unified approach for both ParticleSystem and SolidParticleSystem */ - private _applyEmissionBursts(system: EffectParticleSystem | EffectSolidParticleSystem, config: IParticleSystemConfig, duration: number): void { + private _applyEmissionBursts(system: EffectParticleSystem | EffectSolidParticleSystem, config: IParticleSystemConfig): void { if (!config.emissionBursts || config.emissionBursts.length === 0) { return; } + const duration = config.targetStopDuration !== undefined && config.targetStopDuration > 0 ? config.targetStopDuration : 5; const baseEmitRate = config.emitRate || 10; for (const burst of config.emissionBursts) { if (burst.time !== undefined && burst.count !== undefined) { @@ -366,7 +302,7 @@ export class SystemFactory { /** * Create a ParticleSystem instance */ - private _createEffectParticleSystem(emitter: IEmitter, _parentGroup: Nullable, cumulativeScale: Vector3, _depth: number): Nullable { + private _createEffectParticleSystem(emitter: IEmitter, parentNode: IEffectNode | null): EffectParticleSystem { const { name, config } = emitter; this._logger.log(`Creating ParticleSystem: ${name}`); @@ -413,16 +349,18 @@ export class SystemFactory { this._applyCommonOptions(particleSystem, config); // Apply emission bursts (converted to gradients) - this._applyEmissionBursts(particleSystem, config, duration); + this._applyEmissionBursts(particleSystem, config); // ParticleSystem-specific: billboard mode if (config.billboardMode !== undefined) { particleSystem.billboardMode = config.billboardMode; } - // === Создание emitter === - const rotationMatrix = emitter.matrix ? MatrixUtils.extractRotationMatrix(emitter.matrix) : null; - particleSystem.configureEmitterFromShape(config.shape, cumulativeScale, rotationMatrix); + // // === Создание emitter === + // const rotationMatrix = emitter.matrix ? MatrixUtils.extractRotationMatrix(emitter.matrix) : null; + if (config.shape) { + particleSystem.configureEmitterFromShape(config.shape); + } this._logger.log(`ParticleSystem created: ${name}`); return particleSystem; @@ -431,20 +369,14 @@ export class SystemFactory { /** * Create a SolidParticleSystem instance */ - private _createEffectSolidParticleSystem(emitter: IEmitter, parentGroup: Nullable): Nullable { + private _createEffectSolidParticleSystem(emitter: IEmitter, parentNode: IEffectNode | null): EffectSolidParticleSystem { const { name, config } = emitter; this._logger.log(`Creating SolidParticleSystem: ${name}`); // Create or load particle mesh const particleMesh = this._geometryFactory.createParticleMesh(config, name, this._scene); - if (!particleMesh) { - return null; - } - // Apply material if provided - // Note: Vertex colors are automatically used by PBR materials if mesh has vertex colors - // The SPS mesh will have vertex colors because _computeParticleColor is enabled if (emitter.materialId) { const material = this._materialFactory.createMaterial(emitter.materialId, name); if (material) { @@ -452,39 +384,19 @@ export class SystemFactory { } } - // Create SPS instance (simple constructor) const sps = new EffectSolidParticleSystem(name, this._scene, { updatable: true, - isPickable: false, - enableDepthSort: false, - particleIntersection: false, - useModelMaterial: true, }); - // Set particle mesh and emitter (like ParticleSystem interface) - sps.particleMesh = particleMesh; - if (parentGroup) { - sps.emitter = parentGroup as AbstractMesh; - } - - // Apply common properties and gradients this._applyCommonProperties(sps, config); this._applyGradients(sps, config); - - // Apply common rendering and behavior options this._applyCommonOptions(sps, config); + this._applyEmissionBursts(sps, config); - // Apply emission bursts (converted to gradients) - const duration = config.targetStopDuration !== undefined && config.targetStopDuration > 0 ? config.targetStopDuration : 5; - this._applyEmissionBursts(sps, config, duration); - - // === SolidParticleSystem-specific properties === - // Distance-based emission if (config.emissionOverDistance !== undefined) { sps.emissionOverDistance = config.emissionOverDistance; } - // === Создание emitter === sps.configureEmitterFromShape(config.shape); this._logger.log(`SolidParticleSystem created: ${name}`); @@ -508,56 +420,48 @@ export class SystemFactory { return cumulativeScale; } - // Type guards - private _isGroup(Obj: IGroup | IEmitter): Obj is IGroup { - return "children" in Obj; - } - /** * Apply transform to a node */ - private _applyTransform(node: TransformNode, transform: ITransform, depth: number): void { + private _applyTransform(node: IEffectNode, transform: ITransform): void { if (!transform) { this._logger.warn(`Transform is undefined for node: ${node.name}`); return; } - if (transform.position && node.position) { - node.position.copyFrom(transform.position); - } + if (!isSystem(node.data)) { + if (transform.position && node.data.position) { + node.data.position.copyFrom(transform.position); + } - if (transform.rotation) { - node.rotationQuaternion = transform.rotation.clone(); - } + if (transform.rotation) { + node.data.rotationQuaternion = transform.rotation.clone(); + } - if (transform.scale && node.scaling) { - node.scaling.copyFrom(transform.scale); + if (transform.scale && node.data.scaling) { + node.data.scaling.copyFrom(transform.scale); + } } - if (transform.position && transform.scale) { - const indent = " ".repeat(depth); - this._logger.log( - `${indent}Applied transform: pos=(${transform.position.x.toFixed(2)}, ${transform.position.y.toFixed(2)}, ${transform.position.z.toFixed(2)}), scale=(${transform.scale.x.toFixed(2)}, ${transform.scale.y.toFixed(2)}, ${transform.scale.z.toFixed(2)})` - ); - } + this._logger.log( + `Applied transform: pos=(${transform.position.x.toFixed(2)}, ${transform.position.y.toFixed(2)}, ${transform.position.z.toFixed(2)}), scale=(${transform.scale.x.toFixed(2)}, ${transform.scale.y.toFixed(2)}, ${transform.scale.z.toFixed(2)})` + ); } /** * Set parent for a node */ - private _setParent(node: TransformNode | any, parent: Nullable, depth: number): void { - if (!parent || !node) { + private _setParent(node: IEffectNode, parent: IEffectNode | null): void { + if (!parent) { return; } - - // Check if node has setParent method (TransformNode, AbstractMesh, etc.) - if (typeof node.setParent === "function") { - node.setParent(parent, false, true); - const indent = " ".repeat(depth); - this._logger.log(`${indent}Set parent: ${node.name || "unknown"} -> ${parent.name}`); + if (isSystem(parent.data)) { + // to-do emmiter as vector3 + node.data.setParent(parent.data.emitter as AbstractMesh | null); } else { - const indent = " ".repeat(depth); - this._logger.warn(`${indent}Node does not support setParent: ${node.constructor?.name || "unknown"}`); + node.data.setParent(parent.data); } + + this._logger.log(`Set parent: ${node.name} -> ${parent?.name || "none"}`); } } diff --git a/tools/src/effect/parsers/dataConverter.ts b/tools/src/effect/parsers/dataConverter.ts deleted file mode 100644 index 0a48afd7f..000000000 --- a/tools/src/effect/parsers/dataConverter.ts +++ /dev/null @@ -1,1118 +0,0 @@ -import { Vector3, Matrix, Quaternion, Color3, Texture as BabylonTexture, ParticleSystem, Color4 } from "babylonjs"; -import type { ILoaderOptions } from "../types/loader"; -import type { - IQuarksJSON, - IQuarksMaterial, - IQuarksTexture, - IQuarksImage, - IQuarksGeometry, - IQuarksObject, - IQuarksParticleEmitterConfig, - IQuarksBehavior, - IQuarksValue, - IQuarksColor, - IQuarksRotation, - IQuarksGradientKey, - IQuarksShape, - IQuarksColorOverLifeBehavior, - IQuarksGradientColor, - IQuarksConstantColorColor, - IQuarksRandomColorBetweenGradient, - IQuarksSizeOverLifeBehavior, - IQuarksRotationOverLifeBehavior, - IQuarksForceOverLifeBehavior, - IQuarksGravityForceBehavior, - IQuarksSpeedOverLifeBehavior, - IQuarksFrameOverLifeBehavior, - IQuarksLimitSpeedOverLifeBehavior, - IQuarksColorBySpeedBehavior, - IQuarksSizeBySpeedBehavior, - IQuarksRotationBySpeedBehavior, - IQuarksOrbitOverLifeBehavior, -} from "../types/quarksTypes"; -import type { ITransform, IGroup, IEmitter, IData } from "../types/hierarchy"; -import type { IMaterial, ITexture, IImage, IGeometry, IGeometryData } from "../types/resources"; -import type { IParticleSystemConfig } from "../types/emitter"; -import type { - Behavior, - IColorFunction, - ISizeOverLifeBehavior, - IForceOverLifeBehavior, - ISpeedOverLifeBehavior, - ILimitSpeedOverLifeBehavior, - ISizeBySpeedBehavior, -} from "../types/behaviors"; -import type { Value } from "../types/values"; -import type { IGradientKey } from "../types/gradients"; -import type { IShape } from "../types/shapes"; -import { Logger } from "../loggers/logger"; - -/** - * Converts IQuarks/Three.js JSON (right-handed) to Babylon.js format (left-handed) - * All coordinate system conversions happen here, once - */ -export class DataConverter { - private _logger: Logger; - - constructor(options?: ILoaderOptions) { - this._logger = new Logger("[DataConverter]", options); - } - - /** - * Convert IQuarks/Three.js JSON to Babylon.js format - * Handles errors gracefully and returns partial data if conversion fails - */ - public convert(IQuarksData: IQuarksJSON): IData { - this._logger.log("=== Converting IQuarks to Babylon.js format ==="); - - const groups = new Map(); - const emitters = new Map(); - - let root: IGroup | IEmitter | null = null; - - try { - if (IQuarksData.object) { - root = this._convertObject(IQuarksData.object, null, groups, emitters, 0); - } - } catch (error) { - this._logger.error(`Failed to convert root object: ${error instanceof Error ? error.message : String(error)}`); - } - - // Convert all resources with error handling - let materials: IMaterial[] = []; - let textures: ITexture[] = []; - let images: IImage[] = []; - let geometries: IGeometry[] = []; - - try { - materials = this._convertMaterials(IQuarksData.materials || []); - } catch (error) { - this._logger.error(`Failed to convert materials: ${error instanceof Error ? error.message : String(error)}`); - } - - try { - textures = this._convertTextures(IQuarksData.textures || []); - } catch (error) { - this._logger.error(`Failed to convert textures: ${error instanceof Error ? error.message : String(error)}`); - } - - try { - images = this._convertImages(IQuarksData.images || []); - } catch (error) { - this._logger.error(`Failed to convert images: ${error instanceof Error ? error.message : String(error)}`); - } - - try { - geometries = this._convertGeometries(IQuarksData.geometries || []); - } catch (error) { - this._logger.error(`Failed to convert geometries: ${error instanceof Error ? error.message : String(error)}`); - } - - this._logger.log( - `=== Conversion complete. Groups: ${groups.size}, Emitters: ${emitters.size}, Materials: ${materials.length}, Textures: ${textures.length}, Images: ${images.length}, Geometries: ${geometries.length} ===` - ); - - return { - root, - groups, - emitters, - materials, - textures, - images, - geometries, - }; - } - - /** - * Convert a IQuarks/Three.js object to Babylon.js format - */ - private _convertObject(obj: IQuarksObject, parentUuid: string | null, groups: Map, emitters: Map, depth: number): IGroup | IEmitter | null { - const indent = " ".repeat(depth); - - if (!obj || typeof obj !== "object") { - return null; - } - - this._logger.log(`${indent}Converting object: ${obj.type || "unknown"} (name: ${obj.name || "unnamed"})`); - - // Convert transform from right-handed to left-handed - const transform = this._convertTransform(obj.matrix, obj.position, obj.rotation, obj.scale); - - if (obj.type === "Group") { - const group: IGroup = { - uuid: obj.uuid || `group_${groups.size}`, - name: obj.name || "Group", - transform, - children: [], - }; - - // Convert children - if (obj.children && Array.isArray(obj.children)) { - for (const child of obj.children) { - const convertedChild = this._convertObject(child, group.uuid, groups, emitters, depth + 1); - if (convertedChild) { - if ("config" in convertedChild) { - // It's an emitter - group.children.push(convertedChild as IEmitter); - } else { - // It's a group - group.children.push(convertedChild as IGroup); - } - } - } - } - - groups.set(group.uuid, group); - this._logger.log(`${indent}Converted Group: ${group.name} (uuid: ${group.uuid})`); - return group; - } else if (obj.type === "ParticleEmitter" && obj.ps) { - // Convert emitter config from IQuarks to format - const Config = this._convertEmitterConfig(obj.ps); - - const emitter: IEmitter = { - uuid: obj.uuid || `emitter_${emitters.size}`, - name: obj.name || "ParticleEmitter", - transform, - config: Config, - materialId: obj.ps.material, - parentUuid: parentUuid || undefined, - systemType: Config.systemType, // systemType is set in _convertEmitterConfig - matrix: obj.matrix, // Store original matrix for rotation extraction - }; - - emitters.set(emitter.uuid, emitter); - this._logger.log(`${indent}Converted Emitter: ${emitter.name} (uuid: ${emitter.uuid}, systemType: ${Config.systemType})`); - return emitter; - } - - return null; - } - - /** - * Convert transform from IQuarks/Three.js (right-handed) to Babylon.js (left-handed) - * This is the ONLY place where handedness conversion happens - */ - private _convertTransform(matrixArray?: number[], positionArray?: number[], rotationArray?: number[], scaleArray?: number[]): ITransform { - const position = Vector3.Zero(); - const rotation = Quaternion.Identity(); - const scale = Vector3.One(); - - if (matrixArray && Array.isArray(matrixArray) && matrixArray.length >= 16) { - // Use matrix (most accurate) - const matrix = Matrix.FromArray(matrixArray); - const tempPos = Vector3.Zero(); - const tempRot = Quaternion.Zero(); - const tempScale = Vector3.Zero(); - matrix.decompose(tempScale, tempRot, tempPos); - - // Convert from right-handed to left-handed - position.copyFrom(tempPos); - position.z = -position.z; // Negate Z position - - rotation.copyFrom(tempRot); - // Convert rotation quaternion: invert X component for proper X-axis rotation conversion - // This handles the case where X=-90° in RH looks like X=0° in LH - rotation.x *= -1; - - scale.copyFrom(tempScale); - } else { - // Use individual components - if (positionArray && Array.isArray(positionArray)) { - position.set(positionArray[0] || 0, positionArray[1] || 0, positionArray[2] || 0); - position.z = -position.z; // Convert to left-handed - } - - if (rotationArray && Array.isArray(rotationArray)) { - // If rotation is Euler angles, convert to quaternion - const eulerX = rotationArray[0] || 0; - const eulerY = rotationArray[1] || 0; - const eulerZ = rotationArray[2] || 0; - Quaternion.RotationYawPitchRollToRef(eulerY, eulerX, -eulerZ, rotation); // Negate Z for handedness - rotation.x *= -1; // Adjust X rotation component - } - - if (scaleArray && Array.isArray(scaleArray)) { - scale.set(scaleArray[0] || 1, scaleArray[1] || 1, scaleArray[2] || 1); - } - } - - return { - position, - rotation, - scale, - }; - } - - /** - * Convert emitter config from IQuarks to format - */ - private _convertEmitterConfig(IQuarksConfig: IQuarksParticleEmitterConfig): IParticleSystemConfig { - // Determine system type based on renderMode: 2 = solid, otherwise base - const systemType: "solid" | "base" = IQuarksConfig.renderMode === 2 ? "solid" : "base"; - - // Convert duration/looping to native targetStopDuration - // In Babylon.js: targetStopDuration = 0 means infinite loop - const duration = IQuarksConfig.duration ?? 5; - const targetStopDuration = IQuarksConfig.looping ? 0 : duration; - - // Convert prewarm to native preWarmCycles - // In Babylon.js: preWarmCycles > 0 means prewarm enabled - let preWarmCycles = 0; - let preWarmStepOffset = 0.016; - if (IQuarksConfig.prewarm) { - preWarmCycles = Math.ceil(duration * 60); // Simulate ~60fps for duration - preWarmStepOffset = 1 / 60; - } - - // Convert worldSpace to native isLocal (inverse) - const isLocal = IQuarksConfig.worldSpace === undefined ? false : !IQuarksConfig.worldSpace; - - // Convert autoDestroy to native disposeOnStop - const disposeOnStop = IQuarksConfig.autoDestroy ?? false; - - const Config: IParticleSystemConfig = { - version: IQuarksConfig.version, - systemType, - // Native properties - targetStopDuration, - preWarmCycles, - preWarmStepOffset, - isLocal, - disposeOnStop, - // Other properties - instancingGeometry: IQuarksConfig.instancingGeometry, // Custom geometry for SPS - renderOrder: IQuarksConfig.renderOrder, - layers: IQuarksConfig.layers, - // Sprite animation (ParticleSystem only) - uTileCount: IQuarksConfig.uTileCount, - vTileCount: IQuarksConfig.vTileCount, - }; - - // === Convert Quarks values to native Babylon.js properties === - - // Convert startLife → minLifeTime, maxLifeTime, lifeTimeGradients - if (IQuarksConfig.startLife !== undefined) { - const lifeResult = this._convertValueToMinMax(IQuarksConfig.startLife); - Config.minLifeTime = lifeResult.min; - Config.maxLifeTime = lifeResult.max; - if (lifeResult.gradients) { - Config.lifeTimeGradients = lifeResult.gradients; - } - } - - // Convert startSpeed → minEmitPower, maxEmitPower - if (IQuarksConfig.startSpeed !== undefined) { - const speedResult = this._convertValueToMinMax(IQuarksConfig.startSpeed); - Config.minEmitPower = speedResult.min; - Config.maxEmitPower = speedResult.max; - } - - // Convert startSize → minSize, maxSize, startSizeGradients - if (IQuarksConfig.startSize !== undefined) { - const sizeResult = this._convertValueToMinMax(IQuarksConfig.startSize); - Config.minSize = sizeResult.min; - Config.maxSize = sizeResult.max; - if (sizeResult.gradients) { - Config.startSizeGradients = sizeResult.gradients; - } - } - - // Convert startRotation → minInitialRotation, maxInitialRotation - if (IQuarksConfig.startRotation !== undefined) { - const rotResult = this._convertRotationToMinMax(IQuarksConfig.startRotation); - Config.minInitialRotation = rotResult.min; - Config.maxInitialRotation = rotResult.max; - } - - // Convert startColor → color1, color2 - if (IQuarksConfig.startColor !== undefined) { - const colorResult = this._convertColorToColor4(IQuarksConfig.startColor); - Config.color1 = colorResult.color1; - Config.color2 = colorResult.color2; - } else { - } - - // Convert emissionOverTime → emitRate, emitRateGradients - if (IQuarksConfig.emissionOverTime !== undefined) { - const emitResult = this._convertValueToMinMax(IQuarksConfig.emissionOverTime); - Config.emitRate = emitResult.min; // Use min as base rate - if (emitResult.gradients) { - Config.emitRateGradients = emitResult.gradients; - } - } - - // emissionOverDistance - only for SPS, keep as Value - if (IQuarksConfig.emissionOverDistance !== undefined) { - Config.emissionOverDistance = this._convertValue(IQuarksConfig.emissionOverDistance); - } - - // startTileIndex - for sprite animation (ParticleSystem only) - if (IQuarksConfig.startTileIndex !== undefined) { - Config.startTileIndex = this._convertValue(IQuarksConfig.startTileIndex); - } - - // Convert shape - if (IQuarksConfig.shape !== undefined) { - Config.shape = this._convertShape(IQuarksConfig.shape); - } - - // Convert emission bursts - if (IQuarksConfig.emissionBursts !== undefined && Array.isArray(IQuarksConfig.emissionBursts)) { - Config.emissionBursts = IQuarksConfig.emissionBursts.map((burst) => ({ - time: this._convertValue(burst.time), - count: this._convertValue(burst.count), - })); - } - - // Convert behaviors - if (IQuarksConfig.behaviors !== undefined && Array.isArray(IQuarksConfig.behaviors)) { - Config.behaviors = IQuarksConfig.behaviors.map((behavior) => this._convertBehavior(behavior)); - } - - // Convert renderMode to systemType, billboardMode and isBillboardBased - // IQuarks RenderMode: - // 0 = BillBoard → systemType = "base", isBillboardBased = true, billboardMode = ALL (default) - // 1 = StretchedBillBoard → systemType = "base", isBillboardBased = true, billboardMode = STRETCHED - // 2 = Mesh → systemType = "solid", isBillboardBased = false (always) - // 3 = Trail → systemType = "base", isBillboardBased = true, billboardMode = ALL (not directly supported, treat as billboard) - // 4 = HorizontalBillBoard → systemType = "base", isBillboardBased = true, billboardMode = Y - // 5 = VerticalBillBoard → systemType = "base", isBillboardBased = true, billboardMode = Y (same as horizontal) - if (IQuarksConfig.renderMode !== undefined) { - if (IQuarksConfig.renderMode === 0) { - // BillBoard - Config.isBillboardBased = true; - Config.billboardMode = ParticleSystem.BILLBOARDMODE_ALL; - } else if (IQuarksConfig.renderMode === 1) { - // StretchedBillBoard - Config.isBillboardBased = true; - Config.billboardMode = ParticleSystem.BILLBOARDMODE_STRETCHED; - } else if (IQuarksConfig.renderMode === 2) { - // Mesh (SolidParticleSystem) - always false - Config.isBillboardBased = false; - // billboardMode not applicable for mesh - } else if (IQuarksConfig.renderMode === 3) { - // Trail - not directly supported, treat as billboard - Config.isBillboardBased = true; - Config.billboardMode = ParticleSystem.BILLBOARDMODE_ALL; - } else if (IQuarksConfig.renderMode === 4 || IQuarksConfig.renderMode === 5) { - // HorizontalBillBoard or VerticalBillBoard - Config.isBillboardBased = true; - Config.billboardMode = ParticleSystem.BILLBOARDMODE_Y; - } else { - // Unknown renderMode, default to billboard - Config.isBillboardBased = true; - Config.billboardMode = ParticleSystem.BILLBOARDMODE_ALL; - } - } else { - // Default: billboard mode - Config.isBillboardBased = true; - Config.billboardMode = ParticleSystem.BILLBOARDMODE_ALL; - } - - return Config; - } - - /** - * Convert IQuarks value to value - */ - private _convertValue(IQuarksValue: IQuarksValue): Value { - if (typeof IQuarksValue === "number") { - return IQuarksValue; - } - if (IQuarksValue.type === "ConstantValue") { - return { - type: "ConstantValue", - value: IQuarksValue.value, - }; - } - if (IQuarksValue.type === "IntervalValue") { - return { - type: "IntervalValue", - min: IQuarksValue.a ?? 0, - max: IQuarksValue.b ?? 0, - }; - } - if (IQuarksValue.type === "PiecewiseBezier") { - return { - type: "PiecewiseBezier", - functions: IQuarksValue.functions.map((f) => ({ - function: f.function, - start: f.start, - })), - }; - } - return IQuarksValue; - } - - /** - * Convert IQuarks value to native Babylon.js min/max + gradients - * - ConstantValue → min = max = value - * - IntervalValue → min = a, max = b - * - PiecewiseBezier → gradients array - */ - private _convertValueToMinMax(IQuarksValue: IQuarksValue): { min: number; max: number; gradients?: Array<{ gradient: number; factor: number; factor2?: number }> } { - if (typeof IQuarksValue === "number") { - return { min: IQuarksValue, max: IQuarksValue }; - } - if (IQuarksValue.type === "ConstantValue") { - return { min: IQuarksValue.value, max: IQuarksValue.value }; - } - if (IQuarksValue.type === "IntervalValue") { - return { min: IQuarksValue.a ?? 0, max: IQuarksValue.b ?? 0 }; - } - if (IQuarksValue.type === "PiecewiseBezier" && IQuarksValue.functions) { - // Convert PiecewiseBezier to gradients - const gradients: Array<{ gradient: number; factor: number; factor2?: number }> = []; - let minVal = Infinity; - let maxVal = -Infinity; - - for (const func of IQuarksValue.functions) { - const startTime = func.start; - // Evaluate bezier at start and end points - const startValue = this._evaluateBezierAt(func.function, 0); - const endValue = this._evaluateBezierAt(func.function, 1); - - gradients.push({ gradient: startTime, factor: startValue }); - - // Track min/max for fallback - minVal = Math.min(minVal, startValue, endValue); - maxVal = Math.max(maxVal, startValue, endValue); - } - - // Add final point at gradient 1.0 if not present - if (gradients.length > 0 && gradients[gradients.length - 1].gradient < 1) { - const lastFunc = IQuarksValue.functions[IQuarksValue.functions.length - 1]; - const endValue = this._evaluateBezierAt(lastFunc.function, 1); - gradients.push({ gradient: 1, factor: endValue }); - } - - return { - min: minVal === Infinity ? 1 : minVal, - max: maxVal === -Infinity ? 1 : maxVal, - gradients: gradients.length > 0 ? gradients : undefined, - }; - } - return { min: 1, max: 1 }; - } - - /** - * Evaluate bezier curve at time t - * Bezier format: { p0, p1, p2, p3 } for cubic bezier - */ - private _evaluateBezierAt(bezier: { p0: number; p1: number; p2: number; p3: number }, t: number): number { - const { p0, p1, p2, p3 } = bezier; - const t2 = t * t; - const t3 = t2 * t; - const mt = 1 - t; - const mt2 = mt * mt; - const mt3 = mt2 * mt; - return mt3 * p0 + 3 * mt2 * t * p1 + 3 * mt * t2 * p2 + t3 * p3; - } - - /** - * Convert IQuarks rotation to native min/max radians - * Supports: number, ConstantValue, IntervalValue, Euler, AxisAngle, RandomQuat - */ - private _convertRotationToMinMax(IQuarksRotation: IQuarksRotation): { min: number; max: number } { - if (typeof IQuarksRotation === "number") { - return { min: IQuarksRotation, max: IQuarksRotation }; - } - - if (typeof IQuarksRotation === "object" && IQuarksRotation !== null && "type" in IQuarksRotation) { - const rotationType = IQuarksRotation.type; - - if (rotationType === "ConstantValue") { - const val = (IQuarksRotation as any).value ?? 0; - return { min: val, max: val }; - } - - if (rotationType === "IntervalValue") { - return { min: (IQuarksRotation as any).a ?? 0, max: (IQuarksRotation as any).b ?? 0 }; - } - - // Handle Euler type - for 2D/billboard particles we use angleZ - if (rotationType === "Euler") { - const euler = IQuarksRotation as any; - // angleZ is the rotation around forward axis (most common for 2D particles) - const angleZ = euler.angleZ; - if (angleZ) { - if (typeof angleZ === "number") { - return { min: angleZ, max: angleZ }; - } - if (angleZ.type === "ConstantValue") { - const val = angleZ.value ?? 0; - return { min: val, max: val }; - } - if (angleZ.type === "IntervalValue") { - return { min: angleZ.a ?? 0, max: angleZ.b ?? 0 }; - } - } - // Fallback to angleX if no angleZ (for different orientations) - const angleX = euler.angleX; - if (angleX) { - if (typeof angleX === "number") { - return { min: angleX, max: angleX }; - } - if (angleX.type === "ConstantValue") { - const val = angleX.value ?? 0; - return { min: val, max: val }; - } - if (angleX.type === "IntervalValue") { - return { min: angleX.a ?? 0, max: angleX.b ?? 0 }; - } - } - return { min: 0, max: 0 }; - } - } - - return { min: 0, max: 0 }; - } - - /** - * Convert IQuarks color to native Babylon.js Color4 (color1, color2) - */ - private _convertColorToColor4(IQuarksColor: IQuarksColor): { color1: Color4; color2: Color4 } { - if (Array.isArray(IQuarksColor)) { - const c = new Color4(IQuarksColor[0] ?? 1, IQuarksColor[1] ?? 1, IQuarksColor[2] ?? 1, IQuarksColor[3] ?? 1); - return { color1: c, color2: c }; - } - - if (typeof IQuarksColor === "object" && IQuarksColor !== null && "type" in IQuarksColor) { - if (IQuarksColor.type === "ConstantColor") { - const constColor = IQuarksColor as any; - if (constColor.value && Array.isArray(constColor.value)) { - const c = new Color4(constColor.value[0] ?? 1, constColor.value[1] ?? 1, constColor.value[2] ?? 1, constColor.value[3] ?? 1); - return { color1: c, color2: c }; - } - if (constColor.color) { - const colorObj = constColor.color; - const c = new Color4( - colorObj.r !== undefined ? colorObj.r : 1, - colorObj.g !== undefined ? colorObj.g : 1, - colorObj.b !== undefined ? colorObj.b : 1, - colorObj.a !== undefined ? colorObj.a : 1 - ); - return { color1: c, color2: c }; - } - } - // Handle RandomColor (interpolation between two colors) - const anyColor = IQuarksColor as any; - if (anyColor.type === "RandomColor" && anyColor.a && anyColor.b) { - const color1 = new Color4(anyColor.a[0] ?? 1, anyColor.a[1] ?? 1, anyColor.a[2] ?? 1, anyColor.a[3] ?? 1); - const color2 = new Color4(anyColor.b[0] ?? 1, anyColor.b[1] ?? 1, anyColor.b[2] ?? 1, anyColor.b[3] ?? 1); - return { color1, color2 }; - } - } - - // Default white - return { color1: new Color4(1, 1, 1, 1), color2: new Color4(1, 1, 1, 1) }; - } - - /** - * Convert IQuarks gradient key to gradient key - */ - private _convertGradientKey(IQuarksKey: IQuarksGradientKey): IGradientKey { - return { - time: IQuarksKey.time, - value: IQuarksKey.value, - pos: IQuarksKey.pos, - }; - } - - /** - * Convert IQuarks shape to shape - */ - private _convertShape(IQuarksShape: IQuarksShape): IShape { - const Shape: IShape = { - type: IQuarksShape.type, - radius: IQuarksShape.radius, - arc: IQuarksShape.arc, - thickness: IQuarksShape.thickness, - angle: IQuarksShape.angle, - mode: IQuarksShape.mode, - spread: IQuarksShape.spread, - size: IQuarksShape.size, - height: IQuarksShape.height, - }; - if (IQuarksShape.speed !== undefined) { - Shape.speed = this._convertValue(IQuarksShape.speed); - } - return Shape; - } - - /** - * Convert IQuarks behavior to behavior - */ - private _convertBehavior(IQuarksBehavior: IQuarksBehavior): Behavior { - switch (IQuarksBehavior.type) { - case "ColorOverLife": { - const behavior = IQuarksBehavior as IQuarksColorOverLifeBehavior; - if (!behavior.color) { - return { - type: "ColorOverLife", - color: { - colorFunctionType: "ConstantColor", - data: {}, - }, - }; - } - - const colorType = behavior.color.type; - - // Convert color to unified IColorFunction structure - let colorFunction: IColorFunction; - - if (colorType === "Gradient") { - const gradientColor = behavior.color as IQuarksGradientColor; - colorFunction = { - colorFunctionType: "Gradient", - data: { - colorKeys: gradientColor.color?.keys ? gradientColor.color.keys.map((k) => this._convertGradientKey(k)) : [], - alphaKeys: gradientColor.alpha?.keys ? gradientColor.alpha.keys.map((k) => this._convertGradientKey(k)) : [], - }, - }; - } else if (colorType === "ConstantColor") { - const constantColor = behavior.color as IQuarksConstantColorColor; - const color = - constantColor.color || - (constantColor.value - ? { r: constantColor.value[0], g: constantColor.value[1], b: constantColor.value[2], a: constantColor.value[3] } - : { r: 1, g: 1, b: 1, a: 1 }); - colorFunction = { - colorFunctionType: "ConstantColor", - data: { - color: { - r: color.r ?? 1, - g: color.g ?? 1, - b: color.b ?? 1, - a: color.a !== undefined ? color.a : 1, - }, - }, - }; - } else if (colorType === "RandomColorBetweenGradient") { - const randomColor = behavior.color as IQuarksRandomColorBetweenGradient; - colorFunction = { - colorFunctionType: "RandomColorBetweenGradient", - data: { - gradient1: { - colorKeys: randomColor.gradient1?.color?.keys ? randomColor.gradient1.color.keys.map((k) => this._convertGradientKey(k)) : [], - alphaKeys: randomColor.gradient1?.alpha?.keys ? randomColor.gradient1.alpha.keys.map((k) => this._convertGradientKey(k)) : [], - }, - gradient2: { - colorKeys: randomColor.gradient2?.color?.keys ? randomColor.gradient2.color.keys.map((k) => this._convertGradientKey(k)) : [], - alphaKeys: randomColor.gradient2?.alpha?.keys ? randomColor.gradient2.alpha.keys.map((k) => this._convertGradientKey(k)) : [], - }, - }, - }; - } else { - // Fallback: try to detect format from keys - const hasColorKeys = (behavior.color as any).color?.keys && (behavior.color as any).color.keys.length > 0; - const hasAlphaKeys = (behavior.color as any).alpha?.keys && (behavior.color as any).alpha.keys.length > 0; - const hasKeys = (behavior.color as any).keys && (behavior.color as any).keys.length > 0; - - if (hasColorKeys || hasAlphaKeys || hasKeys) { - colorFunction = { - colorFunctionType: "Gradient", - data: { - colorKeys: hasColorKeys - ? (behavior.color as any).color.keys.map((k: any) => this._convertGradientKey(k)) - : hasKeys - ? (behavior.color as any).keys.map((k: any) => this._convertGradientKey(k)) - : [], - alphaKeys: hasAlphaKeys ? (behavior.color as any).alpha.keys.map((k: any) => this._convertGradientKey(k)) : [], - }, - }; - } else { - // Default to ConstantColor - colorFunction = { - colorFunctionType: "ConstantColor", - data: {}, - }; - } - } - - return { - type: "ColorOverLife", - color: colorFunction, - }; - } - - case "SizeOverLife": { - const behavior = IQuarksBehavior as IQuarksSizeOverLifeBehavior; - if (behavior.size) { - const Size: ISizeOverLifeBehavior["size"] = {}; - if (behavior.size.keys) { - Size.keys = behavior.size.keys.map((k: IQuarksGradientKey) => this._convertGradientKey(k)); - } - if (behavior.size.functions) { - Size.functions = behavior.size.functions; - } - return { type: "SizeOverLife", size: Size }; - } - return { type: "SizeOverLife" }; - } - - case "RotationOverLife": - case "Rotation3DOverLife": { - const behavior = IQuarksBehavior as IQuarksRotationOverLifeBehavior; - return { - type: behavior.type, - angularVelocity: behavior.angularVelocity !== undefined ? this._convertValue(behavior.angularVelocity) : undefined, - }; - } - - case "ForceOverLife": - case "ApplyForce": { - const behavior = IQuarksBehavior as IQuarksForceOverLifeBehavior; - const Behavior: IForceOverLifeBehavior = { type: behavior.type }; - if (behavior.force) { - Behavior.force = { - x: behavior.force.x !== undefined ? this._convertValue(behavior.force.x) : undefined, - y: behavior.force.y !== undefined ? this._convertValue(behavior.force.y) : undefined, - z: behavior.force.z !== undefined ? this._convertValue(behavior.force.z) : undefined, - }; - } - if (behavior.x !== undefined) { - Behavior.x = this._convertValue(behavior.x); - } - if (behavior.y !== undefined) { - Behavior.y = this._convertValue(behavior.y); - } - if (behavior.z !== undefined) { - Behavior.z = this._convertValue(behavior.z); - } - return Behavior; - } - - case "GravityForce": { - const behavior = IQuarksBehavior as IQuarksGravityForceBehavior; - const Behavior: { type: string; gravity?: Value } = { - type: "GravityForce", - gravity: behavior.gravity !== undefined ? this._convertValue(behavior.gravity) : undefined, - }; - return Behavior as Behavior; - } - - case "SpeedOverLife": { - const behavior = IQuarksBehavior as IQuarksSpeedOverLifeBehavior; - if (behavior.speed) { - if (typeof behavior.speed === "object" && behavior.speed !== null && "keys" in behavior.speed) { - const Speed: ISpeedOverLifeBehavior["speed"] = {}; - if (behavior.speed.keys) { - Speed.keys = behavior.speed.keys.map((k: IQuarksGradientKey) => this._convertGradientKey(k)); - } - if (behavior.speed.functions) { - Speed.functions = behavior.speed.functions; - } - return { type: "SpeedOverLife", speed: Speed }; - } else if (typeof behavior.speed === "number" || (typeof behavior.speed === "object" && behavior.speed !== null && "type" in behavior.speed)) { - return { type: "SpeedOverLife", speed: this._convertValue(behavior.speed as IQuarksValue) }; - } - } - return { type: "SpeedOverLife" }; - } - - case "FrameOverLife": { - const behavior = IQuarksBehavior as IQuarksFrameOverLifeBehavior; - const Behavior: { type: string; frame?: Value | { keys?: IGradientKey[] } } = { type: "FrameOverLife" }; - if (behavior.frame) { - if (typeof behavior.frame === "object" && behavior.frame !== null && "keys" in behavior.frame) { - Behavior.frame = { - keys: behavior.frame.keys?.map((k: IQuarksGradientKey) => this._convertGradientKey(k)), - }; - } else if (typeof behavior.frame === "number" || (typeof behavior.frame === "object" && behavior.frame !== null && "type" in behavior.frame)) { - Behavior.frame = this._convertValue(behavior.frame as IQuarksValue); - } - } - return Behavior as Behavior; - } - - case "LimitSpeedOverLife": { - const behavior = IQuarksBehavior as IQuarksLimitSpeedOverLifeBehavior; - const Behavior: ILimitSpeedOverLifeBehavior = { type: "LimitSpeedOverLife" }; - if (behavior.maxSpeed !== undefined) { - Behavior.maxSpeed = this._convertValue(behavior.maxSpeed); - } - if (behavior.speed !== undefined) { - if (typeof behavior.speed === "object" && behavior.speed !== null && "keys" in behavior.speed) { - Behavior.speed = { keys: behavior.speed.keys?.map((k: IQuarksGradientKey) => this._convertGradientKey(k)) }; - } else if (typeof behavior.speed === "number" || (typeof behavior.speed === "object" && behavior.speed !== null && "type" in behavior.speed)) { - Behavior.speed = this._convertValue(behavior.speed as IQuarksValue); - } - } - if (behavior.dampen !== undefined) { - Behavior.dampen = this._convertValue(behavior.dampen); - } - return Behavior; - } - - case "ColorBySpeed": { - const behavior = IQuarksBehavior as IQuarksColorBySpeedBehavior; - const colorFunction: IColorFunction = behavior.color?.keys - ? { - colorFunctionType: "Gradient", - data: { - colorKeys: behavior.color.keys.map((k: IQuarksGradientKey) => this._convertGradientKey(k)), - alphaKeys: [], - }, - } - : { - colorFunctionType: "ConstantColor", - data: {}, - }; - - return { - type: "ColorBySpeed", - color: colorFunction, - minSpeed: behavior.minSpeed !== undefined ? this._convertValue(behavior.minSpeed) : undefined, - maxSpeed: behavior.maxSpeed !== undefined ? this._convertValue(behavior.maxSpeed) : undefined, - }; - } - - case "SizeBySpeed": { - const behavior = IQuarksBehavior as IQuarksSizeBySpeedBehavior; - const Behavior: ISizeBySpeedBehavior = { - type: "SizeBySpeed", - minSpeed: behavior.minSpeed !== undefined ? this._convertValue(behavior.minSpeed) : undefined, - maxSpeed: behavior.maxSpeed !== undefined ? this._convertValue(behavior.maxSpeed) : undefined, - }; - if (behavior.size?.keys) { - Behavior.size = { keys: behavior.size.keys.map((k: IQuarksGradientKey) => this._convertGradientKey(k)) }; - } - return Behavior; - } - - case "RotationBySpeed": { - const behavior = IQuarksBehavior as IQuarksRotationBySpeedBehavior; - const Behavior: { type: string; angularVelocity?: Value; minSpeed?: Value; maxSpeed?: Value } = { - type: "RotationBySpeed", - angularVelocity: behavior.angularVelocity !== undefined ? this._convertValue(behavior.angularVelocity) : undefined, - minSpeed: behavior.minSpeed !== undefined ? this._convertValue(behavior.minSpeed) : undefined, - maxSpeed: behavior.maxSpeed !== undefined ? this._convertValue(behavior.maxSpeed) : undefined, - }; - return Behavior as Behavior; - } - - case "OrbitOverLife": { - const behavior = IQuarksBehavior as IQuarksOrbitOverLifeBehavior; - const Behavior: { type: string; center?: { x?: number; y?: number; z?: number }; radius?: Value; speed?: Value } = { - type: "OrbitOverLife", - center: behavior.center, - radius: behavior.radius !== undefined ? this._convertValue(behavior.radius) : undefined, - speed: behavior.speed !== undefined ? this._convertValue(behavior.speed) : undefined, - }; - return Behavior as Behavior; - } - - default: - // Fallback for unknown behaviors - copy as-is - return IQuarksBehavior as Behavior; - } - } - - /** - * Convert IQuarks materials to materials - */ - private _convertMaterials(IQuarksMaterials: IQuarksMaterial[]): IMaterial[] { - return IQuarksMaterials.map((IQuarks) => { - const material: IMaterial = { - uuid: IQuarks.uuid, - type: IQuarks.type, - transparent: IQuarks.transparent, - depthWrite: IQuarks.depthWrite, - side: IQuarks.side, - map: IQuarks.map, - }; - - // Convert color from hex to Color3 - if (IQuarks.color !== undefined) { - const colorHex = typeof IQuarks.color === "number" ? IQuarks.color : parseInt(String(IQuarks.color).replace("#", ""), 16) || 0xffffff; - const r = ((colorHex >> 16) & 0xff) / 255; - const g = ((colorHex >> 8) & 0xff) / 255; - const b = (colorHex & 0xff) / 255; - material.color = new Color3(r, g, b); - } - - // Convert blending mode (Three.js → Babylon.js) - if (IQuarks.blending !== undefined) { - const blendModeMap: Record = { - 0: 0, // NoBlending → ALPHA_DISABLE - 1: 1, // NormalBlending → ALPHA_COMBINE - 2: 2, // AdditiveBlending → ALPHA_ADD - }; - material.blending = blendModeMap[IQuarks.blending] ?? IQuarks.blending; - } - - return material; - }); - } - - /** - * Convert IQuarks textures to textures - */ - private _convertTextures(IQuarksTextures: IQuarksTexture[]): ITexture[] { - return IQuarksTextures.map((IQuarks) => { - const texture: ITexture = { - uuid: IQuarks.uuid, - image: IQuarks.image, - generateMipmaps: IQuarks.generateMipmaps, - flipY: IQuarks.flipY, - }; - - // Convert wrap mode (Three.js → Babylon.js) - if (IQuarks.wrap && Array.isArray(IQuarks.wrap)) { - const wrapModeMap: Record = { - 1000: BabylonTexture.WRAP_ADDRESSMODE, // RepeatWrapping - 1001: BabylonTexture.CLAMP_ADDRESSMODE, // ClampToEdgeWrapping - 1002: BabylonTexture.MIRROR_ADDRESSMODE, // MirroredRepeatWrapping - }; - texture.wrapU = wrapModeMap[IQuarks.wrap[0]] ?? BabylonTexture.WRAP_ADDRESSMODE; - texture.wrapV = wrapModeMap[IQuarks.wrap[1]] ?? BabylonTexture.WRAP_ADDRESSMODE; - } - - // Convert repeat to scale - if (IQuarks.repeat && Array.isArray(IQuarks.repeat)) { - texture.uScale = IQuarks.repeat[0] || 1; - texture.vScale = IQuarks.repeat[1] || 1; - } - - // Convert offset - if (IQuarks.offset && Array.isArray(IQuarks.offset)) { - texture.uOffset = IQuarks.offset[0] || 0; - texture.vOffset = IQuarks.offset[1] || 0; - } - - // Convert rotation - if (IQuarks.rotation !== undefined) { - texture.uAng = IQuarks.rotation; - } - - // Convert channel - if (typeof IQuarks.channel === "number") { - texture.coordinatesIndex = IQuarks.channel; - } - - // Convert sampling mode (Three.js filters → Babylon.js sampling mode) - if (IQuarks.minFilter !== undefined) { - if (IQuarks.minFilter === 1008 || IQuarks.minFilter === 1009) { - texture.samplingMode = BabylonTexture.TRILINEAR_SAMPLINGMODE; - } else if (IQuarks.minFilter === 1007 || IQuarks.minFilter === 1006) { - texture.samplingMode = BabylonTexture.BILINEAR_SAMPLINGMODE; - } else { - texture.samplingMode = BabylonTexture.NEAREST_SAMPLINGMODE; - } - } else if (IQuarks.magFilter !== undefined) { - texture.samplingMode = IQuarks.magFilter === 1006 ? BabylonTexture.BILINEAR_SAMPLINGMODE : BabylonTexture.NEAREST_SAMPLINGMODE; - } else { - texture.samplingMode = BabylonTexture.TRILINEAR_SAMPLINGMODE; - } - - return texture; - }); - } - - /** - * Convert IQuarks images to images (normalize URLs) - */ - private _convertImages(IQuarksImages: IQuarksImage[]): IImage[] { - return IQuarksImages.map((IQuarks) => ({ - uuid: IQuarks.uuid, - url: IQuarks.url || "", - })); - } - - /** - * Convert IQuarks geometries to geometries (convert to left-handed) - */ - private _convertGeometries(IQuarksGeometries: IQuarksGeometry[]): IGeometry[] { - return IQuarksGeometries.map((IQuarks) => { - if (IQuarks.type === "PlaneGeometry") { - // PlaneGeometry - simple properties - const geometry: IGeometry = { - uuid: IQuarks.uuid, - type: "PlaneGeometry", - width: (IQuarks as any).width ?? 1, - height: (IQuarks as any).height ?? 1, - }; - return geometry; - } else if (IQuarks.type === "BufferGeometry") { - // BufferGeometry - convert attributes to left-handed - const geometry: IGeometry = { - uuid: IQuarks.uuid, - type: "BufferGeometry", - }; - - if (IQuarks.data?.attributes) { - const attributes: IGeometryData["attributes"] = {}; - const IQuarksAttrs = IQuarks.data.attributes; - - // Convert position (right-hand → left-hand: flip Z) - if (IQuarksAttrs.position) { - const positions = Array.from(IQuarksAttrs.position.array); - // Flip Z coordinate for left-handed system - for (let i = 2; i < positions.length; i += 3) { - positions[i] = -positions[i]; - } - attributes.position = { - array: positions, - itemSize: IQuarksAttrs.position.itemSize, - }; - } - - // Convert normal (right-hand → left-hand: flip Z) - if (IQuarksAttrs.normal) { - const normals = Array.from(IQuarksAttrs.normal.array); - for (let i = 2; i < normals.length; i += 3) { - normals[i] = -normals[i]; - } - attributes.normal = { - array: normals, - itemSize: IQuarksAttrs.normal.itemSize, - }; - } - - // UV and color - no conversion needed - if (IQuarksAttrs.uv) { - attributes.uv = { - array: Array.from(IQuarksAttrs.uv.array), - itemSize: IQuarksAttrs.uv.itemSize, - }; - } - - if (IQuarksAttrs.color) { - attributes.color = { - array: Array.from(IQuarksAttrs.color.array), - itemSize: IQuarksAttrs.color.itemSize, - }; - } - - geometry.data = { - attributes, - }; - - // Convert indices (reverse winding order for left-handed) - if (IQuarks.data.index) { - const indices = Array.from(IQuarks.data.index.array); - // Reverse winding: swap every 2nd and 3rd index in each triangle - for (let i = 0; i < indices.length; i += 3) { - const temp = indices[i + 1]; - indices[i + 1] = indices[i + 2]; - indices[i + 2] = temp; - } - geometry.data.index = { - array: indices, - }; - } - } - - return geometry; - } - - // Unknown geometry type - return as-is - return { - uuid: IQuarks.uuid, - type: IQuarks.type as "PlaneGeometry" | "BufferGeometry", - }; - }); - } -} diff --git a/tools/src/effect/parsers/index.ts b/tools/src/effect/parsers/index.ts deleted file mode 100644 index 9d5a42bab..000000000 --- a/tools/src/effect/parsers/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./parser"; -export * from "./dataConverter"; diff --git a/tools/src/effect/parsers/parser.ts b/tools/src/effect/parsers/parser.ts deleted file mode 100644 index a94d4aa85..000000000 --- a/tools/src/effect/parsers/parser.ts +++ /dev/null @@ -1,122 +0,0 @@ -import { Scene, TransformNode } from "babylonjs"; -import type { IQuarksJSON, ILoaderOptions, IData } from "../types"; -import { Logger } from "../loggers/logger"; -import { MaterialFactory, GeometryFactory, SystemFactory } from "../factories"; -import { DataConverter } from "./dataConverter"; -import type { EffectParticleSystem, EffectSolidParticleSystem } from "../systems"; - -/** - * Result of parsing JSON - */ -export interface IParseResult { - /** Created particle systems */ - systems: (EffectParticleSystem | EffectSolidParticleSystem)[]; - /** Converted data */ - data: IData; - /** Map of group UUIDs to TransformNodes */ - groupNodesMap: Map; -} - -/** - * Main parser for Three.js particle JSON files - * Orchestrates the parsing process using modular components - */ -export class Parser { - private _logger: Logger; - private _materialFactory: MaterialFactory; - private _geometryFactory: GeometryFactory; - private _systemFactory: SystemFactory; - private _data: IData; - private _groupNodesMap: Map; - private _options: ILoaderOptions; - - constructor(scene: Scene, rootUrl: string, jsondata: IQuarksJSON, options?: ILoaderOptions) { - const opts = options || {}; - this._options = opts; - this._groupNodesMap = new Map(); - - this._logger = new Logger("[Parser]", opts); - - // Convert Quarks JSON to data first - const dataConverter = new DataConverter(opts); - this._data = dataConverter.convert(jsondata); - - // Create factories with data instead of QuarksJSON - this._materialFactory = new MaterialFactory(scene, this._data, rootUrl, opts); - this._geometryFactory = new GeometryFactory(this._data, opts); - this._systemFactory = new SystemFactory(scene, opts, this._groupNodesMap, this._materialFactory, this._geometryFactory); - } - - /** - * Parse the JSON data and create particle systems - * Returns all necessary data for building the effect hierarchy - */ - public parse(): IParseResult { - this._logger.log("=== Starting Particle System Parsing ==="); - - if (!this._data) { - this._logger.warn("data is missing"); - return { - systems: [], - data: this._data, - groupNodesMap: this._groupNodesMap, - }; - } - - if (this._options.validate) { - this._validateJSONStructure(this._data); - } - - const particleSystems = this._systemFactory.createSystems(this._data); - - this._logger.log(`=== Parsing complete. Created ${particleSystems.length} particle system(s) ===`); - return { - systems: particleSystems, - data: this._data, - groupNodesMap: this._groupNodesMap, - }; - } - - /** - * Validate data structure - */ - private _validateJSONStructure(data: IData): void { - this._logger.log("Validating data structure..."); - - if (!data.root) { - this._logger.warn(" data missing 'root' property"); - } - - if (!data.materials || data.materials.length === 0) { - this._logger.warn(" data has no materials"); - } - - if (!data.textures || data.textures.length === 0) { - this._logger.warn(" data has no textures"); - } - - if (!data.images || data.images.length === 0) { - this._logger.warn(" data has no images"); - } - - if (!data.geometries || data.geometries.length === 0) { - this._logger.warn(" data has no geometries"); - } - - this._logger.log("Validation complete"); - } - - /** - * Get the material factory (for advanced use cases) - */ - public getMaterialFactory(): MaterialFactory { - return this._materialFactory; - } - - /** - * Get the geometry factory (for advanced use cases) - */ - public getGeometryFactory(): GeometryFactory { - return this._geometryFactory; - } -} diff --git a/tools/src/effect/systems/effectParticleSystem.ts b/tools/src/effect/systems/effectParticleSystem.ts index e4b5a2b99..e0c8245c9 100644 --- a/tools/src/effect/systems/effectParticleSystem.ts +++ b/tools/src/effect/systems/effectParticleSystem.ts @@ -16,6 +16,7 @@ import type { PerParticleBehaviorFunction, ISystem, ParticleWithSystem, + IShape, } from "../types"; import { applyColorOverLifePS, @@ -40,6 +41,7 @@ import { export class EffectParticleSystem extends ParticleSystem implements ISystem { private _perParticleBehaviors: PerParticleBehaviorFunction[]; private _behaviorConfigs: Behavior[]; + private _parent: AbstractMesh | TransformNode | null; /** Store reference to default updateFunction */ private _defaultUpdateFunction: (particles: Particle[]) => void; @@ -56,6 +58,18 @@ export class EffectParticleSystem extends ParticleSystem implements ISystem { this._setupCustomUpdateFunction(); } + public get parent(): AbstractMesh | TransformNode | null { + return this._parent; + } + + public set parent(parent: AbstractMesh | TransformNode | null) { + this._parent = parent; + } + + public setParent(parent: AbstractMesh | TransformNode | null): void { + this._parent = parent; + } + /** * Setup custom updateFunction that extends default behavior * with per-particle behavior execution @@ -132,7 +146,7 @@ export class EffectParticleSystem extends ParticleSystem implements ISystem { switch (behavior.type) { case "ColorBySpeed": { const b = behavior as IColorBySpeedBehavior; - functions.push((particle: Particle) => applyColorBySpeedPS(particle, b)); + functions.push((particle: Particle) => applyColorBySpeedPS(b, particle)); break; } @@ -204,14 +218,14 @@ export class EffectParticleSystem extends ParticleSystem implements ISystem { * Configure emitter from shape config * This replaces the need for EmitterFactory */ - public configureEmitterFromShape(shape: any, cumulativeScale: any, _rotationMatrix: any): void { + public configureEmitterFromShape(shape: IShape): void { if (!shape || !shape.type) { this.createPointEmitter(new Vector3(0, 1, 0), new Vector3(0, 1, 0)); return; } const shapeType = shape.type.toLowerCase(); - const radius = (shape.radius ?? 1) * ((cumulativeScale.x + cumulativeScale.y + cumulativeScale.z) / 3); + const radius = shape.radius ?? 1; const angle = shape.angle ?? Math.PI / 4; switch (shapeType) { @@ -225,7 +239,7 @@ export class EffectParticleSystem extends ParticleSystem implements ISystem { this.createPointEmitter(new Vector3(0, 1, 0), new Vector3(0, 1, 0)); break; case "box": { - const boxSize = (shape.size || [1, 1, 1]).map((s: number, i: number) => s * [cumulativeScale.x, cumulativeScale.y, cumulativeScale.z][i]); + const boxSize = shape.size || [1, 1, 1]; const minBox = new Vector3(-boxSize[0] / 2, -boxSize[1] / 2, -boxSize[2] / 2); const maxBox = new Vector3(boxSize[0] / 2, boxSize[1] / 2, boxSize[2] / 2); this.createBoxEmitter(new Vector3(0, 1, 0), new Vector3(0, 1, 0), minBox, maxBox); @@ -235,7 +249,7 @@ export class EffectParticleSystem extends ParticleSystem implements ISystem { this.createHemisphericEmitter(radius); break; case "cylinder": { - const height = (shape.height ?? 1) * cumulativeScale.y; + const height = shape.height ?? 1; this.createCylinderEmitter(radius, height); break; } diff --git a/tools/src/effect/systems/effectSolidParticleSystem.ts b/tools/src/effect/systems/effectSolidParticleSystem.ts index 6f82b9d4d..a488e245e 100644 --- a/tools/src/effect/systems/effectSolidParticleSystem.ts +++ b/tools/src/effect/systems/effectSolidParticleSystem.ts @@ -58,7 +58,7 @@ export class EffectSolidParticleSystem extends SolidParticleSystem implements IS public particleEmitterType: ISolidParticleEmitterType | null; private _emitEnded: boolean; private _emitter: AbstractMesh | null; - + private _parent: AbstractMesh | TransformNode | null; // Gradient systems for "OverLife" behaviors (similar to ParticleSystem native gradients) private _colorGradients: ColorGradientSystem; private _sizeGradients: NumberGradientSystem; @@ -195,6 +195,17 @@ export class EffectSolidParticleSystem extends SolidParticleSystem implements IS return this.mesh || null; } + public get parent(): AbstractMesh | TransformNode | null { + return this._parent; + } + + public set parent(parent: AbstractMesh | TransformNode | null) { + this._parent = parent; + } + + public setParent(parent: AbstractMesh | TransformNode | null): void { + this._parent = parent; + } /** * Emitter property (like ParticleSystem) * Sets the parent for the mesh - the point from which particles emit diff --git a/tools/src/effect/types/factories.ts b/tools/src/effect/types/factories.ts index 54448a73a..117a631c2 100644 --- a/tools/src/effect/types/factories.ts +++ b/tools/src/effect/types/factories.ts @@ -1,15 +1,15 @@ -import { Nullable, Mesh, PBRMaterial, Texture, Scene } from "babylonjs"; +import { Mesh, PBRMaterial, Texture, Scene } from "babylonjs"; /** * Factory interfaces for dependency injection */ export interface IMaterialFactory { - createMaterial(materialId: string, name: string): Nullable; - createTexture(materialId: string): Nullable; + createMaterial(materialId: string, name: string): PBRMaterial; + createTexture(materialId: string): Texture; getBlendMode(materialId: string): number | undefined; } export interface IGeometryFactory { - createMesh(geometryId: string, name: string, scene: Scene): Nullable; - createParticleMesh(config: { instancingGeometry?: string }, name: string, scene: Scene): Nullable; + createMesh(geometryId: string, name: string, scene: Scene): Mesh; + createParticleMesh(config: { instancingGeometry?: string }, name: string, scene: Scene): Mesh; } diff --git a/tools/src/effect/types/hierarchy.ts b/tools/src/effect/types/hierarchy.ts index cace99240..0eb5c1b88 100644 --- a/tools/src/effect/types/hierarchy.ts +++ b/tools/src/effect/types/hierarchy.ts @@ -41,9 +41,6 @@ export interface IEmitter { */ export interface IData { root: IGroup | IEmitter | null; - groups: Map; - emitters: Map; - // Resources (converted from Quarks, ready for Babylon.js) materials: IMaterial[]; textures: ITexture[]; images: IImage[]; diff --git a/tools/src/effect/types/loader.ts b/tools/src/effect/types/loader.ts index 5aaf2cd91..ea9636afb 100644 --- a/tools/src/effect/types/loader.ts +++ b/tools/src/effect/types/loader.ts @@ -1,5 +1,5 @@ /** - * Options for parsing Quarks/Three.js particle JSON + * Options for loading effect */ export interface ILoaderOptions { /** diff --git a/tools/src/effect/types/system.ts b/tools/src/effect/types/system.ts index aefdf6e95..aa3c75f87 100644 --- a/tools/src/effect/types/system.ts +++ b/tools/src/effect/types/system.ts @@ -52,3 +52,19 @@ export function isSystem(system: unknown): system is ISystem { typeof (system as ISystem).stop === "function" ); } + +/** + * Effect Node - represents either a particle system or a group + */ +export interface IEffectNode { + /** Node name */ + name: string; + /** Node UUID from original JSON */ + uuid: string; + /** Particle system (if this is a particle emitter) */ + data: EffectParticleSystem | EffectSolidParticleSystem | TransformNode; + /** Child nodes */ + children: IEffectNode[]; + /** Node type */ + type: "particle" | "group"; +} diff --git a/yarn.lock b/yarn.lock index f6abf1617..08ef32bee 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1474,6 +1474,11 @@ resolved "https://registry.yarnpkg.com/@radix-ui/number/-/number-1.1.0.tgz#1e95610461a09cdf8bb05c152e76ca1278d5da46" integrity sha512-V3gRzhVNU1ldS5XhAPTom1fOIo4ccrjjJgmE+LI2h/WaFpHmx0MQApT+KZHnx8abG6Avtfcz4WoEciMnpFT3HQ== +"@radix-ui/number@1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@radix-ui/number/-/number-1.1.1.tgz#7b2c9225fbf1b126539551f5985769d0048d9090" + integrity sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g== + "@radix-ui/primitive@1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@radix-ui/primitive/-/primitive-1.0.1.tgz#e46f9958b35d10e9f6dc71c497305c22e3e55dbd" @@ -2012,6 +2017,21 @@ "@radix-ui/react-use-callback-ref" "1.1.1" "@radix-ui/react-use-controllable-state" "1.2.2" +"@radix-ui/react-scroll-area@^1.2.10": + version "1.2.10" + resolved "https://registry.yarnpkg.com/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.10.tgz#e4fd3b4a79bb77bec1a52f0c8f26d8f3f1ca4b22" + integrity sha512-tAXIa1g3sM5CGpVT0uIbUx/U3Gs5N8T52IICuCtObaos1S8fzsrPXG5WObkQN3S6NVl6wKgPhAIiBGbWnvc97A== + dependencies: + "@radix-ui/number" "1.1.1" + "@radix-ui/primitive" "1.1.3" + "@radix-ui/react-compose-refs" "1.1.2" + "@radix-ui/react-context" "1.1.2" + "@radix-ui/react-direction" "1.1.1" + "@radix-ui/react-presence" "1.1.5" + "@radix-ui/react-primitive" "2.1.3" + "@radix-ui/react-use-callback-ref" "1.1.1" + "@radix-ui/react-use-layout-effect" "1.1.1" + "@radix-ui/react-select@^2.0.0": version "2.0.0" resolved "https://registry.yarnpkg.com/@radix-ui/react-select/-/react-select-2.0.0.tgz#a3511792a51a7018d6559357323a7f52e0e38887" @@ -2786,6 +2806,13 @@ dependencies: tslib "^2.4.0" +"@types/adm-zip@^0.5.7": + version "0.5.7" + resolved "https://registry.yarnpkg.com/@types/adm-zip/-/adm-zip-0.5.7.tgz#eec10b6f717d3948beb64aca0abebc4b344ac7e9" + integrity sha512-DNEs/QvmyRLurdQPChqq0Md4zGvPwHerAJYWk9l2jCbD1VPpnzRJorOdiq4zsw09NFbYnhfsoEhWtxIzXpn2yw== + dependencies: + "@types/node" "*" + "@types/babel__core@^7.20.4": version "7.20.5" resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.20.5.tgz#3df15f27ba85319caa07ba08d0721889bb39c017" @@ -3276,6 +3303,11 @@ acorn@^8.9.0: resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.11.3.tgz#71e0b14e13a4ec160724b38fb7b0f233b1b81d7a" integrity sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg== +adm-zip@^0.5.16: + version "0.5.16" + resolved "https://registry.yarnpkg.com/adm-zip/-/adm-zip-0.5.16.tgz#0b5e4c779f07dedea5805cdccb1147071d94a909" + integrity sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ== + agent-base@6, agent-base@^6.0.2: version "6.0.2" resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77" @@ -3674,6 +3706,7 @@ babylonjs-editor-tools@latest: "@radix-ui/react-popover" "^1.0.7" "@radix-ui/react-progress" "^1.0.3" "@radix-ui/react-radio-group" "^1.1.3" + "@radix-ui/react-scroll-area" "^1.2.10" "@radix-ui/react-select" "^2.0.0" "@radix-ui/react-separator" "^1.0.3" "@radix-ui/react-slider" "^1.2.0" @@ -3687,6 +3720,7 @@ babylonjs-editor-tools@latest: "@recast-navigation/generators" "^0.43.0" "@xterm/addon-fit" "^0.10.0" "@xterm/xterm" "^5.6.0-beta.119" + adm-zip "^0.5.16" assimpjs "0.0.10" axios "^1.12.0" babylonjs "8.41.0" @@ -3714,6 +3748,7 @@ babylonjs-editor-tools@latest: framer-motion "12.23.24" fs-extra "11.2.0" glob "11.1.0" + js-yaml "^4.1.1" markdown-to-jsx "7.6.2" math-expression-evaluator "^2.0.6" md5 "^2.3.0" @@ -6550,6 +6585,13 @@ js-yaml@^4.1.0: dependencies: argparse "^2.0.1" +js-yaml@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.1.tgz#854c292467705b699476e1a2decc0c8a3458806b" + integrity sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA== + dependencies: + argparse "^2.0.1" + jsesc@^3.0.2: version "3.1.0" resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-3.1.0.tgz#74d335a234f67ed19907fdadfac7ccf9d409825d" From 431857016ab058b7623b67b4062a41a8c3a7f4e3 Mon Sep 17 00:00:00 2001 From: Mikalai Lazitski Date: Wed, 31 Dec 2025 12:41:58 +0300 Subject: [PATCH 53/62] refactor: streamline effect class by consolidating system and group management, enhancing node handling with a new NodeFactory, and improving recursive search methods for better performance and maintainability --- tools/src/effect/effect.ts | 398 +++++++++------------- tools/src/effect/factories/nodeFactory.ts | 85 +++-- 2 files changed, 223 insertions(+), 260 deletions(-) diff --git a/tools/src/effect/effect.ts b/tools/src/effect/effect.ts index d86f826b8..7feafa948 100644 --- a/tools/src/effect/effect.ts +++ b/tools/src/effect/effect.ts @@ -1,7 +1,7 @@ -import { Scene, IDisposable, TransformNode, MeshBuilder, Texture, Color4, AbstractMesh, Tools } from "babylonjs"; +import { Scene, IDisposable, TransformNode } from "babylonjs"; import { EffectParticleSystem } from "./systems/effectParticleSystem"; import { EffectSolidParticleSystem } from "./systems/effectSolidParticleSystem"; -import { IGroup, IEmitter, IData, isSystem, IEffectNode, ILoaderOptions, IParticleSystemConfig } from "./types"; +import { IData, IEffectNode, ILoaderOptions, IParticleSystemConfig } from "./types"; import { NodeFactory } from "./factories"; /** @@ -19,20 +19,8 @@ export class Effect implements IDisposable { return this._root; } - /** Map of systems by name for quick lookup */ - private readonly _systemsByName = new Map(); - - /** Map of systems by UUID for quick lookup */ - private readonly _systemsByUuid = new Map(); - - /** Map of groups by name */ - private readonly _groupsByName = new Map(); - - /** Map of groups by UUID */ - private readonly _groupsByUuid = new Map(); - - /** Scene reference for creating new systems */ - private _scene: Scene | null = null; + /** NodeFactory for creating groups and systems */ + private _nodeFactory: NodeFactory | null = null; /** * Create Effect from IData @@ -48,51 +36,122 @@ export class Effect implements IDisposable { throw new Error("Effect constructor requires IData and Scene"); } - this._scene = scene; - const nodeFactory = new NodeFactory(scene, data, rootUrl, options); - this._root = nodeFactory.create(); + this._nodeFactory = new NodeFactory(scene, data, rootUrl, options); + this._root = this._nodeFactory.create(); + } + + /** + * Recursively find a node by name in the tree + */ + private _findNodeByName(node: IEffectNode | null, name: string): IEffectNode | null { + if (!node) { + return null; + } + if (node.name === name) { + return node; + } + for (const child of node.children) { + const found = this._findNodeByName(child, name); + if (found) { + return found; + } + } + return null; + } + + /** + * Recursively find a node by UUID in the tree + */ + private _findNodeByUuid(node: IEffectNode | null, uuid: string): IEffectNode | null { + if (!node) { + return null; + } + if (node.uuid === uuid) { + return node; + } + for (const child of node.children) { + const found = this._findNodeByUuid(child, uuid); + if (found) { + return found; + } + } + return null; + } + + /** + * Recursively collect all systems from the tree + */ + private _collectAllSystems(node: IEffectNode | null, systems: (EffectParticleSystem | EffectSolidParticleSystem)[]): void { + if (!node) { + return; + } + if (node.type === "particle") { + const system = node.data as EffectParticleSystem | EffectSolidParticleSystem; + if (system) { + systems.push(system); + } + } + for (const child of node.children) { + this._collectAllSystems(child, systems); + } } /** * Find a particle system by name */ public findSystemByName(name: string): EffectParticleSystem | EffectSolidParticleSystem | null { - return this._systemsByName.get(name) || null; + const node = this._findNodeByName(this._root, name); + if (node && node.type === "particle") { + return node.data as EffectParticleSystem | EffectSolidParticleSystem; + } + return null; } /** * Find a particle system by UUID */ public findSystemByUuid(uuid: string): EffectParticleSystem | EffectSolidParticleSystem | null { - return this._systemsByUuid.get(uuid) || null; + const node = this._findNodeByUuid(this._root, uuid); + if (node && node.type === "particle") { + return node.data as EffectParticleSystem | EffectSolidParticleSystem; + } + return null; } /** * Find a group by name */ public findGroupByName(name: string): TransformNode | null { - return this._groupsByName.get(name) || null; + const node = this._findNodeByName(this._root, name); + if (node && node.type === "group") { + return node.data as TransformNode; + } + return null; } /** * Find a group by UUID */ public findGroupByUuid(uuid: string): TransformNode | null { - return this._groupsByUuid.get(uuid) || null; + const node = this._findNodeByUuid(this._root, uuid); + if (node && node.type === "group") { + return node.data as TransformNode; + } + return null; } /** * Find a node (system or group) by name */ public findNodeByName(name: string): IEffectNode | null { - return this._nodes.get(name) || null; + return this._findNodeByName(this._root, name); } /** * Find a node (system or group) by UUID */ public findNodeByUuid(uuid: string): IEffectNode | null { - return this._nodes.get(uuid) || null; + return this._findNodeByUuid(this._root, uuid); } /** @@ -102,40 +161,28 @@ export class Effect implements IDisposable { * then getSystemsInGroup("Group1") will return System1. */ public getSystemsInGroup(groupName: string): (EffectParticleSystem | EffectSolidParticleSystem)[] { - const group = this.findGroupByName(groupName); - if (!group) { + const groupNode = this.findNodeByName(groupName); + if (!groupNode || groupNode.type !== "group") { return []; } const systems: (EffectParticleSystem | EffectSolidParticleSystem)[] = []; - this._collectSystemsInGroup(group, systems); + this._collectSystemsInGroupNode(groupNode, systems); return systems; } /** - * Recursively collect systems in a group (including systems from all nested child groups) - * This method: - * 1. Collects all systems that have this group as direct parent - * 2. Recursively processes all child groups and collects their systems too + * Recursively collect systems in a group node (including systems from all nested child groups) */ - private _collectSystemsInGroup(group: TransformNode, systems: (EffectParticleSystem | EffectSolidParticleSystem)[]): void { - // Step 1: Find systems that have this group as direct parent - for (const system of this._systems) { - if (isSystem(system)) { - const parentNode = system.getParentNode(); - if (parentNode && parentNode.parent === group) { - systems.push(system); - } + private _collectSystemsInGroupNode(groupNode: IEffectNode, systems: (EffectParticleSystem | EffectSolidParticleSystem)[]): void { + if (groupNode.type === "particle") { + const system = groupNode.data as EffectParticleSystem | EffectSolidParticleSystem; + if (system) { + systems.push(system); } } - - // Step 2: Recursively process all child groups - // This ensures systems from nested groups are also collected - for (const [, groupNode] of this._groupsByUuid) { - if (groupNode.parent === group) { - // Recursively collect systems from child group (and its nested groups) - this._collectSystemsInGroup(groupNode, systems); - } + for (const child of groupNode.children) { + this._collectSystemsInGroupNode(child, systems); } } @@ -187,9 +234,12 @@ export class Effect implements IDisposable { * Start a node (system or group) */ public startNode(node: IEffectNode): void { - if (node.type === "particle" && node.system) { - node.system.start(); - } else if (node.type === "group" && node.group) { + if (node.type === "particle") { + const system = node.data as EffectParticleSystem | EffectSolidParticleSystem; + if (system && typeof system.start === "function") { + system.start(); + } + } else if (node.type === "group") { // Find all systems in this group recursively const systems = this._getSystemsInNode(node); for (const system of systems) { @@ -202,9 +252,12 @@ export class Effect implements IDisposable { * Stop a node (system or group) */ public stopNode(node: IEffectNode): void { - if (node.type === "particle" && node.system) { - node.system.stop(); - } else if (node.type === "group" && node.group) { + if (node.type === "particle") { + const system = node.data as EffectParticleSystem | EffectSolidParticleSystem; + if (system && typeof system.stop === "function") { + system.stop(); + } + } else if (node.type === "group") { // Find all systems in this group recursively const systems = this._getSystemsInNode(node); for (const system of systems) { @@ -217,9 +270,12 @@ export class Effect implements IDisposable { * Reset a node (system or group) */ public resetNode(node: IEffectNode): void { - if (node.type === "particle" && node.system) { - node.system.reset(); - } else if (node.type === "group" && node.group) { + if (node.type === "particle") { + const system = node.data as EffectParticleSystem | EffectSolidParticleSystem; + if (system && typeof system.reset === "function") { + system.reset(); + } + } else if (node.type === "group") { // Find all systems in this group recursively const systems = this._getSystemsInNode(node); for (const system of systems) { @@ -232,14 +288,15 @@ export class Effect implements IDisposable { * Check if a node is started (system or group) */ public isNodeStarted(node: IEffectNode): boolean { - if (node.type === "particle" && node.system) { - if (node.system instanceof EffectParticleSystem) { - return (node.system as any).isStarted ? (node.system as any).isStarted() : false; - } else if (node.system instanceof EffectSolidParticleSystem) { - return (node.system as any)._started && !(node.system as any)._stopped; + if (node.type === "particle") { + const system = node.data as EffectParticleSystem | EffectSolidParticleSystem; + if (system instanceof EffectParticleSystem) { + return (system as any).isStarted ? (system as any).isStarted() : false; + } else if (system instanceof EffectSolidParticleSystem) { + return (system as any)._started && !(system as any)._stopped; } return false; - } else if (node.type === "group" && node.group) { + } else if (node.type === "group") { // Check if any system in this group is started const systems = this._getSystemsInNode(node); return systems.some((system) => { @@ -260,8 +317,11 @@ export class Effect implements IDisposable { private _getSystemsInNode(node: IEffectNode): (EffectParticleSystem | EffectSolidParticleSystem)[] { const systems: (EffectParticleSystem | EffectSolidParticleSystem)[] = []; - if (node.type === "particle" && node.system) { - systems.push(node.system); + if (node.type === "particle") { + const system = node.data as EffectParticleSystem | EffectSolidParticleSystem; + if (system) { + systems.push(system); + } } else if (node.type === "group") { // Recursively collect all systems from children for (const child of node.children) { @@ -276,7 +336,9 @@ export class Effect implements IDisposable { * Start all particle systems */ public start(): void { - for (const system of this._systems) { + const systems: (EffectParticleSystem | EffectSolidParticleSystem)[] = []; + this._collectAllSystems(this._root, systems); + for (const system of systems) { system.start(); } } @@ -285,7 +347,9 @@ export class Effect implements IDisposable { * Stop all particle systems */ public stop(): void { - for (const system of this._systems) { + const systems: (EffectParticleSystem | EffectSolidParticleSystem)[] = []; + this._collectAllSystems(this._root, systems); + for (const system of systems) { system.stop(); } } @@ -294,7 +358,9 @@ export class Effect implements IDisposable { * Reset all particle systems (stop and clear particles) */ public reset(): void { - for (const system of this._systems) { + const systems: (EffectParticleSystem | EffectSolidParticleSystem)[] = []; + this._collectAllSystems(this._root, systems); + for (const system of systems) { system.reset(); } } @@ -303,7 +369,9 @@ export class Effect implements IDisposable { * Check if any system is started */ public isStarted(): boolean { - for (const system of this._systems) { + const systems: (EffectParticleSystem | EffectSolidParticleSystem)[] = []; + this._collectAllSystems(this._root, systems); + for (const system of systems) { if (system instanceof EffectParticleSystem) { if ((system as any).isStarted && (system as any).isStarted()) { return true; @@ -325,8 +393,8 @@ export class Effect implements IDisposable { * @returns Created group node */ public createGroup(parentNode: IEffectNode | null = null, name: string = "Group"): IEffectNode | null { - if (!this._scene) { - console.error("Cannot create group: scene is not available"); + if (!this._nodeFactory) { + console.error("Cannot create group: NodeFactory is not available"); return null; } @@ -339,38 +407,17 @@ export class Effect implements IDisposable { // Ensure unique name let uniqueName = name; let counter = 1; - while (this._nodes.has(uniqueName)) { + while (this._findNodeByName(this._root, uniqueName)) { uniqueName = `${name} ${counter}`; counter++; } - const groupUuid = Tools.RandomId(); - const groupNode = new TransformNode(uniqueName, this._scene); - groupNode.id = groupUuid; - - // Set parent transform - if (parent.group) { - groupNode.setParent(parent.group, false, true); - } - - const newNode: IEffectNode = { - name: uniqueName, - uuid: groupUuid, - group: groupNode, - parent, - children: [], - type: "group", - }; + // Create group using NodeFactory + const newNode = this._nodeFactory.createGroup(uniqueName, parent); // Add to parent's children parent.children.push(newNode); - // Store in maps - this._groupsByName.set(uniqueName, groupNode); - this._groupsByUuid.set(groupUuid, groupNode); - this._nodes.set(groupUuid, newNode); - this._nodes.set(uniqueName, newNode); - return newNode; } @@ -379,11 +426,17 @@ export class Effect implements IDisposable { * @param parentNode Parent node (if null, adds to root) * @param systemType Type of system ("solid" or "base") * @param name Optional name (defaults to "ParticleSystem") + * @param config Optional particle system config * @returns Created particle system node */ - public createParticleSystem(parentNode: IEffectNode | null = null, systemType: "solid" | "base" = "base", name: string = "ParticleSystem"): IEffectNode | null { - if (!this._scene) { - console.error("Cannot create particle system: scene is not available"); + public createParticleSystem( + parentNode: IEffectNode | null = null, + systemType: "solid" | "base" = "base", + name: string = "ParticleSystem", + config?: Partial + ): IEffectNode | null { + if (!this._nodeFactory) { + console.error("Cannot create particle system: NodeFactory is not available"); return null; } @@ -396,150 +449,17 @@ export class Effect implements IDisposable { // Ensure unique name let uniqueName = name; let counter = 1; - while (this._nodes.has(uniqueName)) { + while (this._findNodeByName(this._root, uniqueName)) { uniqueName = `${name} ${counter}`; counter++; } - const systemUuid = Tools.RandomId(); - - // Create default config - const config: IParticleSystemConfig = { - systemType, - targetStopDuration: 0, // looping - manualEmitCount: -1, - emitRate: 10, - minLifeTime: 1, - maxLifeTime: 1, - minEmitPower: 1, - maxEmitPower: 1, - minSize: 1, - maxSize: 1, - color1: new Color4(1, 1, 1, 1), - color2: new Color4(1, 1, 1, 1), - colorDead: new Color4(1, 1, 1, 0), - behaviors: [], - }; - - let system: EffectParticleSystem | EffectSolidParticleSystem; - - // Create system instance based on type - if (systemType === "solid") { - system = new EffectSolidParticleSystem(uniqueName, this._scene, { - updatable: true, - isPickable: false, - enableDepthSort: false, - particleIntersection: false, - useModelMaterial: true, - }); - const particleMesh = MeshBuilder.CreateSphere("particleMesh", { segments: 16, diameter: 1 }, this._scene); - system.particleMesh = particleMesh; - } else { - const capacity = 500; - system = new EffectParticleSystem(uniqueName, capacity, this._scene); - system.particleTexture = new Texture("https://assets.babylonjs.com/core/textures/flare.png", this._scene); - } - - // Set system name - system.name = uniqueName; - system.emitter = parent.group as AbstractMesh; - // === Assign native properties (shared by both systems) === - if (config.minSize !== undefined) { - system.minSize = config.minSize; - } - if (config.maxSize !== undefined) { - system.maxSize = config.maxSize; - } - if (config.minLifeTime !== undefined) { - system.minLifeTime = config.minLifeTime; - } - if (config.maxLifeTime !== undefined) { - system.maxLifeTime = config.maxLifeTime; - } - if (config.minEmitPower !== undefined) { - system.minEmitPower = config.minEmitPower; - } - if (config.maxEmitPower !== undefined) { - system.maxEmitPower = config.maxEmitPower; - } - if (config.emitRate !== undefined) { - system.emitRate = config.emitRate; - } - if (config.targetStopDuration !== undefined) { - system.targetStopDuration = config.targetStopDuration; - } - if (config.manualEmitCount !== undefined) { - system.manualEmitCount = config.manualEmitCount; - } - if (config.preWarmCycles !== undefined) { - system.preWarmCycles = config.preWarmCycles; - } - if (config.preWarmStepOffset !== undefined) { - system.preWarmStepOffset = config.preWarmStepOffset; - } - if (config.color1 !== undefined) { - system.color1 = config.color1; - } - if (config.color2 !== undefined) { - system.color2 = config.color2; - } - if (config.colorDead !== undefined) { - system.colorDead = config.colorDead; - } - if (config.minInitialRotation !== undefined) { - system.minInitialRotation = config.minInitialRotation; - } - if (config.maxInitialRotation !== undefined) { - system.maxInitialRotation = config.maxInitialRotation; - } - if (config.isLocal !== undefined) { - system.isLocal = config.isLocal; - } - if (config.disposeOnStop !== undefined) { - system.disposeOnStop = config.disposeOnStop; - } - - // === Apply gradients (shared by both systems) === - if (config.startSizeGradients) { - for (const grad of config.startSizeGradients) { - system.addStartSizeGradient(grad.gradient, grad.factor, grad.factor2); - } - } - if (config.lifeTimeGradients) { - for (const grad of config.lifeTimeGradients) { - system.addLifeTimeGradient(grad.gradient, grad.factor, grad.factor2); - } - } - if (config.emitRateGradients) { - for (const grad of config.emitRateGradients) { - system.addEmitRateGradient(grad.gradient, grad.factor, grad.factor2); - } - } - - // === Apply behaviors (shared by both systems) === - if (config.behaviors !== undefined) { - system.setBehaviors(config.behaviors); - } - - const newNode: IEffectNode = { - name: uniqueName, - uuid: systemUuid, - system, - parent, - children: [], - type: "particle", - }; + // Create particle system using NodeFactory + const newNode = this._nodeFactory.createParticleSystem(uniqueName, systemType, config, parent); // Add to parent's children parent.children.push(newNode); - // Store in maps - this._systems.push(system); - this._systemsByName.set(uniqueName, system); - this._systemsByUuid.set(systemUuid, system); - this._nodes.set(systemUuid, newNode); - this._nodes.set(uniqueName, newNode); - return newNode; } @@ -547,15 +467,11 @@ export class Effect implements IDisposable { * Dispose all resources */ public dispose(): void { - for (const system of this._systems) { + const systems: (EffectParticleSystem | EffectSolidParticleSystem)[] = []; + this._collectAllSystems(this._root, systems); + for (const system of systems) { system.dispose(); } - this._systems = []; this._root = null; - this._systemsByName.clear(); - this._systemsByUuid.clear(); - this._groupsByName.clear(); - this._groupsByUuid.clear(); - this._nodes.clear(); } } diff --git a/tools/src/effect/factories/nodeFactory.ts b/tools/src/effect/factories/nodeFactory.ts index 7e17e1b90..141e90629 100644 --- a/tools/src/effect/factories/nodeFactory.ts +++ b/tools/src/effect/factories/nodeFactory.ts @@ -1,8 +1,8 @@ -import { Nullable, Vector3, TransformNode, Scene, AbstractMesh, Tools } from "babylonjs"; +import { Vector3, TransformNode, Scene, AbstractMesh, Tools, Quaternion, Color4 } from "babylonjs"; import { EffectParticleSystem, EffectSolidParticleSystem } from "../systems"; import { IData, IGroup, IEmitter, ITransform, IParticleSystemConfig, ILoaderOptions, IMaterialFactory, IGeometryFactory, IEffectNode, isSystem } from "../types"; import { Logger } from "../loggers/logger"; -import { CapacityCalculator, ValueUtils, MatrixUtils } from "../utils"; +import { CapacityCalculator, ValueUtils } from "../utils"; import { MaterialFactory } from "./materialFactory"; import { GeometryFactory } from "./geometryFactory"; /** @@ -403,23 +403,6 @@ export class NodeFactory { return sps; } - /** - * Calculate cumulative scale from parent groups - */ - private _calculateCumulativeScale(parent: Nullable): Vector3 { - const cumulativeScale = new Vector3(1, 1, 1); - let current = parent; - - while (current) { - cumulativeScale.x *= current.scaling.x; - cumulativeScale.y *= current.scaling.y; - cumulativeScale.z *= current.scaling.z; - current = current.parent as TransformNode; - } - - return cumulativeScale; - } - /** * Apply transform to a node */ @@ -464,4 +447,68 @@ export class NodeFactory { this._logger.log(`Set parent: ${node.name} -> ${parent?.name || "none"}`); } + + /** + * Create a new group node + * @param name Group name + * @param parentNode Parent node (optional) + * @returns Created group node + */ + public createGroup(name: string, parentNode: IEffectNode | null = null): IEffectNode { + const groupUuid = Tools.RandomId(); + const group: IGroup = { + uuid: groupUuid, + name, + transform: { + position: Vector3.Zero(), + rotation: Quaternion.Identity(), + scale: Vector3.One(), + }, + children: [], + }; + + return this._createGroupNode(group, parentNode); + } + + /** + * Create a new particle system node + * @param name System name + * @param systemType Type of system ("solid" or "base") + * @param config Optional particle system config + * @param parentNode Parent node (optional) + * @returns Created particle system node + */ + public createParticleSystem(name: string, systemType: "solid" | "base" = "base", config?: Partial, parentNode: IEffectNode | null = null): IEffectNode { + const systemUuid = Tools.RandomId(); + const defaultConfig: IParticleSystemConfig = { + systemType, + targetStopDuration: 0, // looping + manualEmitCount: -1, + emitRate: 10, + minLifeTime: 1, + maxLifeTime: 1, + minEmitPower: 1, + maxEmitPower: 1, + minSize: 1, + maxSize: 1, + color1: new Color4(1, 1, 1, 1), + color2: new Color4(1, 1, 1, 1), + behaviors: [], + ...config, + }; + + const emitter: IEmitter = { + uuid: systemUuid, + name, + transform: { + position: Vector3.Zero(), + rotation: Quaternion.Identity(), + scale: Vector3.One(), + }, + config: defaultConfig, + systemType, + }; + + return this._createParticleNode(emitter, parentNode); + } } From 4a17a45aeb7932d6bc98809f262f9c700051fe2e Mon Sep 17 00:00:00 2001 From: Mikalai Lazitski Date: Mon, 5 Jan 2026 13:19:11 +0300 Subject: [PATCH 54/62] fix: imports --- tools/src/effect/behaviors/colorBySpeed.ts | 4 ++-- tools/src/effect/behaviors/colorOverLife.ts | 8 +++----- tools/src/effect/behaviors/forceOverLife.ts | 8 ++++---- tools/src/effect/behaviors/frameOverLife.ts | 6 +++--- .../src/effect/behaviors/limitSpeedOverLife.ts | 7 +++---- tools/src/effect/behaviors/orbitOverLife.ts | 8 ++++---- tools/src/effect/behaviors/rotationBySpeed.ts | 9 +++++---- tools/src/effect/behaviors/rotationOverLife.ts | 7 +++---- tools/src/effect/behaviors/sizeBySpeed.ts | 8 +++++--- tools/src/effect/behaviors/sizeOverLife.ts | 5 ++--- tools/src/effect/behaviors/speedOverLife.ts | 7 +++---- tools/src/effect/behaviors/utils.ts | 2 +- tools/src/effect/effect.ts | 3 ++- tools/src/effect/emitters/solidBoxEmitter.ts | 11 +++-------- tools/src/effect/emitters/solidConeEmitter.ts | 2 +- .../src/effect/emitters/solidCylinderEmitter.ts | 10 +++------- .../effect/emitters/solidHemisphericEmitter.ts | 3 +-- tools/src/effect/emitters/solidPointEmitter.ts | 3 ++- tools/src/effect/emitters/solidSphereEmitter.ts | 2 +- tools/src/effect/factories/geometryFactory.ts | 12 +++++++----- tools/src/effect/factories/materialFactory.ts | 14 +++++++++----- tools/src/effect/factories/nodeFactory.ts | 13 ++++++++++--- tools/src/effect/index.ts | 1 - .../src/effect/systems/effectParticleSystem.ts | 7 ++++++- .../effect/systems/effectSolidParticleSystem.ts | 8 +++++++- tools/src/effect/types/behaviors.ts | 5 ++++- tools/src/effect/types/emitter.ts | 16 ++++++++++------ tools/src/effect/types/factories.ts | 5 ++++- tools/src/effect/types/hierarchy.ts | 3 ++- tools/src/effect/types/index.ts | 1 - tools/src/effect/types/resources.ts | 2 +- tools/src/effect/types/system.ts | 8 +++++--- yarn.lock | 17 ++++------------- 33 files changed, 120 insertions(+), 105 deletions(-) diff --git a/tools/src/effect/behaviors/colorBySpeed.ts b/tools/src/effect/behaviors/colorBySpeed.ts index e737a51f7..9bbb495c0 100644 --- a/tools/src/effect/behaviors/colorBySpeed.ts +++ b/tools/src/effect/behaviors/colorBySpeed.ts @@ -1,5 +1,5 @@ -import type { IColorBySpeedBehavior } from "../types/behaviors"; -import type { Particle } from "babylonjs"; +import type { IColorBySpeedBehavior } from "../types"; +import type { Particle } from "@babylonjs/core/Particles/particle"; import { interpolateColorKeys } from "./utils"; /** diff --git a/tools/src/effect/behaviors/colorOverLife.ts b/tools/src/effect/behaviors/colorOverLife.ts index fd2853c60..30ee7e11f 100644 --- a/tools/src/effect/behaviors/colorOverLife.ts +++ b/tools/src/effect/behaviors/colorOverLife.ts @@ -1,9 +1,7 @@ -import { Color4 } from "babylonjs"; -import type { IColorOverLifeBehavior } from "../types/behaviors"; +import { Color4 } from "@babylonjs/core/Maths/math.color"; +import type { IColorOverLifeBehavior } from "../types"; import { extractColorFromValue, extractAlphaFromValue } from "./utils"; -import type { EffectSolidParticleSystem } from "../systems/effectSolidParticleSystem"; -import type { EffectParticleSystem } from "../systems/effectParticleSystem"; - +import type { EffectSolidParticleSystem, EffectParticleSystem } from "../systems"; /** * Apply ColorOverLife behavior to ParticleSystem * Uses unified IColorFunction structure: behavior.color = { colorFunctionType, data } diff --git a/tools/src/effect/behaviors/forceOverLife.ts b/tools/src/effect/behaviors/forceOverLife.ts index 488e03bb4..fd4a8f842 100644 --- a/tools/src/effect/behaviors/forceOverLife.ts +++ b/tools/src/effect/behaviors/forceOverLife.ts @@ -1,7 +1,7 @@ -import { Vector3 } from "babylonjs"; -import type { IForceOverLifeBehavior, IGravityForceBehavior } from "../types/behaviors"; -import { ValueUtils } from "../utils/valueParser"; -import type { EffectParticleSystem } from "../systems/effectParticleSystem"; +import { Vector3 } from "@babylonjs/core/Maths/math.vector"; +import type { IForceOverLifeBehavior, IGravityForceBehavior } from "../types"; +import { ValueUtils } from "../utils"; +import type { EffectParticleSystem } from "../systems"; /** * Apply ForceOverLife behavior to ParticleSystem */ diff --git a/tools/src/effect/behaviors/frameOverLife.ts b/tools/src/effect/behaviors/frameOverLife.ts index 960d539a8..3d92c0a4c 100644 --- a/tools/src/effect/behaviors/frameOverLife.ts +++ b/tools/src/effect/behaviors/frameOverLife.ts @@ -1,6 +1,6 @@ -import type { IFrameOverLifeBehavior } from "../types/behaviors"; -import { ValueUtils } from "../utils/valueParser"; -import type { EffectParticleSystem } from "../systems/effectParticleSystem"; +import type { IFrameOverLifeBehavior } from "../types"; +import { ValueUtils } from "../utils"; +import type { EffectParticleSystem } from "../systems"; /** * Apply FrameOverLife behavior to ParticleSystem */ diff --git a/tools/src/effect/behaviors/limitSpeedOverLife.ts b/tools/src/effect/behaviors/limitSpeedOverLife.ts index 2b3ae2f72..6266a0ba5 100644 --- a/tools/src/effect/behaviors/limitSpeedOverLife.ts +++ b/tools/src/effect/behaviors/limitSpeedOverLife.ts @@ -1,8 +1,7 @@ -import type { ILimitSpeedOverLifeBehavior } from "../types/behaviors"; +import type { ILimitSpeedOverLifeBehavior } from "../types"; import { extractNumberFromValue } from "./utils"; -import { ValueUtils } from "../utils/valueParser"; -import type { EffectSolidParticleSystem } from "../systems/effectSolidParticleSystem"; -import type { EffectParticleSystem } from "../systems/effectParticleSystem"; +import { ValueUtils } from "../utils"; +import type { EffectSolidParticleSystem, EffectParticleSystem } from "../systems"; /** * Apply LimitSpeedOverLife behavior to ParticleSystem */ diff --git a/tools/src/effect/behaviors/orbitOverLife.ts b/tools/src/effect/behaviors/orbitOverLife.ts index 32321fd1f..f10ec4c9c 100644 --- a/tools/src/effect/behaviors/orbitOverLife.ts +++ b/tools/src/effect/behaviors/orbitOverLife.ts @@ -1,8 +1,8 @@ -import { Particle, SolidParticle } from "babylonjs"; -import type { IOrbitOverLifeBehavior } from "../types/behaviors"; +import { Particle } from "@babylonjs/core/Particles/particle"; +import { SolidParticle } from "@babylonjs/core/Particles/solidParticle"; +import type { IOrbitOverLifeBehavior, Value } from "../types"; import { extractNumberFromValue, interpolateGradientKeys } from "./utils"; -import { ValueUtils } from "../utils/valueParser"; -import type { Value } from "../types/values"; +import { ValueUtils } from "../utils"; /** * Apply OrbitOverLife behavior to Particle diff --git a/tools/src/effect/behaviors/rotationBySpeed.ts b/tools/src/effect/behaviors/rotationBySpeed.ts index b6fe763dd..b46f7a35d 100644 --- a/tools/src/effect/behaviors/rotationBySpeed.ts +++ b/tools/src/effect/behaviors/rotationBySpeed.ts @@ -1,8 +1,9 @@ -import { Particle, SolidParticle, Vector3 } from "babylonjs"; -import type { IRotationBySpeedBehavior } from "../types/behaviors"; +import { Particle } from "@babylonjs/core/Particles/particle"; +import { SolidParticle } from "@babylonjs/core/Particles/solidParticle"; +import { Vector3 } from "@babylonjs/core/Maths/math.vector"; import { extractNumberFromValue, interpolateGradientKeys } from "./utils"; -import { ValueUtils } from "../utils/valueParser"; -import { ParticleWithSystem, SolidParticleWithSystem } from "../types/system"; +import { ValueUtils } from "../utils"; +import { ParticleWithSystem, SolidParticleWithSystem, type IRotationBySpeedBehavior } from "../types"; /** * Apply RotationBySpeed behavior to Particle diff --git a/tools/src/effect/behaviors/rotationOverLife.ts b/tools/src/effect/behaviors/rotationOverLife.ts index 2fc38e86e..3e086311d 100644 --- a/tools/src/effect/behaviors/rotationOverLife.ts +++ b/tools/src/effect/behaviors/rotationOverLife.ts @@ -1,8 +1,7 @@ -import type { IRotationOverLifeBehavior } from "../types/behaviors"; -import { ValueUtils } from "../utils/valueParser"; +import type { IRotationOverLifeBehavior } from "../types"; +import { ValueUtils } from "../utils"; import { extractNumberFromValue } from "./utils"; -import type { EffectSolidParticleSystem } from "../systems/effectSolidParticleSystem"; -import type { EffectParticleSystem } from "../systems/effectParticleSystem"; +import type { EffectSolidParticleSystem, EffectParticleSystem } from "../systems"; /** * Apply RotationOverLife behavior to ParticleSystem * Uses addAngularSpeedGradient for gradient support (Babylon.js native) diff --git a/tools/src/effect/behaviors/sizeBySpeed.ts b/tools/src/effect/behaviors/sizeBySpeed.ts index 5dffd1496..ed18708e5 100644 --- a/tools/src/effect/behaviors/sizeBySpeed.ts +++ b/tools/src/effect/behaviors/sizeBySpeed.ts @@ -1,7 +1,9 @@ -import { Particle, SolidParticle, Vector3 } from "babylonjs"; -import type { ISizeBySpeedBehavior } from "../types/behaviors"; +import { Particle } from "@babylonjs/core/Particles/particle"; +import { SolidParticle } from "@babylonjs/core/Particles/solidParticle"; +import { Vector3 } from "@babylonjs/core/Maths/math.vector"; +import type { ISizeBySpeedBehavior } from "../types"; import { extractNumberFromValue, interpolateGradientKeys } from "./utils"; -import { ValueUtils } from "../utils/valueParser"; +import { ValueUtils } from "../utils"; /** * Apply SizeBySpeed behavior to Particle diff --git a/tools/src/effect/behaviors/sizeOverLife.ts b/tools/src/effect/behaviors/sizeOverLife.ts index d43dba19c..c6e9b6891 100644 --- a/tools/src/effect/behaviors/sizeOverLife.ts +++ b/tools/src/effect/behaviors/sizeOverLife.ts @@ -1,7 +1,6 @@ -import type { ISizeOverLifeBehavior } from "../types/behaviors"; +import type { ISizeOverLifeBehavior } from "../types"; import { extractNumberFromValue } from "./utils"; -import type { EffectSolidParticleSystem } from "../systems/effectSolidParticleSystem"; -import type { EffectParticleSystem } from "../systems/effectParticleSystem"; +import type { EffectSolidParticleSystem, EffectParticleSystem } from "../systems"; /** * Apply SizeOverLife behavior to ParticleSystem * In Quarks, SizeOverLife values are multipliers relative to initial particle size diff --git a/tools/src/effect/behaviors/speedOverLife.ts b/tools/src/effect/behaviors/speedOverLife.ts index 8eeb4c87e..6a6b3b219 100644 --- a/tools/src/effect/behaviors/speedOverLife.ts +++ b/tools/src/effect/behaviors/speedOverLife.ts @@ -1,8 +1,7 @@ -import type { ISpeedOverLifeBehavior } from "../types/behaviors"; +import type { ISpeedOverLifeBehavior } from "../types"; import { extractNumberFromValue } from "./utils"; -import { ValueUtils } from "../utils/valueParser"; -import type { EffectSolidParticleSystem } from "../systems/effectSolidParticleSystem"; -import type { EffectParticleSystem } from "../systems/effectParticleSystem"; +import { ValueUtils } from "../utils"; +import type { EffectSolidParticleSystem, EffectParticleSystem } from "../systems"; /** * Apply SpeedOverLife behavior to ParticleSystem */ diff --git a/tools/src/effect/behaviors/utils.ts b/tools/src/effect/behaviors/utils.ts index 17d1d73cf..f7afcac4c 100644 --- a/tools/src/effect/behaviors/utils.ts +++ b/tools/src/effect/behaviors/utils.ts @@ -1,4 +1,4 @@ -import type { IGradientKey } from "../types/gradients"; +import type { IGradientKey } from "../types"; /** * Extract RGB color from gradient key value diff --git a/tools/src/effect/effect.ts b/tools/src/effect/effect.ts index 7feafa948..5b3fa77bc 100644 --- a/tools/src/effect/effect.ts +++ b/tools/src/effect/effect.ts @@ -1,4 +1,5 @@ -import { Scene, IDisposable, TransformNode } from "babylonjs"; +import { IDisposable, Scene } from "@babylonjs/core/scene"; +import { TransformNode } from "@babylonjs/core/Meshes/transformNode"; import { EffectParticleSystem } from "./systems/effectParticleSystem"; import { EffectSolidParticleSystem } from "./systems/effectSolidParticleSystem"; import { IData, IEffectNode, ILoaderOptions, IParticleSystemConfig } from "./types"; diff --git a/tools/src/effect/emitters/solidBoxEmitter.ts b/tools/src/effect/emitters/solidBoxEmitter.ts index 0beca0d8d..3fedcc5c2 100644 --- a/tools/src/effect/emitters/solidBoxEmitter.ts +++ b/tools/src/effect/emitters/solidBoxEmitter.ts @@ -1,4 +1,5 @@ -import { SolidParticle, Vector3 } from "babylonjs"; +import { SolidParticle } from "@babylonjs/core/Particles/solidParticle"; +import { Vector3 } from "@babylonjs/core/Maths/math.vector"; import { ISolidParticleEmitterType } from "../types"; /** @@ -26,12 +27,7 @@ export class SolidBoxParticleEmitter implements ISolidParticleEmitterType { */ public maxEmitBox: Vector3 = new Vector3(0.5, 0.5, 0.5); - constructor( - direction1?: Vector3, - direction2?: Vector3, - minEmitBox?: Vector3, - maxEmitBox?: Vector3 - ) { + constructor(direction1?: Vector3, direction2?: Vector3, minEmitBox?: Vector3, maxEmitBox?: Vector3) { if (direction1) { this.direction1 = direction1; } @@ -72,4 +68,3 @@ export class SolidBoxParticleEmitter implements ISolidParticleEmitterType { particle.velocity.set(dirX * startSpeed, dirY * startSpeed, dirZ * startSpeed); } } - diff --git a/tools/src/effect/emitters/solidConeEmitter.ts b/tools/src/effect/emitters/solidConeEmitter.ts index 7ba985166..18d4b1ff9 100644 --- a/tools/src/effect/emitters/solidConeEmitter.ts +++ b/tools/src/effect/emitters/solidConeEmitter.ts @@ -1,4 +1,4 @@ -import { SolidParticle } from "babylonjs"; +import { SolidParticle } from "@babylonjs/core/Particles/solidParticle"; import { ISolidParticleEmitterType } from "../types"; /** diff --git a/tools/src/effect/emitters/solidCylinderEmitter.ts b/tools/src/effect/emitters/solidCylinderEmitter.ts index 90aa083bd..c024ad5eb 100644 --- a/tools/src/effect/emitters/solidCylinderEmitter.ts +++ b/tools/src/effect/emitters/solidCylinderEmitter.ts @@ -1,4 +1,5 @@ -import { SolidParticle, Vector3 } from "babylonjs"; +import { SolidParticle } from "@babylonjs/core/Particles/solidParticle"; +import { Vector3 } from "@babylonjs/core/Maths/math.vector"; import { ISolidParticleEmitterType } from "../types"; /** @@ -71,13 +72,8 @@ export class SolidCylinderParticleEmitter implements ISolidParticleEmitterType { let dirAngle = Math.atan2(this._tempVector.x, this._tempVector.z); dirAngle += this._randomRange(-Math.PI / 2, Math.PI / 2) * this.directionRandomizer; - particle.velocity.set( - Math.sin(dirAngle), - randY, - Math.cos(dirAngle) - ); + particle.velocity.set(Math.sin(dirAngle), randY, Math.cos(dirAngle)); particle.velocity.normalize(); particle.velocity.scaleInPlace(startSpeed); } } - diff --git a/tools/src/effect/emitters/solidHemisphericEmitter.ts b/tools/src/effect/emitters/solidHemisphericEmitter.ts index e20e033e7..5c74d16f1 100644 --- a/tools/src/effect/emitters/solidHemisphericEmitter.ts +++ b/tools/src/effect/emitters/solidHemisphericEmitter.ts @@ -1,4 +1,4 @@ -import { SolidParticle } from "babylonjs"; +import { SolidParticle } from "@babylonjs/core/Particles/solidParticle"; import { ISolidParticleEmitterType } from "../types"; /** @@ -66,4 +66,3 @@ export class SolidHemisphericParticleEmitter implements ISolidParticleEmitterTyp particle.velocity.scaleInPlace(startSpeed); } } - diff --git a/tools/src/effect/emitters/solidPointEmitter.ts b/tools/src/effect/emitters/solidPointEmitter.ts index b4195d3ad..c03402663 100644 --- a/tools/src/effect/emitters/solidPointEmitter.ts +++ b/tools/src/effect/emitters/solidPointEmitter.ts @@ -1,4 +1,5 @@ -import { SolidParticle, Vector3 } from "babylonjs"; +import { SolidParticle } from "@babylonjs/core/Particles/solidParticle"; +import { Vector3 } from "@babylonjs/core/Maths/math.vector"; import { ISolidParticleEmitterType } from "../types"; /** diff --git a/tools/src/effect/emitters/solidSphereEmitter.ts b/tools/src/effect/emitters/solidSphereEmitter.ts index 205bf77a8..43bfc986c 100644 --- a/tools/src/effect/emitters/solidSphereEmitter.ts +++ b/tools/src/effect/emitters/solidSphereEmitter.ts @@ -1,4 +1,4 @@ -import { SolidParticle } from "babylonjs"; +import { SolidParticle } from "@babylonjs/core/Particles/solidParticle"; import { ISolidParticleEmitterType } from "../types"; /** diff --git a/tools/src/effect/factories/geometryFactory.ts b/tools/src/effect/factories/geometryFactory.ts index e50e5c456..f8b40f9f7 100644 --- a/tools/src/effect/factories/geometryFactory.ts +++ b/tools/src/effect/factories/geometryFactory.ts @@ -1,9 +1,11 @@ -import { Mesh, VertexData, CreatePlane, Nullable, Scene } from "babylonjs"; -import type { IGeometryFactory } from "../types/factories"; +import { Mesh } from "@babylonjs/core/Meshes/mesh"; +import { VertexData } from "@babylonjs/core/Meshes/mesh.vertexData"; +import { CreatePlane } from "@babylonjs/core/Meshes/Builders/planeBuilder"; +import { Scene } from "@babylonjs/core/scene"; +import type { IGeometryFactory } from "../types"; import { Logger } from "../loggers/logger"; -import type { IData } from "../types/hierarchy"; -import type { IGeometry } from "../types/resources"; -import type { ILoaderOptions } from "../types/loader"; +import type { IData, IGeometry, ILoaderOptions } from "../types"; +import { Nullable } from "@babylonjs/core/types"; /** * Factory for creating meshes from Three.js geometry data diff --git a/tools/src/effect/factories/materialFactory.ts b/tools/src/effect/factories/materialFactory.ts index 43b41af9a..71d72a168 100644 --- a/tools/src/effect/factories/materialFactory.ts +++ b/tools/src/effect/factories/materialFactory.ts @@ -1,9 +1,13 @@ -import { Nullable, Texture as BabylonTexture, PBRMaterial, Material as BabylonMaterial, Constants, Tools, Scene, Color3 } from "babylonjs"; -import type { IMaterialFactory } from "../types/factories"; +import { Texture as BabylonTexture } from "@babylonjs/core/Materials/Textures/texture"; +import { PBRMaterial } from "@babylonjs/core/Materials/PBR/pbrMaterial"; +import { Material as BabylonMaterial } from "@babylonjs/core/Materials/material"; +import { Constants } from "@babylonjs/core/Engines/constants"; +import { Tools } from "@babylonjs/core/Misc/tools"; +import { Scene } from "@babylonjs/core/scene"; +import { Color3 } from "@babylonjs/core/Maths/math.color"; + import { Logger } from "../loggers/logger"; -import type { ILoaderOptions } from "../types/loader"; -import type { IData } from "../types/hierarchy"; -import type { IMaterial, ITexture, IImage } from "../types/resources"; +import type { IMaterialFactory, ILoaderOptions, IData, IMaterial, ITexture, IImage } from "../types"; /** * Factory for creating materials and textures from Three.js JSON data diff --git a/tools/src/effect/factories/nodeFactory.ts b/tools/src/effect/factories/nodeFactory.ts index 141e90629..557e927ed 100644 --- a/tools/src/effect/factories/nodeFactory.ts +++ b/tools/src/effect/factories/nodeFactory.ts @@ -1,4 +1,11 @@ -import { Vector3, TransformNode, Scene, AbstractMesh, Tools, Quaternion, Color4 } from "babylonjs"; +import { Vector3 } from "@babylonjs/core/Maths/math.vector"; +import { TransformNode } from "@babylonjs/core/Meshes/transformNode"; +import { Scene } from "@babylonjs/core/scene"; +import { AbstractMesh } from "@babylonjs/core/Meshes/abstractMesh"; +import { Tools } from "@babylonjs/core/Misc/tools"; +import { Quaternion } from "@babylonjs/core/Maths/math.vector"; +import { Color4 } from "@babylonjs/core/Maths/math.color"; + import { EffectParticleSystem, EffectSolidParticleSystem } from "../systems"; import { IData, IGroup, IEmitter, ITransform, IParticleSystemConfig, ILoaderOptions, IMaterialFactory, IGeometryFactory, IEffectNode, isSystem } from "../types"; import { Logger } from "../loggers/logger"; @@ -302,7 +309,7 @@ export class NodeFactory { /** * Create a ParticleSystem instance */ - private _createEffectParticleSystem(emitter: IEmitter, parentNode: IEffectNode | null): EffectParticleSystem { + private _createEffectParticleSystem(emitter: IEmitter, _parentNode: IEffectNode | null): EffectParticleSystem { const { name, config } = emitter; this._logger.log(`Creating ParticleSystem: ${name}`); @@ -369,7 +376,7 @@ export class NodeFactory { /** * Create a SolidParticleSystem instance */ - private _createEffectSolidParticleSystem(emitter: IEmitter, parentNode: IEffectNode | null): EffectSolidParticleSystem { + private _createEffectSolidParticleSystem(emitter: IEmitter, _parentNode: IEffectNode | null): EffectSolidParticleSystem { const { name, config } = emitter; this._logger.log(`Creating SolidParticleSystem: ${name}`); diff --git a/tools/src/effect/index.ts b/tools/src/effect/index.ts index 02285d8d8..013ac32fb 100644 --- a/tools/src/effect/index.ts +++ b/tools/src/effect/index.ts @@ -1,5 +1,4 @@ export * from "./types"; -export * from "./parsers"; export * from "./factories"; export * from "./utils"; export * from "./systems"; diff --git a/tools/src/effect/systems/effectParticleSystem.ts b/tools/src/effect/systems/effectParticleSystem.ts index e0c8245c9..6b158f79f 100644 --- a/tools/src/effect/systems/effectParticleSystem.ts +++ b/tools/src/effect/systems/effectParticleSystem.ts @@ -1,4 +1,9 @@ -import { ParticleSystem, Scene, AbstractMesh, TransformNode, Particle, Vector3 } from "babylonjs"; +import { ParticleSystem } from "@babylonjs/core/Particles/particleSystem"; +import { Scene } from "@babylonjs/core/scene"; +import { AbstractMesh } from "@babylonjs/core/Meshes/abstractMesh"; +import { TransformNode } from "@babylonjs/core/Meshes/transformNode"; +import { Particle } from "@babylonjs/core/Particles/particle"; +import { Vector3 } from "@babylonjs/core/Maths/math.vector"; import type { Behavior, IColorOverLifeBehavior, diff --git a/tools/src/effect/systems/effectSolidParticleSystem.ts b/tools/src/effect/systems/effectSolidParticleSystem.ts index a488e245e..eca00255e 100644 --- a/tools/src/effect/systems/effectSolidParticleSystem.ts +++ b/tools/src/effect/systems/effectSolidParticleSystem.ts @@ -1,4 +1,10 @@ -import { Vector3, Quaternion, Matrix, Color4, SolidParticle, TransformNode, Mesh, AbstractMesh, SolidParticleSystem } from "babylonjs"; +import { Quaternion, Vector3, Matrix } from "@babylonjs/core/Maths/math.vector"; +import { Color4 } from "@babylonjs/core/Maths/math.color"; +import { SolidParticle } from "@babylonjs/core/Particles/solidParticle"; +import { TransformNode } from "@babylonjs/core/Meshes/transformNode"; +import { Mesh } from "@babylonjs/core/Meshes/mesh"; +import { AbstractMesh } from "@babylonjs/core/Meshes/abstractMesh"; +import { SolidParticleSystem } from "@babylonjs/core/Particles/solidParticleSystem"; import type { Behavior, IForceOverLifeBehavior, diff --git a/tools/src/effect/types/behaviors.ts b/tools/src/effect/types/behaviors.ts index 85326a5fa..0531e79e4 100644 --- a/tools/src/effect/types/behaviors.ts +++ b/tools/src/effect/types/behaviors.ts @@ -1,6 +1,9 @@ import type { Value } from "./values"; import type { IGradientKey } from "./gradients"; -import { Particle, ParticleSystem, SolidParticle, SolidParticleSystem } from "babylonjs"; +import { Particle } from "@babylonjs/core/Particles/particle"; +import { ParticleSystem } from "@babylonjs/core/Particles/particleSystem"; +import { SolidParticle } from "@babylonjs/core/Particles/solidParticle"; +import { SolidParticleSystem } from "@babylonjs/core/Particles/solidParticleSystem"; /** * Per-particle behavior function for ParticleSystem diff --git a/tools/src/effect/types/emitter.ts b/tools/src/effect/types/emitter.ts index a2b608f02..e7f0dc090 100644 --- a/tools/src/effect/types/emitter.ts +++ b/tools/src/effect/types/emitter.ts @@ -1,4 +1,8 @@ -import { Nullable, SolidParticle, TransformNode, Vector3 } from "babylonjs"; +import { Nullable } from "@babylonjs/core/types"; +import { SolidParticle } from "@babylonjs/core/Particles/solidParticle"; +import { TransformNode } from "@babylonjs/core/Meshes/transformNode"; +import { Vector3 } from "@babylonjs/core/Maths/math.vector"; +import { Color4 } from "@babylonjs/core/Maths/math.color"; import type { IEmitter } from "./hierarchy"; import type { Value } from "./values"; import type { IShape } from "./shapes"; @@ -43,9 +47,9 @@ export interface IParticleSystemConfig { maxAngularSpeed?: number; // Color - color1?: import("babylonjs").Color4; - color2?: import("babylonjs").Color4; - colorDead?: import("babylonjs").Color4; + color1?: Color4; + color2?: Color4; + colorDead?: Color4; // Duration & Looping targetStopDuration?: number; // 0 = infinite (looping), >0 = duration @@ -56,8 +60,8 @@ export interface IParticleSystemConfig { preWarmStepOffset?: number; // Physics - gravity?: import("babylonjs").Vector3; - noiseStrength?: import("babylonjs").Vector3; + gravity?: Vector3; + noiseStrength?: Vector3; updateSpeed?: number; // World space diff --git a/tools/src/effect/types/factories.ts b/tools/src/effect/types/factories.ts index 117a631c2..f63b31481 100644 --- a/tools/src/effect/types/factories.ts +++ b/tools/src/effect/types/factories.ts @@ -1,4 +1,7 @@ -import { Mesh, PBRMaterial, Texture, Scene } from "babylonjs"; +import { Mesh } from "@babylonjs/core/Meshes/mesh"; +import { PBRMaterial } from "@babylonjs/core/Materials/PBR/pbrMaterial"; +import { Texture } from "@babylonjs/core/Materials/Textures/texture"; +import { Scene } from "@babylonjs/core/scene"; /** * Factory interfaces for dependency injection diff --git a/tools/src/effect/types/hierarchy.ts b/tools/src/effect/types/hierarchy.ts index 0eb5c1b88..851053698 100644 --- a/tools/src/effect/types/hierarchy.ts +++ b/tools/src/effect/types/hierarchy.ts @@ -1,4 +1,5 @@ -import { Vector3, Quaternion } from "babylonjs"; +import { Vector3 } from "@babylonjs/core/Maths/math.vector"; +import { Quaternion } from "@babylonjs/core/Maths/math.vector"; import type { IParticleSystemConfig } from "./emitter"; import type { IMaterial, ITexture, IImage, IGeometry } from "./resources"; diff --git a/tools/src/effect/types/index.ts b/tools/src/effect/types/index.ts index 4a9c51822..82e419c92 100644 --- a/tools/src/effect/types/index.ts +++ b/tools/src/effect/types/index.ts @@ -6,7 +6,6 @@ export * from "./shapes"; export * from "./behaviors"; export * from "./emitter"; export * from "./system"; -export * from "./quarksTypes"; export * from "./loader"; export * from "./hierarchy"; export * from "./resources"; diff --git a/tools/src/effect/types/resources.ts b/tools/src/effect/types/resources.ts index 64810f8a1..cfa14e87f 100644 --- a/tools/src/effect/types/resources.ts +++ b/tools/src/effect/types/resources.ts @@ -1,4 +1,4 @@ -import { Color3 } from "babylonjs"; +import { Color3 } from "@babylonjs/core/Maths/math.color"; /** * Material (converted from Quarks, ready for Babylon.js) diff --git a/tools/src/effect/types/system.ts b/tools/src/effect/types/system.ts index aa3c75f87..996b210e7 100644 --- a/tools/src/effect/types/system.ts +++ b/tools/src/effect/types/system.ts @@ -1,6 +1,8 @@ -import { TransformNode, AbstractMesh, Particle, SolidParticle } from "babylonjs"; -import type { EffectParticleSystem } from "../systems/effectParticleSystem"; -import type { EffectSolidParticleSystem } from "../systems/effectSolidParticleSystem"; +import { TransformNode } from "@babylonjs/core/Meshes/transformNode"; +import { AbstractMesh } from "@babylonjs/core/Meshes/abstractMesh"; +import { Particle } from "@babylonjs/core/Particles/particle"; +import { SolidParticle } from "@babylonjs/core/Particles/solidParticle"; +import type { EffectParticleSystem, EffectSolidParticleSystem } from "../systems"; /** * Common interface for all particle systems diff --git a/yarn.lock b/yarn.lock index e49626d74..db0a37489 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3981,8 +3981,9 @@ babylonjs-editor-tools@latest: resolved "https://registry.yarnpkg.com/babylonjs-editor-tools/-/babylonjs-editor-tools-5.0.0.tgz#d2e1919cc5d4defbcbcf60259a1eb73a589cf643" integrity sha512-AREjL0WjtjyOvud0EMG/II3zH73KlSif/u0HV965tPWmUZHrxr+g/4iX6eU0mIYlIjOuepfRAopaF04IYJOaHA== -"babylonjs-editor-tools@link:../../AppData/Local/Yarn/Cache/v6/npm-babylonjs-editor-5.2.4-3cce3a704dc0c4572a85041a993264060376230a-integrity/node_modules/tools": +"babylonjs-editor-tools@link:../../../Library/Caches/Yarn/v6/npm-babylonjs-editor-5.2.4-3cce3a704dc0c4572a85041a993264060376230a-integrity/node_modules/tools": version "0.0.0" + uid "" "babylonjs-editor-tools@link:tools": version "5.2.4" @@ -4012,7 +4013,6 @@ babylonjs-editor@latest: "@radix-ui/react-popover" "^1.0.7" "@radix-ui/react-progress" "^1.0.3" "@radix-ui/react-radio-group" "^1.1.3" - "@radix-ui/react-scroll-area" "^1.2.10" "@radix-ui/react-select" "^2.0.0" "@radix-ui/react-separator" "^1.0.3" "@radix-ui/react-slider" "^1.2.0" @@ -4026,12 +4026,11 @@ babylonjs-editor@latest: "@recast-navigation/generators" "^0.43.0" "@xterm/addon-fit" "^0.10.0" "@xterm/xterm" "^5.6.0-beta.119" - adm-zip "^0.5.16" assimpjs "0.0.10" axios "1.12.0" babylonjs "8.41.0" babylonjs-addons "8.41.0" - babylonjs-editor-tools "link:../../../Library/Caches/Yarn/v6/npm-babylonjs-editor-5.2.4-3cce3a704dc0c4572a85041a993264060376230a-integrity/node_modules/tools" + babylonjs-editor-tools "link:C:/Users/soull/AppData/Local/Yarn/Cache/v6/npm-babylonjs-editor-5.2.4-3cce3a704dc0c4572a85041a993264060376230a-integrity/node_modules/tools" babylonjs-gui "8.41.0" babylonjs-gui-editor "8.41.0" babylonjs-loaders "8.41.0" @@ -4055,7 +4054,6 @@ babylonjs-editor@latest: framer-motion "12.23.24" fs-extra "11.2.0" glob "11.1.0" - js-yaml "^4.1.1" markdown-to-jsx "7.6.2" math-expression-evaluator "^2.0.6" md5 "^2.3.0" @@ -6998,14 +6996,7 @@ js-tokens@^9.0.1: resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-9.0.1.tgz#2ec43964658435296f6761b34e10671c2d9527f4" integrity sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ== -js-yaml@^4.1.0: - version "4.1.1" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.1.tgz#854c292467705b699476e1a2decc0c8a3458806b" - integrity sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA== - dependencies: - argparse "^2.0.1" - -js-yaml@^4.1.1: +js-yaml@^4.1.0, js-yaml@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.1.tgz#854c292467705b699476e1a2decc0c8a3458806b" integrity sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA== From 769920a13b641f20d5034d5f89cd43428e76f3a3 Mon Sep 17 00:00:00 2001 From: Mikalai Lazitski Date: Mon, 5 Jan 2026 14:36:47 +0300 Subject: [PATCH 55/62] refactor: update effect editor to use QuarksConverter and improve data handling - Replaced the use of Parser from babylonjs-editor-tools with QuarksConverter for converting Quarks JSON to IData. - Updated references from `nodeData.system` to `nodeData.data` across various components to standardize data handling. - Adjusted imports to use specific Babylon.js core modules for better performance and clarity. - Removed unnecessary prewarm application calls in effect initialization. --- .../converters/quarksConverter.ts | 9 ++- .../editor/windows/effect-editor/graph.tsx | 27 +++---- .../editor/windows/effect-editor/index.tsx | 10 +-- .../editor/windows/effect-editor/preview.tsx | 11 ++- .../effect-editor/properties/behaviors.tsx | 4 +- .../effect-editor/properties/emission.tsx | 32 ++++---- .../properties/initialization.tsx | 60 +++++++-------- .../effect-editor/properties/object.tsx | 12 +-- .../effect-editor/properties/renderer.tsx | 75 +++++++++++-------- .../windows/effect-editor/properties/tab.tsx | 2 +- tools/src/effect/effect.ts | 2 +- 11 files changed, 130 insertions(+), 114 deletions(-) diff --git a/editor/src/editor/windows/effect-editor/converters/quarksConverter.ts b/editor/src/editor/windows/effect-editor/converters/quarksConverter.ts index 7ae52c361..3a01d9a20 100644 --- a/editor/src/editor/windows/effect-editor/converters/quarksConverter.ts +++ b/editor/src/editor/windows/effect-editor/converters/quarksConverter.ts @@ -1,4 +1,11 @@ -import { Vector3, Matrix, Quaternion, Color3, Texture as BabylonTexture, ParticleSystem, Color4, Tools } from "babylonjs"; +import { Matrix, Vector3 } from "@babylonjs/core/Maths/math.vector"; + +import { Quaternion } from "@babylonjs/core/Maths/math.vector"; +import { Color3 } from "@babylonjs/core/Maths/math.color"; +import { Texture as BabylonTexture } from "@babylonjs/core/Materials/Textures"; +import { ParticleSystem } from "@babylonjs/core/Particles/particleSystem"; +import { Color4 } from "@babylonjs/core/Maths/math.color"; +import { Tools } from "@babylonjs/core/Misc/tools"; import type { IQuarksJSON, IQuarksMaterial, diff --git a/editor/src/editor/windows/effect-editor/graph.tsx b/editor/src/editor/windows/effect-editor/graph.tsx index 177423d61..d4877a222 100644 --- a/editor/src/editor/windows/effect-editor/graph.tsx +++ b/editor/src/editor/windows/effect-editor/graph.tsx @@ -20,6 +20,8 @@ import { saveSingleFileDialog } from "../../../tools/dialog"; import { writeJSON } from "fs-extra"; import { toast } from "sonner"; import { Effect, type IEffectNode, EffectSolidParticleSystem, type IData } from "babylonjs-editor-tools"; +import { IQuarksJSON } from "./converters/quarksTypes"; +import { QuarksConverter } from "./converters"; export interface IEffectEditorGraphProps { filePath: string | null; @@ -122,13 +124,12 @@ export class EffectEditorGraph extends Component 0 ? Node.children.map((child) => this._convertNodeToTreeNode(child, false)) : undefined; // Check if solid particle system - const isSolid = Node.system instanceof EffectSolidParticleSystem; + const isSolid = Node.data instanceof EffectSolidParticleSystem; // Determine icon based on node type (sparkles for all particles, with color coding) let icon: JSX.Element; @@ -800,12 +795,12 @@ export class EffectEditorGraph extends Component { try { - // Import Unity converter - const { convertUnityPrefabToData } = await import("babylonjs-editor-tools"); - // Get Scene from preview for model loading - let scene = this.editor.preview?.scene || null; + let scene = this.editor.preview?.scene || undefined; if (!scene) { // Try waiting a bit for preview to initialize await new Promise((resolve) => setTimeout(resolve, 100)); - scene = this.editor.preview?.scene || null; + scene = this.editor.preview?.scene || undefined; } if (!scene) { console.warn("Scene not available for model loading, models will be placeholders"); @@ -179,7 +177,7 @@ export default class EffectEditorWindow extends Component { diff --git a/editor/src/editor/windows/effect-editor/properties/emission.tsx b/editor/src/editor/windows/effect-editor/properties/emission.tsx index 82e32fe9e..49f522377 100644 --- a/editor/src/editor/windows/effect-editor/properties/emission.tsx +++ b/editor/src/editor/windows/effect-editor/properties/emission.tsx @@ -331,11 +331,11 @@ function renderParticleSystemEmitter(system: EffectParticleSystem, onChange: () * Renders emitter shape properties */ function renderEmitterShape(nodeData: IEffectNode, onChange: () => void): ReactNode { - if (nodeData.type !== "particle" || !nodeData.system) { + if (nodeData.type !== "particle" || !nodeData.data) { return null; } - const system = nodeData.system; + const system = nodeData.data; if (system instanceof EffectSolidParticleSystem) { return renderSolidParticleSystemEmitter(system, onChange); @@ -420,22 +420,22 @@ function renderBursts(system: EffectParticleSystem | EffectSolidParticleSystem, * Renders emission parameters (looping, duration, emit over time/distance, bursts) */ function renderEmissionParameters(nodeData: IEffectNode, onChange: () => void): ReactNode { - if (nodeData.type !== "particle" || !nodeData.system) { + if (nodeData.type !== "particle" || !nodeData.data) { return null; } - const system = nodeData.system; + const system = nodeData.data; // Proxy for looping (targetStopDuration === 0 means looping) const loopingProxy = { get isLooping() { - return system.targetStopDuration === 0; + return (system as any).targetStopDuration === 0; }, set isLooping(value: boolean) { if (value) { - system.targetStopDuration = 0; - } else if (system.targetStopDuration === 0) { - system.targetStopDuration = 5; // Default duration + (system as any).targetStopDuration = 0; + } else if ((system as any).targetStopDuration === 0) { + (system as any).targetStopDuration = 5; // Default duration } }, }; @@ -443,14 +443,14 @@ function renderEmissionParameters(nodeData: IEffectNode, onChange: () => void): // Proxy for prewarm (preWarmCycles > 0 means prewarm enabled) const prewarmProxy = { get prewarm() { - return system.preWarmCycles > 0; + return (system as any).preWarmCycles > 0; }, set prewarm(value: boolean) { - if (value && system.preWarmCycles === 0) { - system.preWarmCycles = Math.ceil((system.targetStopDuration || 5) * 60); - system.preWarmStepOffset = 1 / 60; + if (value && (system as any).preWarmCycles === 0) { + (system as any).preWarmCycles = Math.ceil((system as any).targetStopDuration || 5) * 60; + (system as any).preWarmStepOffset = 1 / 60; } else if (!value) { - system.preWarmCycles = 0; + (system as any).preWarmCycles = 0; } }, }; @@ -458,11 +458,11 @@ function renderEmissionParameters(nodeData: IEffectNode, onChange: () => void): return ( <> - + {/* Emit Rate (native Babylon.js property) */} - + {/* Emit Over Distance - only for SolidParticleSystem */} {system instanceof EffectSolidParticleSystem && ( @@ -490,7 +490,7 @@ function renderEmissionParameters(nodeData: IEffectNode, onChange: () => void): export function EffectEditorEmissionProperties(props: IEffectEditorEmissionPropertiesProps): ReactNode { const { nodeData, onChange } = props; - if (nodeData.type !== "particle" || !nodeData.system) { + if (nodeData.type !== "particle" || !nodeData.data) { return null; } diff --git a/editor/src/editor/windows/effect-editor/properties/initialization.tsx b/editor/src/editor/windows/effect-editor/properties/initialization.tsx index 7c2f171f2..408b038cd 100644 --- a/editor/src/editor/windows/effect-editor/properties/initialization.tsx +++ b/editor/src/editor/windows/effect-editor/properties/initialization.tsx @@ -15,29 +15,29 @@ export function EffectEditorParticleInitializationProperties(props: IEffectEdito const { nodeData } = props; const onChange = props.onChange || (() => {}); - if (nodeData.type !== "particle" || !nodeData.system) { + if (nodeData.type !== "particle" || !nodeData.data) { return null; } - const system = nodeData.system; + const system = nodeData.data; // Helper to get/set startLife - both systems use native minLifeTime/maxLifeTime const getStartLife = (): Value | undefined => { // Both systems have native minLifeTime/maxLifeTime properties - return { type: "IntervalValue", min: system.minLifeTime, max: system.maxLifeTime }; + return { type: "IntervalValue", min: (system as any).minLifeTime, max: (system as any).maxLifeTime }; }; const setStartLife = (value: Value): void => { const interval = ValueUtils.parseIntervalValue(value); - system.minLifeTime = interval.min; - system.maxLifeTime = interval.max; + (system as any).minLifeTime = interval.min; + (system as any).maxLifeTime = interval.max; onChange(); }; // Helper to get/set startSize - both systems use native minSize/maxSize const getStartSize = (): Value | IVec3Function | undefined => { // Both systems have native minSize/maxSize properties - return { type: "IntervalValue", min: system.minSize, max: system.maxSize }; + return { type: "IntervalValue", min: (system as any).minSize, max: (system as any).maxSize }; }; const setStartSize = (value: Value | IVec3Function): void => { @@ -47,12 +47,12 @@ export function EffectEditorParticleInitializationProperties(props: IEffectEdito const y = ValueUtils.parseConstantValue(value.y); const z = ValueUtils.parseConstantValue(value.z); const avg = (x + y + z) / 3; - system.minSize = avg; - system.maxSize = avg; + (system as any).minSize = avg; + (system as any).maxSize = avg; } else { const interval = ValueUtils.parseIntervalValue(value as Value); - system.minSize = interval.min; - system.maxSize = interval.max; + (system as any).minSize = interval.min; + (system as any).maxSize = interval.max; } onChange(); }; @@ -60,28 +60,28 @@ export function EffectEditorParticleInitializationProperties(props: IEffectEdito // Helper to get/set startSpeed - both systems use native minEmitPower/maxEmitPower const getStartSpeed = (): Value | undefined => { // Both systems have native minEmitPower/maxEmitPower properties - return { type: "IntervalValue", min: system.minEmitPower, max: system.maxEmitPower }; + return { type: "IntervalValue", min: (system as any).minEmitPower, max: (system as any).maxEmitPower }; }; const setStartSpeed = (value: Value): void => { const interval = ValueUtils.parseIntervalValue(value); - system.minEmitPower = interval.min; - system.maxEmitPower = interval.max; + (system as any).minEmitPower = interval.min; + (system as any).maxEmitPower = interval.max; onChange(); }; // Helper to get/set startColor - both systems use native color1 const getStartColor = (): Color | undefined => { // Both systems have native color1 property - if (system.color1) { - return { type: "ConstantColor", value: [system.color1.r, system.color1.g, system.color1.b, system.color1.a] }; + if ((system as any).color1) { + return { type: "ConstantColor", value: [(system as any).color1.r, (system as any).color1.g, (system as any).color1.b, (system as any).color1.a] }; } return undefined; }; const setStartColor = (value: Color): void => { const color = ValueUtils.parseConstantColor(value); - system.color1 = color; + (system as any).color1 = color; onChange(); }; @@ -90,7 +90,7 @@ export function EffectEditorParticleInitializationProperties(props: IEffectEdito // Both systems have native minInitialRotation/maxInitialRotation properties return { type: "Euler", - angleZ: { type: "IntervalValue", min: system.minInitialRotation, max: system.maxInitialRotation }, + angleZ: { type: "IntervalValue", min: (system as any).minInitialRotation, max: (system as any).maxInitialRotation }, order: "xyz", }; }; @@ -99,52 +99,52 @@ export function EffectEditorParticleInitializationProperties(props: IEffectEdito // Extract angleZ from rotation if (typeof value === "object" && "type" in value && value.type === "Euler" && value.angleZ) { const interval = ValueUtils.parseIntervalValue(value.angleZ); - system.minInitialRotation = interval.min; - system.maxInitialRotation = interval.max; + (system as any).minInitialRotation = interval.min; + (system as any).maxInitialRotation = interval.max; } else if ( typeof value === "number" || (typeof value === "object" && "type" in value && (value.type === "ConstantValue" || value.type === "IntervalValue" || value.type === "PiecewiseBezier")) ) { const interval = ValueUtils.parseIntervalValue(value as Value); - system.minInitialRotation = interval.min; - system.maxInitialRotation = interval.max; + (system as any).minInitialRotation = interval.min; + (system as any).maxInitialRotation = interval.max; } onChange(); }; // Helper to get/set angular speed - both systems use native minAngularSpeed/maxAngularSpeed const getAngularSpeed = (): Value | undefined => { - return { type: "IntervalValue", min: system.minAngularSpeed, max: system.maxAngularSpeed }; + return { type: "IntervalValue", min: (system as any).minAngularSpeed, max: (system as any).maxAngularSpeed }; }; const setAngularSpeed = (value: Value): void => { const interval = ValueUtils.parseIntervalValue(value); - system.minAngularSpeed = interval.min; - system.maxAngularSpeed = interval.max; + (system as any).minAngularSpeed = interval.min; + (system as any).maxAngularSpeed = interval.max; onChange(); }; // Helper to get/set scale X - both systems use native minScaleX/maxScaleX const getScaleX = (): Value | undefined => { - return { type: "IntervalValue", min: system.minScaleX, max: system.maxScaleX }; + return { type: "IntervalValue", min: (system as any).minScaleX, max: (system as any).maxScaleX }; }; const setScaleX = (value: Value): void => { const interval = ValueUtils.parseIntervalValue(value); - system.minScaleX = interval.min; - system.maxScaleX = interval.max; + (system as any).minScaleX = interval.min; + (system as any).maxScaleX = interval.max; onChange(); }; // Helper to get/set scale Y - both systems use native minScaleY/maxScaleY const getScaleY = (): Value | undefined => { - return { type: "IntervalValue", min: system.minScaleY, max: system.maxScaleY }; + return { type: "IntervalValue", min: (system as any).minScaleY, max: (system as any).maxScaleY }; }; const setScaleY = (value: Value): void => { const interval = ValueUtils.parseIntervalValue(value); - system.minScaleY = interval.min; - system.maxScaleY = interval.max; + (system as any).minScaleY = interval.min; + (system as any).maxScaleY = interval.max; onChange(); }; diff --git a/editor/src/editor/windows/effect-editor/properties/object.tsx b/editor/src/editor/windows/effect-editor/properties/object.tsx index f48fbfd96..c0b0174b0 100644 --- a/editor/src/editor/windows/effect-editor/properties/object.tsx +++ b/editor/src/editor/windows/effect-editor/properties/object.tsx @@ -55,23 +55,23 @@ export function EffectEditorObjectProperties(props: IEffectEditorObjectPropertie const { nodeData, onChange } = props; // For groups, use transformNode directly - if (nodeData.type === "group" && nodeData.group) { - const group = nodeData.group; + if (nodeData.type === "group" && nodeData.data) { + const group = nodeData.data; return ( <> - {group.position && } + {(group as any).position && } {getRotationInspector(group, onChange)} - {group.scaling && } + {(group as any).scaling && } ); } // For particles, use system.emitter for VEffectParticleSystem or system.mesh for VEffectSolidParticleSystem - if (nodeData.type === "particle" && nodeData.system) { - const system = nodeData.system; + if (nodeData.type === "particle" && nodeData.data) { + const system = nodeData.data; // For VEffectSolidParticleSystem, use mesh (common mesh for all particles) if (system instanceof EffectSolidParticleSystem) { diff --git a/editor/src/editor/windows/effect-editor/properties/renderer.tsx b/editor/src/editor/windows/effect-editor/properties/renderer.tsx index 904216dfd..f416f3128 100644 --- a/editor/src/editor/windows/effect-editor/properties/renderer.tsx +++ b/editor/src/editor/windows/effect-editor/properties/renderer.tsx @@ -7,7 +7,9 @@ import { EditorInspectorListField } from "../../../layout/inspector/fields/list" import { EditorInspectorTextureField } from "../../../layout/inspector/fields/texture"; import { EditorInspectorGeometryField } from "../../../layout/inspector/fields/geometry"; -import { PBRMaterial, StandardMaterial, NodeMaterial, MultiMaterial, Material, ParticleSystem, Mesh } from "babylonjs"; +import { Material } from "@babylonjs/core/Materials/material"; +import { ParticleSystem } from "@babylonjs/core/Particles/particleSystem"; +import { Mesh } from "@babylonjs/core/Meshes/mesh"; import { EditorPBRMaterialInspector } from "../../../layout/inspector/material/pbr"; import { EditorStandardMaterialInspector } from "../../../layout/inspector/material/standard"; @@ -25,7 +27,6 @@ import { EditorGradientMaterialInspector } from "../../../layout/inspector/mater import { type IEffectNode, EffectSolidParticleSystem, EffectParticleSystem } from "babylonjs-editor-tools"; import { IEffectEditor } from ".."; -import { CellMaterial, FireMaterial, GradientMaterial, GridMaterial, LavaMaterial, NormalMaterial, SkyMaterial, TriPlanarMaterial, WaterMaterial } from "babylonjs-materials"; export interface IEffectEditorParticleRendererPropertiesProps { nodeData: IEffectNode; @@ -48,11 +49,11 @@ export class EffectEditorParticleRendererProperties extends Component { const proxy = { get worldSpace() { - return !system.isLocal; + return !(system as any).isLocal; }, set worldSpace(value: boolean) { - system.isLocal = !value; + (system as any).isLocal = !value; }, }; return this.props.onChange()} />; @@ -138,11 +139,11 @@ export class EffectEditorParticleRendererProperties extends Component; + return ; case "StandardMaterial": - return ; + return ; case "NodeMaterial": - return ; + return ; case "MultiMaterial": - return ; + return ; case "SkyMaterial": - return ; + return ; case "GridMaterial": - return ; + return ; case "NormalMaterial": - return ; + return ; case "WaterMaterial": - return ; + return ; case "LavaMaterial": - return ; + return ; case "TriPlanarMaterial": - return ; + return ; case "CellMaterial": - return ; + return ; case "FireMaterial": - return ; + return ; case "GradientMaterial": - return ; + return ; default: return null; @@ -202,16 +203,24 @@ export class EffectEditorParticleRendererProperties extends Component this.props.onChange()} />; + return ( + this.props.onChange()} + /> + ); } return null; @@ -220,11 +229,11 @@ export class EffectEditorParticleRendererProperties extends Component this.props.onChange()} />; + return this.props.onChange()} />; } } diff --git a/editor/src/editor/windows/effect-editor/properties/tab.tsx b/editor/src/editor/windows/effect-editor/properties/tab.tsx index a2f4144b3..f69eee40b 100644 --- a/editor/src/editor/windows/effect-editor/properties/tab.tsx +++ b/editor/src/editor/windows/effect-editor/properties/tab.tsx @@ -48,7 +48,7 @@ export class EffectEditorPropertiesTab extends Component

Select a particle system

diff --git a/tools/src/effect/effect.ts b/tools/src/effect/effect.ts index 5b3fa77bc..63a036d91 100644 --- a/tools/src/effect/effect.ts +++ b/tools/src/effect/effect.ts @@ -32,7 +32,7 @@ export class Effect implements IDisposable { * @param rootUrl Root URL for loading textures (optional) * @param options Optional parsing options */ - constructor(data: IData, scene: Scene, rootUrl: string = "", options: ILoaderOptions) { + constructor(data: IData, scene: Scene, rootUrl: string = "", options?: ILoaderOptions) { if (!data || !scene) { throw new Error("Effect constructor requires IData and Scene"); } From e86402bc4d3c1fff70c91ab12eb6fffc79ef80b2 Mon Sep 17 00:00:00 2001 From: Mikalai Lazitski Date: Mon, 5 Jan 2026 16:51:34 +0300 Subject: [PATCH 56/62] feat: enhance effect editor with Quarks file import functionality - Added a new method to import Quarks files in the EffectEditorWindow, allowing users to load effects from Quarks JSON format. - Refactored the existing loadFromFile method to load from Quarks files and updated the corresponding import logic in the toolbar. - Improved error handling and user feedback with toast notifications for successful and failed imports. - Updated imports to utilize specific Babylon.js core modules for better clarity and performance. --- .../converters/unityConverter.ts | 13 +++- .../editor/windows/effect-editor/graph.tsx | 69 +++++++++++++------ .../editor/windows/effect-editor/index.tsx | 15 +++- .../editor/windows/effect-editor/toolbar.tsx | 4 +- 4 files changed, 76 insertions(+), 25 deletions(-) diff --git a/editor/src/editor/windows/effect-editor/converters/unityConverter.ts b/editor/src/editor/windows/effect-editor/converters/unityConverter.ts index 1b6b29614..3056d9838 100644 --- a/editor/src/editor/windows/effect-editor/converters/unityConverter.ts +++ b/editor/src/editor/windows/effect-editor/converters/unityConverter.ts @@ -7,7 +7,13 @@ * Based on extracted Unity → Quarks converter logic, but outputs IData format. */ -import { Vector3, Color4, Quaternion, Color3, Scene, Mesh, VertexData, SceneLoader, Tools } from "babylonjs"; +import { Vector3, Quaternion } from "@babylonjs/core/Maths/math.vector"; +import { Color4, Color3 } from "@babylonjs/core/Maths/math.color"; +import { Scene } from "@babylonjs/core/scene"; +import { SceneLoader } from "@babylonjs/core/Loading/sceneLoader"; +import { Mesh } from "@babylonjs/core/Meshes/mesh"; +import { VertexData } from "@babylonjs/core/Meshes/mesh.vertexData"; +import { Tools } from "@babylonjs/core/Misc/tools"; import type { IData, IEmitter, @@ -19,8 +25,11 @@ import type { IGradientColor, IRandomColor, IRandomColorBetweenGradient, + IMaterial, + ITexture, + IImage, + IGeometry, } from "babylonjs-editor-tools/src/effect/types"; -import type { IMaterial, ITexture, IImage, IGeometry } from "babylonjs-editor-tools/src/effect/types/resources"; import * as yaml from "js-yaml"; // Note: Babylon.js loaders (FBXFileLoader, OBJFileLoader) are imported in toolbar.tsx diff --git a/editor/src/editor/windows/effect-editor/graph.tsx b/editor/src/editor/windows/effect-editor/graph.tsx index d4877a222..cab58db10 100644 --- a/editor/src/editor/windows/effect-editor/graph.tsx +++ b/editor/src/editor/windows/effect-editor/graph.tsx @@ -17,11 +17,12 @@ import { } from "../../../ui/shadcn/ui/context-menu"; import { IEffectEditor } from "."; import { saveSingleFileDialog } from "../../../tools/dialog"; -import { writeJSON } from "fs-extra"; +import { readJSON, writeJSON } from "fs-extra"; import { toast } from "sonner"; import { Effect, type IEffectNode, EffectSolidParticleSystem, type IData } from "babylonjs-editor-tools"; import { IQuarksJSON } from "./converters/quarksTypes"; import { QuarksConverter } from "./converters"; +import { basename, dirname } from "path"; export interface IEffectEditorGraphProps { filePath: string | null; @@ -109,33 +110,65 @@ export class EffectEditorGraph extends Component { + public async loadFromQuarksFile(filePath: string): Promise { try { if (!this.props.editor.preview?.scene) { console.error("Scene is not available"); return; } - // Load Quarks JSON and parse to IData - const dirname = require("path").dirname(filePath); - const fs = require("fs-extra"); - const originalJsonData = await fs.readJSON(filePath); - - // Use Parser to convert Quarks JSON to IData + const dirnamePath = dirname(filePath); + const originalJsonData = await readJSON(filePath); const parser = new QuarksConverter(); const parseResult = parser.convert(originalJsonData as IQuarksJSON); - // Create Effect from IData - const effect = new Effect(parseResult, this.props.editor.preview!.scene, dirname + "/"); + const effect = new Effect(parseResult, this.props.editor.preview.scene, dirnamePath + "/"); - // Generate unique ID for effect - const effectId = `effect-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; - const effectName = require("path").basename(filePath, ".json") || "Effect"; + const effectId = `effect-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`; + const effectName = basename(filePath, ".json") || "Effect"; + console.log(effect); + this._effects.set(effectId, { + id: effectId, + name: effectName, + effect: effect, + originalJsonData, + }); + + this._rebuildTree(); + + effect.start(); + + setTimeout(() => { + if (this.props.editor?.preview) { + (this.props.editor.preview as any).forceUpdate?.(); + } + }, 100); + } catch (error) { + console.error("Failed to load Effect file:", error); + } + } + /** + * Loads nodes from JSON file + */ + public async loadFromFile(filePath: string): Promise { + try { + if (!this.props.editor.preview?.scene) { + console.error("Scene is not available"); + return; + } + + // Load Quarks JSON and parse to IData + const dirnamePath = dirname(filePath); + const originalJsonData = await readJSON(filePath); + + const effect = new Effect(originalJsonData, this.props.editor.preview!.scene, dirnamePath + "/"); + + const effectId = `effect-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`; + const effectName = basename(filePath, ".json") || "Effect"; - // Store effect with original JSON data for export this._effects.set(effectId, { id: effectId, name: effectName, @@ -143,14 +176,10 @@ export class EffectEditorGraph extends Component { if (this.props.editor?.preview) { (this.props.editor.preview as any).forceUpdate?.(); @@ -176,7 +205,7 @@ export class EffectEditorGraph extends Component { try { - // Get graph component reference from layout if (this.editor.graph) { await this.editor.graph.loadFromFile(filePath); toast.success("Effect imported"); @@ -132,6 +131,20 @@ export default class EffectEditorWindow extends Component { + try { + if (this.editor.graph) { + await this.editor.graph.loadFromQuarksFile(filePath); + toast.success("Quarks file imported"); + } else { + toast.error("Failed to import Quarks file: Graph not available"); + } + } catch (error) { + console.error("Failed to import Quarks file:", error); + toast.error("Failed to import Quarks file"); + } + } + /** * Import Unity prefab data and create Effect * @param contexts - Array of Unity asset contexts (parsed components + dependencies) diff --git a/editor/src/editor/windows/effect-editor/toolbar.tsx b/editor/src/editor/windows/effect-editor/toolbar.tsx index 89b96e466..b83cb31b9 100644 --- a/editor/src/editor/windows/effect-editor/toolbar.tsx +++ b/editor/src/editor/windows/effect-editor/toolbar.tsx @@ -125,7 +125,7 @@ export class EffectEditorToolbar extends Component Date: Tue, 6 Jan 2026 11:01:17 +0300 Subject: [PATCH 57/62] refactor: update imports to use specific Babylon.js core modules - Consolidated imports from individual files to a single import statement for EffectParticleSystem and EffectSolidParticleSystem. - Updated logger and utility imports to reference the appropriate core modules from @babylonjs. - Improved code clarity and maintainability by standardizing import paths. --- tools/src/effect/effect.ts | 3 +-- tools/src/effect/loggers/logger.ts | 2 +- tools/src/effect/utils/gradientSystem.ts | 2 +- tools/src/effect/utils/matrixUtils.ts | 2 +- tools/src/effect/utils/valueParser.ts | 14 ++++---------- 5 files changed, 8 insertions(+), 15 deletions(-) diff --git a/tools/src/effect/effect.ts b/tools/src/effect/effect.ts index 63a036d91..87f971300 100644 --- a/tools/src/effect/effect.ts +++ b/tools/src/effect/effect.ts @@ -1,7 +1,6 @@ import { IDisposable, Scene } from "@babylonjs/core/scene"; import { TransformNode } from "@babylonjs/core/Meshes/transformNode"; -import { EffectParticleSystem } from "./systems/effectParticleSystem"; -import { EffectSolidParticleSystem } from "./systems/effectSolidParticleSystem"; +import { EffectParticleSystem, EffectSolidParticleSystem } from "./systems"; import { IData, IEffectNode, ILoaderOptions, IParticleSystemConfig } from "./types"; import { NodeFactory } from "./factories"; diff --git a/tools/src/effect/loggers/logger.ts b/tools/src/effect/loggers/logger.ts index 9170caf16..838a885f7 100644 --- a/tools/src/effect/loggers/logger.ts +++ b/tools/src/effect/loggers/logger.ts @@ -1,4 +1,4 @@ -import { Logger as BabylonLogger } from "babylonjs"; +import { Logger as BabylonLogger } from "@babylonjs/core/Misc/logger"; import type { ILoaderOptions } from "../types"; /** diff --git a/tools/src/effect/utils/gradientSystem.ts b/tools/src/effect/utils/gradientSystem.ts index ec75d1c50..3bdcc4c3a 100644 --- a/tools/src/effect/utils/gradientSystem.ts +++ b/tools/src/effect/utils/gradientSystem.ts @@ -1,4 +1,4 @@ -import { Color4 } from "babylonjs"; +import { Color4 } from "@babylonjs/core/Maths/math.color"; /** * Generic gradient system for storing and interpolating gradient values diff --git a/tools/src/effect/utils/matrixUtils.ts b/tools/src/effect/utils/matrixUtils.ts index 2944e34c0..32b0dbfa1 100644 --- a/tools/src/effect/utils/matrixUtils.ts +++ b/tools/src/effect/utils/matrixUtils.ts @@ -1,4 +1,4 @@ -import { Matrix } from "babylonjs"; +import { Matrix } from "@babylonjs/core/Maths/math.vector"; /** * Utility functions for matrix operations diff --git a/tools/src/effect/utils/valueParser.ts b/tools/src/effect/utils/valueParser.ts index b5b436636..b5e1e10cc 100644 --- a/tools/src/effect/utils/valueParser.ts +++ b/tools/src/effect/utils/valueParser.ts @@ -1,7 +1,6 @@ -import { Color4, ColorGradient } from "babylonjs"; -import type { IPiecewiseBezier, Value } from "../types/values"; -import type { Color } from "../types/colors"; -import type { IGradientKey } from "../types/gradients"; +import { Color4 } from "@babylonjs/core/Maths/math.color"; +import { ColorGradient } from "@babylonjs/core/Misc/gradients"; +import type { IPiecewiseBezier, Value, Color, IGradientKey } from "../types"; /** * Static utility functions for parsing values @@ -49,12 +48,7 @@ export class ValueUtils { // Format: { type: "ConstantColor", color: { r, g, b, a } } const anyValue = value as any; if (anyValue.color && typeof anyValue.color === "object") { - return new Color4( - anyValue.color.r ?? 1, - anyValue.color.g ?? 1, - anyValue.color.b ?? 1, - anyValue.color.a !== undefined ? anyValue.color.a : 1 - ); + return new Color4(anyValue.color.r ?? 1, anyValue.color.g ?? 1, anyValue.color.b ?? 1, anyValue.color.a !== undefined ? anyValue.color.a : 1); } } } From 1fd26000cb7675870b2a6f9b3eed034bbb92b30d Mon Sep 17 00:00:00 2001 From: Mikalai Lazitski Date: Tue, 6 Jan 2026 11:16:40 +0300 Subject: [PATCH 58/62] refactor: standardize Babylon.js imports across effect editor components - Updated import statements to use specific modules from @babylonjs/core for Color4, Vector3, and Quaternion. - Improved code clarity and maintainability by ensuring consistent import paths across multiple files. --- .../editor/windows/effect-editor/converters/quarksConverter.ts | 1 - .../editor/windows/effect-editor/editors/color-function.tsx | 3 ++- editor/src/editor/windows/effect-editor/editors/color.tsx | 2 +- .../src/editor/windows/effect-editor/properties/behaviors.tsx | 3 ++- .../src/editor/windows/effect-editor/properties/emission.tsx | 2 +- editor/src/editor/windows/effect-editor/properties/object.tsx | 2 +- 6 files changed, 7 insertions(+), 6 deletions(-) diff --git a/editor/src/editor/windows/effect-editor/converters/quarksConverter.ts b/editor/src/editor/windows/effect-editor/converters/quarksConverter.ts index 3a01d9a20..e6c35d717 100644 --- a/editor/src/editor/windows/effect-editor/converters/quarksConverter.ts +++ b/editor/src/editor/windows/effect-editor/converters/quarksConverter.ts @@ -1,5 +1,4 @@ import { Matrix, Vector3 } from "@babylonjs/core/Maths/math.vector"; - import { Quaternion } from "@babylonjs/core/Maths/math.vector"; import { Color3 } from "@babylonjs/core/Maths/math.color"; import { Texture as BabylonTexture } from "@babylonjs/core/Materials/Textures"; diff --git a/editor/src/editor/windows/effect-editor/editors/color-function.tsx b/editor/src/editor/windows/effect-editor/editors/color-function.tsx index 2fd6723cc..f177aaa75 100644 --- a/editor/src/editor/windows/effect-editor/editors/color-function.tsx +++ b/editor/src/editor/windows/effect-editor/editors/color-function.tsx @@ -1,5 +1,6 @@ import { ReactNode } from "react"; -import { Color4, Vector3 } from "babylonjs"; +import { Color4 } from "@babylonjs/core/Maths/math.color"; +import { Vector3 } from "@babylonjs/core/Maths/math.vector"; import { EditorInspectorColorField } from "../../../layout/inspector/fields/color"; import { EditorInspectorColorGradientField } from "../../../layout/inspector/fields/gradient"; diff --git a/editor/src/editor/windows/effect-editor/editors/color.tsx b/editor/src/editor/windows/effect-editor/editors/color.tsx index e5229259a..442d7a53c 100644 --- a/editor/src/editor/windows/effect-editor/editors/color.tsx +++ b/editor/src/editor/windows/effect-editor/editors/color.tsx @@ -1,5 +1,5 @@ import { ReactNode } from "react"; -import { Color4 } from "babylonjs"; +import { Color4 } from "@babylonjs/core/Maths/math.color"; import { EditorInspectorColorField } from "../../../layout/inspector/fields/color"; import { EditorInspectorColorGradientField } from "../../../layout/inspector/fields/gradient"; diff --git a/editor/src/editor/windows/effect-editor/properties/behaviors.tsx b/editor/src/editor/windows/effect-editor/properties/behaviors.tsx index b32ec8048..bfe10c176 100644 --- a/editor/src/editor/windows/effect-editor/properties/behaviors.tsx +++ b/editor/src/editor/windows/effect-editor/properties/behaviors.tsx @@ -1,5 +1,6 @@ import { ReactNode } from "react"; -import { Vector3, Color4 } from "babylonjs"; +import { Color4 } from "@babylonjs/core/Maths/math.color"; +import { Vector3 } from "@babylonjs/core/Maths/math.vector"; import { EditorInspectorNumberField } from "../../../layout/inspector/fields/number"; import { EditorInspectorVectorField } from "../../../layout/inspector/fields/vector"; diff --git a/editor/src/editor/windows/effect-editor/properties/emission.tsx b/editor/src/editor/windows/effect-editor/properties/emission.tsx index 49f522377..97c861ee1 100644 --- a/editor/src/editor/windows/effect-editor/properties/emission.tsx +++ b/editor/src/editor/windows/effect-editor/properties/emission.tsx @@ -1,5 +1,5 @@ import { ReactNode } from "react"; -import { Vector3 } from "babylonjs"; +import { Vector3 } from "@babylonjs/core/Maths/math.vector"; import { EditorInspectorNumberField } from "../../../layout/inspector/fields/number"; import { EditorInspectorVectorField } from "../../../layout/inspector/fields/vector"; diff --git a/editor/src/editor/windows/effect-editor/properties/object.tsx b/editor/src/editor/windows/effect-editor/properties/object.tsx index c0b0174b0..be8cb3d78 100644 --- a/editor/src/editor/windows/effect-editor/properties/object.tsx +++ b/editor/src/editor/windows/effect-editor/properties/object.tsx @@ -1,5 +1,5 @@ import { ReactNode } from "react"; -import { Quaternion } from "babylonjs"; +import { Quaternion } from "@babylonjs/core/Maths/math.vector"; import { EditorInspectorStringField } from "../../../layout/inspector/fields/string"; import { EditorInspectorVectorField } from "../../../layout/inspector/fields/vector"; From b53b8bfb56a320d8c085a48ffbf5e178bd36abda Mon Sep 17 00:00:00 2001 From: Mikalai Lazitski Date: Tue, 6 Jan 2026 11:27:32 +0300 Subject: [PATCH 59/62] feat: extend EffectSolidParticleSystem options with new properties - Added new optional properties to the EffectSolidParticleSystem constructor, including boundingSphereOnly, bSphereRadiusFactor, expandable, enableMultiMaterial, computeBoundingBox, and autoFixFaceOrientation. - Enhanced flexibility and configurability of the particle system for improved user experience. --- tools/src/effect/systems/effectSolidParticleSystem.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tools/src/effect/systems/effectSolidParticleSystem.ts b/tools/src/effect/systems/effectSolidParticleSystem.ts index eca00255e..72d2e29c0 100644 --- a/tools/src/effect/systems/effectSolidParticleSystem.ts +++ b/tools/src/effect/systems/effectSolidParticleSystem.ts @@ -452,7 +452,13 @@ export class EffectSolidParticleSystem extends SolidParticleSystem implements IS isPickable?: boolean; enableDepthSort?: boolean; particleIntersection?: boolean; + boundingSphereOnly?: boolean; + bSphereRadiusFactor?: number; + expandable?: boolean; useModelMaterial?: boolean; + enableMultiMaterial?: boolean; + computeBoundingBox?: boolean; + autoFixFaceOrientation?: boolean; } ) { super(name, scene, options); From 0b10100dfb699bbf615ae82655a4cb869503e773 Mon Sep 17 00:00:00 2001 From: Mikalai Lazitski Date: Tue, 6 Jan 2026 19:02:15 +0300 Subject: [PATCH 60/62] feat: enhance NodeFactory with new node creation methods - Introduced createGroup and createParticleSystem methods to NodeFactory for creating group nodes and particle systems with specified configurations. - Refactored create method to utilize a private _createRootNode method for improved readability and maintainability. - Updated internal logic to streamline node creation and enhance the overall structure of the NodeFactory class. --- tools/src/effect/factories/nodeFactory.ts | 228 ++++++++---------- .../effect/systems/effectParticleSystem.ts | 40 ++- .../systems/effectSolidParticleSystem.ts | 131 +++++----- tools/src/effect/types/system.ts | 5 - 4 files changed, 173 insertions(+), 231 deletions(-) diff --git a/tools/src/effect/factories/nodeFactory.ts b/tools/src/effect/factories/nodeFactory.ts index 557e927ed..9169eabf9 100644 --- a/tools/src/effect/factories/nodeFactory.ts +++ b/tools/src/effect/factories/nodeFactory.ts @@ -1,7 +1,6 @@ import { Vector3 } from "@babylonjs/core/Maths/math.vector"; import { TransformNode } from "@babylonjs/core/Meshes/transformNode"; import { Scene } from "@babylonjs/core/scene"; -import { AbstractMesh } from "@babylonjs/core/Meshes/abstractMesh"; import { Tools } from "@babylonjs/core/Misc/tools"; import { Quaternion } from "@babylonjs/core/Maths/math.vector"; import { Color4 } from "@babylonjs/core/Maths/math.color"; @@ -39,21 +38,88 @@ export class NodeFactory { public create(): IEffectNode { if (!this._data.root) { this._logger.warn("No root object found in data"); - const rootGroup = new TransformNode("Root", this._scene); - const rootUuid = Tools.RandomId(); - rootGroup.id = rootUuid; - - const rootNode: IEffectNode = { - name: "Root", - uuid: rootUuid, - data: rootGroup, - children: [], - type: "group", - }; - return rootNode; + return this._createRootNode(); } return this._createNode(this._data.root, null); } + + /** + * Create a new group node + * @param name Group name + * @param parentNode Parent node (optional) + * @returns Created group node + */ + public createGroup(name: string, parentNode: IEffectNode | null = null): IEffectNode { + const groupUuid = Tools.RandomId(); + const group: IGroup = { + uuid: groupUuid, + name, + transform: { + position: Vector3.Zero(), + rotation: Quaternion.Identity(), + scale: Vector3.One(), + }, + children: [], + }; + + return this._createGroupNode(group, parentNode); + } + + /** + * Create a new particle system node + * @param name System name + * @param systemType Type of system ("solid" or "base") + * @param config Optional particle system config + * @param parentNode Parent node (optional) + * @returns Created particle system node + */ + public createParticleSystem(name: string, systemType: "solid" | "base" = "base", config?: Partial, parentNode: IEffectNode | null = null): IEffectNode { + const systemUuid = Tools.RandomId(); + const defaultConfig: IParticleSystemConfig = { + systemType, + targetStopDuration: 0, // looping + manualEmitCount: -1, + emitRate: 10, + minLifeTime: 1, + maxLifeTime: 1, + minEmitPower: 1, + maxEmitPower: 1, + minSize: 1, + maxSize: 1, + color1: new Color4(1, 1, 1, 1), + color2: new Color4(1, 1, 1, 1), + behaviors: [], + ...config, + }; + + const emitter: IEmitter = { + uuid: systemUuid, + name, + transform: { + position: Vector3.Zero(), + rotation: Quaternion.Identity(), + scale: Vector3.One(), + }, + config: defaultConfig, + systemType, + }; + + return this._createParticleNode(emitter, parentNode); + } + + private _createRootNode(): IEffectNode { + const rootGroup = new TransformNode("Root", this._scene); + const rootUuid = Tools.RandomId(); + rootGroup.id = rootUuid; + const rootNode: IEffectNode = { + name: "Root", + uuid: rootUuid, + data: rootGroup, + children: [], + type: "group", + }; + return rootNode; + } /** * Recursively process object hierarchy * Creates nodes, sets parents, and applies transformations in one pass @@ -63,28 +129,13 @@ export class NodeFactory { if ("children" in obj && obj.children) { const groupNode = this._createGroupNode(obj as IGroup, parentNode); - groupNode.children = this._createChildrenNodes(obj.children, groupNode); + groupNode.children = obj.children.map((child) => this._createNode(child, groupNode)); return groupNode; } else { - const emitterNode = this._createParticleNode(obj as IEmitter, parentNode); - return emitterNode; + return this._createParticleNode(obj as IEmitter, parentNode); } } - /** - * Process children of a group recursively - */ - private _createChildrenNodes(children: (IGroup | IEmitter)[] | undefined, parentNode: IEffectNode | null): IEffectNode[] { - if (!children || children.length === 0) { - return []; - } - - this._logger.log(`Processing ${children.length} children for parent node: ${parentNode?.name || "none"}`); - return children.map((child) => { - return this._createNode(child, parentNode); - }); - } - /** * Create a TransformNode for a Group */ @@ -102,7 +153,7 @@ export class NodeFactory { this._applyTransform(node, group.transform); if (parentNode) { - this._setParent(node, parentNode); + node.data.parent = parentNode.data as TransformNode; } this._logger.log(`Created group node: ${group.name}`); @@ -113,18 +164,12 @@ export class NodeFactory { * Create a particle system from a Emitter */ private _createParticleNode(emitter: IEmitter, parentNode: IEffectNode | null): IEffectNode { - const parentName = parentNode ? parentNode.name : "none"; - const systemType = emitter.systemType; - this._logger.log(`Processing emitter: ${emitter.name} (parent: ${parentName})`); - - // const cumulativeScale = this._calculateCumulativeScale(parentGroup); - let particleSystem: EffectParticleSystem | EffectSolidParticleSystem; - if (systemType === "solid") { - particleSystem = this._createEffectSolidParticleSystem(emitter, parentNode); + if (emitter.config.systemType === "solid") { + particleSystem = this._createEffectSolidParticleSystem(emitter); } else { - particleSystem = this._createEffectParticleSystem(emitter, parentNode); + particleSystem = this._createEffectParticleSystem(emitter); } const node: IEffectNode = { @@ -135,6 +180,10 @@ export class NodeFactory { type: "particle", }; + if (parentNode) { + node.data.parent = parentNode.data as TransformNode; + } + this._logger.log(`Created particle system: ${emitter.name}`); return node; @@ -309,7 +358,7 @@ export class NodeFactory { /** * Create a ParticleSystem instance */ - private _createEffectParticleSystem(emitter: IEmitter, _parentNode: IEffectNode | null): EffectParticleSystem { + private _createEffectParticleSystem(emitter: IEmitter): EffectParticleSystem { const { name, config } = emitter; this._logger.log(`Creating ParticleSystem: ${name}`); @@ -376,12 +425,11 @@ export class NodeFactory { /** * Create a SolidParticleSystem instance */ - private _createEffectSolidParticleSystem(emitter: IEmitter, _parentNode: IEffectNode | null): EffectSolidParticleSystem { + private _createEffectSolidParticleSystem(emitter: IEmitter): EffectSolidParticleSystem { const { name, config } = emitter; this._logger.log(`Creating SolidParticleSystem: ${name}`); - // Create or load particle mesh const particleMesh = this._geometryFactory.createParticleMesh(config, name, this._scene); if (emitter.materialId) { @@ -393,8 +441,13 @@ export class NodeFactory { const sps = new EffectSolidParticleSystem(name, this._scene, { updatable: true, + expandable: true, + useModelMaterial: true, }); + // Set particle mesh (only adds shape, doesn't build mesh or dispose) + sps.particleMesh = particleMesh; + this._applyCommonProperties(sps, config); this._applyGradients(sps, config); this._applyCommonOptions(sps, config); @@ -404,7 +457,13 @@ export class NodeFactory { sps.emissionOverDistance = config.emissionOverDistance; } - sps.configureEmitterFromShape(config.shape); + if (config.shape) { + sps.configureEmitterFromShape(config.shape); + } + + // Dispose source mesh after it's been added as shape + // SPS will clone it in buildMesh() during start() + particleMesh.dispose(); this._logger.log(`SolidParticleSystem created: ${name}`); return sps; @@ -437,85 +496,4 @@ export class NodeFactory { `Applied transform: pos=(${transform.position.x.toFixed(2)}, ${transform.position.y.toFixed(2)}, ${transform.position.z.toFixed(2)}), scale=(${transform.scale.x.toFixed(2)}, ${transform.scale.y.toFixed(2)}, ${transform.scale.z.toFixed(2)})` ); } - - /** - * Set parent for a node - */ - private _setParent(node: IEffectNode, parent: IEffectNode | null): void { - if (!parent) { - return; - } - if (isSystem(parent.data)) { - // to-do emmiter as vector3 - node.data.setParent(parent.data.emitter as AbstractMesh | null); - } else { - node.data.setParent(parent.data); - } - - this._logger.log(`Set parent: ${node.name} -> ${parent?.name || "none"}`); - } - - /** - * Create a new group node - * @param name Group name - * @param parentNode Parent node (optional) - * @returns Created group node - */ - public createGroup(name: string, parentNode: IEffectNode | null = null): IEffectNode { - const groupUuid = Tools.RandomId(); - const group: IGroup = { - uuid: groupUuid, - name, - transform: { - position: Vector3.Zero(), - rotation: Quaternion.Identity(), - scale: Vector3.One(), - }, - children: [], - }; - - return this._createGroupNode(group, parentNode); - } - - /** - * Create a new particle system node - * @param name System name - * @param systemType Type of system ("solid" or "base") - * @param config Optional particle system config - * @param parentNode Parent node (optional) - * @returns Created particle system node - */ - public createParticleSystem(name: string, systemType: "solid" | "base" = "base", config?: Partial, parentNode: IEffectNode | null = null): IEffectNode { - const systemUuid = Tools.RandomId(); - const defaultConfig: IParticleSystemConfig = { - systemType, - targetStopDuration: 0, // looping - manualEmitCount: -1, - emitRate: 10, - minLifeTime: 1, - maxLifeTime: 1, - minEmitPower: 1, - maxEmitPower: 1, - minSize: 1, - maxSize: 1, - color1: new Color4(1, 1, 1, 1), - color2: new Color4(1, 1, 1, 1), - behaviors: [], - ...config, - }; - - const emitter: IEmitter = { - uuid: systemUuid, - name, - transform: { - position: Vector3.Zero(), - rotation: Quaternion.Identity(), - scale: Vector3.One(), - }, - config: defaultConfig, - systemType, - }; - - return this._createParticleNode(emitter, parentNode); - } } diff --git a/tools/src/effect/systems/effectParticleSystem.ts b/tools/src/effect/systems/effectParticleSystem.ts index 6b158f79f..c17bd0fe9 100644 --- a/tools/src/effect/systems/effectParticleSystem.ts +++ b/tools/src/effect/systems/effectParticleSystem.ts @@ -44,22 +44,13 @@ import { * into the native Babylon.js particle update loop */ export class EffectParticleSystem extends ParticleSystem implements ISystem { - private _perParticleBehaviors: PerParticleBehaviorFunction[]; - private _behaviorConfigs: Behavior[]; + private _perParticleBehaviors: PerParticleBehaviorFunction[] = []; + private _behaviorConfigs: Behavior[] = []; private _parent: AbstractMesh | TransformNode | null; - /** Store reference to default updateFunction */ - private _defaultUpdateFunction: (particles: Particle[]) => void; - constructor(name: string, capacity: number, scene: Scene) { super(name, capacity, scene); - this._perParticleBehaviors = []; - this._behaviorConfigs = []; - - // Store reference to the default updateFunction created by ParticleSystem - this._defaultUpdateFunction = this.updateFunction; - - // Override updateFunction to integrate per-particle behaviors + this._setupEmitter(); this._setupCustomUpdateFunction(); } @@ -69,10 +60,14 @@ export class EffectParticleSystem extends ParticleSystem implements ISystem { public set parent(parent: AbstractMesh | TransformNode | null) { this._parent = parent; + // Set emitter's parent (emitter is a TransformNode) + if (this.emitter && this.emitter instanceof TransformNode) { + this.emitter.parent = parent; + } } - public setParent(parent: AbstractMesh | TransformNode | null): void { - this._parent = parent; + private _setupEmitter(): void { + this.emitter = new TransformNode("Emitter", this._scene) as AbstractMesh; } /** @@ -80,10 +75,11 @@ export class EffectParticleSystem extends ParticleSystem implements ISystem { * with per-particle behavior execution */ private _setupCustomUpdateFunction(): void { + const defaultUpdateFunction = this.updateFunction; this.updateFunction = (particles: Particle[]): void => { // First, run the default Babylon.js update logic // This handles: age, gradients (color, size, angular speed, velocity), position, gravity, etc. - this._defaultUpdateFunction(particles); + defaultUpdateFunction(particles); // Then apply per-particle behaviors if any exist if (this._perParticleBehaviors.length === 0) { @@ -104,18 +100,10 @@ export class EffectParticleSystem extends ParticleSystem implements ISystem { } /** - * Get the parent node (emitter) for hierarchy operations - * Required by ISystem interface - */ - public getParentNode(): AbstractMesh | TransformNode | null { - return this.emitter instanceof AbstractMesh ? this.emitter : null; - } - - /** - * Get current behavior configurations + * Get current behavior configurations (read-only copy) */ public get behaviorConfigs(): Behavior[] { - return this._behaviorConfigs; + return [...this._behaviorConfigs]; } /** @@ -123,7 +111,7 @@ export class EffectParticleSystem extends ParticleSystem implements ISystem { * System-level behaviors configure gradients, per-particle behaviors run each frame */ public setBehaviors(behaviors: Behavior[]): void { - this._behaviorConfigs = behaviors; + this._behaviorConfigs = [...behaviors]; // Copy array // Apply system-level behaviors (gradients) to ParticleSystem this._applySystemLevelBehaviors(behaviors); diff --git a/tools/src/effect/systems/effectSolidParticleSystem.ts b/tools/src/effect/systems/effectSolidParticleSystem.ts index 72d2e29c0..b7af2b387 100644 --- a/tools/src/effect/systems/effectSolidParticleSystem.ts +++ b/tools/src/effect/systems/effectSolidParticleSystem.ts @@ -63,7 +63,6 @@ export class EffectSolidParticleSystem extends SolidParticleSystem implements IS private _behaviors: PerSolidParticleBehaviorFunction[]; public particleEmitterType: ISolidParticleEmitterType | null; private _emitEnded: boolean; - private _emitter: AbstractMesh | null; private _parent: AbstractMesh | TransformNode | null; // Gradient systems for "OverLife" behaviors (similar to ParticleSystem native gradients) private _colorGradients: ColorGradientSystem; @@ -117,18 +116,18 @@ export class EffectSolidParticleSystem extends SolidParticleSystem implements IS private _behaviorConfigs: Behavior[]; /** - * Get current behavior configurations + * Get current behavior configurations (read-only copy) */ public get behaviorConfigs(): Behavior[] { - return this._behaviorConfigs; + return [...this._behaviorConfigs]; } /** * Set behaviors and apply them to the system */ public setBehaviors(behaviors: Behavior[]): void { - this._behaviorConfigs = behaviors; - this._applyBehaviors(); + this._behaviorConfigs = [...behaviors]; // Copy array + this._rebuildBehaviors(); } /** @@ -136,13 +135,13 @@ export class EffectSolidParticleSystem extends SolidParticleSystem implements IS */ public addBehavior(behavior: Behavior): void { this._behaviorConfigs.push(behavior); - this._applyBehaviors(); + this._rebuildBehaviors(); } /** - * Apply behaviors - system-level (gradients) and per-particle + * Rebuild all behavior functions and gradients */ - private _applyBehaviors(): void { + private _rebuildBehaviors(): void { // Clear existing gradients this._colorGradients.clear(); this._sizeGradients.clear(); @@ -193,14 +192,6 @@ export class EffectSolidParticleSystem extends SolidParticleSystem implements IS } } - /** - * Get the parent node (mesh) for hierarchy operations - * Implements ISystem interface - */ - public getParentNode(): AbstractMesh | TransformNode | null { - return this.mesh || null; - } - public get parent(): AbstractMesh | TransformNode | null { return this._parent; } @@ -209,30 +200,14 @@ export class EffectSolidParticleSystem extends SolidParticleSystem implements IS this._parent = parent; } - public setParent(parent: AbstractMesh | TransformNode | null): void { - this._parent = parent; - } - /** - * Emitter property (like ParticleSystem) - * Sets the parent for the mesh - the point from which particles emit - */ - public get emitter(): AbstractMesh | null { - return this._emitter; - } - public set emitter(value: AbstractMesh | null) { - this._emitter = value; - // If mesh is already created, set its parent - if (this.mesh && value) { - this.mesh.setParent(value, false, true); - } - } - /** * Set particle mesh to use for rendering - * Initializes the SPS with this mesh + * Only adds shape and configures billboard mode. + * Does NOT build mesh - building happens in start(). + * Does NOT dispose source mesh - disposal is caller's responsibility. */ public set particleMesh(mesh: Mesh) { - this._initializeMesh(mesh); + this._addParticleMeshShape(mesh); } /** @@ -259,19 +234,15 @@ export class EffectSolidParticleSystem extends SolidParticleSystem implements IS this.particles = []; this.nbParticles = 0; - // Calculate capacity (same as before) + // Calculate capacity const isLooping = this.targetStopDuration === 0; const capacity = CapacityCalculator.calculateForSolidParticleSystem(this.emitRate, this.targetStopDuration, isLooping); // Add new shape this.addShape(newMesh, capacity); - // Set billboard mode - if (this.isBillboardBased !== undefined) { - this.billboard = this.isBillboardBased; - } else { - this.billboard = false; - } + // Configure billboard mode + this._configureBillboard(); // Build mesh this.buildMesh(); @@ -411,32 +382,53 @@ export class EffectSolidParticleSystem extends SolidParticleSystem implements IS } /** - * Initialize mesh for SPS (internal use) - * Adds the mesh as a shape and configures billboard mode + * Add particle mesh as shape to SPS + * Only adds shape and configures billboard - does NOT build mesh */ - private _initializeMesh(particleMesh: Mesh): void { + private _addParticleMeshShape(particleMesh: Mesh): void { if (!particleMesh) { return; } + // Stop if already running + const wasStarted = this._started; + if (wasStarted) { + this.stop(); + } + + // Dispose old mesh if exists + if (this.mesh) { + this.mesh.dispose(false, true); + } + + // Clear existing shapes + this.particles = []; + this.nbParticles = 0; + + // Calculate capacity and add shape const isLooping = this.targetStopDuration === 0; const capacity = CapacityCalculator.calculateForSolidParticleSystem(this.emitRate, this.targetStopDuration, isLooping); this.addShape(particleMesh, capacity); + // Configure billboard mode before buildMesh() + this._configureBillboard(); + + // Restart if was running (start() will call buildMesh()) + if (wasStarted) { + this.start(); + } + } + + /** + * Configure billboard mode based on isBillboardBased property + * Must be called after addShape() but before buildMesh() + */ + private _configureBillboard(): void { if (this.isBillboardBased !== undefined) { this.billboard = this.isBillboardBased; } else { this.billboard = false; } - - this.buildMesh(); - this._setupMeshProperties(); - - // Initialize all particles as dead/invisible immediately after build - this._initializeDeadParticles(); - this.setParticles(); // Apply visibility changes to mesh - - particleMesh.dispose(); } private _normalMatrix: Matrix; @@ -466,7 +458,7 @@ export class EffectSolidParticleSystem extends SolidParticleSystem implements IS this.name = name; this._behaviors = []; this.particleEmitterType = new SolidBoxParticleEmitter(); // Default emitter (like ParticleSystem) - this._emitter = null; + this._parent = null; // Gradient systems for "OverLife" behaviors this._colorGradients = new ColorGradientSystem(); @@ -901,43 +893,32 @@ export class EffectSolidParticleSystem extends SolidParticleSystem implements IS } /** - * Override buildMesh to enable vertex colors and alpha - * This is required for ColorOverLife behavior to work visually - * Note: PBR materials automatically use vertex colors if mesh has them - * The VERTEXCOLOR define is set automatically based on mesh.isVerticesDataPresent(VertexBuffer.ColorKind) + * Setup mesh properties after buildMesh() + * Sets parent, rendering group, layers, vertex alpha */ - public override buildMesh(): Mesh { - const mesh = super.buildMesh(); - - if (mesh) { - mesh.hasVertexAlpha = true; - // Vertex colors are already enabled via _computeParticleColor = true - // PBR materials will automatically use them if mesh has vertex color data - } - - return mesh; - } - private _setupMeshProperties(): void { if (!this.mesh) { return; } + // Enable vertex alpha for color blending if (!this.mesh.hasVertexAlpha) { this.mesh.hasVertexAlpha = true; } + // Set rendering group if (this.renderOrder !== undefined) { this.mesh.renderingGroupId = this.renderOrder; } + // Set layer mask if (this.layers !== undefined) { this.mesh.layerMask = this.layers; } - // Emitter is the point from which particles emit (like ParticleSystem.emitter) - if (this._emitter) { - this.mesh.setParent(this._emitter, false, true); + // Set parent (transform hierarchy) + if (this._parent) { + this.mesh.parent = this._parent; } } diff --git a/tools/src/effect/types/system.ts b/tools/src/effect/types/system.ts index 996b210e7..d153316b9 100644 --- a/tools/src/effect/types/system.ts +++ b/tools/src/effect/types/system.ts @@ -1,5 +1,4 @@ import { TransformNode } from "@babylonjs/core/Meshes/transformNode"; -import { AbstractMesh } from "@babylonjs/core/Meshes/abstractMesh"; import { Particle } from "@babylonjs/core/Particles/particle"; import { SolidParticle } from "@babylonjs/core/Particles/solidParticle"; import type { EffectParticleSystem, EffectSolidParticleSystem } from "../systems"; @@ -11,8 +10,6 @@ import type { EffectParticleSystem, EffectSolidParticleSystem } from "../systems export interface ISystem { /** System name */ name: string; - /** Get the parent node (mesh or emitter) for hierarchy operations */ - getParentNode(): AbstractMesh | TransformNode | null; /** Start the particle system */ start(): void; /** Stop the particle system */ @@ -46,8 +43,6 @@ export function isSystem(system: unknown): system is ISystem { return ( typeof system === "object" && system !== null && - "getParentNode" in system && - typeof (system as ISystem).getParentNode === "function" && "start" in system && typeof (system as ISystem).start === "function" && "stop" in system && From 9779440573a0bc82f647200d49e38139b9a454c1 Mon Sep 17 00:00:00 2001 From: Mikalai Lazitski Date: Sat, 24 Jan 2026 12:02:53 +0300 Subject: [PATCH 61/62] fix: add shader imports for particle systems in effect editor preview - Included necessary shader imports to enable particle system functionality. - Note: This approach is not ideal but is currently required for proper shader loading. --- editor/src/editor/windows/effect-editor/preview.tsx | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/editor/src/editor/windows/effect-editor/preview.tsx b/editor/src/editor/windows/effect-editor/preview.tsx index cafdf5da3..487ecd57f 100644 --- a/editor/src/editor/windows/effect-editor/preview.tsx +++ b/editor/src/editor/windows/effect-editor/preview.tsx @@ -16,6 +16,13 @@ import { IoPlay, IoStop, IoRefresh } from "react-icons/io5"; import type { IEffectEditor } from "."; import { Effect, type IEffectNode } from "babylonjs-editor-tools"; +// don't like because it's not a good practice, but it's the only way to load the shaders +import "@babylonjs/core/Particles/particleSystemComponent"; +import "@babylonjs/core/Shaders/particles.vertex"; +import "@babylonjs/core/Shaders/particles.fragment"; +import "@babylonjs/core/Shaders/rgbdDecode.fragment"; + + export interface IEffectEditorPreviewProps { filePath: string | null; onSceneReady?: (scene: Scene) => void; From 59d2b7081fb25108054243b7b3a38ac1854725db Mon Sep 17 00:00:00 2001 From: Mikalai Lazitski Date: Sat, 24 Jan 2026 12:53:36 +0300 Subject: [PATCH 62/62] refactor: update rotation handling in converters and properties - Changed rotation representation from Quaternion to Vector3 in QuarksConverter and UnityConverter for consistency. - Simplified rotation handling in EffectEditorObjectProperties by removing the custom rotation inspector and directly using Vector3. - Updated NodeFactory and EffectSolidParticleSystem to reflect the new rotation type, ensuring compatibility across the system. - Adjusted ITransform interface to use Vector3 for rotation, enhancing clarity in transformation definitions. --- .../converters/quarksConverter.ts | 20 ++- .../converters/unityConverter.ts | 6 +- .../effect-editor/properties/object.tsx | 114 +++--------------- tools/src/effect/factories/nodeFactory.ts | 17 +-- .../systems/effectSolidParticleSystem.ts | 17 ++- tools/src/effect/types/hierarchy.ts | 3 +- 6 files changed, 43 insertions(+), 134 deletions(-) diff --git a/editor/src/editor/windows/effect-editor/converters/quarksConverter.ts b/editor/src/editor/windows/effect-editor/converters/quarksConverter.ts index e6c35d717..38993759b 100644 --- a/editor/src/editor/windows/effect-editor/converters/quarksConverter.ts +++ b/editor/src/editor/windows/effect-editor/converters/quarksConverter.ts @@ -176,41 +176,35 @@ export class QuarksConverter { */ private _convertTransform(matrixArray?: number[], positionArray?: number[], rotationArray?: number[], scaleArray?: number[]): ITransform { const position = Vector3.Zero(); - const rotation = Quaternion.Identity(); + const rotation = Vector3.Zero(); const scale = Vector3.One(); if (matrixArray && Array.isArray(matrixArray) && matrixArray.length >= 16) { - // Use matrix (most accurate) const matrix = Matrix.FromArray(matrixArray); const tempPos = Vector3.Zero(); const tempRot = Quaternion.Zero(); const tempScale = Vector3.Zero(); matrix.decompose(tempScale, tempRot, tempPos); - // Convert from right-handed to left-handed position.copyFrom(tempPos); - position.z = -position.z; // Negate Z position + position.z = -position.z; - rotation.copyFrom(tempRot); - // Convert rotation quaternion: invert X component for proper X-axis rotation conversion - // This handles the case where X=-90° in RH looks like X=0° in LH - rotation.x *= -1; + tempRot.x *= -1; + const eulerAngles = tempRot.toEulerAngles(); + rotation.copyFrom(eulerAngles); scale.copyFrom(tempScale); } else { - // Use individual components if (positionArray && Array.isArray(positionArray)) { position.set(positionArray[0] || 0, positionArray[1] || 0, positionArray[2] || 0); - position.z = -position.z; // Convert to left-handed + position.z = -position.z; } if (rotationArray && Array.isArray(rotationArray)) { - // If rotation is Euler angles, convert to quaternion const eulerX = rotationArray[0] || 0; const eulerY = rotationArray[1] || 0; const eulerZ = rotationArray[2] || 0; - Quaternion.RotationYawPitchRollToRef(eulerY, eulerX, -eulerZ, rotation); // Negate Z for handedness - rotation.x *= -1; // Adjust X rotation component + rotation.set(eulerX, eulerY, -eulerZ); } if (scaleArray && Array.isArray(scaleArray)) { diff --git a/editor/src/editor/windows/effect-editor/converters/unityConverter.ts b/editor/src/editor/windows/effect-editor/converters/unityConverter.ts index 3056d9838..7060bb8cb 100644 --- a/editor/src/editor/windows/effect-editor/converters/unityConverter.ts +++ b/editor/src/editor/windows/effect-editor/converters/unityConverter.ts @@ -7,7 +7,7 @@ * Based on extracted Unity → Quarks converter logic, but outputs IData format. */ -import { Vector3, Quaternion } from "@babylonjs/core/Maths/math.vector"; +import { Vector3 } from "@babylonjs/core/Maths/math.vector"; import { Color4, Color3 } from "@babylonjs/core/Maths/math.color"; import { Scene } from "@babylonjs/core/scene"; import { SceneLoader } from "@babylonjs/core/Loading/sceneLoader"; @@ -790,7 +790,7 @@ function _convertToIDataFormat(converted: IIntermediateGameObject): IGroup | IEm name: converted.name, transform: { position: new Vector3(converted.position[0], converted.position[1], converted.position[2]), - rotation: new Quaternion(converted.rotation[0], converted.rotation[1], converted.rotation[2], converted.rotation[3]), + rotation: new Vector3(converted.rotation[0], converted.rotation[1], converted.rotation[2]), scale: new Vector3(converted.scale[0], converted.scale[1], converted.scale[2]), }, children: children, @@ -806,7 +806,7 @@ function _convertToIDataFormat(converted: IIntermediateGameObject): IGroup | IEm name: converted.name, transform: { position: new Vector3(converted.position[0], converted.position[1], converted.position[2]), - rotation: new Quaternion(converted.rotation[0], converted.rotation[1], converted.rotation[2], converted.rotation[3]), + rotation: new Vector3(converted.rotation[0], converted.rotation[1], converted.rotation[2]), scale: new Vector3(converted.scale[0], converted.scale[1], converted.scale[2]), }, config: converted.emitter, diff --git a/editor/src/editor/windows/effect-editor/properties/object.tsx b/editor/src/editor/windows/effect-editor/properties/object.tsx index be8cb3d78..1910c6163 100644 --- a/editor/src/editor/windows/effect-editor/properties/object.tsx +++ b/editor/src/editor/windows/effect-editor/properties/object.tsx @@ -1,123 +1,37 @@ import { ReactNode } from "react"; -import { Quaternion } from "@babylonjs/core/Maths/math.vector"; import { EditorInspectorStringField } from "../../../layout/inspector/fields/string"; import { EditorInspectorVectorField } from "../../../layout/inspector/fields/vector"; import { EditorInspectorSwitchField } from "../../../layout/inspector/fields/switch"; -import { type IEffectNode, EffectSolidParticleSystem, EffectParticleSystem } from "babylonjs-editor-tools"; +import { type IEffectNode, EffectSolidParticleSystem, EffectParticleSystem, isSystem } from "babylonjs-editor-tools"; export interface IEffectEditorObjectPropertiesProps { nodeData: IEffectNode; onChange?: () => void; } -/** - * Creates a rotation inspector that handles rotationQuaternion properly - */ -function getRotationInspector(object: any, onChange?: () => void): ReactNode { - if (!object) { - return null; - } - - // Check if rotationQuaternion exists and is valid - if (object.rotationQuaternion && object.rotationQuaternion instanceof Quaternion) { - const valueRef = object.rotationQuaternion.toEulerAngles(); - - const proxy = new Proxy(valueRef, { - get(target, prop) { - return target[prop as keyof typeof target]; - }, - set(obj, prop, value) { - (obj as any)[prop] = value; - if (object.rotationQuaternion) { - object.rotationQuaternion.copyFrom((obj as any).toQuaternion()); - } - onChange?.(); - return true; - }, - }); - - const o = { proxy }; - - return ; - } - - // Fallback to rotation if it exists - if (object.rotation && typeof object.rotation === "object" && object.rotation.x !== undefined) { - return ; - } - - return null; -} - export function EffectEditorObjectProperties(props: IEffectEditorObjectPropertiesProps): ReactNode { const { nodeData, onChange } = props; - // For groups, use transformNode directly - if (nodeData.type === "group" && nodeData.data) { - const group = nodeData.data; - + if(!nodeData.data) { return ( <> - - {(group as any).position && } - {getRotationInspector(group, onChange)} - {(group as any).scaling && } +
Data not available
); } - // For particles, use system.emitter for VEffectParticleSystem or system.mesh for VEffectSolidParticleSystem - if (nodeData.type === "particle" && nodeData.data) { - const system = nodeData.data; - - // For VEffectSolidParticleSystem, use mesh (common mesh for all particles) - if (system instanceof EffectSolidParticleSystem) { - const mesh = system.mesh; - if (!mesh) { - return ( - <> - -
Mesh not available
- - ); - } - - return ( - <> - - - {mesh.position && } - {getRotationInspector(mesh, onChange)} - {mesh.scaling && } - - ); - } - - // For VEffectParticleSystem, use emitter - if (system instanceof EffectParticleSystem) { - const emitter = (system as any).emitter; - if (!emitter) { - return ( - <> - -
Emitter not available
- - ); - } - - return ( - <> - - {emitter.position && } - {getRotationInspector(emitter, onChange)} - {emitter.scaling && } - - ); - } - } - - return null; + const object = isSystem(nodeData.data) ? nodeData.data.emitter : nodeData.data; + + return ( + <> + + + + + + + ); } diff --git a/tools/src/effect/factories/nodeFactory.ts b/tools/src/effect/factories/nodeFactory.ts index 9169eabf9..a64909621 100644 --- a/tools/src/effect/factories/nodeFactory.ts +++ b/tools/src/effect/factories/nodeFactory.ts @@ -2,8 +2,8 @@ import { Vector3 } from "@babylonjs/core/Maths/math.vector"; import { TransformNode } from "@babylonjs/core/Meshes/transformNode"; import { Scene } from "@babylonjs/core/scene"; import { Tools } from "@babylonjs/core/Misc/tools"; -import { Quaternion } from "@babylonjs/core/Maths/math.vector"; import { Color4 } from "@babylonjs/core/Maths/math.color"; +import { AbstractMesh } from "@babylonjs/core/Meshes/abstractMesh"; import { EffectParticleSystem, EffectSolidParticleSystem } from "../systems"; import { IData, IGroup, IEmitter, ITransform, IParticleSystemConfig, ILoaderOptions, IMaterialFactory, IGeometryFactory, IEffectNode, isSystem } from "../types"; @@ -56,7 +56,7 @@ export class NodeFactory { name, transform: { position: Vector3.Zero(), - rotation: Quaternion.Identity(), + rotation: Vector3.Zero(), scale: Vector3.One(), }, children: [], @@ -97,7 +97,7 @@ export class NodeFactory { name, transform: { position: Vector3.Zero(), - rotation: Quaternion.Identity(), + rotation: Vector3.Zero(), scale: Vector3.One(), }, config: defaultConfig, @@ -153,7 +153,7 @@ export class NodeFactory { this._applyTransform(node, group.transform); if (parentNode) { - node.data.parent = parentNode.data as TransformNode; + (node.data as TransformNode).parent = parentNode.data as TransformNode; } this._logger.log(`Created group node: ${group.name}`); @@ -180,8 +180,10 @@ export class NodeFactory { type: "particle", }; + particleSystem.emitter = new TransformNode(emitter.name + "_emitter", this._scene) as AbstractMesh; + if (parentNode) { - node.data.parent = parentNode.data as TransformNode; + particleSystem.emitter.parent = parentNode.data as TransformNode; } this._logger.log(`Created particle system: ${emitter.name}`); @@ -483,8 +485,9 @@ export class NodeFactory { node.data.position.copyFrom(transform.position); } - if (transform.rotation) { - node.data.rotationQuaternion = transform.rotation.clone(); + if (transform.rotation && node.data.rotation) { + node.data.rotationQuaternion = null; + node.data.rotation.copyFrom(transform.rotation); } if (transform.scale && node.data.scaling) { diff --git a/tools/src/effect/systems/effectSolidParticleSystem.ts b/tools/src/effect/systems/effectSolidParticleSystem.ts index b7af2b387..502ef567c 100644 --- a/tools/src/effect/systems/effectSolidParticleSystem.ts +++ b/tools/src/effect/systems/effectSolidParticleSystem.ts @@ -1,7 +1,6 @@ import { Quaternion, Vector3, Matrix } from "@babylonjs/core/Maths/math.vector"; import { Color4 } from "@babylonjs/core/Maths/math.color"; import { SolidParticle } from "@babylonjs/core/Particles/solidParticle"; -import { TransformNode } from "@babylonjs/core/Meshes/transformNode"; import { Mesh } from "@babylonjs/core/Meshes/mesh"; import { AbstractMesh } from "@babylonjs/core/Meshes/abstractMesh"; import { SolidParticleSystem } from "@babylonjs/core/Particles/solidParticleSystem"; @@ -63,7 +62,7 @@ export class EffectSolidParticleSystem extends SolidParticleSystem implements IS private _behaviors: PerSolidParticleBehaviorFunction[]; public particleEmitterType: ISolidParticleEmitterType | null; private _emitEnded: boolean; - private _parent: AbstractMesh | TransformNode | null; + private _emitter: AbstractMesh | Vector3 | null; // Gradient systems for "OverLife" behaviors (similar to ParticleSystem native gradients) private _colorGradients: ColorGradientSystem; private _sizeGradients: NumberGradientSystem; @@ -192,12 +191,12 @@ export class EffectSolidParticleSystem extends SolidParticleSystem implements IS } } - public get parent(): AbstractMesh | TransformNode | null { - return this._parent; + public get emitter(): AbstractMesh | Vector3 | null { + return this._emitter; } - public set parent(parent: AbstractMesh | TransformNode | null) { - this._parent = parent; + public set emitter(emitter: AbstractMesh | Vector3 | null) { + this._emitter = emitter; } /** @@ -458,7 +457,7 @@ export class EffectSolidParticleSystem extends SolidParticleSystem implements IS this.name = name; this._behaviors = []; this.particleEmitterType = new SolidBoxParticleEmitter(); // Default emitter (like ParticleSystem) - this._parent = null; + this._emitter = null; // Gradient systems for "OverLife" behaviors this._colorGradients = new ColorGradientSystem(); @@ -917,8 +916,8 @@ export class EffectSolidParticleSystem extends SolidParticleSystem implements IS } // Set parent (transform hierarchy) - if (this._parent) { - this.mesh.parent = this._parent; + if (this._emitter) { + this.mesh.parent = this._emitter as AbstractMesh; } } diff --git a/tools/src/effect/types/hierarchy.ts b/tools/src/effect/types/hierarchy.ts index 851053698..e13df2530 100644 --- a/tools/src/effect/types/hierarchy.ts +++ b/tools/src/effect/types/hierarchy.ts @@ -1,5 +1,4 @@ import { Vector3 } from "@babylonjs/core/Maths/math.vector"; -import { Quaternion } from "@babylonjs/core/Maths/math.vector"; import type { IParticleSystemConfig } from "./emitter"; import type { IMaterial, ITexture, IImage, IGeometry } from "./resources"; @@ -8,7 +7,7 @@ import type { IMaterial, ITexture, IImage, IGeometry } from "./resources"; */ export interface ITransform { position: Vector3; - rotation: Quaternion; + rotation: Vector3; scale: Vector3; }