diff --git a/cli/package.json b/cli/package.json index 4688e4a66..fe3191ae8 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.1", "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..85129eb9c 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.1", "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/layout/graph.tsx b/editor/src/editor/layout/graph.tsx index 0ac14c65b..22c2197e1 100644 --- a/editor/src/editor/layout/graph.tsx +++ b/editor/src/editor/layout/graph.tsx @@ -1,3 +1,5 @@ +import { extname } from "path/posix"; + import { Component, DragEvent, ReactNode } from "react"; import { Button, Tree, TreeNodeInfo } from "@blueprintjs/core"; @@ -31,13 +33,13 @@ import { ContextMenuSubTrigger, } from "../../ui/shadcn/ui/context-menu"; -import { isSound } from "../../tools/guards/sound"; import { cloneNode } from "../../tools/node/clone"; import { registerUndoRedo } from "../../tools/undoredo"; import { isDomTextInputFocused } from "../../tools/dom"; import { isSceneLinkNode } from "../../tools/guards/scene"; import { updateAllLights } from "../../tools/light/shadows"; import { isClusteredLight } from "../../tools/light/cluster"; +import { isSound, isSoundNode } from "../../tools/guards/sound"; import { getCollisionMeshFor } from "../../tools/mesh/collision"; import { isNodeVisibleInGraph } from "../../tools/node/metadata"; import { isAdvancedDynamicTexture } from "../../tools/guards/texture"; @@ -79,8 +81,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"; @@ -264,6 +269,8 @@ export class EditorGraph extends Component ); })} + addSoundNode(this.props.editor)}>Sound Node + {getSpriteCommands(this.props.editor).map((command) => { return ( @@ -1105,7 +1112,7 @@ export class EditorGraph extends Component return ; } - if (isSound(object)) { + if (isSound(object) || isSoundNode(object)) { return ; } @@ -1158,10 +1165,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..e044394de 100644 --- a/editor/src/editor/layout/graph/context-menu.tsx +++ b/editor/src/editor/layout/graph/context-menu.tsx @@ -40,12 +40,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"; @@ -203,6 +205,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) => ( diff --git a/editor/src/editor/layout/graph/label.tsx b/editor/src/editor/layout/graph/label.tsx index 37bc2e8b6..375b134a5 100644 --- a/editor/src/editor/layout/graph/label.tsx +++ b/editor/src/editor/layout/graph/label.tsx @@ -11,7 +11,7 @@ import { isDarwin } from "../../../tools/os"; import { isScene } from "../../../tools/guards/scene"; import { registerUndoRedo } from "../../../tools/undoredo"; import { isNodeSerializable, isNodeLocked } from "../../../tools/node/metadata"; -import { isClusteredLightContainer, isInstancedMesh, isMesh, isNode } from "../../../tools/guards/nodes"; +import { isClusteredLightContainer, isInstancedMesh, isMesh, isNode, isTransformNode } from "../../../tools/guards/nodes"; import { applySoundAsset } from "../preview/import/sound"; import { applyTextureAssetToObject } from "../preview/import/texture"; @@ -146,7 +146,7 @@ export function EditorGraphLabel(props: IEditorGraphLabelProps) { case ".ogg": case ".wav": case ".wave": - if (isScene(props.object) || isMesh(props.object) || isInstancedMesh(props.object)) { + if (isScene(props.object) || isMesh(props.object) || isInstancedMesh(props.object) || isTransformNode(props.object)) { applySoundAsset(props.editor, props.object, absolutePath).then(() => { props.editor.layout.graph.refresh(); }); diff --git a/editor/src/editor/layout/inspector.tsx b/editor/src/editor/layout/inspector.tsx index 1975426d6..37e742857 100644 --- a/editor/src/editor/layout/inspector.tsx +++ b/editor/src/editor/layout/inspector.tsx @@ -36,6 +36,7 @@ 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"; @@ -87,6 +88,8 @@ export class EditorInspector extends Component, 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.sound?.stop(); + this.props.object.sound?.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/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..e1e8138d3 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,14 +36,19 @@ 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"; @@ -61,24 +66,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 +83,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 +125,7 @@ export interface IEditorPreviewState { playEnabled: boolean; playSceneLoadingProgress: number; + gizmoSnap: IGizmoSnapPreferences; activeGizmo: "position" | "rotation" | "scaling" | "none"; /** @@ -136,8 +133,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 +201,6 @@ export class EditorPreview extends Component = { - translationStep: 0, - rotationStepDegrees: 0, - scaleStep: 0, - }; - /** @internal */ public _previewCamera: Camera | null = null; @@ -568,6 +553,8 @@ 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 +888,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 +961,10 @@ export class EditorPreview extends Component + + + + - {this._getGizmoSnapToolbarControls()} - - -