From 8c8c8d4e9ea01410aebf14f14ca10da5f585660c Mon Sep 17 00:00:00 2001 From: Victor Giers Date: Fri, 1 May 2026 17:41:42 +0200 Subject: [PATCH] auto-git: [add] src/commands/apply-terrain-brush-patch-command.ts --- .../apply-terrain-brush-patch-command.ts | 120 ++++++++++++++++++ 1 file changed, 120 insertions(+) create mode 100644 src/commands/apply-terrain-brush-patch-command.ts diff --git a/src/commands/apply-terrain-brush-patch-command.ts b/src/commands/apply-terrain-brush-patch-command.ts new file mode 100644 index 00000000..7b085648 --- /dev/null +++ b/src/commands/apply-terrain-brush-patch-command.ts @@ -0,0 +1,120 @@ +import { createOpaqueId } from "../core/ids"; +import { cloneEditorSelection, type EditorSelection } from "../core/selection"; +import type { + TerrainBrushPatch, + TerrainSampleValuePatch +} from "../core/terrain-brush"; +import type { ToolMode } from "../core/tool-mode"; + +import type { EditorCommand } from "./command"; + +interface ApplyTerrainBrushPatchCommandOptions { + patch: TerrainBrushPatch; + label?: string; +} + +function setSingleTerrainSelection(terrainId: string): EditorSelection { + return { + kind: "terrains", + ids: [terrainId] + }; +} + +function assertValidPatchEntry( + entry: TerrainSampleValuePatch, + length: number, + label: string +) { + if ( + !Number.isInteger(entry.index) || + entry.index < 0 || + entry.index >= length + ) { + throw new Error(`${label} patch index ${entry.index} is out of range.`); + } + + if (!Number.isFinite(entry.before) || !Number.isFinite(entry.after)) { + throw new Error(`${label} patch values must remain finite.`); + } +} + +export function isTerrainBrushPatchEmpty(patch: TerrainBrushPatch): boolean { + return patch.heightSamples.length === 0 && patch.paintWeights.length === 0; +} + +export function createApplyTerrainBrushPatchCommand( + options: ApplyTerrainBrushPatchCommandOptions +): EditorCommand { + const patch: TerrainBrushPatch = { + terrainId: options.patch.terrainId, + heightSamples: options.patch.heightSamples.map((entry) => ({ ...entry })), + paintWeights: options.patch.paintWeights.map((entry) => ({ ...entry })) + }; + let previousSelection: EditorSelection | null = null; + let previousToolMode: ToolMode | null = null; + + const applyPatch = ( + context: Parameters[0], + direction: "forward" | "backward" + ) => { + const currentDocument = context.getDocument(); + const terrain = currentDocument.terrains[patch.terrainId]; + + if (terrain === undefined) { + throw new Error(`Terrain ${patch.terrainId} does not exist.`); + } + + for (const entry of patch.heightSamples) { + assertValidPatchEntry(entry, terrain.heights.length, "Terrain height"); + terrain.heights[entry.index] = + direction === "forward" ? entry.after : entry.before; + } + + for (const entry of patch.paintWeights) { + assertValidPatchEntry( + entry, + terrain.paintWeights.length, + "Terrain paint weight" + ); + terrain.paintWeights[entry.index] = + direction === "forward" ? entry.after : entry.before; + } + + context.setDocument({ + ...currentDocument, + terrains: { + ...currentDocument.terrains, + [patch.terrainId]: terrain + } + }); + }; + + return { + id: createOpaqueId("command"), + label: options.label ?? "Apply terrain brush patch", + execute(context) { + if (previousSelection === null) { + previousSelection = cloneEditorSelection(context.getSelection()); + } + + if (previousToolMode === null) { + previousToolMode = context.getToolMode(); + } + + applyPatch(context, "forward"); + context.setSelection(setSingleTerrainSelection(patch.terrainId)); + context.setToolMode("select"); + }, + undo(context) { + applyPatch(context, "backward"); + + if (previousSelection !== null) { + context.setSelection(previousSelection); + } + + if (previousToolMode !== null) { + context.setToolMode(previousToolMode); + } + } + }; +}