From 4d8ab49ea6ab9e6775273ff58a485c03d70b9767 Mon Sep 17 00:00:00 2001 From: Victor Giers Date: Mon, 18 May 2026 16:08:37 +0200 Subject: [PATCH] auto-git: [add] src/commands/create-custom-material-command.ts --- .../create-custom-material-command.ts | 205 ++++++++++++++++++ 1 file changed, 205 insertions(+) create mode 100644 src/commands/create-custom-material-command.ts diff --git a/src/commands/create-custom-material-command.ts b/src/commands/create-custom-material-command.ts new file mode 100644 index 00000000..c3a1230c --- /dev/null +++ b/src/commands/create-custom-material-command.ts @@ -0,0 +1,205 @@ +import { createOpaqueId } from "../core/ids"; +import type { EditorSelection } from "../core/selection"; +import type { ToolMode } from "../core/tool-mode"; +import { updateBrush, type WhiteboxFaceId } from "../document/brushes"; +import { getBrushFaceIds } from "../geometry/whitebox-topology"; +import { + cloneMaterialDef, + type CustomMaterialDef +} from "../materials/starter-material-library"; + +import { + cloneSelectionForCommand, + getBoxBrushOrThrow, + replaceBrush, + replaceBoxBrushFace, + setSingleBrushFaceSelection, + setSingleBrushSelection +} from "./brush-command-helpers"; +import type { EditorCommand } from "./command"; + +type CreateCustomMaterialApplyTarget = + | { kind: "brush"; brushId: string } + | { kind: "face"; brushId: string; faceId: WhiteboxFaceId }; + +interface CreateCustomMaterialCommandOptions { + material: CustomMaterialDef; + applyTo?: CreateCustomMaterialApplyTarget; + label?: string; +} + +export function createCreateCustomMaterialCommand( + options: CreateCustomMaterialCommandOptions +): EditorCommand { + const nextMaterial = cloneMaterialDef(options.material) as CustomMaterialDef; + let previousSelection: EditorSelection | null = null; + let previousToolMode: ToolMode | null = null; + let previousMaterialIds: Record | null = null; + + return { + id: createOpaqueId("command"), + label: options.label ?? "Create custom material", + execute(context) { + const currentDocument = context.getDocument(); + + if (nextMaterial.kind !== "custom") { + throw new Error("Only custom materials can be created by this command."); + } + + if (currentDocument.materials[nextMaterial.id] !== undefined) { + throw new Error(`Material ${nextMaterial.id} already exists.`); + } + + if (previousSelection === null) { + previousSelection = cloneSelectionForCommand(context.getSelection()); + } + + if (previousToolMode === null) { + previousToolMode = context.getToolMode(); + } + + let nextDocument = { + ...currentDocument, + materials: { + ...currentDocument.materials, + [nextMaterial.id]: cloneMaterialDef(nextMaterial) + } + }; + + if (options.applyTo?.kind === "brush") { + const currentBrush = getBoxBrushOrThrow( + currentDocument, + options.applyTo.brushId + ); + + if (previousMaterialIds === null) { + previousMaterialIds = Object.fromEntries( + getBrushFaceIds(currentBrush).map((faceId) => [ + faceId, + currentBrush.faces[faceId].materialId + ]) + ); + } + + nextDocument = replaceBrush( + nextDocument, + updateBrush(currentBrush, { + faces: Object.fromEntries( + getBrushFaceIds(currentBrush).map((faceId) => [ + faceId, + { + ...currentBrush.faces[faceId], + materialId: nextMaterial.id + } + ]) + ) as typeof currentBrush.faces + }) + ); + context.setSelection(setSingleBrushSelection(options.applyTo.brushId)); + } else if (options.applyTo?.kind === "face") { + const currentBrush = getBoxBrushOrThrow( + currentDocument, + options.applyTo.brushId + ); + const currentFace = currentBrush.faces[options.applyTo.faceId]; + + if (currentFace === undefined) { + throw new Error( + `Whitebox solid ${options.applyTo.brushId} does not contain face ${options.applyTo.faceId}.` + ); + } + + if (previousMaterialIds === null) { + previousMaterialIds = { + [options.applyTo.faceId]: currentFace.materialId + } as Record; + } + + nextDocument = replaceBoxBrushFace( + nextDocument, + options.applyTo.brushId, + options.applyTo.faceId, + { + ...currentFace, + materialId: nextMaterial.id + } + ); + context.setSelection( + setSingleBrushFaceSelection( + options.applyTo.brushId, + options.applyTo.faceId + ) + ); + } + + context.setDocument(nextDocument); + context.setToolMode("select"); + }, + undo(context) { + const currentDocument = context.getDocument(); + const nextMaterials = { + ...currentDocument.materials + }; + + delete nextMaterials[nextMaterial.id]; + + let nextDocument = { + ...currentDocument, + materials: nextMaterials + }; + + if (options.applyTo?.kind === "brush" && previousMaterialIds !== null) { + const currentBrush = getBoxBrushOrThrow( + currentDocument, + options.applyTo.brushId + ); + + nextDocument = replaceBrush( + nextDocument, + updateBrush(currentBrush, { + faces: Object.fromEntries( + getBrushFaceIds(currentBrush).map((faceId) => [ + faceId, + { + ...currentBrush.faces[faceId], + materialId: previousMaterialIds?.[faceId] ?? null + } + ]) + ) as typeof currentBrush.faces + }) + ); + } else if ( + options.applyTo?.kind === "face" && + previousMaterialIds !== null + ) { + const currentBrush = getBoxBrushOrThrow( + currentDocument, + options.applyTo.brushId + ); + const currentFace = currentBrush.faces[options.applyTo.faceId]; + + if (currentFace !== undefined) { + nextDocument = replaceBoxBrushFace( + nextDocument, + options.applyTo.brushId, + options.applyTo.faceId, + { + ...currentFace, + materialId: previousMaterialIds[options.applyTo.faceId] ?? null + } + ); + } + } + + context.setDocument(nextDocument); + + if (previousSelection !== null) { + context.setSelection(previousSelection); + } + + if (previousToolMode !== null) { + context.setToolMode(previousToolMode); + } + } + }; +}