diff --git a/src/commands/brush-command-helpers.ts b/src/commands/brush-command-helpers.ts new file mode 100644 index 00000000..b0df5a07 --- /dev/null +++ b/src/commands/brush-command-helpers.ts @@ -0,0 +1,47 @@ +import { cloneEditorSelection, type EditorSelection } from "../core/selection"; +import type { BoxBrush } from "../document/brushes"; +import type { SceneDocument } from "../document/scene-document"; + +export function getBoxBrushOrThrow(document: SceneDocument, brushId: string): BoxBrush { + const brush = document.brushes[brushId]; + + if (brush === undefined) { + throw new Error(`Box brush ${brushId} does not exist.`); + } + + if (brush.kind !== "box") { + throw new Error(`Brush ${brushId} is not a supported box brush.`); + } + + return brush; +} + +export function setSingleBrushSelection(brushId: string): EditorSelection { + return { + kind: "brushes", + ids: [brushId] + }; +} + +export function cloneSelectionForCommand(selection: EditorSelection): EditorSelection { + return cloneEditorSelection(selection); +} + +export function replaceBrush(document: SceneDocument, brush: BoxBrush): SceneDocument { + return { + ...document, + brushes: { + ...document.brushes, + [brush.id]: brush + } + }; +} + +export function removeBrush(document: SceneDocument, brushId: string): SceneDocument { + const { [brushId]: _removedBrush, ...remainingBrushes } = document.brushes; + + return { + ...document, + brushes: remainingBrushes + }; +} diff --git a/src/commands/create-box-brush-command.ts b/src/commands/create-box-brush-command.ts new file mode 100644 index 00000000..e981085b --- /dev/null +++ b/src/commands/create-box-brush-command.ts @@ -0,0 +1,67 @@ +import type { ToolMode } from "../core/tool-mode"; +import { createBoxBrush, DEFAULT_BOX_BRUSH_CENTER, DEFAULT_BOX_BRUSH_SIZE } from "../document/brushes"; +import { DEFAULT_GRID_SIZE, snapPositiveSizeToGrid, snapVec3ToGrid } from "../geometry/grid-snapping"; + +import { createOpaqueId } from "../core/ids"; +import type { EditorSelection } from "../core/selection"; +import type { Vec3 } from "../core/vector"; + +import { cloneSelectionForCommand, removeBrush, setSingleBrushSelection } from "./brush-command-helpers"; +import type { EditorCommand } from "./command"; + +interface CreateBoxBrushCommandOptions { + center?: Vec3; + size?: Vec3; + gridSize?: number; +} + +export function createCreateBoxBrushCommand(options: CreateBoxBrushCommandOptions = {}): EditorCommand { + const brush = createBoxBrush({ + center: snapVec3ToGrid(options.center ?? DEFAULT_BOX_BRUSH_CENTER, options.gridSize ?? DEFAULT_GRID_SIZE), + size: snapPositiveSizeToGrid(options.size ?? DEFAULT_BOX_BRUSH_SIZE, options.gridSize ?? DEFAULT_GRID_SIZE) + }); + + let previousSelection: EditorSelection | null = null; + let previousToolMode: ToolMode | null = null; + + return { + id: createOpaqueId("command"), + label: "Create box brush", + execute(context) { + const currentDocument = context.getDocument(); + + if (currentDocument.brushes[brush.id] !== undefined) { + throw new Error(`Box brush ${brush.id} already exists.`); + } + + if (previousSelection === null) { + previousSelection = cloneSelectionForCommand(context.getSelection()); + } + + if (previousToolMode === null) { + previousToolMode = context.getToolMode(); + } + + context.setDocument({ + ...currentDocument, + brushes: { + ...currentDocument.brushes, + [brush.id]: brush + } + }); + context.setSelection(setSingleBrushSelection(brush.id)); + context.setToolMode("select"); + }, + undo(context) { + context.setDocument(removeBrush(context.getDocument(), brush.id)); + + if (previousSelection !== null) { + context.setSelection(previousSelection); + } + + if (previousToolMode !== null) { + context.setToolMode(previousToolMode); + } + } + }; +} diff --git a/src/commands/move-box-brush-command.ts b/src/commands/move-box-brush-command.ts new file mode 100644 index 00000000..a85d68c8 --- /dev/null +++ b/src/commands/move-box-brush-command.ts @@ -0,0 +1,82 @@ +import type { ToolMode } from "../core/tool-mode"; +import { DEFAULT_GRID_SIZE, snapVec3ToGrid } from "../geometry/grid-snapping"; + +import { createOpaqueId } from "../core/ids"; +import type { EditorSelection } from "../core/selection"; +import type { Vec3 } from "../core/vector"; + +import { cloneSelectionForCommand, getBoxBrushOrThrow, replaceBrush, setSingleBrushSelection } from "./brush-command-helpers"; +import type { EditorCommand } from "./command"; + +interface MoveBoxBrushCommandOptions { + brushId: string; + center: Vec3; + gridSize?: number; +} + +export function createMoveBoxBrushCommand(options: MoveBoxBrushCommandOptions): EditorCommand { + const snappedCenter = snapVec3ToGrid(options.center, options.gridSize ?? DEFAULT_GRID_SIZE); + + let previousCenter: Vec3 | null = null; + let previousSelection: EditorSelection | null = null; + let previousToolMode: ToolMode | null = null; + + return { + id: createOpaqueId("command"), + label: "Move box brush", + execute(context) { + const currentDocument = context.getDocument(); + const brush = getBoxBrushOrThrow(currentDocument, options.brushId); + + if (previousCenter === null) { + previousCenter = { + ...brush.center + }; + } + + if (previousSelection === null) { + previousSelection = cloneSelectionForCommand(context.getSelection()); + } + + if (previousToolMode === null) { + previousToolMode = context.getToolMode(); + } + + context.setDocument( + replaceBrush(currentDocument, { + ...brush, + center: { + ...snappedCenter + } + }) + ); + context.setSelection(setSingleBrushSelection(options.brushId)); + context.setToolMode("select"); + }, + undo(context) { + if (previousCenter === null) { + return; + } + + const currentDocument = context.getDocument(); + const brush = getBoxBrushOrThrow(currentDocument, options.brushId); + + context.setDocument( + replaceBrush(currentDocument, { + ...brush, + center: { + ...previousCenter + } + }) + ); + + if (previousSelection !== null) { + context.setSelection(previousSelection); + } + + if (previousToolMode !== null) { + context.setToolMode(previousToolMode); + } + } + }; +} diff --git a/src/commands/resize-box-brush-command.ts b/src/commands/resize-box-brush-command.ts new file mode 100644 index 00000000..9df0c11b --- /dev/null +++ b/src/commands/resize-box-brush-command.ts @@ -0,0 +1,82 @@ +import type { ToolMode } from "../core/tool-mode"; +import { DEFAULT_GRID_SIZE, snapPositiveSizeToGrid } from "../geometry/grid-snapping"; + +import { createOpaqueId } from "../core/ids"; +import type { EditorSelection } from "../core/selection"; +import type { Vec3 } from "../core/vector"; + +import { cloneSelectionForCommand, getBoxBrushOrThrow, replaceBrush, setSingleBrushSelection } from "./brush-command-helpers"; +import type { EditorCommand } from "./command"; + +interface ResizeBoxBrushCommandOptions { + brushId: string; + size: Vec3; + gridSize?: number; +} + +export function createResizeBoxBrushCommand(options: ResizeBoxBrushCommandOptions): EditorCommand { + const snappedSize = snapPositiveSizeToGrid(options.size, options.gridSize ?? DEFAULT_GRID_SIZE); + + let previousSize: Vec3 | null = null; + let previousSelection: EditorSelection | null = null; + let previousToolMode: ToolMode | null = null; + + return { + id: createOpaqueId("command"), + label: "Resize box brush", + execute(context) { + const currentDocument = context.getDocument(); + const brush = getBoxBrushOrThrow(currentDocument, options.brushId); + + if (previousSize === null) { + previousSize = { + ...brush.size + }; + } + + if (previousSelection === null) { + previousSelection = cloneSelectionForCommand(context.getSelection()); + } + + if (previousToolMode === null) { + previousToolMode = context.getToolMode(); + } + + context.setDocument( + replaceBrush(currentDocument, { + ...brush, + size: { + ...snappedSize + } + }) + ); + context.setSelection(setSingleBrushSelection(options.brushId)); + context.setToolMode("select"); + }, + undo(context) { + if (previousSize === null) { + return; + } + + const currentDocument = context.getDocument(); + const brush = getBoxBrushOrThrow(currentDocument, options.brushId); + + context.setDocument( + replaceBrush(currentDocument, { + ...brush, + size: { + ...previousSize + } + }) + ); + + if (previousSelection !== null) { + context.setSelection(previousSelection); + } + + if (previousToolMode !== null) { + context.setToolMode(previousToolMode); + } + } + }; +}