From 1fbe979eaecd68c7d9b14ee4e0fd6c62b55fd4c3 Mon Sep 17 00:00:00 2001 From: Victor Giers Date: Wed, 15 Apr 2026 07:46:58 +0200 Subject: [PATCH] Update commands and core to support generic brushes --- src/commands/delete-box-brush-command.ts | 8 +- src/commands/resize-box-brush-command.ts | 10 +- .../set-box-brush-transform-command.ts | 29 ++-- src/core/transform-session.ts | 163 +++++++++++++----- 4 files changed, 150 insertions(+), 60 deletions(-) diff --git a/src/commands/delete-box-brush-command.ts b/src/commands/delete-box-brush-command.ts index 41695c3c..d0461ec8 100644 --- a/src/commands/delete-box-brush-command.ts +++ b/src/commands/delete-box-brush-command.ts @@ -1,7 +1,7 @@ import { createOpaqueId } from "../core/ids"; import type { EditorSelection } from "../core/selection"; import type { ToolMode } from "../core/tool-mode"; -import { cloneBoxBrush, type BoxBrush } from "../document/brushes"; +import { cloneBrush, type Brush } from "../document/brushes"; import { cloneSelectionForCommand, removeBrush } from "./brush-command-helpers"; import type { EditorCommand } from "./command"; @@ -15,7 +15,7 @@ function selectionIncludesBrush(selection: EditorSelection, brushId: string): bo } export function createDeleteBoxBrushCommand(brushId: string): EditorCommand { - let previousBrush: BoxBrush | null = null; + let previousBrush: Brush | null = null; let previousSelection: EditorSelection | null = null; let previousToolMode: ToolMode | null = null; @@ -31,7 +31,7 @@ export function createDeleteBoxBrushCommand(brushId: string): EditorCommand { } if (previousBrush === null) { - previousBrush = cloneBoxBrush(currentBrush); + previousBrush = cloneBrush(currentBrush); } if (previousSelection === null) { @@ -63,7 +63,7 @@ export function createDeleteBoxBrushCommand(brushId: string): EditorCommand { ...currentDocument, brushes: { ...currentDocument.brushes, - [previousBrush.id]: cloneBoxBrush(previousBrush) + [previousBrush.id]: cloneBrush(previousBrush) } }); diff --git a/src/commands/resize-box-brush-command.ts b/src/commands/resize-box-brush-command.ts index f7e2df77..692dac87 100644 --- a/src/commands/resize-box-brush-command.ts +++ b/src/commands/resize-box-brush-command.ts @@ -1,6 +1,6 @@ import type { ToolMode } from "../core/tool-mode"; import { DEFAULT_GRID_SIZE, snapPositiveSizeToGrid } from "../geometry/grid-snapping"; -import { cloneBoxBrushGeometry, scaleBoxBrushGeometryToSize } from "../document/brushes"; +import { cloneBrushGeometry, scaleBrushGeometryToSize } from "../document/brushes"; import { createOpaqueId } from "../core/ids"; import type { EditorSelection } from "../core/selection"; @@ -22,7 +22,7 @@ export function createResizeBoxBrushCommand(options: ResizeBoxBrushCommandOption options.snapToGrid === false ? options.size : snapPositiveSizeToGrid(options.size, options.gridSize ?? DEFAULT_GRID_SIZE); let previousSize: Vec3 | null = null; - let previousGeometry: ReturnType | null = null; + let previousGeometry: ReturnType | null = null; let previousSelection: EditorSelection | null = null; let previousToolMode: ToolMode | null = null; @@ -37,7 +37,7 @@ export function createResizeBoxBrushCommand(options: ResizeBoxBrushCommandOption previousSize = { ...brush.size }; - previousGeometry = cloneBoxBrushGeometry(brush.geometry); + previousGeometry = cloneBrushGeometry(brush.geometry); } if (previousSelection === null) { @@ -48,7 +48,7 @@ export function createResizeBoxBrushCommand(options: ResizeBoxBrushCommandOption previousToolMode = context.getToolMode(); } - const nextGeometry = scaleBoxBrushGeometryToSize(brush.geometry, resolvedSize); + const nextGeometry = scaleBrushGeometryToSize(brush.geometry, resolvedSize); context.setDocument( replaceBrush(currentDocument, { @@ -76,7 +76,7 @@ export function createResizeBoxBrushCommand(options: ResizeBoxBrushCommandOption size: { ...previousSize }, - geometry: cloneBoxBrushGeometry(previousGeometry) + geometry: cloneBrushGeometry(previousGeometry) }) ); diff --git a/src/commands/set-box-brush-transform-command.ts b/src/commands/set-box-brush-transform-command.ts index 73cc8879..e9bf782e 100644 --- a/src/commands/set-box-brush-transform-command.ts +++ b/src/commands/set-box-brush-transform-command.ts @@ -3,7 +3,15 @@ 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 { cloneBoxBrushGeometry, deriveBoxBrushSizeFromGeometry, scaleBoxBrushGeometryToSize, type BoxBrushGeometry } from "../document/brushes"; +import { + cloneBrushGeometry, + deriveBrushSizeFromGeometry, + scaleBrushGeometryToSize, + type BrushGeometry, + type WhiteboxEdgeId, + type WhiteboxFaceId, + type WhiteboxVertexId +} from "../document/brushes"; import { cloneSelectionForCommand, @@ -15,20 +23,19 @@ import { 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 }; + | { kind: "brushFace"; brushId: string; faceId: WhiteboxFaceId } + | { kind: "brushEdge"; brushId: string; edgeId: WhiteboxEdgeId } + | { kind: "brushVertex"; brushId: string; vertexId: WhiteboxVertexId }; interface SetBoxBrushTransformCommandOptions { selection: BrushTransformCommandSelection; center: Vec3; rotationDegrees: Vec3; size: Vec3; - geometry?: BoxBrushGeometry; + geometry?: BrushGeometry; label?: string; } @@ -36,7 +43,7 @@ interface BrushTransformSnapshot { center: Vec3; rotationDegrees: Vec3; size: Vec3; - geometry: ReturnType; + geometry: ReturnType; } function cloneVec3(vector: Vec3): Vec3 { @@ -94,7 +101,7 @@ export function createSetBoxBrushTransformCommand(options: SetBoxBrushTransformC center: cloneVec3(brush.center), rotationDegrees: cloneVec3(brush.rotationDegrees), size: cloneVec3(brush.size), - geometry: cloneBoxBrushGeometry(brush.geometry) + geometry: cloneBrushGeometry(brush.geometry) }; } @@ -106,8 +113,8 @@ export function createSetBoxBrushTransformCommand(options: SetBoxBrushTransformC previousToolMode = context.getToolMode(); } - const nextGeometry = options.geometry === undefined ? scaleBoxBrushGeometryToSize(brush.geometry, options.size) : cloneBoxBrushGeometry(options.geometry); - const nextSize = deriveBoxBrushSizeFromGeometry(nextGeometry); + const nextGeometry = options.geometry === undefined ? scaleBrushGeometryToSize(brush.geometry, options.size) : cloneBrushGeometry(options.geometry); + const nextSize = deriveBrushSizeFromGeometry(nextGeometry); assertPositiveSize(nextSize); @@ -138,7 +145,7 @@ export function createSetBoxBrushTransformCommand(options: SetBoxBrushTransformC center: cloneVec3(previousSnapshot.center), rotationDegrees: cloneVec3(previousSnapshot.rotationDegrees), size: cloneVec3(previousSnapshot.size), - geometry: cloneBoxBrushGeometry(previousSnapshot.geometry) + geometry: cloneBrushGeometry(previousSnapshot.geometry) }) ); diff --git a/src/core/transform-session.ts b/src/core/transform-session.ts index 375a18cf..403a7ad7 100644 --- a/src/core/transform-session.ts +++ b/src/core/transform-session.ts @@ -3,15 +3,16 @@ import type { EditorSelection } from "./selection"; import type { WhiteboxSelectionMode } from "./whitebox-selection-mode"; import type { Vec3 } from "./vector"; import { - BOX_VERTEX_IDS, - BOX_EDGE_LABELS, - BOX_FACE_LABELS, - BOX_VERTEX_LABELS, - cloneBoxBrushGeometry, - type BoxBrushGeometry, - type BoxEdgeId, - type BoxFaceId, - type BoxVertexId + cloneBrushGeometry, + createBoxBrush, + createRadialPrismBrush, + createWedgeBrush, + type Brush, + type BrushGeometry, + type BrushKind, + type WhiteboxEdgeId, + type WhiteboxFaceId, + type WhiteboxVertexId } from "../document/brushes"; import { getScenePathPoint } from "../document/paths"; import type { SceneDocument } from "../document/scene-document"; @@ -26,6 +27,13 @@ import { getModelInstanceKindLabel } from "../assets/model-instances"; import type { ViewportPanelId } from "../viewport-three/viewport-layout"; +import { + getBrushEdgeLabel, + getBrushFaceLabel, + getBrushKindLabel, + getBrushVertexIds, + getBrushVertexLabel +} from "../geometry/whitebox-topology"; export type TransformOperation = "translate" | "rotate" | "scale"; export type TransformAxis = "x" | "y" | "z"; @@ -54,40 +62,48 @@ export type EntityTransformRotationState = export interface BrushTransformTarget { kind: "brush"; brushId: string; + brushKind: BrushKind; + sideCount?: number; initialCenter: Vec3; initialRotationDegrees: Vec3; initialSize: Vec3; - initialGeometry: BoxBrushGeometry; + initialGeometry: BrushGeometry; } export interface BrushFaceTransformTarget { kind: "brushFace"; brushId: string; - faceId: BoxFaceId; + brushKind: BrushKind; + sideCount?: number; + faceId: WhiteboxFaceId; initialCenter: Vec3; initialRotationDegrees: Vec3; initialSize: Vec3; - initialGeometry: BoxBrushGeometry; + initialGeometry: BrushGeometry; } export interface BrushEdgeTransformTarget { kind: "brushEdge"; brushId: string; - edgeId: BoxEdgeId; + brushKind: BrushKind; + sideCount?: number; + edgeId: WhiteboxEdgeId; initialCenter: Vec3; initialRotationDegrees: Vec3; initialSize: Vec3; - initialGeometry: BoxBrushGeometry; + initialGeometry: BrushGeometry; } export interface BrushVertexTransformTarget { kind: "brushVertex"; brushId: string; - vertexId: BoxVertexId; + brushKind: BrushKind; + sideCount?: number; + vertexId: WhiteboxVertexId; initialCenter: Vec3; initialRotationDegrees: Vec3; initialSize: Vec3; - initialGeometry: BoxBrushGeometry; + initialGeometry: BrushGeometry; } export interface ModelInstanceTransformTarget { @@ -128,17 +144,24 @@ export interface BrushTransformPreview { center: Vec3; rotationDegrees: Vec3; size: Vec3; - geometry: BoxBrushGeometry; + geometry: BrushGeometry; } function areBrushGeometriesEqual( - left: BoxBrushGeometry, - right: BoxBrushGeometry + left: BrushGeometry, + right: BrushGeometry ): boolean { - return BOX_VERTEX_IDS.every((vertexId) => { + const leftVertexIds = Object.keys(left.vertices); + const rightVertexIds = Object.keys(right.vertices); + + if (leftVertexIds.length !== rightVertexIds.length) { + return false; + } + + return leftVertexIds.every((vertexId) => { const leftVertex = left.vertices[vertexId]; const rightVertex = right.vertices[vertexId]; - return areVec3Equal(leftVertex, rightVertex); + return rightVertex !== undefined && areVec3Equal(leftVertex, rightVertex); }); } @@ -218,6 +241,42 @@ function cloneEntityTransformRotationState( } } +function createBrushSnapshotFromTarget( + target: + | BrushTransformTarget + | BrushFaceTransformTarget + | BrushEdgeTransformTarget + | BrushVertexTransformTarget +): Brush { + switch (target.brushKind) { + case "box": + return createBoxBrush({ + id: target.brushId, + center: target.initialCenter, + rotationDegrees: target.initialRotationDegrees, + size: target.initialSize, + geometry: target.initialGeometry + }); + case "wedge": + return createWedgeBrush({ + id: target.brushId, + center: target.initialCenter, + rotationDegrees: target.initialRotationDegrees, + size: target.initialSize, + geometry: target.initialGeometry + }); + case "radialPrism": + return createRadialPrismBrush({ + id: target.brushId, + center: target.initialCenter, + rotationDegrees: target.initialRotationDegrees, + size: target.initialSize, + sideCount: target.sideCount, + geometry: target.initialGeometry + }); + } +} + function areEntityTransformRotationsEqual( left: EntityTransformRotationState, right: EntityTransformRotationState @@ -251,40 +310,48 @@ export function cloneTransformTarget(target: TransformTarget): TransformTarget { return { kind: "brush", brushId: target.brushId, + brushKind: target.brushKind, + sideCount: target.sideCount, initialCenter: cloneVec3(target.initialCenter), initialRotationDegrees: cloneVec3(target.initialRotationDegrees), initialSize: cloneVec3(target.initialSize), - initialGeometry: cloneBoxBrushGeometry(target.initialGeometry) + initialGeometry: cloneBrushGeometry(target.initialGeometry) }; case "brushFace": return { kind: "brushFace", brushId: target.brushId, + brushKind: target.brushKind, + sideCount: target.sideCount, faceId: target.faceId, initialCenter: cloneVec3(target.initialCenter), initialRotationDegrees: cloneVec3(target.initialRotationDegrees), initialSize: cloneVec3(target.initialSize), - initialGeometry: cloneBoxBrushGeometry(target.initialGeometry) + initialGeometry: cloneBrushGeometry(target.initialGeometry) }; case "brushEdge": return { kind: "brushEdge", brushId: target.brushId, + brushKind: target.brushKind, + sideCount: target.sideCount, edgeId: target.edgeId, initialCenter: cloneVec3(target.initialCenter), initialRotationDegrees: cloneVec3(target.initialRotationDegrees), initialSize: cloneVec3(target.initialSize), - initialGeometry: cloneBoxBrushGeometry(target.initialGeometry) + initialGeometry: cloneBrushGeometry(target.initialGeometry) }; case "brushVertex": return { kind: "brushVertex", brushId: target.brushId, + brushKind: target.brushKind, + sideCount: target.sideCount, vertexId: target.vertexId, initialCenter: cloneVec3(target.initialCenter), initialRotationDegrees: cloneVec3(target.initialRotationDegrees), initialSize: cloneVec3(target.initialSize), - initialGeometry: cloneBoxBrushGeometry(target.initialGeometry) + initialGeometry: cloneBrushGeometry(target.initialGeometry) }; case "modelInstance": return { @@ -325,7 +392,7 @@ export function cloneTransformPreview( center: cloneVec3(preview.center), rotationDegrees: cloneVec3(preview.rotationDegrees), size: cloneVec3(preview.size), - geometry: cloneBoxBrushGeometry(preview.geometry) + geometry: cloneBrushGeometry(preview.geometry) }; case "modelInstance": return { @@ -405,6 +472,8 @@ function areTransformTargetsEqual( return ( right.kind === "brush" && left.brushId === right.brushId && + left.brushKind === right.brushKind && + left.sideCount === right.sideCount && areVec3Equal(left.initialCenter, right.initialCenter) && areVec3Equal( left.initialRotationDegrees, @@ -417,6 +486,8 @@ function areTransformTargetsEqual( return ( right.kind === "brushFace" && left.brushId === right.brushId && + left.brushKind === right.brushKind && + left.sideCount === right.sideCount && left.faceId === right.faceId && areVec3Equal(left.initialCenter, right.initialCenter) && areVec3Equal( @@ -430,6 +501,8 @@ function areTransformTargetsEqual( return ( right.kind === "brushEdge" && left.brushId === right.brushId && + left.brushKind === right.brushKind && + left.sideCount === right.sideCount && left.edgeId === right.edgeId && areVec3Equal(left.initialCenter, right.initialCenter) && areVec3Equal( @@ -443,6 +516,8 @@ function areTransformTargetsEqual( return ( right.kind === "brushVertex" && left.brushId === right.brushId && + left.brushKind === right.brushKind && + left.sideCount === right.sideCount && left.vertexId === right.vertexId && areVec3Equal(left.initialCenter, right.initialCenter) && areVec3Equal( @@ -557,7 +632,7 @@ export function createTransformPreviewFromTarget( center: cloneVec3(target.initialCenter), rotationDegrees: cloneVec3(target.initialRotationDegrees), size: cloneVec3(target.initialSize), - geometry: cloneBoxBrushGeometry(target.initialGeometry) + geometry: cloneBrushGeometry(target.initialGeometry) }; case "modelInstance": return { @@ -668,13 +743,13 @@ export function getTransformAxisSpaceLabel( export function getTransformTargetLabel(target: TransformTarget): string { switch (target.kind) { case "brush": - return "Whitebox Box"; + return getBrushKindLabel(createBrushSnapshotFromTarget(target)); case "brushFace": - return `Whitebox Face (${BOX_FACE_LABELS[target.faceId]})`; + return `Whitebox Face (${getBrushFaceLabel(createBrushSnapshotFromTarget(target), target.faceId)})`; case "brushEdge": - return `Whitebox Edge (${BOX_EDGE_LABELS[target.edgeId]})`; + return `Whitebox Edge (${getBrushEdgeLabel(createBrushSnapshotFromTarget(target), target.edgeId)})`; case "brushVertex": - return `Whitebox Vertex (${BOX_VERTEX_LABELS[target.vertexId]})`; + return `Whitebox Vertex (${getBrushVertexLabel(createBrushSnapshotFromTarget(target), target.vertexId)})`; case "modelInstance": return getModelInstanceKindLabel(); case "pathPoint": @@ -856,10 +931,10 @@ function createBrushTransformTarget( ): TransformTargetResolution { const brush = document.brushes[brushId]; - if (brush === undefined || brush.kind !== "box") { + if (brush === undefined) { return { target: null, - message: "Select a supported whitebox box before transforming it." + message: "Select a supported whitebox solid before transforming it." }; } @@ -867,10 +942,12 @@ function createBrushTransformTarget( target: { kind: "brush", brushId: brush.id, + brushKind: brush.kind, + sideCount: brush.kind === "radialPrism" ? brush.sideCount : undefined, initialCenter: cloneVec3(brush.center), initialRotationDegrees: cloneVec3(brush.rotationDegrees), initialSize: cloneVec3(brush.size), - initialGeometry: cloneBoxBrushGeometry(brush.geometry) + initialGeometry: cloneBrushGeometry(brush.geometry) }, message: null }; @@ -879,7 +956,7 @@ function createBrushTransformTarget( function createBrushFaceTransformTarget( document: SceneDocument, brushId: string, - faceId: BoxFaceId + faceId: WhiteboxFaceId ): TransformTargetResolution { const brushResolution = createBrushTransformTarget(document, brushId); @@ -894,13 +971,15 @@ function createBrushFaceTransformTarget( target: { kind: "brushFace", brushId, + brushKind: brushResolution.target.brushKind, + sideCount: brushResolution.target.sideCount, faceId, initialCenter: cloneVec3(brushResolution.target.initialCenter), initialRotationDegrees: cloneVec3( brushResolution.target.initialRotationDegrees ), initialSize: cloneVec3(brushResolution.target.initialSize), - initialGeometry: cloneBoxBrushGeometry( + initialGeometry: cloneBrushGeometry( brushResolution.target.initialGeometry ) }, @@ -911,7 +990,7 @@ function createBrushFaceTransformTarget( function createBrushEdgeTransformTarget( document: SceneDocument, brushId: string, - edgeId: BoxEdgeId + edgeId: WhiteboxEdgeId ): TransformTargetResolution { const brushResolution = createBrushTransformTarget(document, brushId); @@ -926,13 +1005,15 @@ function createBrushEdgeTransformTarget( target: { kind: "brushEdge", brushId, + brushKind: brushResolution.target.brushKind, + sideCount: brushResolution.target.sideCount, edgeId, initialCenter: cloneVec3(brushResolution.target.initialCenter), initialRotationDegrees: cloneVec3( brushResolution.target.initialRotationDegrees ), initialSize: cloneVec3(brushResolution.target.initialSize), - initialGeometry: cloneBoxBrushGeometry( + initialGeometry: cloneBrushGeometry( brushResolution.target.initialGeometry ) }, @@ -943,7 +1024,7 @@ function createBrushEdgeTransformTarget( function createBrushVertexTransformTarget( document: SceneDocument, brushId: string, - vertexId: BoxVertexId + vertexId: WhiteboxVertexId ): TransformTargetResolution { const brushResolution = createBrushTransformTarget(document, brushId); @@ -958,13 +1039,15 @@ function createBrushVertexTransformTarget( target: { kind: "brushVertex", brushId, + brushKind: brushResolution.target.brushKind, + sideCount: brushResolution.target.sideCount, vertexId, initialCenter: cloneVec3(brushResolution.target.initialCenter), initialRotationDegrees: cloneVec3( brushResolution.target.initialRotationDegrees ), initialSize: cloneVec3(brushResolution.target.initialSize), - initialGeometry: cloneBoxBrushGeometry( + initialGeometry: cloneBrushGeometry( brushResolution.target.initialGeometry ) },