diff --git a/src/commands/set-box-brush-face-climbable-command.ts b/src/commands/set-box-brush-face-climbable-command.ts new file mode 100644 index 00000000..50f6cac5 --- /dev/null +++ b/src/commands/set-box-brush-face-climbable-command.ts @@ -0,0 +1,91 @@ +import type { ToolMode } from "../core/tool-mode"; +import { createOpaqueId } from "../core/ids"; +import type { EditorSelection } from "../core/selection"; +import type { WhiteboxFaceId } from "../document/brushes"; + +import { + cloneSelectionForCommand, + getBoxBrushFaceOrThrow, + replaceBoxBrushFace, + setSingleBrushFaceSelection +} from "./brush-command-helpers"; +import type { EditorCommand } from "./command"; + +interface SetBoxBrushFaceClimbableCommandOptions { + brushId: string; + faceId: WhiteboxFaceId; + climbable: boolean; +} + +export function createSetBoxBrushFaceClimbableCommand( + options: SetBoxBrushFaceClimbableCommandOptions +): EditorCommand { + let previousClimbable: boolean | null = null; + let previousSelection: EditorSelection | null = null; + let previousToolMode: ToolMode | null = null; + + return { + id: createOpaqueId("command"), + label: options.climbable + ? `Mark ${options.faceId} face climbable` + : `Clear ${options.faceId} face climbable`, + execute(context) { + const currentDocument = context.getDocument(); + const currentFace = getBoxBrushFaceOrThrow( + currentDocument, + options.brushId, + options.faceId + ); + + if (previousClimbable === null) { + previousClimbable = currentFace.climbable; + } + + if (previousSelection === null) { + previousSelection = cloneSelectionForCommand(context.getSelection()); + } + + if (previousToolMode === null) { + previousToolMode = context.getToolMode(); + } + + context.setDocument( + replaceBoxBrushFace(currentDocument, options.brushId, options.faceId, { + ...currentFace, + climbable: options.climbable + }) + ); + context.setSelection( + setSingleBrushFaceSelection(options.brushId, options.faceId) + ); + context.setToolMode("select"); + }, + undo(context) { + if (previousClimbable === null) { + return; + } + + const currentDocument = context.getDocument(); + const currentFace = getBoxBrushFaceOrThrow( + currentDocument, + options.brushId, + options.faceId + ); + + context.setDocument( + replaceBoxBrushFace(currentDocument, options.brushId, options.faceId, { + ...currentFace, + climbable: previousClimbable + }) + ); + + if (previousSelection !== null) { + context.setSelection(previousSelection); + } + + if (previousToolMode !== null) { + context.setToolMode(previousToolMode); + } + } + }; +} diff --git a/src/document/migrate-scene-document.ts b/src/document/migrate-scene-document.ts index 4dc017fc..6a01f7dc 100644 --- a/src/document/migrate-scene-document.ts +++ b/src/document/migrate-scene-document.ts @@ -244,6 +244,7 @@ import { DAWN_DUSK_BACKGROUND_IMAGE_SCENE_DOCUMENT_VERSION, WHITEBOX_PRIMITIVES_SCENE_DOCUMENT_VERSION, WHITEBOX_BEVEL_SCENE_DOCUMENT_VERSION, + WHITEBOX_FACE_CLIMBABLE_SCENE_DOCUMENT_VERSION, WHITEBOX_BOX_VOLUME_SCENE_DOCUMENT_VERSION, WHITEBOX_FLOAT_TRANSFORM_SCENE_DOCUMENT_VERSION, WHITEBOX_GEOMETRY_SCENE_DOCUMENT_VERSION, @@ -2475,7 +2476,8 @@ function readBrushFace( uv: value.uv === undefined && allowMissingUvState ? createDefaultFaceUvState() - : readFaceUvState(value.uv, `${label}.uv`) + : readFaceUvState(value.uv, `${label}.uv`), + climbable: readOptionalBoolean(value.climbable, `${label}.climbable`, false) }; } @@ -5704,6 +5706,7 @@ export function migrateSceneDocument(source: unknown): SceneDocument { source.version !== GOD_RAYS_SOURCE_SIZE_SCENE_DOCUMENT_VERSION && source.version !== ATMOSPHERE_POLISH_SCENE_DOCUMENT_VERSION && source.version !== SCENE_DOCUMENT_VERSION && + source.version !== WHITEBOX_FACE_CLIMBABLE_SCENE_DOCUMENT_VERSION && source.version !== FOLLOW_ACTOR_PATH_SMOOTH_SCENE_DOCUMENT_VERSION ) { throw new Error( diff --git a/src/document/scene-document-validation.ts b/src/document/scene-document-validation.ts index 2574e21d..320c1559 100644 --- a/src/document/scene-document-validation.ts +++ b/src/document/scene-document-validation.ts @@ -6874,7 +6874,21 @@ export function validateSceneDocument( } for (const faceId of getBrushFaceIds(brush)) { - const materialId = brush.faces[faceId].materialId; + const face = brush.faces[faceId]; + + if (face === undefined) { + diagnostics.push( + createDiagnostic( + "error", + "missing-brush-face", + `Whitebox face ${faceId} must exist in brush face data.`, + `${path}.faces.${faceId}` + ) + ); + continue; + } + + const materialId = face.materialId; if (materialId !== null && document.materials[materialId] === undefined) { diagnostics.push( @@ -6886,6 +6900,17 @@ export function validateSceneDocument( ) ); } + + if (!isBoolean(face.climbable)) { + diagnostics.push( + createDiagnostic( + "error", + "invalid-brush-face-climbable", + "Whitebox face climbable must remain a boolean.", + `${path}.faces.${faceId}.climbable` + ) + ); + } } const volume = brush.volume as Record; diff --git a/src/document/scene-document.ts b/src/document/scene-document.ts index 3ebd5fed..3f44e920 100644 --- a/src/document/scene-document.ts +++ b/src/document/scene-document.ts @@ -29,7 +29,8 @@ import { } from "../sequencer/project-sequences"; import type { Terrain } from "./terrains"; -export const SCENE_DOCUMENT_VERSION = 89 as const; +export const SCENE_DOCUMENT_VERSION = 90 as const; +export const WHITEBOX_FACE_CLIMBABLE_SCENE_DOCUMENT_VERSION = 90 as const; export const GOD_RAYS_SOURCE_SIZE_SCENE_DOCUMENT_VERSION = 89 as const; export const ATMOSPHERE_POLISH_SCENE_DOCUMENT_VERSION = 88 as const; export const GOD_RAYS_SCENE_DOCUMENT_VERSION = 87 as const;