diff --git a/src/commands/commit-transform-session-command.ts b/src/commands/commit-transform-session-command.ts index 02d7ac97..e7d5bbf1 100644 --- a/src/commands/commit-transform-session-command.ts +++ b/src/commands/commit-transform-session-command.ts @@ -1,6 +1,7 @@ import { createMoveBoxBrushCommand } from "./move-box-brush-command"; import { createResizeBoxBrushCommand } from "./resize-box-brush-command"; import { createRotateBoxBrushCommand } from "./rotate-box-brush-command"; +import { createSetBoxBrushTransformCommand } from "./set-box-brush-transform-command"; import { createUpsertEntityCommand } from "./upsert-entity-command"; import { createUpsertModelInstanceCommand } from "./upsert-model-instance-command"; import type { EditorCommand } from "./command"; @@ -24,6 +25,12 @@ function createTransformCommandLabel(session: ActiveTransformSession): string { return `${getTransformOperationLabel(session.operation)} ${ session.target.kind === "brush" ? "whitebox box" + : session.target.kind === "brushFace" + ? "whitebox face" + : session.target.kind === "brushEdge" + ? "whitebox edge" + : session.target.kind === "brushVertex" + ? "whitebox vertex" : session.target.kind === "entity" ? session.target.entityKind === "playerStart" ? "player start" @@ -71,6 +78,54 @@ export function createCommitTransformSessionCommand(document: SceneDocument, ses label: createTransformCommandLabel(session) }); } + case "brushFace": + if (session.preview.kind !== "brush") { + throw new Error("Whitebox face transform preview is invalid."); + } + + return createSetBoxBrushTransformCommand({ + selection: { + kind: "brushFace", + brushId: session.target.brushId, + faceId: session.target.faceId + }, + center: session.preview.center, + rotationDegrees: session.preview.rotationDegrees, + size: session.preview.size, + label: createTransformCommandLabel(session) + }); + case "brushEdge": + if (session.preview.kind !== "brush") { + throw new Error("Whitebox edge transform preview is invalid."); + } + + return createSetBoxBrushTransformCommand({ + selection: { + kind: "brushEdge", + brushId: session.target.brushId, + edgeId: session.target.edgeId + }, + center: session.preview.center, + rotationDegrees: session.preview.rotationDegrees, + size: session.preview.size, + label: createTransformCommandLabel(session) + }); + case "brushVertex": + if (session.preview.kind !== "brush") { + throw new Error("Whitebox vertex transform preview is invalid."); + } + + return createSetBoxBrushTransformCommand({ + selection: { + kind: "brushVertex", + brushId: session.target.brushId, + vertexId: session.target.vertexId + }, + center: session.preview.center, + rotationDegrees: session.preview.rotationDegrees, + size: session.preview.size, + label: createTransformCommandLabel(session) + }); case "modelInstance": { if (session.preview.kind !== "modelInstance") { throw new Error("Model instance transform preview is invalid."); diff --git a/src/commands/set-box-brush-transform-command.ts b/src/commands/set-box-brush-transform-command.ts new file mode 100644 index 00000000..724bea26 --- /dev/null +++ b/src/commands/set-box-brush-transform-command.ts @@ -0,0 +1,143 @@ +import type { ToolMode } from "../core/tool-mode"; + +import { createOpaqueId } from "../core/ids"; +import type { EditorSelection } from "../core/selection"; +import type { Vec3 } from "../core/vector"; + +import { + cloneSelectionForCommand, + getBoxBrushOrThrow, + replaceBrush, + setSingleBrushEdgeSelection, + setSingleBrushFaceSelection, + setSingleBrushSelection, + setSingleBrushVertexSelection +} from "./brush-command-helpers"; +import type { EditorCommand } from "./command"; +import type { BoxEdgeId, BoxFaceId, BoxVertexId } from "../document/brushes"; + +type BrushTransformCommandSelection = + | { kind: "brush"; brushId: string } + | { kind: "brushFace"; brushId: string; faceId: BoxFaceId } + | { kind: "brushEdge"; brushId: string; edgeId: BoxEdgeId } + | { kind: "brushVertex"; brushId: string; vertexId: BoxVertexId }; + +interface SetBoxBrushTransformCommandOptions { + selection: BrushTransformCommandSelection; + center: Vec3; + rotationDegrees: Vec3; + size: Vec3; + label?: string; +} + +interface BrushTransformSnapshot { + center: Vec3; + rotationDegrees: Vec3; + size: Vec3; +} + +function cloneVec3(vector: Vec3): Vec3 { + return { + x: vector.x, + y: vector.y, + z: vector.z + }; +} + +function selectionToEditorSelection(selection: BrushTransformCommandSelection): EditorSelection { + switch (selection.kind) { + case "brush": + return setSingleBrushSelection(selection.brushId); + case "brushFace": + return setSingleBrushFaceSelection(selection.brushId, selection.faceId); + case "brushEdge": + return setSingleBrushEdgeSelection(selection.brushId, selection.edgeId); + case "brushVertex": + return setSingleBrushVertexSelection(selection.brushId, selection.vertexId); + } +} + +function getBrushId(selection: BrushTransformCommandSelection): string { + return selection.brushId; +} + +function assertPositiveSize(size: Vec3) { + if (!(size.x > 0 && size.y > 0 && size.z > 0)) { + throw new Error("Whitebox box size must remain positive on every axis."); + } + + if (!Number.isFinite(size.x) || !Number.isFinite(size.y) || !Number.isFinite(size.z)) { + throw new Error("Whitebox box size values must be finite numbers."); + } +} + +export function createSetBoxBrushTransformCommand(options: SetBoxBrushTransformCommandOptions): EditorCommand { + assertPositiveSize(options.size); + + let previousSnapshot: BrushTransformSnapshot | null = null; + let previousSelection: EditorSelection | null = null; + let previousToolMode: ToolMode | null = null; + + return { + id: createOpaqueId("command"), + label: options.label ?? "Set box brush transform", + execute(context) { + const currentDocument = context.getDocument(); + const brushId = getBrushId(options.selection); + const brush = getBoxBrushOrThrow(currentDocument, brushId); + + if (previousSnapshot === null) { + previousSnapshot = { + center: cloneVec3(brush.center), + rotationDegrees: cloneVec3(brush.rotationDegrees), + size: cloneVec3(brush.size) + }; + } + + if (previousSelection === null) { + previousSelection = cloneSelectionForCommand(context.getSelection()); + } + + if (previousToolMode === null) { + previousToolMode = context.getToolMode(); + } + + context.setDocument( + replaceBrush(currentDocument, { + ...brush, + center: cloneVec3(options.center), + rotationDegrees: cloneVec3(options.rotationDegrees), + size: cloneVec3(options.size) + }) + ); + context.setSelection(selectionToEditorSelection(options.selection)); + context.setToolMode("select"); + }, + undo(context) { + if (previousSnapshot === null) { + return; + } + + const currentDocument = context.getDocument(); + const brushId = getBrushId(options.selection); + const brush = getBoxBrushOrThrow(currentDocument, brushId); + + context.setDocument( + replaceBrush(currentDocument, { + ...brush, + center: cloneVec3(previousSnapshot.center), + rotationDegrees: cloneVec3(previousSnapshot.rotationDegrees), + size: cloneVec3(previousSnapshot.size) + }) + ); + + if (previousSelection !== null) { + context.setSelection(previousSelection); + } + + if (previousToolMode !== null) { + context.setToolMode(previousToolMode); + } + } + }; +}