From 0845ac436cbf98e31bb8c332fa9240931cb57f5a Mon Sep 17 00:00:00 2001 From: Victor Giers Date: Sun, 5 Apr 2026 01:57:31 +0200 Subject: [PATCH] Add methods for creating and updating brush previews in ViewportHost --- src/viewport-three/viewport-host.ts | 290 ++++++++++++++++++++++++++++ 1 file changed, 290 insertions(+) diff --git a/src/viewport-three/viewport-host.ts b/src/viewport-three/viewport-host.ts index 207a582c..cf74138c 100644 --- a/src/viewport-three/viewport-host.ts +++ b/src/viewport-three/viewport-host.ts @@ -1732,6 +1732,296 @@ export class ViewportHost { }; } + private createTargetPreviewBrush(session: ActiveTransformSession): BoxBrush | null { + if ( + session.target.kind !== "brush" && + session.target.kind !== "brushFace" && + session.target.kind !== "brushEdge" && + session.target.kind !== "brushVertex" + ) { + return null; + } + + const currentBrush = this.currentDocument?.brushes[session.target.brushId]; + + if (currentBrush === undefined || currentBrush.kind !== "box") { + return null; + } + + return { + ...currentBrush, + center: { + ...session.target.initialCenter + }, + rotationDegrees: { + ...session.target.initialRotationDegrees + }, + size: { + ...session.target.initialSize + } + }; + } + + private createBrushPreviewFromExtents( + brush: BoxBrush, + extents: { min: Vec3; max: Vec3 }, + rotationDegrees?: Vec3 + ): { kind: "brush"; center: Vec3; rotationDegrees: Vec3; size: Vec3 } { + const localCenter = { + x: (extents.min.x + extents.max.x) * 0.5, + y: (extents.min.y + extents.max.y) * 0.5, + z: (extents.min.z + extents.max.z) * 0.5 + }; + const centerOffset = transformBoxBrushLocalPointToWorld( + { + ...brush, + center: { x: 0, y: 0, z: 0 }, + rotationDegrees: rotationDegrees ?? brush.rotationDegrees + }, + localCenter + ); + const nextSize = { + x: this.snapWhiteboxSizeValue(extents.max.x - extents.min.x), + y: this.snapWhiteboxSizeValue(extents.max.y - extents.min.y), + z: this.snapWhiteboxSizeValue(extents.max.z - extents.min.z) + }; + + return { + kind: "brush", + center: { + x: this.snapWhiteboxPositionValue(brush.center.x + centerOffset.x), + y: this.snapWhiteboxPositionValue(brush.center.y + centerOffset.y), + z: this.snapWhiteboxPositionValue(brush.center.z + centerOffset.z) + }, + rotationDegrees: { + ...(rotationDegrees ?? brush.rotationDegrees) + }, + size: nextSize + }; + } + + private getInitialBrushLocalExtents(brush: BoxBrush): { min: Vec3; max: Vec3 } { + return { + min: { + x: -brush.size.x * 0.5, + y: -brush.size.y * 0.5, + z: -brush.size.z * 0.5 + }, + max: { + x: brush.size.x * 0.5, + y: brush.size.y * 0.5, + z: brush.size.z * 0.5 + } + }; + } + + private buildComponentTranslatedBrushPreview( + session: ActiveTransformSession, + origin: { x: number; y: number }, + current: { x: number; y: number }, + axisConstraint: TransformAxis | null + ) { + const initialBrush = this.createTargetPreviewBrush(session); + + if (initialBrush === null) { + throw new Error("Cannot build a component translation preview without a box brush target."); + } + + const initialPivot = this.getTransformPivotPosition({ + ...session, + preview: { + kind: "brush", + center: { ...initialBrush.center }, + rotationDegrees: { ...initialBrush.rotationDegrees }, + size: { ...initialBrush.size } + } + }); + let worldDelta = { + x: 0, + y: 0, + z: 0 + }; + + if (axisConstraint === null) { + const plane = this.getTransformPlaneForPivot(initialPivot); + const startIntersection = this.getPointerPlaneIntersection(origin.x, origin.y, plane); + const currentIntersection = this.getPointerPlaneIntersection(current.x, current.y, plane); + + if (startIntersection !== null && currentIntersection !== null) { + const delta = currentIntersection.sub(startIntersection); + worldDelta = { + x: delta.x, + y: delta.y, + z: delta.z + }; + } + } else { + const axisDelta = this.getAxisMovementDistance(axisConstraint, initialPivot, origin, current); + worldDelta = this.setAxisComponent(worldDelta, axisConstraint, axisDelta); + } + + const localDelta = transformBoxBrushWorldVectorToLocal(initialBrush, worldDelta); + const extents = this.getInitialBrushLocalExtents(initialBrush); + + if (session.target.kind === "brushFace") { + const meta = getBoxBrushFaceTransformMeta(session.target.faceId); + + for (const axis of ["x", "y", "z"] as const) { + const axisDelta = localDelta[axis]; + + if (axis === meta.axis) { + if (meta.sign > 0) { + extents.max[axis] += axisDelta; + } else { + extents.min[axis] += axisDelta; + } + } else { + extents.min[axis] += axisDelta; + extents.max[axis] += axisDelta; + } + } + } else if (session.target.kind === "brushEdge") { + const meta = getBoxBrushEdgeTransformMeta(session.target.edgeId); + + for (const axis of ["x", "y", "z"] as const) { + const axisDelta = localDelta[axis]; + const axisSign = meta.signs[axis]; + + if (axisSign === null) { + extents.min[axis] += axisDelta; + extents.max[axis] += axisDelta; + continue; + } + + if (axisSign > 0) { + extents.max[axis] += axisDelta; + } else { + extents.min[axis] += axisDelta; + } + } + } else if (session.target.kind === "brushVertex") { + const signs = getBoxBrushVertexSigns(session.target.vertexId); + + for (const axis of ["x", "y", "z"] as const) { + if (signs[axis] > 0) { + extents.max[axis] += localDelta[axis]; + } else { + extents.min[axis] += localDelta[axis]; + } + } + } + + return this.createBrushPreviewFromExtents(initialBrush, extents); + } + + private buildComponentRotatedBrushPreview( + session: ActiveTransformSession, + origin: { x: number; y: number }, + current: { x: number; y: number }, + axisConstraint: TransformAxis | null + ) { + const initialBrush = this.createTargetPreviewBrush(session); + + if (initialBrush === null) { + throw new Error("Cannot build a component rotation preview without a box brush target."); + } + + const effectiveAxis = axisConstraint ?? this.getEffectiveRotationAxis(session); + const pointerDeltaDegrees = (current.x - origin.x - (current.y - origin.y)) * 0.5; + const pivot = this.getTransformPivotPosition({ + ...session, + preview: { + kind: "brush", + center: { ...initialBrush.center }, + rotationDegrees: { ...initialBrush.rotationDegrees }, + size: { ...initialBrush.size } + } + }); + const axisVector = this.axisVector(effectiveAxis).normalize(); + const centerVector = new Vector3(initialBrush.center.x, initialBrush.center.y, initialBrush.center.z); + const pivotVector = new Vector3(pivot.x, pivot.y, pivot.z); + const nextCenter = centerVector + .sub(pivotVector) + .applyAxisAngle(axisVector, (pointerDeltaDegrees * Math.PI) / 180) + .add(pivotVector); + const nextRotationDegrees = { + ...initialBrush.rotationDegrees + }; + + nextRotationDegrees[effectiveAxis] = this.normalizeDegrees(nextRotationDegrees[effectiveAxis] + pointerDeltaDegrees); + + return { + kind: "brush" as const, + center: { + x: this.snapWhiteboxPositionValue(nextCenter.x), + y: this.snapWhiteboxPositionValue(nextCenter.y), + z: this.snapWhiteboxPositionValue(nextCenter.z) + }, + rotationDegrees: nextRotationDegrees, + size: { + ...initialBrush.size + } + }; + } + + private buildComponentScaledBrushPreview( + session: ActiveTransformSession, + origin: { x: number; y: number }, + current: { x: number; y: number }, + axisConstraint: TransformAxis | null + ) { + const initialBrush = this.createTargetPreviewBrush(session); + + if (initialBrush === null) { + throw new Error("Cannot build a component scale preview without a box brush target."); + } + + const extents = this.getInitialBrushLocalExtents(initialBrush); + + if (session.target.kind === "brushFace") { + const meta = getBoxBrushFaceTransformMeta(session.target.faceId); + const scaleFactor = + 1 + + this.getAxisMovementDistance( + axisConstraint ?? meta.axis, + this.getTransformPivotPosition(session), + origin, + current + ) * + 0.45; + + if (meta.sign > 0) { + extents.max[meta.axis] = extents.min[meta.axis] + (extents.max[meta.axis] - extents.min[meta.axis]) * scaleFactor; + } else { + extents.min[meta.axis] = extents.max[meta.axis] - (extents.max[meta.axis] - extents.min[meta.axis]) * scaleFactor; + } + } else if (session.target.kind === "brushEdge") { + const meta = getBoxBrushEdgeTransformMeta(session.target.edgeId); + const affectedAxes = (["x", "y", "z"] as const).filter( + (axis) => meta.signs[axis] !== null && (axisConstraint === null || axisConstraint === axis) + ); + const pivot = this.getTransformPivotPosition(session); + + for (const axis of affectedAxes) { + const sign = meta.signs[axis]; + + if (sign === null) { + continue; + } + + const scaleFactor = 1 + this.getAxisMovementDistance(axis, pivot, origin, current) * 0.45; + + if (sign > 0) { + extents.max[axis] = extents.min[axis] + (extents.max[axis] - extents.min[axis]) * scaleFactor; + } else { + extents.min[axis] = extents.max[axis] - (extents.max[axis] - extents.min[axis]) * scaleFactor; + } + } + } + + return this.createBrushPreviewFromExtents(initialBrush, extents); + } + private applyBrushRenderObjectTransform(brushId: string, center: Vec3, rotationDegrees: Vec3, size: Vec3) { const renderObjects = this.brushRenderObjects.get(brushId); const brush = this.currentDocument?.brushes[brushId];