diff --git a/cli/package.json b/cli/package.json index 4688e4a66..63263a86a 100644 --- a/cli/package.json +++ b/cli/package.json @@ -1,6 +1,6 @@ { "name": "babylonjs-editor-cli", - "version": "5.4.1-rc.0", + "version": "5.4.1-rc.4", "description": "Babylon.js Editor CLI is a command line interface to help you package your scenes made using the Babylon.js Editor", "productName": "Babylon.js Editor CLI", "scripts": { diff --git a/cli/src/pack/scene.mts b/cli/src/pack/scene.mts index 80d5ee002..ab322904b 100644 --- a/cli/src/pack/scene.mts +++ b/cli/src/pack/scene.mts @@ -226,6 +226,7 @@ export async function createBabylonScene(options: ICreateBabylonSceneOptions) { }) ); + // Sprite managers const spriteManagersResult = await Promise.all( options.directories.spriteManagerFiles.map(async (file) => { const data = await fs.readJSON(join(options.sceneFile, "sprite-managers", file)); @@ -248,6 +249,7 @@ export async function createBabylonScene(options: ICreateBabylonSceneOptions) { }) ); + // Sprite maps const spriteMapsResult = await Promise.all( options.directories.spriteMapFiles.map(async (file) => { const data = await fs.readJSON(join(options.sceneFile, "sprite-maps", file)); @@ -270,6 +272,29 @@ export async function createBabylonScene(options: ICreateBabylonSceneOptions) { }) ); + // Sound nodes + const soundNodesResults = await Promise.all( + options.directories.soundNodeFiles.map(async (file) => { + const data = await fs.readJSON(join(options.sceneFile, "soundNodes", file)); + + if (data.metadata?.doNotSerialize) { + return null; + } + + if (data.metadata?.parentId) { + data.parentId = data.metadata.parentId; + } + + return data; + }) + ); + + transformNodes.push( + ...soundNodesResults.filter((soundNode) => { + return soundNode !== null; + }) + ); + // Lights const lightsResult = await Promise.all( options.directories.lightsFiles.map(async (file) => { diff --git a/cli/src/tools/scene.mts b/cli/src/tools/scene.mts index ff0e4d259..a61f7066e 100644 --- a/cli/src/tools/scene.mts +++ b/cli/src/tools/scene.mts @@ -16,6 +16,7 @@ export async function ensureSceneDirectories(scenePath: string) { fs.ensureDir(join(scenePath, "sceneLinks")), fs.ensureDir(join(scenePath, "gui")), fs.ensureDir(join(scenePath, "sounds")), + fs.ensureDir(join(scenePath, "soundNodes")), fs.ensureDir(join(scenePath, "particleSystems")), fs.ensureDir(join(scenePath, "morphTargetManagers")), fs.ensureDir(join(scenePath, "morphTargets")), @@ -38,6 +39,7 @@ export async function readSceneDirectories(scenePath: string) { sceneLinkFiles, guiFiles, soundFiles, + soundNodeFiles, particleSystemFiles, morphTargetManagerFiles, morphTargetFiles, @@ -57,6 +59,7 @@ export async function readSceneDirectories(scenePath: string) { readdir(join(scenePath, "sceneLinks")), readdir(join(scenePath, "gui")), readdir(join(scenePath, "sounds")), + readdir(join(scenePath, "soundNodes")), readdir(join(scenePath, "particleSystems")), readdir(join(scenePath, "morphTargetManagers")), readdir(join(scenePath, "morphTargets")), @@ -78,6 +81,7 @@ export async function readSceneDirectories(scenePath: string) { sceneLinkFiles, guiFiles, soundFiles, + soundNodeFiles, particleSystemFiles, morphTargetManagerFiles, morphTargetFiles, diff --git a/editor/package.json b/editor/package.json index 4380a40b2..7aa51a6d3 100644 --- a/editor/package.json +++ b/editor/package.json @@ -1,6 +1,6 @@ { "name": "babylonjs-editor", - "version": "5.4.1-rc.0", + "version": "5.4.1-rc.4", "description": "Babylon.js Editor is a Web Application helping artists to work with Babylon.js", "productName": "Babylon.js Editor", "main": "build/src/index.js", diff --git a/editor/src/editor/dialogs/command-palette/command-palette.tsx b/editor/src/editor/dialogs/command-palette/command-palette.tsx index 62c4143f8..afe8bf0e1 100644 --- a/editor/src/editor/dialogs/command-palette/command-palette.tsx +++ b/editor/src/editor/dialogs/command-palette/command-palette.tsx @@ -8,7 +8,7 @@ import { FaCirclePlus } from "react-icons/fa6"; import { IoSparklesSharp } from "react-icons/io5"; import { HiMiniCommandLine } from "react-icons/hi2"; -import { Node, IParticleSystem, Sound } from "babylonjs"; +import { Node, IParticleSystem } from "babylonjs"; import { normalizedGlob } from "../../../tools/fs"; import { isNodeVisibleInGraph } from "../../../tools/node/metadata"; @@ -168,10 +168,7 @@ export class CommandPalette extends Component { - objects.push(...soundTrack.soundCollection); - }); + let objects = [...scene.meshes, ...scene.lights, ...scene.cameras, ...scene.particleSystems, ...scene.transformNodes] as (Node | IParticleSystem)[]; objects = objects.filter((o) => { if (isNode(o) && !isNodeVisibleInGraph(o)) { diff --git a/editor/src/editor/layout/assets-browser.tsx b/editor/src/editor/layout/assets-browser.tsx index 661151de9..bef4778b7 100644 --- a/editor/src/editor/layout/assets-browser.tsx +++ b/editor/src/editor/layout/assets-browser.tsx @@ -96,6 +96,7 @@ import { EditorAssetsTreeLabel } from "./assets-browser/label"; import "babylonjs-loaders"; import { AssimpJSLoader } from "../../loader/assimpjs"; +import { isSoundNode } from "../../tools/guards/sound"; const HDRSelectable = createSelectable(AssetBrowserHDRItem); const GuiSelectable = createSelectable(AssetBrowserGUIItem); @@ -481,13 +482,10 @@ export class EditorAssetsBrowser extends Component { - soundtrack.soundCollection.forEach((sound) => { - if (sound.name === oldRelativePath) { - sound.name = newRelativePath; - sound["_url"] = newRelativePath; - } - }); + scene.transformNodes.forEach((node) => { + if (isSoundNode(node) && node.soundRelativePath === oldRelativePath) { + node.soundRelativePath = newRelativePath; + } }); // Scripts diff --git a/editor/src/editor/layout/cinematic/inspector/sound.tsx b/editor/src/editor/layout/cinematic/inspector/sound.tsx index 454b8f92e..dadce772f 100644 --- a/editor/src/editor/layout/cinematic/inspector/sound.tsx +++ b/editor/src/editor/layout/cinematic/inspector/sound.tsx @@ -1,8 +1,9 @@ -import { Sound } from "babylonjs"; import { ICinematicSound, ICinematicTrack } from "babylonjs-editor-tools"; import { CinematicEditor } from "../editor"; +import { SoundNode } from "../../../nodes/sound"; + import { EditorInspectorNumberField } from "../../inspector/fields/number"; import { EditorInspectorSectionField } from "../../inspector/fields/section"; @@ -13,14 +14,13 @@ export interface ICinematicEditorSoundKeyInspectorProps { } export function CinematicEditorSoundKeyInspector(props: ICinematicEditorSoundKeyInspectorProps) { - const sound = props.track.sound as Sound | null; - const buffer = sound?.getAudioBuffer(); + const node = props.track.sound as SoundNode | null; - if (!sound || !buffer) { + if (!node || !node.sound?.buffer) { return null; } - const endFrame = buffer.duration * props.cinematicEditor.cinematic.framesPerSecond; + const endFrame = node.sound.duration * props.cinematicEditor.cinematic.framesPerSecond; return ( diff --git a/editor/src/editor/layout/cinematic/serialization/parse.ts b/editor/src/editor/layout/cinematic/serialization/parse.ts index 3aa296297..16030edf5 100644 --- a/editor/src/editor/layout/cinematic/serialization/parse.ts +++ b/editor/src/editor/layout/cinematic/serialization/parse.ts @@ -1,11 +1,12 @@ -import { Scene, Sound, Vector3, Animation } from "babylonjs"; +import { Scene, Vector3, Animation } from "babylonjs"; import { ICinematic, ICinematicKey, ICinematicKeyCut, ICinematicTrack, parseCinematicKeyValue, getAnimationTypeForObject } from "babylonjs-editor-tools"; -import { getSoundById } from "../../../../tools/sound/tools"; import { getInspectorPropertyValue } from "../../../../tools/property"; import { getDefaultRenderingPipeline } from "../../../rendering/default-pipeline"; +import { SoundNode } from "../../../nodes/sound"; + export function parseCinematic(data: ICinematic, scene: Scene) { const tracks = data.tracks.map((track) => { return parseCinematicTrack(track, scene); @@ -41,9 +42,9 @@ export function parseCinematicTrack(track: ICinematicTrack, scene: Scene) { animationType = getAnimationTypeForObject(value); } - let sound: Sound | null = null; + let sound: SoundNode | null = null; if (track.sound) { - sound = getSoundById(track.sound, scene); + sound = scene.getNodeById(track.sound) as SoundNode; } return { diff --git a/editor/src/editor/layout/cinematic/timelines/add.ts b/editor/src/editor/layout/cinematic/timelines/add.ts index c8bb65971..79904df22 100644 --- a/editor/src/editor/layout/cinematic/timelines/add.ts +++ b/editor/src/editor/layout/cinematic/timelines/add.ts @@ -1,10 +1,12 @@ -import { Sound, AnimationGroup } from "babylonjs"; +import { AnimationGroup } from "babylonjs"; import { ICinematicAnimationGroup, ICinematicKey, ICinematicKeyCut, ICinematicKeyEvent, ICinematicSound, ICinematicTrack, isCinematicKeyCut } from "babylonjs-editor-tools"; +import { showAlert } from "../../../../ui/dialog"; + import { registerUndoRedo } from "../../../../tools/undoredo"; import { getInspectorPropertyValue } from "../../../../tools/property"; -import { showAlert } from "../../../../ui/dialog"; +import { SoundNode } from "../../../nodes/sound"; import { getDefaultRenderingPipeline } from "../../../rendering/default-pipeline"; @@ -116,17 +118,16 @@ export function addSoundKey(cinematicEditor: CinematicEditor, track: ICinematicT return; } - const sound = track.sound as Sound; - const buffer = sound.getAudioBuffer(); + const node = track.sound as SoundNode; - if (!buffer) { + if (!node.sound?.buffer) { return showAlert( "Can't add sound track", "The sound track is not ready yet, please wait until the sound is loaded. If this problem persists, please verify the sound file is correctly loaded." ); } - const duration = buffer.duration; + const duration = node.sound.buffer.duration; const fps = cinematicEditor.cinematic.framesPerSecond; const key = { diff --git a/editor/src/editor/layout/cinematic/tools/sync.ts b/editor/src/editor/layout/cinematic/tools/sync.ts index 34977f364..77a326429 100644 --- a/editor/src/editor/layout/cinematic/tools/sync.ts +++ b/editor/src/editor/layout/cinematic/tools/sync.ts @@ -1,6 +1,7 @@ -import { AnimationGroup, Sound } from "babylonjs"; +import { AnimationGroup } from "babylonjs"; import { ICinematic } from "babylonjs-editor-tools"; +import { SoundNode } from "../../../nodes/sound"; export interface ISyncAnimationGroupsToFrameOptions { pauseAfterSync: boolean; @@ -44,8 +45,8 @@ export function syncAnimationGroupsToFrame(frame: number, cinematic: ICinematic, export function syncSoundsToFrame(frame: number, cinematic: ICinematic) { cinematic.tracks.forEach((track) => { - const sound = track.sound as Sound; - if (!sound) { + const node = track.sound as SoundNode; + if (!node) { return; } @@ -59,7 +60,9 @@ export function syncSoundsToFrame(frame: number, cinematic: ICinematic) { if (frameDiff > 0) { const offset = frameDiff / cinematic.framesPerSecond; - sound.play(0, offset); + node.play({ + startOffset: offset, + }); } }); }); diff --git a/editor/src/editor/layout/cinematic/tracks/sound.tsx b/editor/src/editor/layout/cinematic/tracks/sound.tsx index 54f8a7426..9d4e2cfd0 100644 --- a/editor/src/editor/layout/cinematic/tracks/sound.tsx +++ b/editor/src/editor/layout/cinematic/tracks/sound.tsx @@ -3,13 +3,14 @@ import { HiSpeakerWave } from "react-icons/hi2"; import { ICinematicTrack } from "babylonjs-editor-tools"; -import { getSoundById } from "../../../../tools/sound/tools"; +import { Button } from "../../../../ui/shadcn/ui/button"; + +import { isSoundNode } from "../../../../tools/guards/sound"; import { registerUndoRedo } from "../../../../tools/undoredo"; import { CinematicEditor } from "../editor"; import { CinematicEditorRemoveTrackButton } from "./remove"; -import { Button } from "../../../../ui/shadcn/ui/button"; export interface ICinematicEditorSoundTrackProps { track: ICinematicTrack; @@ -37,7 +38,11 @@ export function CinematicEditorSoundTrack(props: ICinematicEditorSoundTrackProps setDragOver(false); const data = JSON.parse(event.dataTransfer.getData("graph/node")) as string[]; - const sound = getSoundById(data[0], props.cinematicEditor.editor.layout.preview.scene); + + let sound = props.cinematicEditor.editor.layout.preview.scene.getNodeById(data[0]); + if (!isSoundNode(sound)) { + sound = null; + } if (sound && sound !== props.track.sound) { const oldSound = props.track.node; diff --git a/editor/src/editor/layout/graph.tsx b/editor/src/editor/layout/graph.tsx index 0ac14c65b..664a05974 100644 --- a/editor/src/editor/layout/graph.tsx +++ b/editor/src/editor/layout/graph.tsx @@ -1,9 +1,10 @@ +import { extname } from "path/posix"; + import { Component, DragEvent, ReactNode } from "react"; import { Button, Tree, TreeNodeInfo } from "@blueprintjs/core"; import { FaLink } from "react-icons/fa6"; import { IoMdCube } from "react-icons/io"; -import { BsSoundwave } from "react-icons/bs"; import { AiOutlinePlus } from "react-icons/ai"; import { HiSpeakerWave } from "react-icons/hi2"; import { SiBabylondotjs } from "react-icons/si"; @@ -15,7 +16,7 @@ import { TbGhost2Filled, TbServerSpark, TbBrandAdobeIndesign } from "react-icons import { FaCamera, FaImage, FaLightbulb, FaBone, FaRegLightbulb } from "react-icons/fa"; import { AdvancedDynamicTexture } from "babylonjs-gui"; -import { BaseTexture, Node, Scene, Sound, Tools, IParticleSystem, Sprite, Skeleton, TransformNode } from "babylonjs"; +import { BaseTexture, Node, Scene, Tools, IParticleSystem, Sprite, Skeleton, TransformNode, AbstractMesh } from "babylonjs"; import { Editor } from "../main"; @@ -31,8 +32,8 @@ import { ContextMenuSubTrigger, } from "../../ui/shadcn/ui/context-menu"; -import { isSound } from "../../tools/guards/sound"; import { cloneNode } from "../../tools/node/clone"; +import { isSoundNode } from "../../tools/guards/sound"; import { registerUndoRedo } from "../../tools/undoredo"; import { isDomTextInputFocused } from "../../tools/dom"; import { isSceneLinkNode } from "../../tools/guards/scene"; @@ -79,8 +80,11 @@ import { getLightCommands } from "../dialogs/command-palette/light"; import { getCameraCommands } from "../dialogs/command-palette/camera"; import { getSpriteCommands } from "../dialogs/command-palette/sprite"; +import { addSoundNode } from "../../project/add/sound"; import { onProjectConfigurationChangedObservable } from "../../project/configuration"; +import { applySoundAsset } from "./preview/import/sound"; + import { EditorGraphLabel } from "./graph/label"; import { EditorGraphContextMenu } from "./graph/context-menu"; import { setNewParentForGraphSelectedNodes } from "./graph/move"; @@ -123,8 +127,6 @@ export interface IEditorGraphState { } export class EditorGraph extends Component { - private _soundsList: Sound[] = []; - public _nodeToCopyTransform: Node | null = null; public _objectsToCopy: TreeNodeInfo[] = []; @@ -264,6 +266,8 @@ export class EditorGraph extends Component ); })} + addSoundNode(this.props.editor)}>Sound Node + {getSpriteCommands(this.props.editor).map((command) => { return ( @@ -293,8 +297,6 @@ export class EditorGraph extends Component const scene = this.props.editor.layout.preview.scene; const clusteredLightContainer = this.props.editor.layout.preview.clusteredLightContainer; - this._soundsList = scene.soundTracks?.map((st) => st.soundCollection).flat() ?? []; - let nodes: (TreeNodeInfo | null)[] = []; if (this.state.showOnlyLights || this.state.showOnlyDecals) { @@ -314,11 +316,6 @@ export class EditorGraph extends Component nodes.splice(0, 0, guiNode); } - const soundNode = this._parseSoundNode(scene); - if (soundNode) { - nodes.splice(0, 0, soundNode); - } - const skeletonNode = this._parseSkeletonNode(scene); if (skeletonNode) { nodes.splice(0, 0, skeletonNode); @@ -343,8 +340,8 @@ export class EditorGraph extends Component * become unselected to have only the given node selected. All parents are expanded. * @param node defines the reference tot the node to select in the graph. */ - public setSelectedNode(node: Node | Sound | IParticleSystem | Sprite): void { - let source = isSound(node) ? node["_connectedTransformNode"] : isAnyParticleSystem(node) ? node.emitter : node; + public setSelectedNode(node: Node | IParticleSystem | Sprite): void { + let source = (isAnyParticleSystem(node) ? (node.emitter as AbstractMesh) : node) as Node | null; if (!source) { return; @@ -388,7 +385,7 @@ export class EditorGraph extends Component * Sets the given node selected in the graph. All other selected nodes remain selected. * @param node defines the reference to the node to select in the graph. */ - public addToSelectedNodes(node: Node | Sound | IParticleSystem | Sprite): void { + public addToSelectedNodes(node: Node | IParticleSystem | Sprite): void { this._forEachNode(this.state.nodes, (n) => { if (n.nodeData === node) { n.isSelected = true; @@ -760,66 +757,6 @@ export class EditorGraph extends Component return info; } - private _parseSoundNode(scene: Scene): TreeNodeInfo | null { - const soundTracks = scene.soundTracks; - if (!soundTracks?.length) { - return null; - } - - const childNodes: TreeNodeInfo[] = []; - - this._soundsList.forEach((sound) => { - if (sound.spatialSound) { - return; - } - - if (!sound.name.toLowerCase().includes(this.state.search.toLowerCase())) { - return; - } - - childNodes.push(this._getSoundNode(sound)); - }); - - if (!childNodes.length) { - return null; - } - - const rootSoundNode = { - childNodes, - nodeData: scene, - id: "__editor__sounds__", - icon: , - label: this._getNodeLabelComponent(scene, "Sounds", false), - } as TreeNodeInfo; - - this._forEachNode(this.state.nodes, (n) => { - if (n.id === rootSoundNode.id) { - rootSoundNode.isSelected = n.isSelected; - rootSoundNode.isExpanded = n.isExpanded; - } - }); - - return rootSoundNode; - } - - private _getSoundNode(sound: Sound): TreeNodeInfo { - const info = { - nodeData: sound, - id: sound.id, - icon: this._getIcon(sound), - label: this._getNodeLabelComponent(sound, sound.name, false), - } as TreeNodeInfo; - - this._forEachNode(this.state.nodes, (n) => { - if (n.id === info.id) { - info.isSelected = n.isSelected; - info.isExpanded = n.isExpanded; - } - }); - - return info; - } - private _getParticleSystemNode(particleSystem: IParticleSystem): TreeNodeInfo { const info = { nodeData: particleSystem, @@ -959,17 +896,6 @@ export class EditorGraph extends Component info.childNodes = children.map((c) => this._parseSceneNode(c)).filter((c) => c !== null) as TreeNodeInfo[]; } - // Handle sounds - if (isTransformNode(node) || isMesh(node) || isInstancedMesh(node)) { - const sounds = this._soundsList.filter((s) => s["_connectedTransformNode"] === node); - - sounds?.forEach((sound) => { - if (sound.name.toLowerCase().includes(this.state.search.toLowerCase())) { - info.childNodes?.push(this._getSoundNode(sound)); - } - }); - } - // Handle particle systems if (isAbstractMesh(node) && !noChildren) { const particleSystems = this.props.editor.layout.preview.scene.particleSystems.filter((ps) => ps.emitter === node); @@ -1105,7 +1031,7 @@ export class EditorGraph extends Component return ; } - if (isSound(object)) { + if (isSoundNode(object)) { return ; } @@ -1158,10 +1084,28 @@ export class EditorGraph extends Component private _handleDropEmpty(ev: DragEvent): void { const node = ev.dataTransfer.getData("graph/node"); - if (!node) { - return; + if (node) { + setNewParentForGraphSelectedNodes(this.props.editor, this.props.editor.layout.preview.scene, ev.shiftKey); } - setNewParentForGraphSelectedNodes(this.props.editor, this.props.editor.layout.preview.scene, ev.shiftKey); + const asset = ev.dataTransfer.getData("assets"); + if (asset) { + const absolutePaths = this.props.editor.layout.assets.state.selectedKeys; + + absolutePaths.forEach((absolutePath) => { + const extension = extname(absolutePath).toLowerCase(); + + switch (extension) { + case ".mp3": + case ".ogg": + case ".wav": + case ".wave": + applySoundAsset(this.props.editor, this.props.editor.layout.preview.scene, absolutePath).then(() => { + this.props.editor.layout.graph.refresh(); + }); + break; + } + }); + } } } diff --git a/editor/src/editor/layout/graph/context-menu.tsx b/editor/src/editor/layout/graph/context-menu.tsx index ad2a1b19a..693a5678b 100644 --- a/editor/src/editor/layout/graph/context-menu.tsx +++ b/editor/src/editor/layout/graph/context-menu.tsx @@ -29,8 +29,6 @@ import { getLightCommands } from "../../dialogs/command-palette/light"; import { getCameraCommands } from "../../dialogs/command-palette/camera"; import { getSpriteCommands } from "../../dialogs/command-palette/sprite"; -import { isSound } from "../../../tools/guards/sound"; -import { reloadSound } from "../../../tools/sound/tools"; import { registerUndoRedo } from "../../../tools/undoredo"; import { waitNextAnimationFrame } from "../../../tools/tools"; import { isClusteredLight } from "../../../tools/light/cluster"; @@ -40,12 +38,14 @@ import { isAnyParticleSystem } from "../../../tools/guards/particles"; import { isScene, isSceneLinkNode } from "../../../tools/guards/scene"; import { cloneNode, ICloneNodeOptions } from "../../../tools/node/clone"; import { isSprite, isSpriteMapNode } from "../../../tools/guards/sprites"; -import { isAbstractMesh, isCamera, isClusteredLightContainer, isLight, isMesh, isNode } from "../../../tools/guards/nodes"; import { isNodeLocked, isNodeSerializable, isNodeVisibleInGraph, setNodeLocked, setNodeSerializable } from "../../../tools/node/metadata"; +import { isAbstractMesh, isCamera, isClusteredLightContainer, isLight, isMesh, isNode, isTransformNode } from "../../../tools/guards/nodes"; import { addPointLight, addSpotLight } from "../../../project/add/light"; import { addGPUParticleSystem, addParticleSystem } from "../../../project/add/particles"; +import { addSoundNode } from "../../../project/add/sound"; + import { EditorInspectorSwitchField } from "../inspector/fields/switch"; import { configureImportedMaterial, configureImportedNodeIds } from "../preview/import/import"; @@ -93,7 +93,7 @@ export class EditorGraphContextMenu extends Component )} - {!isScene(this.props.object) && !isSound(this.props.object) && !isClusteredLightContainer(this.props.object) && ( + {!isScene(this.props.object) && !isClusteredLightContainer(this.props.object) && ( <> this._cloneNode(this.props.object)}>Clone @@ -145,8 +145,6 @@ export class EditorGraphContextMenu extends Component )} - {isSound(this.props.object) && this._reloadSound()}>Reload} - {isScene(this.props.object) && this.props.editor.state.enableExperimentalFeatures && ( <> exportScene(this.props.editor)}>Export Scene (.babylon) @@ -203,6 +201,14 @@ export class EditorGraphContextMenu extends Component )} + {(isAbstractMesh(this.props.object) || isTransformNode(this.props.object) || isScene(this.props.object)) && ( + <> + + addSoundNode(this.props.editor, isScene(this.props.object) ? null : this.props.object)}> + Sound Node + + + )} {getSpriteCommands(this.props.editor, parent).map((command) => ( @@ -214,7 +220,6 @@ export class EditorGraphContextMenu extends Component { props.editor.layout.graph.refresh(); }); diff --git a/editor/src/editor/layout/graph/move.ts b/editor/src/editor/layout/graph/move.ts index ddbbd6b6f..56ad82b7d 100644 --- a/editor/src/editor/layout/graph/move.ts +++ b/editor/src/editor/layout/graph/move.ts @@ -1,11 +1,10 @@ -import { TransformNode, AbstractMesh, Vector3, Node } from "babylonjs"; +import { TransformNode, AbstractMesh, Node } from "babylonjs"; import { isScene } from "../../../tools/guards/scene"; -import { isSound } from "../../../tools/guards/sound"; import { registerUndoRedo } from "../../../tools/undoredo"; import { isClusteredLight } from "../../../tools/light/cluster"; import { isAnyParticleSystem } from "../../../tools/guards/particles"; -import { isAbstractMesh, isClusteredLightContainer, isInstancedMesh, isLight, isMesh, isNode, isTransformNode } from "../../../tools/guards/nodes"; +import { isAbstractMesh, isClusteredLightContainer, isLight, isNode } from "../../../tools/guards/nodes"; import { applyNodeParentingConfiguration, applyTransformNodeParentingConfiguration, IOldNodeHierarchyConfiguration } from "../../../tools/node/parenting"; import { Editor } from "../../main"; @@ -44,10 +43,6 @@ export function setNewParentForGraphSelectedNodes(editor: Editor, newParent: any } } - if (isSound(n.nodeData)) { - return oldHierarchyMap.set(n.nodeData, n.nodeData["_connectedTransformNode"]); - } - if (isAnyParticleSystem(n.nodeData)) { return oldHierarchyMap.set(n.nodeData, n.nodeData.emitter); } @@ -79,19 +74,6 @@ export function setNewParentForGraphSelectedNodes(editor: Editor, newParent: any return applyNodeParentingConfiguration(n.nodeData, oldHierarchyMap.get(n.nodeData) as IOldNodeHierarchyConfiguration); } - if (isSound(n.nodeData)) { - const oldSoundNode = oldHierarchyMap.get(n.nodeData); - - if (oldSoundNode) { - return n.nodeData.attachToMesh(oldSoundNode as TransformNode); - } - - n.nodeData.detachFromMesh(); - n.nodeData.spatialSound = false; - n.nodeData.setPosition(Vector3.Zero()); - return (n.nodeData["_connectedTransformNode"] = null); - } - if (isAnyParticleSystem(n.nodeData)) { return (n.nodeData.emitter = oldHierarchyMap.get(n.nodeData) as AbstractMesh); } @@ -127,19 +109,6 @@ export function setNewParentForGraphSelectedNodes(editor: Editor, newParent: any return (n.nodeData.parent = isScene(newParent) ? null : newParent); } - if (isSound(n.nodeData)) { - if (isTransformNode(newParent) || isMesh(newParent) || isInstancedMesh(newParent)) { - return n.nodeData.attachToMesh(newParent); - } - - if (isScene(newParent)) { - n.nodeData.detachFromMesh(); - n.nodeData.spatialSound = false; - n.nodeData.setPosition(Vector3.Zero()); - return (n.nodeData["_connectedTransformNode"] = null); - } - } - if (isAnyParticleSystem(n.nodeData)) { if (isAbstractMesh(newParent)) { return (n.nodeData.emitter = newParent); diff --git a/editor/src/editor/layout/graph/remove.ts b/editor/src/editor/layout/graph/remove.ts index 91fb87c53..e479becf3 100644 --- a/editor/src/editor/layout/graph/remove.ts +++ b/editor/src/editor/layout/graph/remove.ts @@ -1,7 +1,6 @@ -import { Node, Light, AbstractMesh, Scene, IParticleSystem, Sound, SoundTrack, Sprite, Skeleton } from "babylonjs"; +import { Node, Light, AbstractMesh, Scene, IParticleSystem, Sprite, Skeleton } from "babylonjs"; import { unique } from "../../../tools/tools"; -import { isSound } from "../../../tools/guards/sound"; import { isSprite } from "../../../tools/guards/sprites"; import { registerUndoRedo } from "../../../tools/undoredo"; import { updateAllLights } from "../../../tools/light/shadows"; @@ -19,10 +18,6 @@ type _RemoveNodeData = { isClusteredLight: boolean; lights: Light[]; - sounds: { - sound: Sound; - soundtrack?: SoundTrack; - }[]; skeletons: Skeleton[]; particleSystems: IParticleSystem[]; }; @@ -52,17 +47,6 @@ export function removeNodes(editor: Editor) { parent: descendant.parent, skeletons: scene.skeletons.filter((skeleton) => isAbstractMesh(descendant) && descendant.skeleton === skeleton), particleSystems: scene.particleSystems.filter((ps) => ps.emitter === descendant), - sounds: - scene.soundTracks - ?.map((soundTrack) => - soundTrack.soundCollection - .filter((sound) => sound.spatialSound && sound["_connectedTransformNode"] === descendant) - .map((sound) => ({ - sound, - soundtrack: scene.soundTracks?.[sound.soundTrackId + 1], - })) - ) - .flat() ?? [], isClusteredLight: isLight(descendant) && isClusteredLight(descendant, editor), lights: scene.lights.filter((light) => { return light @@ -77,20 +61,6 @@ export function removeNodes(editor: Editor) { }) .flat(); - const sounds = unique( - nodes - .map((d) => d.sounds) - .flat() - .concat( - allData - .filter((d) => isSound(d)) - .map((sound) => ({ - sound, - soundtrack: scene.soundTracks?.[sound.soundTrackId + 1], - })) - ) - ); - const skeletons = unique( nodes .map((d) => d.skeletons) @@ -110,7 +80,7 @@ export function removeNodes(editor: Editor) { const sprites = allData.filter((d) => isSprite(d)) as Sprite[]; const advancedGuiTextures = allData.filter((d) => isAdvancedDynamicTexture(d)); - const animationGroups = getLinkedAnimationGroupsFor([...particleSystems, ...advancedGuiTextures, ...sounds.map((d) => d.sound), ...nodes.map((d) => d.node)], scene); + const animationGroups = getLinkedAnimationGroupsFor([...particleSystems, ...advancedGuiTextures, ...nodes.map((d) => d.node)], scene); registerUndoRedo({ executeRedo: true, @@ -126,10 +96,6 @@ export function removeNodes(editor: Editor) { restoreNodeData(editor, d, scene); }); - sounds.forEach((d) => { - d.soundtrack?.addSound(d.sound); - }); - particleSystems.forEach((particleSystem) => { scene.addParticleSystem(particleSystem); }); @@ -166,10 +132,6 @@ export function removeNodes(editor: Editor) { removeNodeData(editor, d, scene); }); - sounds.forEach((d) => { - d.soundtrack?.removeSound(d.sound); - }); - particleSystems.forEach((particleSystem) => { scene.removeParticleSystem(particleSystem); }); diff --git a/editor/src/editor/layout/inspector.tsx b/editor/src/editor/layout/inspector.tsx index 1975426d6..1d0386575 100644 --- a/editor/src/editor/layout/inspector.tsx +++ b/editor/src/editor/layout/inspector.tsx @@ -35,7 +35,7 @@ import { EditorCameraInspector } from "./inspector/camera/editor"; import { EditorFreeCameraInspector } from "./inspector/camera/free"; import { EditorArcRotateCameraInspector } from "./inspector/camera/arc-rotate"; -import { EditorSoundInspector } from "./inspector/sound/sound"; +import { EditorSoundNodeInspector } from "./inspector/sound/sound-node"; import { EditorAdvancedDynamicTextureInspector } from "./inspector/gui/gui"; @@ -86,7 +86,8 @@ export class EditorInspector extends Component extends IEditorInspectorFieldProps { +export interface IEditorInspectorSceneEntityFieldProps extends IEditorInspectorFieldProps { scene: Scene; type?: "node" | "particleSystem" | "sound"; onChange?: (value: T | null) => void; } -export function EditorInspectorSceneEntityField(props: IEditorInspectorSceneEntityFieldProps) { +export function EditorInspectorSceneEntityField(props: IEditorInspectorSceneEntityFieldProps) { const [dragOver, setDragOver] = useState(false); const [value, setValue] = useState(null); @@ -42,7 +41,7 @@ export function EditorInspectorSceneEntityField ps.id === id) as T) ?? (getSoundById(id, props.scene) as T); + return (props.scene.getNodeById(id) as T) ?? (props.scene.particleSystems?.find((ps) => ps.id === id) as T); } function handleDragOver(ev: DragEvent) { @@ -69,7 +68,7 @@ export function EditorInspectorSceneEntityField, IEditorSoundNodeInspectorState> { + /** + * Returns whether or not the given object is supported by this inspector. + * @param object defines the object to check. + * @returns true if the object is supported by this inspector. + */ + public static IsSupported(object: unknown): boolean { + return isSoundNode(object); + } + + public constructor(props: IEditorInspectorImplementationProps) { + super(props); + + this.state = { + dragOver: false, + }; + } + + private _gizmoObserver: Observer | null = null; + + public componentDidMount(): void { + this._gizmoObserver = onGizmoNodeChangedObservable.add((node) => { + if (node === this.props.object) { + this.props.editor.layout.inspector.forceUpdate(); + } + }); + } + + public componentWillUnmount(): void { + if (this._gizmoObserver) { + onGizmoNodeChangedObservable.remove(this._gizmoObserver); + } + + this.props.object.sound?.stop(); + } + + public render(): ReactNode { + return ( + <> + + onNodeModifiedObservable.notifyObservers(this.props.object)} + /> + + + + Position} object={this.props.object} property="position" /> + {EditorTransformNodeInspector.GetRotationInspector(this.props.object)} + Scaling} object={this.props.object} property="scaling" /> + + + + + + {this._getSoundDraggableZone()} + {this.props.object.sound && this._getSoundInspector()} + + + ); + } + + private _getSoundInspector(): ReactNode { + const sound = this.props.object.sound; + if (!sound) { + return; + } + + const isPlaying = this.props.object.sound?.state === SoundState.Started; + + return ( + <> + + + + this.forceUpdate()} /> + + {this.props.object.isSpatial && sound.spatial && ( + <> + + + + + + + )} + + ); + } + + private _handlePlay(): void { + this.props.object?.stop(); + this.props.object?.play({ + loop: true, + }); + + this.forceUpdate(); + } + + private _handleStop(): void { + this.props.object.sound?.stop(); + this.forceUpdate(); + } + + private _getSoundDraggableZone(): ReactNode { + return ( +
{ + ev.preventDefault(); + this.setState({ + dragOver: true, + }); + }} + onDragLeave={(ev) => { + ev.preventDefault(); + this.setState({ + dragOver: false, + }); + }} + onDrop={async (ev) => { + ev.preventDefault(); + this.setState({ + dragOver: false, + }); + + const path = JSON.parse(ev.dataTransfer.getData("assets"))[0]; + const extension = extname(path).toLowerCase(); + + if (supportedSoundExtensions.includes(extension)) { + await this.props.object.setSoundAbsolutePath(path); + this.forceUpdate(); + } + }} + className={` + flex flex-col gap-4 justify-center items-center p-2 rounded-lg + border-[1px] border-secondary-foreground/35 border-dashed + ${this.state.dragOver ? "bg-muted-foreground/75 dark:bg-muted-foreground/20" : ""} + transition-all duration-300 ease-in-out + `} + > + +
+ {!this.props.object.soundRelativePath &&
No Sound file assigned yet.
} + {this.props.object.soundRelativePath && ( +
onSelectedAssetChanged.notifyObservers(join(getProjectAssetsRootUrl()!, this.props.object.soundRelativePath!))} + > + {this.props.object.soundRelativePath} +
+ )} + {!this.props.object.sound &&
Drag'n'drop a Sound file here from the Assets Browser.
} +
+
+ ); + } +} diff --git a/editor/src/editor/layout/inspector/sound/sound.tsx b/editor/src/editor/layout/inspector/sound/sound.tsx deleted file mode 100644 index 3a85a7240..000000000 --- a/editor/src/editor/layout/inspector/sound/sound.tsx +++ /dev/null @@ -1,104 +0,0 @@ -import { clipboard } from "electron"; - -import { FaCopy } from "react-icons/fa"; -import { Component, ReactNode } from "react"; - -import { toast } from "sonner"; - -import { Sound } from "babylonjs"; - -import { Button } from "../../../../ui/shadcn/ui/button"; - -import { isSound } from "../../../../tools/guards/sound"; -import { reloadSound } from "../../../../tools/sound/tools"; -import { registerUndoRedo } from "../../../../tools/undoredo"; - -import { EditorInspectorNumberField } from "../fields/number"; -import { EditorInspectorSectionField } from "../fields/section"; - -import { EditorSpatialSoundInspectorComponent } from "./spatial"; -import { IEditorInspectorImplementationProps } from "../inspector"; - -export class EditorSoundInspector extends Component> { - /** - * Returns whether or not the given object is supported by this inspector. - * @param object defines the object to check. - * @returns true if the object is supported by this inspector. - */ - public static IsSupported(object: unknown): boolean { - return isSound(object); - } - - public render(): ReactNode { - return ( - <> - -
-
Name
- -
-
{this.props.object.name}
- - -
-
- - this.props.object.setVolume(value)} - onFinishChange={(value, oldValue) => { - registerUndoRedo({ - executeRedo: true, - undo: () => this.props.object.setVolume(oldValue), - redo: () => this.props.object.setVolume(value), - }); - }} - /> - - -
- - - - - - {this.props.object.spatialSound && } - - ); - } - - public componentWillUnmount(): void { - this.props.object.stop(); - } - - private _handleCopyName(): void { - clipboard.writeText(this.props.object.name); - toast.success("Name copied to clipboard."); - } - - private _handlePlay(): void { - this.props.object.stop(); - this.props.object.play(0, 0); - this.forceUpdate(); - } - - private _handleStop(): void { - this.props.object.pause(); - this.forceUpdate(); - } - - private _handleReload(): void { - this.props.editor.layout.inspector.setEditedObject(reloadSound(this.props.editor, this.props.object)); - } -} diff --git a/editor/src/editor/layout/inspector/sound/spatial.tsx b/editor/src/editor/layout/inspector/sound/spatial.tsx deleted file mode 100644 index c07c848d9..000000000 --- a/editor/src/editor/layout/inspector/sound/spatial.tsx +++ /dev/null @@ -1,147 +0,0 @@ -import { Component, ReactNode } from "react"; - -import { Sound, ISoundOptions } from "babylonjs"; - -import { registerUndoRedo } from "../../../../tools/undoredo"; - -import { EditorInspectorListField } from "../fields/list"; -import { EditorInspectorNumberField } from "../fields/number"; -import { EditorInspectorSectionField } from "../fields/section"; - -export interface IEditorSpatialSoundInspectorComponentProps { - sound: Sound; -} - -export class EditorSpatialSoundInspectorComponent extends Component { - public render(): ReactNode { - return ( - - {this._getDistanceModelComponent()} - - {this.props.sound.distanceModel === "linear" ? this._getMaxDistanceComponent() : [this._getRollOffFactorComponent(), this._getRefDistanceComponent()]} - - {this._getPanningModelComponent()} - - ); - } - - private _getDistanceModelComponent(): ReactNode { - return ( - { - this.forceUpdate(); - - registerUndoRedo({ - executeRedo: true, - undo: () => this._updateOptions({ distanceModel: oldValue }), - redo: () => this._updateOptions({ distanceModel: value }), - }); - }} - /> - ); - } - - private _getMaxDistanceComponent(): ReactNode { - return ( - this._updateOptions({ maxDistance: value })} - onFinishChange={(value, oldValue) => { - registerUndoRedo({ - executeRedo: true, - undo: () => this._updateOptions({ maxDistance: oldValue }), - redo: () => this._updateOptions({ maxDistance: value }), - }); - }} - /> - ); - } - - private _getRollOffFactorComponent(): ReactNode { - return ( - this._updateOptions({ rolloffFactor: value })} - onFinishChange={(value, oldValue) => { - registerUndoRedo({ - executeRedo: true, - undo: () => this._updateOptions({ rolloffFactor: oldValue }), - redo: () => this._updateOptions({ rolloffFactor: value }), - }); - }} - /> - ); - } - - private _getRefDistanceComponent(): ReactNode { - return ( - this._updateOptions({ refDistance: value })} - onFinishChange={(value, oldValue) => { - registerUndoRedo({ - executeRedo: true, - undo: () => this._updateOptions({ refDistance: oldValue }), - redo: () => this._updateOptions({ refDistance: value }), - }); - }} - /> - ); - } - - private _getPanningModelComponent(): ReactNode { - return ( - { - registerUndoRedo({ - executeRedo: true, - undo: () => (oldValue === "HRTF" ? this.props.sound.switchPanningModelToHRTF() : this.props.sound.switchPanningModelToEqualPower()), - redo: () => (value === "HRTF" ? this.props.sound.switchPanningModelToHRTF() : this.props.sound.switchPanningModelToEqualPower()), - }); - }} - /> - ); - } - - private _updateOptions(options: ISoundOptions): void { - this.props.sound.updateOptions({ - maxDistance: this.props.sound.maxDistance, - refDistance: this.props.sound.refDistance, - rolloffFactor: this.props.sound.rolloffFactor, - distanceModel: this.props.sound.distanceModel, - ...options, - }); - } -} diff --git a/editor/src/editor/layout/inspector/sprites/sprite-manager.tsx b/editor/src/editor/layout/inspector/sprites/sprite-manager.tsx index a798799da..c36836a7e 100644 --- a/editor/src/editor/layout/inspector/sprites/sprite-manager.tsx +++ b/editor/src/editor/layout/inspector/sprites/sprite-manager.tsx @@ -17,7 +17,7 @@ import { computeSpriteManagerPreviews } from "../../../../tools/sprite/preview"; import { getProjectAssetsRootUrl } from "../../../../project/configuration"; -import { onGizmoNodeChangedObservable } from "../../preview/gizmo"; +import { onGizmoNodeChangedObservable } from "../../preview/gizmo/gizmo"; import { ScriptInspectorComponent } from "../script/script"; diff --git a/editor/src/editor/layout/inspector/sprites/sprite-map.tsx b/editor/src/editor/layout/inspector/sprites/sprite-map.tsx index 6c9b6face..5143e9b74 100644 --- a/editor/src/editor/layout/inspector/sprites/sprite-map.tsx +++ b/editor/src/editor/layout/inspector/sprites/sprite-map.tsx @@ -14,7 +14,7 @@ import { Button } from "../../../../ui/shadcn/ui/button"; import { SpriteMapNode } from "../../../nodes/sprite-map"; -import { onGizmoNodeChangedObservable } from "../../preview/gizmo"; +import { onGizmoNodeChangedObservable } from "../../preview/gizmo/gizmo"; import { registerUndoRedo } from "../../../../tools/undoredo"; import { isSpriteMapNode } from "../../../../tools/guards/sprites"; diff --git a/editor/src/editor/layout/inspector/sprites/sprite.tsx b/editor/src/editor/layout/inspector/sprites/sprite.tsx index 44bb94c90..91d76bde7 100644 --- a/editor/src/editor/layout/inspector/sprites/sprite.tsx +++ b/editor/src/editor/layout/inspector/sprites/sprite.tsx @@ -17,7 +17,7 @@ import { computeSpriteManagerPreviews } from "../../../../tools/sprite/preview"; import { SpriteManagerNode } from "../../../nodes/sprite-manager"; -import { onGizmoNodeChangedObservable } from "../../preview/gizmo"; +import { onGizmoNodeChangedObservable } from "../../preview/gizmo/gizmo"; import { ScriptInspectorComponent } from "../script/script"; diff --git a/editor/src/editor/layout/inspector/transform.tsx b/editor/src/editor/layout/inspector/transform.tsx index e14e7e88b..ac73b891a 100644 --- a/editor/src/editor/layout/inspector/transform.tsx +++ b/editor/src/editor/layout/inspector/transform.tsx @@ -13,7 +13,7 @@ import { EditorInspectorSectionField } from "./fields/section"; import { ScriptInspectorComponent } from "./script/script"; import { CustomMetadataInspector } from "./metadata/custom-metadata"; -import { onGizmoNodeChangedObservable } from "../preview/gizmo"; +import { onGizmoNodeChangedObservable } from "../preview/gizmo/gizmo"; import { IEditorInspectorImplementationProps } from "./inspector"; import { EditorInspectorSwitchField } from "./fields/switch"; diff --git a/editor/src/editor/layout/preview.tsx b/editor/src/editor/layout/preview.tsx index 3c62329d1..c23dec8e4 100644 --- a/editor/src/editor/layout/preview.tsx +++ b/editor/src/editor/layout/preview.tsx @@ -8,7 +8,7 @@ import { Grid } from "react-loader-spinner"; import { FaCheck } from "react-icons/fa6"; import { IoIosStats } from "react-icons/io"; -import { LuGrid3X3, LuMove3D, LuRotate3D, LuScale3D, LuScaling, LuRotateCw } from "react-icons/lu"; +import { LuMove3D, LuRotate3D, LuScale3D } from "react-icons/lu"; import { GiArrowCursor, GiTeapot, GiWireframeGlobe } from "react-icons/gi"; import { @@ -36,18 +36,22 @@ import { SelectionOutlineLayer, ClusteredLightContainer, Tools, + _GetAudioEngine, } from "babylonjs"; +import { SpinnerUIComponent } from "../../ui/spinner"; + import { Button } from "../../ui/shadcn/ui/button"; import { Toggle } from "../../ui/shadcn/ui/toggle"; -import { EditorInspectorNumberField } from "./inspector/fields/number"; import { Progress } from "../../ui/shadcn/ui/progress"; +import { Separator } from "../../ui/shadcn/ui/separator"; import { ToolbarRadioGroup, ToolbarRadioGroupItem } from "../../ui/shadcn/ui/toolbar-radio-group"; +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "../../ui/shadcn/ui/tooltip"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../../ui/shadcn/ui/select"; +import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger } from "../../ui/shadcn/ui/dropdown-menu"; import { Editor } from "../main"; -import { isSound } from "../../tools/guards/sound"; import { isVector3 } from "../../tools/guards/math"; import { isDomTextInputFocused } from "../../tools/dom"; import { isNodeLocked } from "../../tools/node/metadata"; @@ -61,24 +65,13 @@ import { getCameraFocusPositionFor } from "../../tools/camera/focus"; import { ITweenConfiguration, Tween } from "../../tools/animation/tween"; import { checkProjectCachedCompressedTextures } from "../../tools/assets/ktx"; import { createSceneLink, getRootSceneLink } from "../../tools/scene/scene-link"; -import { - defaultGizmoSnapPreferences, - gizmoSnapMinStep, - IGizmoSnapPreferences, - roundGizmoSnapSteps, -} from "../../tools/gizmo-snap-preferences"; import { UniqueNumber, waitNextAnimationFrame, waitUntil } from "../../tools/tools"; import { isSprite, isSpriteManagerNode, isSpriteMapNode } from "../../tools/guards/sprites"; -import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger } from "../../ui/shadcn/ui/dropdown-menu"; -import { Popover, PopoverContent, PopoverTrigger } from "../../ui/shadcn/ui/popover"; -import { isAbstractMesh, isAnyTransformNode, isCamera, isCollisionInstancedMesh, isCollisionMesh, isInstancedMesh, isLight, isMesh, isNode } from "../../tools/guards/nodes"; +import { defaultGizmoSnapPreferences, IGizmoSnapPreferences, roundGizmoSnapSteps } from "../../tools/scene/gizmo"; +import { isAbstractMesh, isAnyTransformNode, isCamera, isCollisionInstancedMesh, isCollisionMesh, isLight, isNode } from "../../tools/guards/nodes"; import { EditorCamera } from "../nodes/camera"; -import { SpinnerUIComponent } from "../../ui/spinner"; -import { Separator } from "../../ui/shadcn/ui/separator"; -import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "../../ui/shadcn/ui/tooltip"; - import { saveRenderingConfigurationForCamera } from "../rendering/tools"; import { disposeVLSPostProcess, parseVLSPostProcess, vlsPostProcessCameraConfigurations } from "../rendering/vls"; import { disposeTAARenderingPipeline, parseTAARenderingPipeline, taaPipelineCameraConfigurations } from "../rendering/taa"; @@ -89,12 +82,14 @@ import { defaultPipelineCameraConfigurations, disposeDefaultRenderingPipeline, p import { EditorGraphContextMenu } from "./graph/context-menu"; -import { EditorPreviewGizmo } from "./preview/gizmo"; import { EditorPreviewIcons } from "./preview/icons"; import { EditorPreviewCamera } from "./preview/camera"; import { EditorPreviewAxisHelper } from "./preview/axis"; import { EditorPreviewPlayComponent } from "./preview/play"; +import { EditorPreviewGizmo } from "./preview/gizmo/gizmo"; +import { EditorPreviewGizmoSettings } from "./preview/gizmo/settings"; + import { Stats } from "./preview/stats/stats"; import { StatRow } from "./preview/stats/row"; import { StatsValuesType } from "./preview/stats/types"; @@ -129,6 +124,7 @@ export interface IEditorPreviewState { playEnabled: boolean; playSceneLoadingProgress: number; + gizmoSnap: IGizmoSnapPreferences; activeGizmo: "position" | "rotation" | "scaling" | "none"; /** @@ -136,8 +132,6 @@ export interface IEditorPreviewState { * "fit" means the canvas will fit the entire panel container. */ fixedDimensions: "720p" | "1080p" | "4k" | "fit"; - - gizmoSnap: IGizmoSnapPreferences; } export class EditorPreview extends Component { @@ -206,16 +200,6 @@ export class EditorPreview extends Component = { - translationStep: 0, - rotationStepDegrees: 0, - scaleStep: 0, - }; - /** @internal */ public _previewCamera: Camera | null = null; @@ -455,14 +439,6 @@ export class EditorPreview extends Component { if (this._renderScene && !this.play.state.playing) { - // TODO: remove this once fixed - // Bug report on forum: https://forum.babylonjs.com/t/multi-canvas-and-post-processes/59616/23 - const ppRenderer = this.scene.prePassRenderer; - if (ppRenderer) { - ppRenderer.markAsDirty(); + if (this._previewCamera) { + // TODO: remove this once fixed + // Bug report on forum: https://forum.babylonjs.com/t/multi-canvas-and-post-processes/59616/23 + const ppRenderer = this.scene.prePassRenderer; + if (ppRenderer) { + ppRenderer.markAsDirty(); + } } this.scene.render(); @@ -899,125 +875,12 @@ export class EditorPreview extends Component this._commitGizmoSnap({ ...snap, translationStep: Math.max(min, v) }); - const bumpRotation = (v: number) => this._commitGizmoSnap({ ...snap, rotationStepDegrees: Math.max(min, v) }); - const bumpScale = (v: number) => this._commitGizmoSnap({ ...snap, scaleStep: Math.max(min, v) }); - - const snapRowClass = "grid grid-cols-[minmax(0,7rem)_auto_minmax(0,1fr)] items-center gap-3"; - const snapToggleClass = (enabled: boolean) => - `rounded-md border border-input h-9 w-9 px-0 shrink-0 justify-center shadow-sm ${enabled ? "bg-primary/20" : "bg-background"}`; - - return ( - - - - - -
-
-
Translation
- - - this._commitGizmoSnap({ ...snap, translationEnabled: on })} - className={snapToggleClass(snap.translationEnabled)} - aria-label="Translation grid snap" - > - - - - Translation grid snap - -
- bumpTranslation(v)} - /> -
-
- -
-
Rotation
- - - this._commitGizmoSnap({ ...snap, rotationEnabled: on })} - className={snapToggleClass(snap.rotationEnabled)} - aria-label="Rotation snap" - > - - - - Rotation snap (degrees) - -
- bumpRotation(v)} - /> -
-
- -
-
Scale
- - - this._commitGizmoSnap({ ...snap, scaleEnabled: on })} - className={snapToggleClass(snap.scaleEnabled)} - aria-label="Scale snap" - > - - - - Scale snap (incremental step) - -
- bumpScale(v)} - /> -
-
-
-
-
- ); + const normalized = roundGizmoSnapSteps(prefs); + this.gizmo?.setSnapPreferences(normalized); + this.setState({ + gizmoSnap: normalized, + }); } private _getEditToolbar(): ReactNode { @@ -1085,6 +948,10 @@ export class EditorPreview extends Component + + + + - {this._getGizmoSnapToolbarControls()} - - -