// Copyright Epic Games, Inc. All Rights Reserved. #include "CubeGridTool.h" #include "BaseBehaviors/ClickDragBehavior.h" #include "BaseBehaviors/SingleClickBehavior.h" #include "BaseBehaviors/MouseHoverBehavior.h" #include "BaseGizmos/GizmoMath.h" #include "BaseGizmos/TransformGizmoUtil.h" #include "BaseGizmos/TransformProxy.h" #include "CanvasTypes.h" #include "CompositionOps/CubeGridBooleanOp.h" #include "Distance/DistLine3Ray3.h" #include "Drawing/PreviewGeometryActor.h" #include "Drawing/LineSetComponent.h" #include "DynamicMeshEditor.h" #include "DynamicMesh/DynamicMeshChangeTracker.h" #include "DynamicMesh/MeshTransforms.h" #include "DynamicMeshToMeshDescription.h" #include "Engine/Engine.h" // GEngine->GetSmallFont() #include "InteractiveToolChange.h" #include "InteractiveToolManager.h" #include "Input/Reply.h" #include "InputState.h" #include "Mechanics/DragAlignmentMechanic.h" #include "MeshOpPreviewHelpers.h" //UMeshOpPreviewWithBackgroundCompute #include "MeshDescriptionToDynamicMesh.h" #include "Misc/MessageDialog.h" #include "ModelingObjectsCreationAPI.h" #include "ModelingToolTargetUtil.h" #include "Properties/MeshMaterialProperties.h" #include "PropertySets/CreateMeshObjectTypeProperties.h" #include "SceneView.h" #include "Selection/ToolSelectionUtil.h" #include "ToolContextInterfaces.h" #include "ToolHostCustomizationAPI.h" #include "ToolTargetManager.h" #include "ToolTargets/ToolTarget.h" #include "TargetInterfaces/MaterialProvider.h" #include "TargetInterfaces/DynamicMeshCommitter.h" #include "TargetInterfaces/DynamicMeshProvider.h" #include "TargetInterfaces/PrimitiveComponentBackedTarget.h" #include "ToolSceneQueriesUtil.h" #include "ToolSetupUtil.h" #include UE_INLINE_GENERATED_CPP_BY_NAME(CubeGridTool) using namespace UE::Geometry; #define LOCTEXT_NAMESPACE "UCubeGridTool" // General note: This tool used to largely operate in cube grid space. However it turned out // to be valuable to be able to keep a selection more or less constant while resizing the grid, // and this was easier to do by generally operating in just the frame space of the grid (not // scaled into the grid size). namespace CubeGridToolLocals { FText PushPullModeMessage = LOCTEXT("CubeGridPushPullModeDescription", "Select grid cells and push/pull them to create geometry. " "Refer to side panel for shortcuts."); FText CornerModeMessage = LOCTEXT("CubeGridCornerModeDescription", "Toggle corner selection for push/pulling by clicking or dragging. " "Press Enter or click \"Done\" to accept the result."); FText CreatingAssetMessage = LOCTEXT("CreatingNewAssetLabel", "Creating new asset."); FText EditingAssetMessage = LOCTEXT("EditingExistingAssetLabel", "Editing existing asset."); const FText SelectionChangeTransactionName = LOCTEXT("SelectionChangeTransaction", "Cube Grid Selection Change"); const FText ModeChangeTransactionName = LOCTEXT("ModeChangeTransaction", "Cube Grid Mode Change"); const FText CornerModeExtrudeAmountChangeTransactionName = LOCTEXT("CornerExtrudeAmountTransaction", "Corner Push/Pull Amount"); const FString PropertyCacheIdentifier = TEXT("CubeGridTool"); const FString& HoverLineSetID(TEXT("HoverLines")); const FString& GridLineSetID(TEXT("GridLines")); const FString& SelectionLineSetID(TEXT("SelectionLines")); const FString& CornerModeLineSetID(TEXT("CornerModeLines")); const FColor HoverLineColor(255, 255, 128); // Pale yellow const double HoverLineThickness = 2; const double HoverLineDepthBias = 0.1; const FColor SelectionLineColor(255, 128, 0); // Orange const double SelectionLineDepthBias = 0.1; const double SelectionGridLineThickness = 1; const double SelectionMainLineThickness = 3; const FColor GridLineColor(200, 200, 200); const double GridLineDepthBias = 0.05; const double GridLineThickness = 0.5; const FColor UnselectedCornerLineColor(64, 0, 128); // dark purple const FColor SelectedCornerLineColor = FColor::Yellow; const double CornerLineThickness = 3; const int32 CornerCircleNumSteps = 12; const FColor CornerModeWireframeColor = FColor::Red; const double CornerModeWireframeThickness = SelectionGridLineThickness; const double CornerModeWireframeDepthBias = 0.1; /** * Undoes the actual mesh change (i.e. after boolean operations) */ class FCubeGridToolMeshChange : public FToolCommandChange { public: FCubeGridToolMeshChange(TUniquePtr MeshChangeIn) : MeshChange(MoveTemp(MeshChangeIn)) {}; virtual void Apply(UObject* Object) override { UCubeGridTool* Tool = Cast(Object); Tool->UpdateUsingMeshChange(*MeshChange, false); } virtual void Revert(UObject* Object) override { UCubeGridTool* Tool = Cast(Object); Tool->UpdateUsingMeshChange(*MeshChange, true); } virtual FString ToString() const override { return TEXT("CubeGridToolLocals::FCubeGridToolMeshChange"); } protected: TUniquePtr MeshChange; }; class FCubeGridMeshTransformChange : public FToolCommandChange { public: FCubeGridMeshTransformChange(const FTransform& BeforeIn, const FTransform& AfterIn) : Before(BeforeIn) , After(AfterIn) {}; virtual void Apply(UObject* Object) override { Cast(Object)->SetCurrentMeshTransform(After); } virtual void Revert(UObject* Object) override { Cast(Object)->SetCurrentMeshTransform(Before); } virtual FString ToString() const override { return TEXT("CubeGridToolLocals::FCubeGridMeshTransformChange"); } protected: FTransform Before; FTransform After; }; /** Undoes selection changes */ class FCubeGridToolSelectionChange : public FToolCommandChange { public: FCubeGridToolSelectionChange( bool bHaveStartSelectionBeforeIn, bool bHaveStartSelectionAfterIn, const UCubeGridTool::FSelection& SelectionBeforeIn, const UCubeGridTool::FSelection& SelectionAfterIn ) : bHaveStartSelectionBefore(bHaveStartSelectionBeforeIn) , bHaveStartSelectionAfter(bHaveStartSelectionAfterIn) , SelectionBefore(SelectionBeforeIn) , SelectionAfter(SelectionAfterIn) {}; virtual void Apply(UObject* Object) override { UCubeGridTool* Tool = Cast(Object); if (!bHaveStartSelectionAfter) { Tool->ClearSelection(false); } else { Tool->SetSelection(SelectionAfter, false); } } virtual void Revert(UObject* Object) override { UCubeGridTool* Tool = Cast(Object); if (!bHaveStartSelectionBefore) { Tool->ClearSelection(false); } else { Tool->SetSelection(SelectionBefore, false); } } virtual FString ToString() const override { return TEXT("CubeGridToolLocals::FCubeGridToolSelectionChange"); } protected: bool bHaveStartSelectionBefore; bool bHaveStartSelectionAfter; UCubeGridTool::FSelection SelectionBefore; UCubeGridTool::FSelection SelectionAfter; }; /** Undoes activating "corner" mode. Not redoable. */ class FCubeGridToolModeChange : public FToolCommandChange { public: FCubeGridToolModeChange() {}; virtual bool HasExpired(UObject* Object) const override { return Cast(Object)->IsInDefaultMode(); } virtual void Apply(UObject* Object) override { return; } virtual void Revert(UObject* Object) override { Cast(Object)->RevertToDefaultMode(); } virtual FString ToString() const override { return TEXT("CubeGridToolLocals::FCubeGridToolModeChange"); } }; class FCornerModeExtrudeAmountChange : public FToolCommandChange { public: FCornerModeExtrudeAmountChange(int32 ExtrudeAmountBeforeIn, int32 ExtrudeAmountAfterIn) : ExtrudeAmountBefore(ExtrudeAmountBeforeIn) , ExtrudeAmountAfter(ExtrudeAmountAfterIn) {}; virtual bool HasExpired(UObject* Object) const override { return !Cast(Object)->IsInCornerMode(); } virtual void Apply(UObject* Object) override { Cast(Object)->SetCurrentExtrudeAmount(ExtrudeAmountAfter); } virtual void Revert(UObject* Object) override { Cast(Object)->SetCurrentExtrudeAmount(ExtrudeAmountBefore); } virtual FString ToString() const override { return TEXT("CubeGridToolLocals::FCornerModeExtrudeAmountChange"); } protected: int32 ExtrudeAmountBefore = 0; int32 ExtrudeAmountAfter = 0; }; class FCornerModeSelectedCornerChange : public FToolCommandChange { public: FCornerModeSelectedCornerChange(bool CornerFlagsBeforeIn[4], bool CornerFlagsAfterIn[4]) { for (int i = 0; i < 4; ++i) { CornerFlagsBefore[i] = CornerFlagsBeforeIn[i]; CornerFlagsAfter[i] = CornerFlagsAfterIn[i]; } }; virtual bool HasExpired(UObject* Object) const override { return !Cast(Object)->IsInCornerMode(); } virtual void Apply(UObject* Object) override { Cast(Object)->SetCornerSelection(CornerFlagsAfter); } virtual void Revert(UObject* Object) override { Cast(Object)->SetCornerSelection(CornerFlagsBefore); } virtual FString ToString() const override { return TEXT("CubeGridToolLocals::FCornerModeSelectedCornerChange"); } protected: bool CornerFlagsBefore[4] = { false,false,false,false }; bool CornerFlagsAfter[4] = { false,false,false,false }; }; /** Undoes setting bChangesMade, which is used to determine whether the target needs saving. */ class FCubeGridChangesMadeChange : public FToolCommandChange { public: FCubeGridChangesMadeChange(bool bNowMadeIn = true) : bNowMade(bNowMadeIn) {}; virtual void Apply(UObject* Object) override { Cast(Object)->SetChangesMade(bNowMade); } virtual void Revert(UObject* Object) override { Cast(Object)->SetChangesMade(!bNowMade); } virtual FString ToString() const override { return TEXT("CubeGridToolLocals::FCubeGridChangesMadeChange"); } protected: bool bNowMade = true; }; /** Deals with the tool target portion of an "Accept and Start New" change. The actual asset update and current mesh updates should happen alongside this change.*/ class FCubeGridTargetResetChange : public FToolCommandChange { public: FCubeGridTargetResetChange(UToolTarget* ToolTargetIn) : ToolTarget(ToolTargetIn) {}; virtual void Apply(UObject* Object) override { UCubeGridTool* Tool = Cast(Object); if (ToolTarget.IsValid()) { UE::ToolTarget::SetSourceObjectVisible(ToolTarget.Get(), true); } Tool->SetTarget(nullptr); Tool->GetToolManager()->DisplayMessage(CreatingAssetMessage, EToolMessageLevel::UserWarning); } virtual void Revert(UObject* Object) override { UCubeGridTool* Tool = Cast(Object); Tool->SetTarget(ToolTarget.Get()); Tool->GetToolManager()->DisplayMessage(EditingAssetMessage, EToolMessageLevel::UserWarning); if (ToolTarget.IsValid()) { UE::ToolTarget::SetSourceObjectVisible(ToolTarget.Get(), false); } } virtual FString ToString() const override { return TEXT("CubeGridToolLocals::FCubeGridAcceptAndStartNewChange"); } protected: TWeakObjectPtr ToolTarget; }; // Attach a frame to the box such that Z points along the given direction. // If we change the choices we make here, we may need to adjust how we pick the welded vertices in // subtract mode (i.e., which way the values are mirrored when Z is flipped) and the output of // GetFaceUVOrientations. FOrientedBox3d ConvertToOrientedBox(const FAxisAlignedBox3d& Box, FCubeGrid::EFaceDirection Direction) { int FlatDim = FCubeGrid::DirToFlatDim(Direction); FVector3d GridSpaceZ = FCubeGrid::DirToNormal(Direction); FVector3d GridSpaceX = (FlatDim == 0) ? FVector3d::UnitY() : FVector3d::UnitX(); FVector3d GridSpaceY = GridSpaceZ.Cross(GridSpaceX); FVector3d BoxExtents = Box.Extents(); if (FlatDim == 0) { // If selection dir was along x axis, then frame z is x, frame x is y, and frame y is x BoxExtents = FVector3d(BoxExtents[1], BoxExtents[2], BoxExtents[0]); } else if (FlatDim == 1) { // If selection dir was along y axis, then frame z is y, frame x is x, and frame y is z Swap(BoxExtents[2], BoxExtents[1]); } return FOrientedBox3d(FFrame3d(Box.Center(), GridSpaceX, GridSpaceY, GridSpaceZ), BoxExtents); } // Based on direction of the operation, figures out how the faces of the opmesh need to be rotated // relative to the UV plane to make the UV's always keep the same orientation relative to the grid. // This depends on the choices we make in ConvertToOrientedBox, and is hard to reason through without // trial and error... though there may have been a smarter way to pick our frame from direction that // would have made this table easier to produce. TArray> GetFaceUVOrientations(FCubeGrid::EFaceDirection Direction) { switch (Direction) { case FCubeGrid::EFaceDirection::NegativeZ: return TArray>{ 2, 2, 2, 2, 2, 2 }; break; case FCubeGrid::EFaceDirection::PositiveX: return TArray>{ 0, 0, 3, 1, 3, 1 }; break; case FCubeGrid::EFaceDirection::NegativeX: return TArray>{ 2, 2, 3, 1, 1, 3 }; break; case FCubeGrid::EFaceDirection::PositiveY: return TArray>{ 2, 2, 0, 0, 1, 3 }; break; case FCubeGrid::EFaceDirection::NegativeY: return TArray>{ 0, 0, 2, 2, 3, 1 }; break; } // FCubeGrid::EFaceDirection::PositiveZ: return TArray>{ 0, 0, 0, 0, 0, 0 }; } void GetNewSelectionFaceInBox(const FCubeGrid& Grid, const FAxisAlignedBox3d& Box, const FCubeGrid::FCubeFace& FaceIn, FCubeGrid::FCubeFace& FaceOut) { // Start at the corner of the selection and move a little bit to make sure // you're in the first face in the corner. FVector3d TowardOtherCorner(Box.Max - Box.Min); TowardOtherCorner.Normalize(); FVector3d PointOnDesiredFace = (Box.Min + Grid.GetCurrentGridCellSize() * TowardOtherCorner) / Grid.GetCurrentGridCellSize(); FaceOut = FCubeGrid::FCubeFace( PointOnDesiredFace, FaceIn.GetDirection(), Grid.GetGridPower()); } /** * Given grid, start point, extrude direction, and number of blocks to extrude, produces * the frame-space extrusion distance. If the start point is not on the grid, the first * "block" is counted as the distance to get back onto grid in the extrusion direction. */ double GetFrameSpaceExtrudeDist(const FCubeGrid& CubeGrid, const FVector3d& FrameSpaceStartPoint, int32 CurrentExtrudeAmount, FCubeGrid::EFaceDirection Direction) { double GridSpaceExtrudeDist = CurrentExtrudeAmount; // Will be adjusted int FlatDim = FCubeGrid::DirToFlatDim(Direction); double GridSpaceExtrudeCoord = FrameSpaceStartPoint[FlatDim] / CubeGrid.GetCurrentGridCellSize(); double ClosestOnGridCoord = FMath::RoundToDouble(GridSpaceExtrudeCoord); // See if we're actually off-grid if (FMath::Abs(GridSpaceExtrudeCoord - ClosestOnGridCoord) > KINDA_SMALL_NUMBER) { double NextOnGrid = CurrentExtrudeAmount > 0 ? FMath::CeilToDouble(GridSpaceExtrudeCoord) : FMath::FloorToDouble(GridSpaceExtrudeCoord); GridSpaceExtrudeDist += (NextOnGrid - GridSpaceExtrudeCoord); GridSpaceExtrudeDist += (CurrentExtrudeAmount > 0 ? -1 : 1); } return GridSpaceExtrudeDist * CubeGrid.GetCurrentGridCellSize(); } bool IsAnyCornerSelected(bool CornerSelectedFlags[]) { for (int i = 0; i < 4; ++i) { if (CornerSelectedFlags[i]) { return true; } } return false; } /** * @param CornerVector Lines to draw on the corners, for instance to show the depth direction */ void DrawGridRectangle(ULineSetComponent& LineSet, const FCubeGrid& Grid, const FVector3d& GridMin, const FVector3d& GridMax, const FColor& Color, double Thickness, double DepthBias, const FVector3d* CornerVector = nullptr) { // We'll step from max/min along one of the differing dimensions to get corners int32 DifferingDimension = (GridMin[0] == GridMax[0]) ? 1 : 0; FVector3d Corner1 = GridMin; Corner1[DifferingDimension] = GridMax[DifferingDimension]; FVector3d Corner2 = GridMax; Corner2[DifferingDimension] = GridMin[DifferingDimension]; LineSet.AddLine(GridMin, Corner1, Color, Thickness, DepthBias); LineSet.AddLine(GridMin, Corner2, Color, Thickness, DepthBias); LineSet.AddLine(GridMax, Corner1, Color, Thickness, DepthBias); LineSet.AddLine(GridMax, Corner2, Color, Thickness, DepthBias); if (CornerVector) { LineSet.AddLine(GridMin, GridMin + *CornerVector, Color, Thickness, DepthBias); LineSet.AddLine(GridMax, GridMax + *CornerVector, Color, Thickness, DepthBias); LineSet.AddLine(Corner1, Corner1 + *CornerVector, Color, Thickness, DepthBias); LineSet.AddLine(Corner2, Corner2 + *CornerVector, Color, Thickness, DepthBias); } } const int32 MAX_NUM_INTERIOR_GRID_LINES = 1000; void DrawGridSection(ULineSetComponent& LineSet, const FCubeGrid& Grid, const FAxisAlignedBox3d& BBox, const FColor& Color, double Thickness, double DepthBias, const FVector3d* CornerVector = nullptr) { // Draw the boundary DrawGridRectangle(LineSet, Grid, BBox.Min, BBox.Max, Color, Thickness, DepthBias, CornerVector); // Find the two nonzero dimensions of the box const FVector3d BoxDimensions = BBox.Max - BBox.Min; int32 Dim1 = BoxDimensions[0] != 0 ? 0 : 2; int32 Dim2 = BoxDimensions[1] != 0 ? 1 : 2; double StepSize = Grid.GetCurrentGridCellSize(); // Draw the inside only if there aren't too many lines to draw (approximate) if (StepSize <= 0 || BoxDimensions[Dim1] / StepSize + BoxDimensions[Dim2] / StepSize > MAX_NUM_INTERIOR_GRID_LINES) { return; } // Draws lines that lie in the DimToDrawAlong dimension, along border in DimToStepAlong dimension auto DrawParallelInteriorLines = [&BBox, &LineSet, &Grid, &Color, &BoxDimensions, StepSize, Thickness, DepthBias](int32 DimToStepAlong, int32 DimToDrawAlong) { FVector3d BorderDirection = FVector3d::Zero(); BorderDirection[DimToStepAlong] = StepSize; int NumSteps = BoxDimensions[DimToStepAlong] / StepSize; if (NumSteps * StepSize == BoxDimensions[DimToStepAlong]) { --NumSteps; } for (int32 i = 0; i < NumSteps; ++i) { FVector3d SidePoint = BBox.Min + BorderDirection * (i + 1); FVector3d OtherSidePoint = SidePoint; OtherSidePoint[DimToDrawAlong] = BBox.Max[DimToDrawAlong]; LineSet.AddLine(SidePoint, OtherSidePoint, Color, Thickness, DepthBias); } }; DrawParallelInteriorLines(Dim1, Dim2); DrawParallelInteriorLines(Dim2, Dim1); } void DisplayLengthsOfEveryNonzeroBoxSide(const FAxisAlignedBox3d& Box, const FTransform& BoxTransform, FCanvas& Canvas, const FSceneView& SceneView) { if (Box.IsEmpty()) { return; } FVector3d LocalDimensions = Box.Max - Box.Min; if (LocalDimensions.IsZero()) { return; } FVector3d LocalCenter = Box.Center(); double DPIScale = Canvas.GetDPIScale(); UFont* UseFont = GEngine->GetSmallFont(); for (int MeasuredDim = 0; MeasuredDim < 3; ++MeasuredDim) { if (LocalDimensions[MeasuredDim] == 0) { // Flat on this side continue; } double Length = FMath::Abs(LocalDimensions[MeasuredDim] * BoxTransform.GetScale3D()[MeasuredDim]); FString String; if (FMath::Abs(Length - FMath::RoundToDouble(Length)) < KINDA_SMALL_NUMBER) { String = FString::Printf(TEXT("%.0f"), Length); } else { // Two decimal places if we don't have a round number String = FString::Printf(TEXT("%.2f"), Length); } // Iterate in a square across the other two dimensions int OtherDim1 = MeasuredDim == 0 ? 1 : 0; int OtherDim2 = MeasuredDim == 2 ? 1 : 2; for (int i = 0; i < 2; ++i) { if (i == 1 && LocalDimensions[OtherDim1] == 0) { break; } for (int j = 0; j < 2; ++j) { if (j == 1 && LocalDimensions[OtherDim2] == 0) { break; } FVector3d LocalWriteLocation = Box.Min; LocalWriteLocation[MeasuredDim] = LocalCenter[MeasuredDim]; if (i == 1) { LocalWriteLocation[OtherDim1] = Box.Max[OtherDim1]; } if (j == 1) { LocalWriteLocation[OtherDim2] = Box.Max[OtherDim2]; } FVector3d WorldPosition = BoxTransform.TransformPosition(LocalWriteLocation); FVector2D PixelPosition; SceneView.WorldToPixel(WorldPosition, PixelPosition); Canvas.DrawShadowedString(PixelPosition.X / DPIScale, PixelPosition.Y / DPIScale, *String, UseFont, FLinearColor::White); } } } } /** Given a world hit, get a hit face. */ void ConvertToFaceHit(const FCubeGrid& CubeGrid, ECubeGridToolFaceSelectionMode SelectionMode, const FRay& WorldRay, double HitT, const FVector3d& Normal, FCubeGrid::FCubeFace& FaceOut, double Tolerance) { FVector3d WorldPoint = WorldRay.PointAt(HitT); bool bSuccess = false; switch (SelectionMode) { case ECubeGridToolFaceSelectionMode::OutsideBasedOnNormal: bSuccess = CubeGrid.GetHitGridFaceBasedOnRay(WorldPoint, Normal, FaceOut, false, Tolerance); break; case ECubeGridToolFaceSelectionMode::InsideBasedOnNormal: bSuccess = CubeGrid.GetHitGridFaceBasedOnRay(WorldPoint, Normal, FaceOut, true, Tolerance); break; case ECubeGridToolFaceSelectionMode::OutsideBasedOnViewRay: bSuccess = CubeGrid.GetHitGridFaceBasedOnRay(WorldPoint, -WorldRay.Direction, FaceOut, false, Tolerance); break; case ECubeGridToolFaceSelectionMode::InsideBasedOnViewRay: bSuccess = CubeGrid.GetHitGridFaceBasedOnRay(WorldPoint, -WorldRay.Direction, FaceOut, true, Tolerance); break; } ensureMsgf(bSuccess, TEXT("CubeGridTool: Unable to convert hit location to proper grid face.")); } /** * Given a world ray and a flat box in grid frame space, intersect the ray with the plane containing the box, * find the selected cell in the cube grid, and project that cell onto the same plane to produce a new frame * space box. * * @param bExpandBoxWithStartBox If true, the output is expanded to contain the original box. * @return true If the plane was actually hit. */ bool GetCoplanarFrameSpaceSelectedBox(const FCubeGrid& CubeGrid, const FRay& WorldRay, const FAxisAlignedBox3d& StartBox, bool bExpandOutputBoxWithStartBox, FAxisAlignedBox3d& BoxOut) { FVector3d BoxDims = StartBox.Max - StartBox.Min; int FlatDim = 0; for (int i = 0; i < 3; ++i) { if (BoxDims[i] == 0) { FlatDim = i; break; } } bool bIntersects = false; FVector IntersectionPoint; GizmoMath::RayPlaneIntersectionPoint( CubeGrid.GetFrame().FromFramePoint(StartBox.Min), CubeGrid.GetFrame().GetAxis(FlatDim), WorldRay.Origin, WorldRay.Direction, bIntersects, IntersectionPoint); if (!bIntersects) { return false; } FVector3d GridSpaceIntersection = CubeGrid.ToGridPoint(IntersectionPoint); FVector3d FrameSpaceMin( FMath::Floor(GridSpaceIntersection.X) * CubeGrid.GetCurrentGridCellSize(), FMath::Floor(GridSpaceIntersection.Y) * CubeGrid.GetCurrentGridCellSize(), FMath::Floor(GridSpaceIntersection.Z) * CubeGrid.GetCurrentGridCellSize()); FVector3d FrameSpaceMax( FMath::CeilToDouble(GridSpaceIntersection.X) * CubeGrid.GetCurrentGridCellSize(), FMath::CeilToDouble(GridSpaceIntersection.Y) * CubeGrid.GetCurrentGridCellSize(), FMath::CeilToDouble(GridSpaceIntersection.Z) * CubeGrid.GetCurrentGridCellSize()); // Project the cell we got back onto the plane FrameSpaceMin[FlatDim] = StartBox.Min[FlatDim]; FrameSpaceMax[FlatDim] = StartBox.Min[FlatDim]; BoxOut = FAxisAlignedBox3d(FrameSpaceMin, FrameSpaceMax); if (bExpandOutputBoxWithStartBox) { BoxOut.Contain(StartBox); } return true; } } const FToolTargetTypeRequirements& UCubeGridToolBuilder::GetTargetRequirements() const { static FToolTargetTypeRequirements TypeRequirements({ UMaterialProvider::StaticClass(), UDynamicMeshCommitter::StaticClass(), UDynamicMeshProvider::StaticClass(), UPrimitiveComponentBackedTarget::StaticClass() }); return TypeRequirements; } bool UCubeGridToolBuilder::CanBuildTool(const FToolBuilderState& SceneState) const { return SceneState.TargetManager->CountSelectedAndTargetable(SceneState, GetTargetRequirements()) <= 1; } UInteractiveTool* UCubeGridToolBuilder::BuildTool(const FToolBuilderState& SceneState) const { UCubeGridTool* NewTool = NewObject(SceneState.ToolManager); TObjectPtr Target = SceneState.TargetManager->BuildFirstSelectedTargetable(SceneState, GetTargetRequirements()); NewTool->SetTarget(Target); // May be null NewTool->SetWorld(SceneState.World); return NewTool; } void UCubeGridTool::InvalidatePreview(bool bUpdateCornerLineSet) { using namespace CubeGridToolLocals; // Do the line set first if (bUpdateCornerLineSet && Mode == EMode::Corner) { UpdateCornerModeLineSet(); } // If we're guaranteed not to change the result, just reset the preview if (CurrentExtrudeAmount == 0 || (CurrentMesh->TriangleCount() == 0 && CurrentExtrudeAmount < 0)) { Preview->CancelCompute(); LastOpChangedTids = nullptr; if (bPreviewMayDiffer) { Preview->PreviewMesh->UpdatePreview(CurrentMesh.Get()); Preview->PreviewMesh->SetTransform(CurrentMeshTransform); bPreviewMayDiffer = false; } return; } Preview->InvalidateResult(); bPreviewMayDiffer = true; } TUniquePtr UCubeGridTool::MakeNewOperator() { using namespace CubeGridToolLocals; // Figure out how far to extrude in grid space. This only becomes tricky if the selection // is no longer on grid because we changed grid power. double FrameSpaceExtrudeAmount = GetFrameSpaceExtrudeDist(*CubeGrid, Selection.Box.Min, CurrentExtrudeAmount, Selection.Direction); FCubeGrid::EFaceDirection ZDirectionInGrid = CurrentExtrudeAmount < 0 ? FCubeGrid::FlipDir(Selection.Direction) : Selection.Direction; FOrientedBox3d FrameSpaceBox = ConvertToOrientedBox(Selection.Box, ZDirectionInGrid); // Give the selection box depth FrameSpaceBox.Frame.Origin += (FrameSpaceExtrudeAmount / 2.0f) * FCubeGrid::DirToNormal(Selection.Direction); FrameSpaceBox.Extents.Z = FMath::Abs(FrameSpaceExtrudeAmount) / 2.0f; // Translate the oriented box from grid space to world space const FFrame3d& GridFrame = CubeGrid->GetFrame(); FOrientedBox3d WorldBox(FFrame3d(GridFrame.FromFrame(FrameSpaceBox.Frame)), FrameSpaceBox.Extents); // Make the op. TUniquePtr Op = MakeUnique(); Op->InputMesh = ComputeStartMesh; Op->InputTransform = CurrentMeshTransform; // If we didn't start with an existing mesh, and we are adding to an empty // starting mesh, set the transform such that it is in the (grid) minimum of // the selection. This frequently (though not always, in some corner-mode cases) // places the pivot in a handy corner for snapping. if (!Target && ComputeStartMesh->TriangleCount() == 0 && CurrentExtrudeAmount > 0 && ensure(bHaveSelection)) { FFrame3d TransformFrame = CubeGrid->GetFrame(); TransformFrame.Origin = TransformFrame.FromFramePoint(Selection.Box.Min); Op->InputTransform = TransformFrame.ToTransform(); } Op->bKeepInputTransform = true; Op->WorldBox = WorldBox; Op->bSubtract = CurrentExtrudeAmount < 0; Op->bTrackChangedTids = true; Op->OpMeshMaterialID = OpMeshMaterialID; Op->OpMeshHeightUVOffset = OpMeshHeightUVOffset; if (Settings->bKeepSideGroups) { if (Op->bSubtract && !OpMeshSubtractSideGroups.IsEmpty()) { Op->OpMeshSideGroups = OpMeshSubtractSideGroups; } else if (!Op->bSubtract && !OpMeshAddSideGroups.IsEmpty()) { Op->OpMeshSideGroups = OpMeshAddSideGroups; } } Op->FaceUVOrientations = GetFaceUVOrientations(ZDirectionInGrid); Op->bWorldSpaceUVs = MaterialProperties->bWorldSpaceUVScale; Op->UVScale = MaterialProperties->UVScale; if (Mode == EMode::Corner) { Op->CornerInfo = MakeShared(); for (int i = 0; i < 4; ++i) { Op->CornerInfo->WeldedAtBase[i] = !CornerSelectedFlags[i]; } if (Op->bSubtract) { // If we're flipping the Z direction of our frame, we currently effectively do so by rotating around // the X axis, which determines which way we need to flip these corner labels. Swap(Op->CornerInfo->WeldedAtBase[0], Op->CornerInfo->WeldedAtBase[3]); Swap(Op->CornerInfo->WeldedAtBase[1], Op->CornerInfo->WeldedAtBase[2]); } Op->bCrosswiseDiagonal = Settings->bCrosswiseDiagonal; } return Op; } void UCubeGridTool::SlideSelection(int32 Amount, bool bEmitChange) { using namespace CubeGridToolLocals; FVector3d FrameSpaceDisplacement = FCubeGrid::DirToNormal(Selection.Direction) * GetFrameSpaceExtrudeDist(*CubeGrid, Selection.StartBox.Min, Amount, Selection.Direction); FSelection NewSelection = Selection; NewSelection.StartBox = FAxisAlignedBox3d( Selection.StartBox.Min + FrameSpaceDisplacement, Selection.StartBox.Max + FrameSpaceDisplacement); NewSelection.Box = FAxisAlignedBox3d( Selection.Box.Min + FrameSpaceDisplacement, Selection.Box.Max + FrameSpaceDisplacement); SetSelection(NewSelection, bEmitChange); } void UCubeGridTool::SetSelection(const FSelection& NewSelection, bool bEmitChange) { using namespace CubeGridToolLocals; // Clear op/preview if (Mode != EMode::Corner) { CurrentExtrudeAmount = 0; InvalidatePreview(); } if (bEmitChange && (!bHaveSelection || Selection != NewSelection)) { GetToolManager()->EmitObjectChange(this, MakeUnique(bHaveSelection, true, Selection, NewSelection), SelectionChangeTransactionName); } Selection = NewSelection; bHaveSelection = true; UpdateSelectionLineSet(); if (Mode == EMode::Corner) { InvalidatePreview(); } } void UCubeGridTool::ClearSelection(bool bEmitChange) { using namespace CubeGridToolLocals; if (bEmitChange && bHaveSelection) { GetToolManager()->EmitObjectChange(this, MakeUnique(bHaveSelection, false, Selection, Selection), SelectionChangeTransactionName); } bHaveSelection = false; UpdateSelectionLineSet(); } void UCubeGridTool::Setup() { using namespace CubeGridToolLocals; UInteractiveTool::Setup(); GetToolManager()->DisplayMessage(PushPullModeMessage, EToolMessageLevel::UserNotification); ToolActions = NewObject(this); ToolActions->Initialize(this); AddToolPropertySource(ToolActions); Settings = NewObject(this); Settings->RestoreProperties(this); AddToolPropertySource(Settings); OutputTypeProperties = NewObject(this); OutputTypeProperties->RestoreProperties(this); OutputTypeProperties->InitializeDefault(); OutputTypeProperties->WatchProperty(OutputTypeProperties->OutputType, [this](FString) { OutputTypeProperties->UpdatePropertyVisibility(); }); AddToolPropertySource(OutputTypeProperties); MaterialProperties = NewObject(this); // Change the default by setting it before restoring MaterialProperties->bWorldSpaceUVScale = true; MaterialProperties->RestoreProperties(this, PropertyCacheIdentifier); AddToolPropertySource(MaterialProperties); CurrentMesh = MakeShared(); CurrentMesh->EnableAttributes(); CurrentMeshMaterials.Empty(); if (Target) { *CurrentMesh = UE::ToolTarget::GetDynamicMeshCopy(Target); UE::ToolTarget::SetSourceObjectVisible(Target, false); CurrentMeshTransform = UE::ToolTarget::GetLocalToWorldTransform(Target); CurrentMeshMaterials = UE::ToolTarget::GetMaterialSet(Target).Materials; ToolActions->GridSourceActor = UE::ToolTarget::GetTargetActor(Target); } MeshSpatial = MakeShared(); MeshSpatial->SetMesh(CurrentMesh.Get(), true); Preview = NewObject(this); Preview->Setup(TargetWorld, this); ToolSetupUtil::ApplyRenderingConfigurationToPreview(Preview->PreviewMesh, Target); Preview->PreviewMesh->SetTangentsMode(EDynamicMeshComponentTangentsMode::AutoCalculated); if (Target) { Preview->PreviewMesh->UpdatePreview(CurrentMesh.Get()); } Preview->PreviewMesh->SetTransform((FTransform)CurrentMeshTransform); Preview->OnOpCompleted.AddWeakLambda(this, [this](const FDynamicMeshOperator* UncastOp) { const FCubeGridBooleanOp* Op = static_cast(UncastOp); if (Op->InputMesh == ComputeStartMesh) { LastOpChangedTids = Op->ChangedTids; if (Op->bSubtract) { OpMeshSubtractSideGroups = Op->OpMeshSideGroups; } else { OpMeshAddSideGroups = Op->OpMeshSideGroups; } } }); UpdateOpMaterials(); CubeGrid = MakeShared(); CubeGrid->SetGridFrame(FFrame3d(Settings->GridFrameOrigin, Settings->GridFrameOrientation.Quaternion())); CubeGrid->SetGridPowerMode(Settings->bPowerOfTwoBlockSizes ? FCubeGrid::EPowerMode::PowerOfTwo : FCubeGrid::EPowerMode::FiveAndTen); CubeGrid->SetGridPower(Settings->GridPower); CubeGrid->SetCurrentGridCellSize(Settings->CurrentBlockSize); Settings->BlockBaseSize = CubeGrid->GetBaseGridCellSize(); FTransform GridTransform = CubeGrid->GetFrame().ToFTransform(); GridGizmoTransformProxy = NewObject(this); GridGizmoTransformProxy->SetTransform(GridTransform); GridGizmoTransformProxy->OnBeginTransformEdit.AddWeakLambda(this, [this](UTransformProxy* Proxy) { bInGizmoDrag = true; }); GridGizmoTransformProxy->OnTransformChanged.AddUObject(this, &UCubeGridTool::GridGizmoMoved); GridGizmoTransformProxy->OnEndTransformEdit.AddWeakLambda(this, [this](UTransformProxy* Proxy) { NotifyOfPropertyChangeByTool(Settings); UpdateCornerGeometrySet(); bInGizmoDrag = false; }); GridGizmo = UE::TransformGizmoUtil::CreateCustomTransformGizmo(GetToolManager(), ETransformGizmoSubElements::StandardTranslateRotate, this); GridGizmo->SetActiveTarget(GridGizmoTransformProxy, GetToolManager()); GridGizmoAlignmentMechanic = NewObject(this); GridGizmoAlignmentMechanic->Setup(this); GridGizmoAlignmentMechanic->InitializeDeformedMeshRayCast([this]() { return MeshSpatial.Get(); }, CurrentMeshTransform, nullptr); GridGizmoAlignmentMechanic->AddToGizmo(GridGizmo); GridGizmo->SetVisibility(false); LineSets = NewObject(); LineSets->CreateInWorld(TargetWorld, GridTransform); LineSets->AddLineSet(HoverLineSetID); LineSets->AddLineSet(SelectionLineSetID); LineSets->AddLineSet(CornerModeLineSetID); LineSets->SetAllLineSetsMaterial(ToolSetupUtil::GetDefaultLineComponentMaterial(GetToolManager(), /*bDepthTested*/ false)); LineSets->AddLineSet(GridLineSetID); LineSets->SetLineSetMaterial(GridLineSetID, ToolSetupUtil::GetDefaultLineComponentMaterial(GetToolManager(), /*bDepthTested*/ true)); UpdateGridLineSet(); SelectedCornerRenderer.LineThickness = CornerLineThickness; ClickDragBehavior = NewObject(); ClickDragBehavior->Initialize(this); AddInputBehavior(ClickDragBehavior, this); HoverBehavior = NewObject(); HoverBehavior->Modifiers.RegisterModifier(ShiftModifierID, FInputDeviceState::IsShiftKeyDown); HoverBehavior->Modifiers.RegisterModifier(CtrlModifierID, FInputDeviceState::IsCtrlKeyDown); HoverBehavior->Initialize(this); AddInputBehavior(HoverBehavior, this); CtrlMiddleClickBehavior = NewObject(); CtrlMiddleClickBehavior->Initialize(); CtrlMiddleClickBehavior->SetUseMiddleMouseButton(); CtrlMiddleClickBehavior->ModifierCheckFunc = [this](const FInputDeviceState& InputState) { return FInputDeviceState::IsCtrlKeyDown(InputState); }; CtrlMiddleClickBehavior->IsHitByClickFunc = [this](const FInputDeviceRay& InputRay) { FCubeGrid::FCubeFace Face; FInputRayHit OutResult; OutResult.bHit = GetHitGridFace(InputRay.WorldRay, Face); return OutResult; }; CtrlMiddleClickBehavior->OnClickedFunc = [this](const FInputDeviceRay& ClickPos) { OnCtrlMiddleClick(ClickPos); }; AddInputBehavior(CtrlMiddleClickBehavior); MiddleClickDragBehavior = NewObject(); MiddleClickDragBehavior->Initialize(); MiddleClickDragBehavior->SetUseMiddleMouseButton(); MiddleClickDragBehavior->ModifierCheckFunc = [this](const FInputDeviceState& InputState) { return !FInputDeviceState::IsCtrlKeyDown(InputState); }; MiddleClickDragBehavior->CanBeginClickDragFunc = [this](const FInputDeviceRay& ClickPos) { return CanBeginMiddleClickDrag(ClickPos); }; MiddleClickDragBehavior->OnClickPressFunc = [this](const FInputDeviceRay& ClickPos) { PrepForSelectionChange(); RayCastSelectionPlane((FRay3d)ClickPos.WorldRay, MiddleClickDragStart); }; MiddleClickDragBehavior->OnClickDragFunc = [this](const FInputDeviceRay& ClickPos) { OnMiddleClickDrag(ClickPos); }; MiddleClickDragBehavior->OnClickReleaseFunc = [this](const FInputDeviceRay&) { EndSelectionChange(); }; MiddleClickDragBehavior->OnTerminateFunc = [this]() { EndSelectionChange(); }; AddInputBehavior(MiddleClickDragBehavior); GridPowerWatcherIdx = Settings->WatchProperty(Settings->GridPower, [this](uint8 NewPower) { SetGridPowerClamped(Settings->GridPower); }); auto ChangeCurrentBlockSize = [this](double BlockSizeIn) { ClearHover(); CubeGrid->SetCurrentGridCellSize(BlockSizeIn); // Update our views of size. The current block size may end up slightly different due to // rounding error, since it's actually the base block size that is authoratative in cubegrid. Settings->CurrentBlockSize = CubeGrid->GetCurrentGridCellSize(); Settings->BlockBaseSize = CubeGrid->GetBaseGridCellSize(); Settings->SilentUpdateWatcherAtIndex(CurrentBlockSizeWatcherIdx); Settings->SilentUpdateWatcherAtIndex(BlockBaseSizeWatcherIdx); NotifyOfPropertyChangeByTool(Settings); UpdateSelectionLineSet(); UpdateGridLineSet(); }; Settings->WatchProperty(Settings->bPowerOfTwoBlockSizes, [ChangeCurrentBlockSize, this](bool bOn) { CubeGrid->SetGridPowerMode(Settings->bPowerOfTwoBlockSizes ? FCubeGrid::EPowerMode::PowerOfTwo : FCubeGrid::EPowerMode::FiveAndTen); // We reset grid power and cell size to default. The reason we do this is because a user is likely to // only change this setting if they want the cube sizes to match up to editor grid snapping values. This // requires the grid size to be set a particular way, and while a user could realize this and set it // appropriately, it feels very clunky to do so. SetGridPowerClamped(Settings->DEFAULT_GRID_POWER); ChangeCurrentBlockSize(Settings->DEFAULT_CURRENT_BLOCK_SIZE); }); CurrentBlockSizeWatcherIdx = Settings->WatchProperty(Settings->CurrentBlockSize, [ChangeCurrentBlockSize](double NewSize) { ChangeCurrentBlockSize(NewSize); }); BlockBaseSizeWatcherIdx = Settings->WatchProperty(Settings->BlockBaseSize, [this](double NewBaseSize) { ClearHover(); CubeGrid->SetBaseGridCellSize(NewBaseSize); Settings->CurrentBlockSize = CubeGrid->GetCurrentGridCellSize(); Settings->SilentUpdateWatcherAtIndex(CurrentBlockSizeWatcherIdx); NotifyOfPropertyChangeByTool(Settings); UpdateSelectionLineSet(); UpdateGridLineSet(); }); Settings->WatchProperty(Settings->bShowGizmo, [this](bool bOn) { UpdateGizmoVisibility(bOn); }); Settings->WatchProperty(Settings->bShowGrid, [this](bool bShow) { if (ULineSetComponent* LineSet = LineSets->FindLineSet(GridLineSetID)) { LineSet->SetVisibility(bShow); } }); Settings->WatchProperty(Settings->bCrosswiseDiagonal, [this](bool bOn) { InvalidatePreview(); }); Settings->WatchProperty(Settings->bKeepSideGroups, [this](bool bOn) { InvalidatePreview(false); }); Settings->WatchProperty(Settings->PlaneTolerance, [this](double Tolerance) { InvalidatePreview(false); }); MaterialProperties->WatchProperty(MaterialProperties->Material, [this](TWeakObjectPtr Material) { UpdateOpMaterials(); InvalidatePreview(false); }); MaterialProperties->WatchProperty(MaterialProperties->UVScale, [this](float UVScale) { InvalidatePreview(false); }); MaterialProperties->WatchProperty(MaterialProperties->bShowWireframe, [this](float UVScale) { Preview->PreviewMesh->EnableWireframe(MaterialProperties->bShowWireframe); }); MaterialProperties->WatchProperty(MaterialProperties->bWorldSpaceUVScale, [this](float UVScale) { InvalidatePreview(false); }); GridFrameOriginWatcherIdx = Settings->WatchProperty(Settings->GridFrameOrigin, [this](FVector) { FTransform Transform(Settings->GridFrameOrientation, Settings->GridFrameOrigin, FVector::One()); UpdateGridGizmo(Transform, /* bSilentlyUpdate */ true); // Silent to avoid undo/redo during drag in detail panel UpdateGridTransform(Transform, /*bUpdateDetailPanel */ false); }); GridFrameOrientationWatcherIdx = Settings->WatchProperty(Settings->GridFrameOrientation, [this](FRotator) { FTransform Transform(Settings->GridFrameOrientation, Settings->GridFrameOrigin, FVector::One()); UpdateGridGizmo(Transform, /* bSilentlyUpdate */ true); // Silent to avoid undo/redo during drag in detail panel UpdateGridTransform(Transform, /*bUpdateDetailPanel */ false); }); MaterialProperties->SilentUpdateWatched(); Settings->SilentUpdateWatched(); UpdateComputeInputs(); if (Target) { GetToolManager()->DisplayMessage(EditingAssetMessage, EToolMessageLevel::UserWarning); } else { GetToolManager()->DisplayMessage(CreatingAssetMessage, EToolMessageLevel::UserWarning); } } void UCubeGridTool::UpdateOpMaterials() { TArray PreviewMaterials = CurrentMeshMaterials; OpMeshMaterialID = PreviewMaterials.Find(MaterialProperties->Material.Get()); if (OpMeshMaterialID == INDEX_NONE) { OpMeshMaterialID = PreviewMaterials.Add(MaterialProperties->Material.Get()); } Preview->ConfigureMaterials(PreviewMaterials, ToolSetupUtil::GetDefaultWorkingMaterial(GetToolManager())); } void UCubeGridTool::UpdateComputeInputs() { ComputeStartMesh = MakeShared(*CurrentMesh); } void UCubeGridTool::Shutdown(EToolShutdownType ShutdownType) { using namespace CubeGridToolLocals; Settings->SaveProperties(this); MaterialProperties->SaveProperties(this, PropertyCacheIdentifier); OutputTypeProperties->SaveProperties(this); if (Mode == EMode::Corner) { ApplyCornerMode(true); } if (Target) { UE::ToolTarget::SetSourceObjectVisible(Target.Get(), true); } // CubeGrid might get used for long stretches at a time, and an accidental Esc hit could result in a fair // amount of wasted work. So if the user made any saveable changes, we'll make sure they definitely want to // discard their work. Unfortunately we can't currently give a classic "are you sure you want to quit" sort // of message because our tool can't actually prevent a shutdown. Hence we have this "do you want to save // the changes instead" message. if (ShutdownType == EToolShutdownType::Cancel && bChangesMade && (Target || CurrentMesh->TriangleCount() > 0)) { EAppReturnType::Type Ret = FMessageDialog::Open(EAppMsgType::YesNo, LOCTEXT("AcceptChangesQuestion", "The tool is being cancelled, which normally discards all changes. " "Would you like to apply the changes instead?\n\n Selecting \"No\" or closing this window will " "discard the tool's current work."), LOCTEXT("AcceptChangesTitle", "Accept changes instead?")); if (Ret == EAppReturnType::Yes) { ShutdownType = EToolShutdownType::Accept; } } if (ShutdownType == EToolShutdownType::Accept && bChangesMade) { OutputCurrentResults(/*bSetSelection =*/ true); } Preview->OnOpCompleted.RemoveAll(this); Preview->Shutdown(); if (LineSets) { LineSets->Disconnect(); LineSets = nullptr; } GridGizmoAlignmentMechanic->Shutdown(); GetToolManager()->GetPairedGizmoManager()->DestroyAllGizmosByOwner(this); // Probably unnecessary by this point, but just in case. ClearViewportButtonCustomization(); } // Applies the tool's results to the target or outputs the mesh. Meant to be called during shutdown // or when doing "Accept and Start New" void UCubeGridTool::OutputCurrentResults(bool bSetSelection) { if (CurrentMesh->TriangleCount() <= 0) { if (Target && Target->IsValid()) { EAppReturnType::Type Ret = FMessageDialog::Open(EAppMsgType::YesNo, LOCTEXT("DestroyComponentQuestion", "The tool has entirely cut away the source mesh, which is not permitted. " "Do you actually want to delete the mesh component? Note that the actor will remain. Choosing No will leave " "the component unchanged instead."), LOCTEXT("DestroyComponentTitle", "Delete mesh components?")); if (Ret == EAppReturnType::No || Ret == EAppReturnType::Cancel) { return; } UPrimitiveComponent* Component = UE::ToolTarget::GetTargetComponent(Target); if (ensure(Component)) { GetToolManager()->BeginUndoTransaction(LOCTEXT("DestroyComponentTransaction", "Delete Mesh Component")); UE::ToolTarget::GetTargetComponent(Target)->DestroyComponent(); GetToolManager()->EndUndoTransaction(); } } return; } bool bCreatingNewAsset = !Target && CurrentMesh->TriangleCount() > 0; if (Target) { if (Target->IsValid()) { GetToolManager()->BeginUndoTransaction(LOCTEXT("CubeGridToolEditTransactionName", "Cube Grid Edit")); FComponentMaterialSet OutputMaterialSet; OutputMaterialSet.Materials = CurrentMeshMaterials; UE::ToolTarget::CommitDynamicMeshUpdate(Target, *CurrentMesh, true, FConversionToMeshDescriptionOptions(), &OutputMaterialSet); GetToolManager()->EndUndoTransaction(); return; } else if (!Target->IsValid() && CurrentMesh->TriangleCount() > 0) { EAppReturnType::Type Ret = FMessageDialog::Open(EAppMsgType::YesNo, LOCTEXT("RecreateAssetQuestion", "The underlying asset that this tool was " "operating on seems to no longer be valid (it was likely forcibly removed). " "Would you like to recreate a new asset from the tool's current working " "mesh? Selecting \"No\" or closing this window will discard the tool's " "current work."), LOCTEXT("RecreateAssetTitle", "Recreate Mesh Asset?")); if (Ret == EAppReturnType::Yes) { bCreatingNewAsset = true; } } } if (bCreatingNewAsset) { GetToolManager()->BeginUndoTransaction(LOCTEXT("CubeGridToolCreateTransactionName", "Cube Grid Create New")); FCreateMeshObjectParams NewMeshObjectParams; NewMeshObjectParams.TargetWorld = TargetWorld; NewMeshObjectParams.Transform = (FTransform)CurrentMeshTransform; NewMeshObjectParams.BaseName = TEXT("CubeGridToolOutput"); NewMeshObjectParams.Materials = CurrentMeshMaterials; NewMeshObjectParams.SetMesh(CurrentMesh.Get()); OutputTypeProperties->ConfigureCreateMeshObjectParams(NewMeshObjectParams); FCreateMeshObjectResult Result = UE::Modeling::CreateMeshObject(GetToolManager(), MoveTemp(NewMeshObjectParams)); if (Result.IsOK() && Result.NewActor != nullptr && bSetSelection) { ToolSelectionUtil::SetNewActorSelection(GetToolManager(), Result.NewActor); } GetToolManager()->EndUndoTransaction(); } } void UCubeGridTool::AcceptToolAndStartNew() { using namespace CubeGridToolLocals; if (Mode == EMode::Corner) { ApplyCornerMode(true); } if (!bChangesMade) { return; } FText TransactionText = LOCTEXT("AcceptAndStartNewTransaction", "Cube Grid Accept and Start New"); GetToolManager()->BeginUndoTransaction(TransactionText); OutputCurrentResults(/*bSetSelection =*/ false); GetToolManager()->EmitObjectChange(this, MakeUnique(false), TransactionText); if (Target) { UE::ToolTarget::SetSourceObjectVisible(Target.Get(), true); GetToolManager()->EmitObjectChange(this, MakeUnique(Target), TransactionText); } // Clear out the current mesh if (CurrentMesh->TriangleCount() > 0) { // TODO: There are probably better ways to do this than a dynamic mesh change tracker. For // instance we could have a change that keeps a weak pointer to the mesh and keep a shared // pointer in the tool, so that the mesh is thrown away once the tool exits (and the change // is expired). Though we might need to be a bit more careful with retargeting the spatial, etc. // For now, we do this easy approach. TArray AllTids; for (int32 Tid : CurrentMesh->TriangleIndicesItr()) { AllTids.Add(Tid); } FDynamicMeshChangeTracker ChangeTracker(CurrentMesh.Get()); ChangeTracker.BeginChange(); ChangeTracker.SaveTriangles(AllTids, true /*bSaveVertices*/); FDynamicMeshEditor Editor(CurrentMesh.Get()); Editor.RemoveTriangles(AllTids, true); GetToolManager()->EmitObjectChange(this, MakeUnique(ChangeTracker.EndChange()), TransactionText); } // Clear out any other structures/state MeshSpatial->Build(); UpdateComputeInputs(); bWaitingToApplyPreview = false; CurrentExtrudeAmount = 0; Preview->CancelCompute(); Preview->PreviewMesh->UpdatePreview(CurrentMesh.Get()); GetToolManager()->EndUndoTransaction(); } void UCubeGridTool::OnTick(float DeltaTime) { if (Preview) { Preview->Tick(DeltaTime); } if (PendingAction != ECubeGridToolAction::NoAction) { ApplyAction(PendingAction); PendingAction = ECubeGridToolAction::NoAction; } if (bWaitingToApplyPreview && // Note that the check for bPreviewMayDiffer is needed, because when we do a forced // reset, HaveValidResult() returns false, yet we may still want to do an application // to slide the selection. (!bPreviewMayDiffer || Preview->HaveValidResult())) { ApplyPreview(); } } void UCubeGridTool::ApplyPreview() { using namespace CubeGridToolLocals; bWaitingToApplyPreview = false; bPreviewMayDiffer = false; bBlockUntilPreviewUpdate = false; const FText TransactionText = LOCTEXT("CubeGridToolTransactionName", "Block Tool Change"); GetToolManager()->BeginUndoTransaction(TransactionText); // See if we're actually making a change to the mesh. if (CurrentExtrudeAmount > 0 || (CurrentExtrudeAmount < 0 && LastOpChangedTids.IsValid() && LastOpChangedTids->Num() > 0)) { FDynamicMeshChangeTracker ChangeTracker(CurrentMesh.Get()); ChangeTracker.BeginChange(); ChangeTracker.SaveTriangles(*LastOpChangedTids, true /*bSaveVertices*/); // Update current mesh bool bWasValid = Preview->ProcessCurrentMesh([this](const FDynamicMesh3& Mesh) { CurrentMesh->Copy(Mesh); }); if (!ensure(bWasValid)) { CurrentExtrudeAmount = 0; return; } // The transform might have changed if we added to an empty mesh if (!FTransform(CurrentMeshTransform).Equals(Preview->PreviewMesh->GetTransform())) { GetToolManager()->EmitObjectChange(this, MakeUnique( CurrentMeshTransform, Preview->PreviewMesh->GetTransform()), TransactionText); CurrentMeshTransform = Preview->PreviewMesh->GetTransform(); GridGizmoAlignmentMechanic->InitializeDeformedMeshRayCast([this]() { return MeshSpatial.Get(); }, CurrentMeshTransform, nullptr); } // Mark our tool as having done something, if we haven't already. if (!bChangesMade) { bChangesMade = true; GetToolManager()->EmitObjectChange(this, MakeUnique(), TransactionText); } MeshSpatial->Build(); CurrentMeshMaterials.Reset(); Preview->PreviewMesh->GetMaterials(CurrentMeshMaterials); UpdateComputeInputs(); GetToolManager()->EmitObjectChange(this, MakeUnique(ChangeTracker.EndChange()), TransactionText); } if (bAdjustSelectionOnPreviewUpdate && CurrentExtrudeAmount != 0) { // Save UV offset before sliding selection OpMeshHeightUVOffset += GetFrameSpaceExtrudeDist(*CubeGrid, Selection.Box.Min, CurrentExtrudeAmount, Selection.Direction); // Change the selection to the new location. Note that this should happen only // after resetting bPreviewMayDiffer to avoid an extra preview reset when selection // changes. SlideSelection(CurrentExtrudeAmount, true); } // This actually also happens as a side effect of SlideSelection above, but here for clarity. CurrentExtrudeAmount = 0; GetToolManager()->EndUndoTransaction(); } void UCubeGridTool::Render(IToolsContextRenderAPI* RenderAPI) { using namespace CubeGridToolLocals; GetToolManager()->GetContextQueriesAPI()->GetCurrentViewState(CameraState); if (Mode == EMode::Corner) { FOrientedBox3d OrientedBox = ConvertToOrientedBox(Selection.Box, Selection.Direction); SelectedCornerRenderer.BeginFrame(RenderAPI, CameraState); for (int i = 0; i < 4; ++i) { FVector WorldPosition = CubeGrid->GetFrame().FromFramePoint(OrientedBox.GetCorner(i)); // Depending on whether we're in an orthographic view or not, we set the radius based on visual angle or based on ortho // viewport width (divided into 90 segments like the FOV is divided into 90 degrees). float Radius = (CameraState.bIsOrthographic) ? CameraState.OrthoWorldCoordinateWidth * 0.5 / 90.0 : (float)ToolSceneQueriesUtil::CalculateDimensionFromVisualAngleD(CameraState, (FVector3d)WorldPosition, 0.5); bool bDepthTested = false; SelectedCornerRenderer.DrawViewFacingCircle( WorldPosition, Radius, CornerCircleNumSteps, CornerSelectedFlags[i] ? SelectedCornerLineColor : UnselectedCornerLineColor, CornerLineThickness, bDepthTested); } SelectedCornerRenderer.EndFrame(); } GridGizmoAlignmentMechanic->Render(RenderAPI); } void UCubeGridTool::DrawHUD(FCanvas* Canvas, IToolsContextRenderAPI* RenderAPI) { using namespace CubeGridToolLocals; if (bHaveSelection && Canvas && CubeGrid && Settings && Settings->bShowSelectionMeasurements) { if (const FSceneView* SceneView = RenderAPI->GetSceneView()) { DisplayLengthsOfEveryNonzeroBoxSide(Selection.Box, CubeGrid->GetFrame().ToFTransform(), *Canvas, *SceneView); } } } void UCubeGridTool::OnPropertyModified(UObject* PropertySet, FProperty* Property) { } void UCubeGridTool::ClearHover() { if (bHaveHoveredSelection) { UpdateHoverLineSet(false, HoveredSelectionBox); } } void UCubeGridTool::GridGizmoMoved(UTransformProxy* Proxy, FTransform Transform) { UpdateGridTransform(Transform, /* bUpdateDetailPanel */ true, // If we're here due to a drag, then we don't want to trigger the detail panel // rebuild (it will be triggered on drag end). !bInGizmoDrag); } void UCubeGridTool::OnCtrlMiddleClick(const FInputDeviceRay& ClickPos) { // Get the selected face FCubeGrid::FCubeFace Face; if (!ensure(GetHitGridFace(ClickPos.WorldRay, Face))) { return; } if (!GridGizmo->IsVisible()) { UpdateGizmoVisibility(true); } // Get the face's four corners in grid space FVector3d FaceMin = Face.GetMinCorner(); FVector3d FaceMax = Face.GetMaxCorner(); FVector3d Corners[4] = { FaceMin, FaceMin, FaceMax, FaceMax }; int32 DifferingDimension = (FaceMin[0] == FaceMax[0]) ? 1 : 0; Corners[1][DifferingDimension] = FaceMax[DifferingDimension]; Corners[3][DifferingDimension] = FaceMin[DifferingDimension]; // Transform the ray to grid space and see which of the corners is closest. FVector3d GridSpaceRayOrigin = CubeGrid->ToGridPoint(ClickPos.WorldRay.Origin); FVector3d GridSpaceRayDirection = CubeGrid->GetFrame().ToFrameVector(FVector3d(ClickPos.WorldRay.Direction)); GridSpaceRayDirection.Normalize(); FRay3d GizmoSpaceRay(GridSpaceRayOrigin, GridSpaceRayDirection); double MinDistSquared = GizmoSpaceRay.DistSquared(Corners[0]); int32 ClosestCornerIndex = 0; for (int32 i = 1; i < 4; ++i) { double DistSquared = GizmoSpaceRay.DistSquared(Corners[i]); if (DistSquared < MinDistSquared) { MinDistSquared = DistSquared; ClosestCornerIndex = i; } } GetToolManager()->BeginUndoTransaction(LOCTEXT("QuickAdjustGizmo", "Transform Gizmo")); // Adjust the selection if needed if (bHaveSelection) { FVector3d GridSpaceDisplacement = Corners[ClosestCornerIndex]; FVector3d FrameSpaceDisplacement = GridSpaceDisplacement * CubeGrid->GetCurrentGridCellSize(); FSelection NewSelection = Selection; NewSelection.StartBox = FAxisAlignedBox3d( Selection.StartBox.Min - FrameSpaceDisplacement, Selection.StartBox.Max - FrameSpaceDisplacement); NewSelection.Box = FAxisAlignedBox3d( Selection.Box.Min - FrameSpaceDisplacement, Selection.Box.Max - FrameSpaceDisplacement); SetSelection(NewSelection, true); } // Move the gizmo to that corner. UpdateGridGizmo(FTransform(Settings->GridFrameOrientation, CubeGrid->ToWorldPoint(Corners[ClosestCornerIndex]), FVector::One())); GetToolManager()->EndUndoTransaction(); } // Tries to intersect the selected box. Used for middle mouse dragging the selection. FInputRayHit UCubeGridTool::RayCastSelectionPlane(const FRay3d& WorldRay, FVector3d& HitPointOut) { FVector3d Normal = CubeGrid->GetFrame().FromFrameVector( FCubeGrid::DirToNormal(Selection.Direction)); FInputRayHit HitResult; FVector HitPoint; GizmoMath::RayPlaneIntersectionPoint(CubeGrid->GetFrame().FromFramePoint(Selection.Box.Min), Normal, WorldRay.Origin, WorldRay.Direction, HitResult.bHit, HitPoint); if (HitResult.bHit) { HitPointOut = HitPoint; HitResult = FInputRayHit(WorldRay.GetParameter(HitPointOut)); } return HitResult; } FInputRayHit UCubeGridTool::CanBeginMiddleClickDrag(const FInputDeviceRay& ClickPos) { FInputRayHit HitResult; if (!bHaveSelection) { return HitResult; } FVector3d WorldHitPoint; HitResult = RayCastSelectionPlane((FRay3d)ClickPos.WorldRay, WorldHitPoint); int FlatDim = FCubeGrid::DirToFlatDim(Selection.Direction); FVector3d FrameSpacePoint = CubeGrid->GetFrame().ToFramePoint(WorldHitPoint); FrameSpacePoint[FlatDim] = Selection.Box.Min[FlatDim]; HitResult.bHit = Selection.Box.Contains(FrameSpacePoint); return HitResult; } void UCubeGridTool::OnMiddleClickDrag(const FInputDeviceRay& DragPos) { if (!bHaveSelection) { return; } FVector3d MiddleClickDragEnd; RayCastSelectionPlane((FRay3d)DragPos.WorldRay, MiddleClickDragEnd); FVector3d DisplacementInGridFrame = CubeGrid->GetFrame().ToFrameVector(MiddleClickDragEnd - MiddleClickDragStart); // Clamp the relevant dimension in the displacement vector DisplacementInGridFrame[FCubeGrid::DirToFlatDim(Selection.Direction)] = 0; // Make the displacement be a multiple of the current grid cell size DisplacementInGridFrame /= CubeGrid->GetCurrentGridCellSize(); DisplacementInGridFrame = FVector3d( FMath::RoundToDouble(DisplacementInGridFrame.X), FMath::RoundToDouble(DisplacementInGridFrame.Y), FMath::RoundToDouble(DisplacementInGridFrame.Z)); DisplacementInGridFrame *= CubeGrid->GetCurrentGridCellSize(); FAxisAlignedBox3d NewSelectionBox( PreviousSelection.Box.Min + DisplacementInGridFrame, PreviousSelection.Box.Max + DisplacementInGridFrame ); // Adjust selection if (NewSelectionBox != Selection.Box) { Selection.StartBox = FAxisAlignedBox3d( PreviousSelection.StartBox.Min + DisplacementInGridFrame, PreviousSelection.StartBox.Max + DisplacementInGridFrame ); Selection.Box = FAxisAlignedBox3d( PreviousSelection.Box.Min + DisplacementInGridFrame, PreviousSelection.Box.Max + DisplacementInGridFrame ); UpdateSelectionLineSet(); InvalidatePreview(); } } void UCubeGridTool::PrepForSelectionChange() { bPreviousHaveSelection = bHaveSelection; PreviousSelection = Selection; } void UCubeGridTool::EndSelectionChange() { using namespace CubeGridToolLocals; if (bPreviousHaveSelection != bHaveSelection || (bHaveSelection && PreviousSelection != Selection)) { ResetMultiStepConsistencyData(); GetToolManager()->EmitObjectChange(this, MakeUnique(bPreviousHaveSelection, bHaveSelection, PreviousSelection, Selection), SelectionChangeTransactionName); } } void UCubeGridTool::ResetMultiStepConsistencyData() { OpMeshHeightUVOffset = 0; OpMeshAddSideGroups.Empty(); OpMeshSubtractSideGroups.Empty(); } void UCubeGridTool::UpdateGizmoVisibility(bool bVisible) { GridGizmo->SetVisibility(bVisible); LineSets->SetLineSetMaterial(CubeGridToolLocals::GridLineSetID, ToolSetupUtil::GetDefaultLineComponentMaterial(GetToolManager(), /*bDepthTested*/ !bVisible)); Settings->bShowGizmo = bVisible; Settings->SilentUpdateWatched(); } void UCubeGridTool::UpdateGridGizmo(const FTransform& NewTransform, bool bSilentlyUpdate) { if (bSilentlyUpdate) { GridGizmo->ReinitializeGizmoTransform(NewTransform); } else { GridGizmo->SetNewGizmoTransform(NewTransform); } } void UCubeGridTool::UpdateGridTransform(const FTransform& NewTransform, bool bUpdateDetailPanel, bool bTriggerDetailPanelRebuild) { if (bUpdateDetailPanel) { Settings->GridFrameOrigin = NewTransform.GetTranslation(); Settings->SilentUpdateWatcherAtIndex(GridFrameOriginWatcherIdx); Settings->GridFrameOrientation = NewTransform.GetRotation().Rotator(); Settings->SilentUpdateWatcherAtIndex(GridFrameOrientationWatcherIdx); if (bTriggerDetailPanelRebuild) { NotifyOfPropertyChangeByTool(Settings); } } CubeGrid->SetGridFrame(FFrame3d(NewTransform)); LineSets->SetTransform(NewTransform); if (Mode == EMode::Corner) { InvalidatePreview(false); if (!bInGizmoDrag) { // Don't need to update corner hit points while dragging since we can't drag and click corners at the same time. UpdateCornerGeometrySet(); } } } bool UCubeGridTool::GetHitGridFace(const FRay& WorldRay, FCubeGrid::FCubeFace& FaceOut) { using namespace CubeGridToolLocals; double BestHitT = TNumericLimits::Max(); // We always hit-test the ground plane... bool bHitPlane; FVector IntersectionPoint; GizmoMath::RayPlaneIntersectionPoint(CubeGrid->GetFrame().Origin, CubeGrid->GetFrame().Z(), WorldRay.Origin, WorldRay.Direction, bHitPlane, IntersectionPoint); if (bHitPlane) { FVector3d ClampedGridPoint = CubeGrid->ToGridPoint(IntersectionPoint); ClampedGridPoint.Z = 0; FaceOut = FCubeGrid::FCubeFace(ClampedGridPoint, CubeGrid->ToGridPoint(WorldRay.Origin).Z >= 0 ? FCubeGrid::EFaceDirection::PositiveZ : FCubeGrid::EFaceDirection::NegativeZ, CubeGrid->GetGridPower()); BestHitT = WorldRay.GetParameter(IntersectionPoint); } // ...However depending on the settings, we may give everything else priority, which we do by // keeping the plane hit distance maximal. if (!Settings->bHitGridGroundPlaneIfCloser) { BestHitT = TNumericLimits::Max(); } if (Settings->bHitUnrelatedGeometry) { FHitResult HitResult; if (ToolSceneQueriesUtil::FindNearestVisibleObjectHit(this, HitResult, WorldRay) && HitResult.Distance < BestHitT) { BestHitT = HitResult.Distance; ConvertToFaceHit(*CubeGrid, Settings->FaceSelectionMode, WorldRay, BestHitT, HitResult.ImpactNormal, FaceOut, Settings->PlaneTolerance); } } if (MeshSpatial) { FRay3d LocalRay(CurrentMeshTransform.InverseTransformPosition((FVector3d)WorldRay.Origin), CurrentMeshTransform.InverseTransformVector((FVector3d)WorldRay.Direction)); int32 Tid; double LocalHitT = TNumericLimits::Max(); if (MeshSpatial->FindNearestHitTriangle(LocalRay, LocalHitT, Tid)) { double HitT = WorldRay.GetParameter(CurrentMeshTransform.TransformPosition(LocalRay.PointAt(LocalHitT))); if (HitT < BestHitT) { BestHitT = HitT; ConvertToFaceHit(*CubeGrid, Settings->FaceSelectionMode, WorldRay, BestHitT, CurrentMeshTransform.TransformNormal(CurrentMesh->GetTriNormal(Tid)), FaceOut, Settings->PlaneTolerance); } } } // We can't go just off of BestHitT because we keep it maximal for plane hits when // bHitGridGroundPlaneIfCloser is false. return bHitPlane || BestHitT != TNumericLimits::Max(); } FInputRayHit UCubeGridTool::CanBeginClickDragSequence(const FInputDeviceRay& PressPos) { FInputRayHit HitResult; HitResult.bHit = (Mode != EMode::FitGrid && !(bBlockUntilPreviewUpdate && bWaitingToApplyPreview)); return HitResult; } void UCubeGridTool::OnClickPress(const FInputDeviceRay& PressPos) { using namespace CubeGridToolLocals; UpdateHoverLineSet(false, HoveredSelectionBox); // clear hover // Ctrl+drag extrude setting works in both corner mode and regular mode if (bMouseDragShouldPushPull) { MouseState = EMouseState::DraggingExtrudeDistance; if (bHaveSelection) { DragProjectionAxis = FRay3d(CubeGrid->GetFrame().FromFramePoint(Selection.Box.Center()), CubeGrid->GetFrame().FromFrameVector(FCubeGrid::DirToNormal(Selection.Direction)), true); FDistLine3Ray3d DistanceCalculator(UE::Geometry::FLine3d(DragProjectionAxis.Origin, DragProjectionAxis.Direction), (FRay3d)PressPos.WorldRay); DistanceCalculator.ComputeResult(); DragProjectedStartParam = DistanceCalculator.LineParameter; DragStartExtrudeAmount = CurrentExtrudeAmount; } return; } // Deal with corner selection if in corner mode if (Mode == EMode::Corner) { MouseState = EMouseState::DraggingCornerSelection; for (int i = 0; i < 4; ++i) { PreDragCornerSelectedFlags[i] = CornerSelectedFlags[i]; } AttemptToSelectCorner((FRay3d)PressPos.WorldRay); return; } // Otherwise, deal with selection PrepForSelectionChange(); MouseState = EMouseState::DraggingRegularSelection; FCubeGrid::FCubeFace HitFace; if (bHaveSelection && bSelectionToggle) { // We're adding to existing selection if (GetCoplanarFrameSpaceSelectedBox(*CubeGrid, PressPos.WorldRay, Selection.StartBox, true, Selection.Box)) { UpdateSelectionLineSet(); } } else if (GetHitGridFace(PressPos.WorldRay, HitFace)) { // Reset start of the selection bHaveSelection = true; double GridScale = CubeGrid->GetCellSize(HitFace.GetSourceCubeGridPower()); Selection.Box = FAxisAlignedBox3d(HitFace.GetMinCorner() * GridScale, HitFace.GetMaxCorner() * GridScale); Selection.StartBox = Selection.Box; Selection.Direction = HitFace.GetDirection(); UpdateSelectionLineSet(); } else { // Clear selection (the event emit, if needed, happens on click release) bHaveSelection = false; Selection.Box = FAxisAlignedBox3d(); UpdateSelectionLineSet(); } } void UCubeGridTool::OnClickDrag(const FInputDeviceRay& DragPos) { using namespace CubeGridToolLocals; if (!bHaveSelection) { return; } if (MouseState == EMouseState::DraggingExtrudeDistance) { if (!bHaveSelection || (Mode == EMode::Corner && !IsAnyCornerSelected(CornerSelectedFlags))) { return; } FDistLine3Ray3d DistanceCalculator( UE::Geometry::FLine3d(DragProjectionAxis.Origin, DragProjectionAxis.Direction), (FRay3d)DragPos.WorldRay); DistanceCalculator.ComputeResult(); double ParamDelta = DistanceCalculator.LineParameter - DragProjectedStartParam; double CubeSize = CubeGrid->GetCurrentGridCellSize(); int32 NewExtrudeDelta = FMath::RoundToInt(ParamDelta / (CubeSize * Settings->BlocksPerStep)) * Settings->BlocksPerStep; int32 NewExtrudeAmount = DragStartExtrudeAmount + NewExtrudeDelta; if (NewExtrudeAmount != CurrentExtrudeAmount) { CurrentExtrudeAmount = NewExtrudeAmount; InvalidatePreview(); } return; } else if (MouseState == EMouseState::DraggingCornerSelection) { AttemptToSelectCorner((FRay3d)DragPos.WorldRay); return; } else // Grid selection { FCubeGrid::FCubeFace HitFace; double HitT = TNumericLimits::Max(); bool bHit = GetCoplanarFrameSpaceSelectedBox(*CubeGrid, DragPos.WorldRay, Selection.StartBox, true, Selection.Box); UpdateSelectionLineSet(); } } void UCubeGridTool::OnClickRelease(const FInputDeviceRay& ReleasePos) { using namespace CubeGridToolLocals; if (MouseState == EMouseState::DraggingCornerSelection) { bool bCornerSelectionChanged = false; for (int i = 0; i < 4; ++i) { if (PreDragCornerSelectedFlags[i] != CornerSelectedFlags[i]) { bCornerSelectionChanged = true; break; } } if (bCornerSelectionChanged) { GetToolManager()->EmitObjectChange(this, MakeUnique(PreDragCornerSelectedFlags, CornerSelectedFlags), LOCTEXT("CornerSelectionTransaction", "Corner Selection")); } } else if (MouseState == EMouseState::DraggingExtrudeDistance) { // Only apply result if we're not in corner mode, because in corner mode // we apply when exiting corner mode (that behavior is particularly important // when using E/Q to set extrude distance, to allow different slopes to be // set). if (Mode != EMode::Corner && CurrentExtrudeAmount != 0) { bWaitingToApplyPreview = true; bBlockUntilPreviewUpdate = false; bAdjustSelectionOnPreviewUpdate = true; } else if (Mode == EMode::Corner && CurrentExtrudeAmount != DragStartExtrudeAmount) { GetToolManager()->EmitObjectChange(this, MakeUnique(DragStartExtrudeAmount, CurrentExtrudeAmount), CornerModeExtrudeAmountChangeTransactionName); } } else if (MouseState == EMouseState::DraggingRegularSelection) { EndSelectionChange(); } MouseState = EMouseState::NotDragging; } void UCubeGridTool::OnTerminateDragSequence() { if (MouseState == EMouseState::DraggingExtrudeDistance) { // Only apply result if we're not in corner mode if (Mode != EMode::Corner && CurrentExtrudeAmount != 0) { bWaitingToApplyPreview = true; bBlockUntilPreviewUpdate = false; bAdjustSelectionOnPreviewUpdate = true; } } MouseState = EMouseState::NotDragging; } void UCubeGridTool::AttemptToSelectCorner(const FRay3d& WorldRay) { TArray HitCorners; CornersGeometrySet.CollectPointsNearRay(WorldRay, HitCorners, [this](const FVector3d& Position1, const FVector3d& Position2) { double ToleranceScale = 3; if (CameraState.bIsOrthographic) { // We could just always use ToolSceneQueriesUtil::PointSnapQuery. But in ortho viewports, we happen to know // that the only points that we will ever give this function will be the closest points between a ray and // some geometry, meaning that the vector between them will be orthogonal to the view ray. With this knowledge, // we can do the tolerance computation more efficiently than PointSnapQuery can, since we don't need to project // down to the view plane. // As in PointSnapQuery, we convert our angle-based tolerance to one we can use in an ortho viewport (instead of // dividing our field of view into 90 visual angle degrees, we divide the plane into 90 units). float OrthoTolerance = ToolSceneQueriesUtil::GetDefaultVisualAngleSnapThreshD() * CameraState.OrthoWorldCoordinateWidth / 90.0; OrthoTolerance *= ToleranceScale; return FVector3d::DistSquared(Position1, Position2) < OrthoTolerance * OrthoTolerance; } else { return ToolSceneQueriesUtil::PointSnapQuery(CameraState, Position1, Position2, ToolSceneQueriesUtil::GetDefaultVisualAngleSnapThreshD() * ToleranceScale); } }); for (FGeometrySet3::FNearest Hit : HitCorners) { CornerSelectedFlags[Hit.ID] = !PreDragCornerSelectedFlags[Hit.ID]; } if (HitCorners.Num() > 0) { InvalidatePreview(); } } void UCubeGridTool::UpdateSelectionLineSet() { using namespace CubeGridToolLocals; ULineSetComponent* LineSet = LineSets->FindLineSet(SelectionLineSetID); LineSet->Clear(); if (bHaveSelection) { FVector3d CornerVector = FCubeGrid::DirToNormal(Selection.Direction) * GetFrameSpaceExtrudeDist(*CubeGrid, Selection.Box.Min, -Settings->BlocksPerStep, Selection.Direction); DrawGridRectangle(*LineSet, *CubeGrid, Selection.StartBox.Min, Selection.StartBox.Max, SelectionLineColor, SelectionMainLineThickness, SelectionLineDepthBias); DrawGridRectangle(*LineSet, *CubeGrid, Selection.Box.Min, Selection.Box.Max, SelectionLineColor, SelectionMainLineThickness, SelectionLineDepthBias); DrawGridSection(*LineSet, *CubeGrid, Selection.Box, SelectionLineColor, SelectionGridLineThickness, SelectionLineDepthBias, &CornerVector); if (Mode == EMode::Corner) { // This isn't quite relevant to updating the selection line set, but it's a convenient place // to put this because if the selection set is changing, the geometry set probably needs // to be doing the same. UpdateCornerGeometrySet(); } } } FInputRayHit UCubeGridTool::BeginHoverSequenceHitTest(const FInputDeviceRay& PressPos) { FInputRayHit HitResult; HitResult.bHit = UpdateHover(PressPos.WorldRay); return HitResult; } bool UCubeGridTool::UpdateHover(const FRay& WorldRay) { using namespace CubeGridToolLocals; if (Mode != EMode::PushPull) { UpdateHoverLineSet(false, HoveredSelectionBox); return false; } bool bHit; UE::Geometry::FAxisAlignedBox3d Box; if (bHaveSelection && bSelectionToggle) { bHit = GetCoplanarFrameSpaceSelectedBox(*CubeGrid, WorldRay, Selection.StartBox, false, Box); } else { FCubeGrid::FCubeFace HitFace; bHit = GetHitGridFace(WorldRay, HitFace); if (bHit) { double HoverScale = CubeGrid->GetCurrentGridCellSize(); Box = FAxisAlignedBox3d(HitFace.GetMinCorner() * HoverScale, HitFace.GetMaxCorner() * HoverScale); } } UpdateHoverLineSet(bHit, Box); return bHit; } void UCubeGridTool::OnBeginHover(const FInputDeviceRay& DevicePos) { } bool UCubeGridTool::OnUpdateHover(const FInputDeviceRay& DevicePos) { return UpdateHover(DevicePos.WorldRay); } // We could have not taken arguments here and done it the way we do selection, but we'd need // to keep track of previous hover to avoid unnecessary updates void UCubeGridTool::UpdateHoverLineSet(bool bNewHaveHover, const UE::Geometry::FAxisAlignedBox3d& NewHoveredBox) { using namespace CubeGridToolLocals; ULineSetComponent* LineSet = LineSets->FindLineSet(HoverLineSetID); if (!bNewHaveHover) { if (bHaveHoveredSelection) { LineSet->Clear(); } } else if (!bHaveHoveredSelection || NewHoveredBox != HoveredSelectionBox) { HoveredSelectionBox = NewHoveredBox; bHaveHoveredSelection = true; LineSet->Clear(); DrawGridRectangle(*LineSet, *CubeGrid, HoveredSelectionBox.Min, HoveredSelectionBox.Max, HoverLineColor, HoverLineThickness, HoverLineDepthBias); } } void UCubeGridTool::OnEndHover() { } void UCubeGridTool::OnUpdateModifierState(int ModifierID, bool bIsOn) { if (ModifierID == ShiftModifierID) { bSelectionToggle = bIsOn; } else if (ModifierID == CtrlModifierID) { bMouseDragShouldPushPull = bIsOn; } } void UCubeGridTool::UpdateGridLineSet() { using namespace CubeGridToolLocals; double CurrentGridScale = CubeGrid->GetCurrentGridCellSize(); FAxisAlignedBox3d GridBox; GridBox.Contain(FVector3d(-50, -50, 0) * CurrentGridScale); GridBox.Contain(FVector3d(50, 50, 0) * CurrentGridScale); ULineSetComponent* LineSet = LineSets->FindLineSet(GridLineSetID); LineSet->Clear(); DrawGridSection(*LineSet, *CubeGrid, GridBox, GridLineColor, GridLineThickness, GridLineDepthBias); LineSet->SetVisibility(Settings->bShowGrid); } void UCubeGridTool::UpdateCornerModeLineSet() { using namespace CubeGridToolLocals; ULineSetComponent* LineSet = LineSets->FindLineSet(CornerModeLineSetID); LineSet->Clear(); if (Mode == EMode::Corner && CurrentExtrudeAmount < 0) { // Since we're subtracting here, we need to do some flipping. FOrientedBox3d FrameSpaceBox = ConvertToOrientedBox(Selection.Box, FCubeGrid::FlipDir(Selection.Direction)); bool CornerWelded[4]; for (int i = 0; i < 4; ++i) { CornerWelded[i] = !CornerSelectedFlags[i]; } Swap(CornerWelded[0], CornerWelded[3]); Swap(CornerWelded[1], CornerWelded[2]); // The choice of diagonal here lines up with the generator in CubeGridBooleanOp for // the bottom face. int DiagStartIdx = 0; if (CornerWelded[1] != CornerWelded[3] || (!CornerWelded[1] && CornerWelded[0] && CornerWelded[2])) { DiagStartIdx = 1; } DiagStartIdx = Settings->bCrosswiseDiagonal ? 1 - DiagStartIdx : DiagStartIdx; bool bDiagonalWelded = CornerWelded[DiagStartIdx] && CornerWelded[DiagStartIdx + 2]; int DeletedVert = -1; if (bDiagonalWelded) { if (CornerWelded[DiagStartIdx + 1]) { DeletedVert = DiagStartIdx + 1; } else if (CornerWelded[(DiagStartIdx + 3) % 4]) { DeletedVert = (DiagStartIdx + 3) % 4; } } FVector3d CornerExtrudeVector = CubeGrid->GetCurrentGridCellSize() * CurrentExtrudeAmount * FCubeGrid::DirToNormal(Selection.Direction); for (int i = 0; i < 4; ++i) { FVector3d CurrentCorner = FrameSpaceBox.GetCorner(i); if (!CornerWelded[i]) { FVector3d UpCorner = CurrentCorner + CornerExtrudeVector; LineSet->AddLine(CurrentCorner, UpCorner, CornerModeWireframeColor, CornerModeWireframeThickness, CornerModeWireframeDepthBias); CurrentCorner = UpCorner; } int NextIdx = (i + 1) % 4; if (i == DeletedVert || NextIdx == DeletedVert) { continue; } FVector3d NextCorner = CornerWelded[NextIdx] ? FrameSpaceBox.GetCorner(NextIdx) : FrameSpaceBox.GetCorner(NextIdx) + CornerExtrudeVector; LineSet->AddLine(CurrentCorner, NextCorner, CornerModeWireframeColor, CornerModeWireframeThickness, CornerModeWireframeDepthBias); } FVector3d DiagCorner1 = CornerWelded[DiagStartIdx] ? FrameSpaceBox.GetCorner(DiagStartIdx) : FrameSpaceBox.GetCorner(DiagStartIdx) + CornerExtrudeVector; FVector3d DiagCorner2 = CornerWelded[DiagStartIdx + 2] ? FrameSpaceBox.GetCorner(DiagStartIdx + 2) : FrameSpaceBox.GetCorner(DiagStartIdx + 2) + CornerExtrudeVector; LineSet->AddLine(DiagCorner1, DiagCorner2, CornerModeWireframeColor, CornerModeWireframeThickness, CornerModeWireframeDepthBias); } } void UCubeGridTool::ApplyFlipSelection() { if (!bHaveSelection) { return; } GetToolManager()->BeginUndoTransaction(LOCTEXT("FlipTransactionName", "Flip Selection")); FSelection NewSelection = Selection; NewSelection.Direction = FCubeGrid::FlipDir(Selection.Direction); SetSelection(NewSelection, true); ResetMultiStepConsistencyData(); GetToolManager()->EndUndoTransaction(); // TODO: We actually probably want some special handling here in Corner mode. For one thing, // we're keeping the selected corners the same, which ends up rotating rather than mirroring // the currently pushed/pulled portion (should fix this, but would need another undo transaction, // at which point we probably want full undo support for corner mode, rather than our current // approach of keeping corner selection and extrude distance...). For another, might a user want // a flip in corner mode to equate to a reversal of push vs pull, instead of a mirror of the same // operation (i.e. you flip a pull and you get a mirrored push rather than mirrored pull)? Not certain. } void UCubeGridTool::ApplySlide(int32 NumBlocks) { if (!bHaveSelection) { return; } GetToolManager()->BeginUndoTransaction(LOCTEXT("SlideTransactionName", "Slide Selection")); SlideSelection(NumBlocks, true); ResetMultiStepConsistencyData(); GetToolManager()->EndUndoTransaction(); } void UCubeGridTool::ApplyPushPull(int32 NumBlocks) { using namespace CubeGridToolLocals; if (!bHaveSelection || (Mode == EMode::Corner && !IsAnyCornerSelected(CornerSelectedFlags))) { return; } int32 PreviousExtrudeAmount = CurrentExtrudeAmount; CurrentExtrudeAmount += NumBlocks; InvalidatePreview(); if (Mode == EMode::PushPull) { bWaitingToApplyPreview = true; bBlockUntilPreviewUpdate = false; bAdjustSelectionOnPreviewUpdate = true; } else if (Mode == EMode::Corner) { // In corner mode, we don't actually appy the change yet, but we transact the extrude amount. // (in regular mode we transact the mesh change once the actual change is applied) GetToolManager()->EmitObjectChange(this, MakeUnique(PreviousExtrudeAmount, CurrentExtrudeAmount), CornerModeExtrudeAmountChangeTransactionName); } } void UCubeGridTool::SetGridPowerClamped(int32 GridPower) { Settings->GridPower = FMath::Clamp(GridPower, 0, Settings->MaxGridPower); CubeGrid->SetGridPower(Settings->GridPower); Settings->SilentUpdateWatcherAtIndex(GridPowerWatcherIdx); Settings->CurrentBlockSize = CubeGrid->GetCurrentGridCellSize(); Settings->SilentUpdateWatcherAtIndex(CurrentBlockSizeWatcherIdx); NotifyOfPropertyChangeByTool(Settings); if (Mode == EMode::Corner) { InvalidatePreview(); // effective extrude distance is now different UpdateCornerModeLineSet(); // Note: If we wanted to, we could adjust the extrude amount in corner mode such that the currently extruded // distance stays roughly the same. However, it's not clear whether that is desirable. On the one hand, it might // make it easier to reach certain far-from-zero values if they are not on a big grid boundary. On the other hand, // the behavior makes it harder to see how the ramp will change on the next E/Q press. // For now we decided against it. If we ever want it, we would use this code instead of the above (requires // keeping track of GridPowerPrevious): if constexpr (false) { // Needs to match behavior in CubeGrid.cpp. We would probably expose it if we used this code block. auto GetMultiplierForGridPower = [](bool bPowerOfTwo, uint8 GridPower) { if (bPowerOfTwo) { // This is split up into two statements to avoid a static analysis warning about // shifting a 32 bit value and casting to a 64 bit value. uint32 ShiftedResult = static_cast(1) << GridPower; return static_cast(ShiftedResult); } else { // For FiveAndTen, we multiply by 2 half the time and by 5 the second half, rounding up for 2's. uint8 FloorHalfGridPower = GridPower / 2; uint32 TwoMultiplier = static_cast(1) << (GridPower - FloorHalfGridPower); return TwoMultiplier * FMath::Pow(5.0, static_cast(FloorHalfGridPower)); } }; // This would be made a member in the tool uint8 GridPowerPrevious = 0; double PreviousMultiplier = GetMultiplierForGridPower(Settings->bPowerOfTwoBlockSizes, GridPowerPrevious); double NewMultiplier = GetMultiplierForGridPower(Settings->bPowerOfTwoBlockSizes, Settings->GridPower);; double NewExtrudeAmount = CurrentExtrudeAmount * PreviousMultiplier / NewMultiplier; // Could consider allowing CurrentExtrudeAmount not to be an int (and moving it to the nearest int // on next manipulation). CurrentExtrudeAmount = FMath::RoundFromZero(NewExtrudeAmount); if (CurrentExtrudeAmount != NewExtrudeAmount) { InvalidatePreview(); UpdateCornerModeLineSet(); } }//end unused adjustment code } ClearHover(); UpdateSelectionLineSet(); // Updates the grid drawn inside UpdateGridLineSet(); } // Action support void UCubeGridToolActions::PostAction(ECubeGridToolAction Action) { if (ParentTool.IsValid()) { ParentTool->RequestAction(Action); } } void UCubeGridTool::RequestAction(ECubeGridToolAction ActionType) { if (PendingAction == ECubeGridToolAction::NoAction) { PendingAction = ActionType; } } void UCubeGridTool::ApplyAction(ECubeGridToolAction ActionType) { switch (ActionType) { case ECubeGridToolAction::Push: ApplyPushPull(-Settings->BlocksPerStep); break; case ECubeGridToolAction::Pull: ApplyPushPull(Settings->BlocksPerStep); break; case ECubeGridToolAction::Flip: ApplyFlipSelection(); break; case ECubeGridToolAction::SlideForward: ApplySlide(-Settings->BlocksPerStep); break; case ECubeGridToolAction::SlideBack: ApplySlide(Settings->BlocksPerStep); break; case ECubeGridToolAction::DecreaseGridPower: // cast is just to be explicit SetGridPowerClamped(static_cast(Settings->GridPower) - 1); break; case ECubeGridToolAction::IncreaseGridPower: SetGridPowerClamped(Settings->GridPower + 1); break; case ECubeGridToolAction::CornerMode: StartCornerMode(); break; //case ECubeGridToolAction::FitGrid: // StartFitGrid(); // break; case ECubeGridToolAction::ResetFromActor: if (ToolActions->GridSourceActor) { FTransform TransformToUse = ToolActions->GridSourceActor->GetTransform(); TransformToUse.SetScale3D(FVector::OneVector); UpdateGridGizmo(TransformToUse); } break; case ECubeGridToolAction::AcceptAndStartNew: AcceptToolAndStartNew(); break; } } void UCubeGridTool::RegisterActions(FInteractiveToolActionSet& ActionSet) { using namespace CubeGridToolLocals; int32 ActionID = (int32)EStandardToolActions::BaseClientDefinedActionID + 1; ActionSet.RegisterAction(this, ActionID++, TEXT("PullBlock"), LOCTEXT("PullAction", "Pull Out Blocks"), LOCTEXT("PullTooltip", ""), EModifierKey::None, EKeys::E, [this]() { RequestAction(ECubeGridToolAction::Pull); }); ActionSet.RegisterAction(this, ActionID++, TEXT("PushBlock"), LOCTEXT("PushAction", "Push In Holes"), LOCTEXT("PushTooltip", ""), EModifierKey::None, EKeys::Q, [this]() { RequestAction(ECubeGridToolAction::Push); }); ActionSet.RegisterAction(this, ActionID++, TEXT("SlideBack"), LOCTEXT("SlideBackAction", "Slide Selection Back"), LOCTEXT("SlideBackTooltip", ""), EModifierKey::Shift, EKeys::E, [this]() { RequestAction(ECubeGridToolAction::SlideBack); }); ActionSet.RegisterAction(this, ActionID++, TEXT("SlideForward"), LOCTEXT("SlideForwardAction", "Slide Selection Forward"), LOCTEXT("SlideForwardTooltip", ""), EModifierKey::Shift, EKeys::Q, [this]() { RequestAction(ECubeGridToolAction::SlideForward); }); ActionSet.RegisterAction(this, ActionID++, TEXT("DecreaseGridPower"), LOCTEXT("DecreaseGridPowerAction", "Decrease Grid Power"), LOCTEXT("DecreaseGridPowerTooltip", ""), // Note that we can't use Ctrl+Q on Mac because that is mapped to Cmd+Q which kills the editor. // At the same time we can't use Option+E because Mac consumes that for typing accented letters #if PLATFORM_MAC EModifierKey::Alt, EKeys::A, #else EModifierKey::Control, EKeys::Q, #endif [this]() { RequestAction(ECubeGridToolAction::DecreaseGridPower); }); ActionSet.RegisterAction(this, ActionID++, TEXT("IncreaseGridPower"), LOCTEXT("IncreaseGridPowerAction", "Increase Grid Power"), LOCTEXT("IncreaseGridPowerTooltip", ""), #if PLATFORM_MAC EModifierKey::Alt, EKeys::D, #else EModifierKey::Control, EKeys::E, #endif [this]() { RequestAction(ECubeGridToolAction::IncreaseGridPower); }); ActionSet.RegisterAction(this, ActionID++, TEXT("ToggleGizmoVisibility"), LOCTEXT("ToggleGizmoVisibilityAction", "Toggle Gizmo Visibility"), LOCTEXT("ToggleGizmoVisibilityTooltip", ""), EModifierKey::None, EKeys::R, [this]() { if (Mode != EMode::FitGrid) { UpdateGizmoVisibility(!GridGizmo->IsVisible()); } }); ActionSet.RegisterAction(this, ActionID++, TEXT("ToggleCornerMode"), LOCTEXT("ToggleCornerModeAction", "Toggle Corner Mode"), LOCTEXT("ToggleCornerModeTooltip", ""), EModifierKey::None, EKeys::Z, [this]() { if (Mode != EMode::Corner) { StartCornerMode(); } else { ApplyCornerMode(); } }); ActionSet.RegisterAction(this, ActionID++, TEXT("ToggleDiagonalMode"), LOCTEXT("ToggleDiagonalModeAction", "Toggle Diagonal Mode"), LOCTEXT("ToggleDiagonalModeTooltip", ""), EModifierKey::None, EKeys::X, [this]() { if (Mode == EMode::Corner) { Settings->bCrosswiseDiagonal = !Settings->bCrosswiseDiagonal; } }); ActionSet.RegisterAction(this, ActionID++, TEXT("FlipSelection"), LOCTEXT("FlipSelectionAction", "Flip Selection"), LOCTEXT("FlipSelectionTooltip", ""), EModifierKey::None, EKeys::T, [this]() { ApplyFlipSelection(); }); } FBox UCubeGridTool::GetWorldSpaceFocusBox() { using namespace CubeGridToolLocals; FAxisAlignedBox3d Bounds = FAxisAlignedBox3d::Empty(); if (bHaveSelection) { FOrientedBox3d FrameSpaceBox = ConvertToOrientedBox(Selection.Box, Selection.Direction); // The resulting oriented box is flat in the z frame direction (which is aligned to Selection.Direction). // So we only need to contain the four corners of a z face instead of doing 8 corners. const FFrame3d& GridFrame = CubeGrid->GetFrame(); int ZFaceIndex = 0; for (int CornerIndex = 0; CornerIndex < 4; ++CornerIndex) { Bounds.Contain(GridFrame.FromFramePoint(FrameSpaceBox.GetCorner(IndexUtil::BoxFaces[ZFaceIndex][CornerIndex]))); } } return (FBox)Bounds; } void UCubeGridTool::StartCornerMode() { using namespace CubeGridToolLocals; if (!bHaveSelection) { // TODO: Write out a message here and clear it at some point return; } if (Mode == EMode::Corner) { return; // Already in mode } // Clear/cancel stuff //if (Mode == EMode::FitGrid) //{ // CancelFitGrid(); //} CurrentExtrudeAmount = 0; InvalidatePreview(); // Clear selected corner render for (int i = 0; i < 4; ++i) { CornerSelectedFlags[i] = false; PreDragCornerSelectedFlags[i] = false; } UpdateCornerGeometrySet(); Mode = EMode::Corner; SetToolPropertySourceEnabled(ToolActions, false); // Customize the tool accept/cancel buttons to the current activity. if (IToolHostCustomizationAPI* ButtonCustomizer = IToolHostCustomizationAPI::Find(GetToolManager()).GetInterface()) { IToolHostCustomizationAPI::FAcceptCancelButtonOverrideParams Params; Params.Label = LOCTEXT("CornerModeActivityLabel","Corner Mode"); Params.OverrideAcceptButtonText = LOCTEXT("DoneButton", "Done"); Params.OverrideAcceptButtonTooltip = LOCTEXT("DoneButtonTooltip", "Apply the current change and exit corner mode."); Params.OverrideCancelButtonText = LOCTEXT("CancelButton", "Cancel"); Params.OverrideCancelButtonTooltip = LOCTEXT("CancelButtonTooltip", "Cancel the current change and exit corner mode."); Params.CanAccept = [this]() { return true; }; Params.OnAcceptCancelTriggered = [this](bool bAccept) { if (bAccept) { if (Mode == EMode::Corner) { ApplyCornerMode(); } else if (Mode == EMode::FitGrid) { //AcceptFitGrid(); } } else { RevertToDefaultMode(); } return FReply::Handled(); }; ButtonCustomizer->RequestAcceptCancelButtonOverride(Params); } Settings->bInCornerMode = true; NotifyOfPropertyChangeByTool(Settings); GetToolManager()->BeginUndoTransaction(ModeChangeTransactionName); GetToolManager()->EmitObjectChange(this, MakeUnique(), ModeChangeTransactionName); GetToolManager()->EndUndoTransaction(); GetToolManager()->DisplayMessage(CornerModeMessage, EToolMessageLevel::UserNotification); } void UCubeGridTool::UpdateCornerGeometrySet() { using namespace CubeGridToolLocals; CornersGeometrySet.Reset(); if (bHaveSelection) { FOrientedBox3d FrameSpaceBox = ConvertToOrientedBox(Selection.Box, Selection.Direction); for (int i = 0; i < 4; ++i) { CornersGeometrySet.AddPoint(i, CubeGrid->GetFrame().FromFramePoint(FrameSpaceBox.GetCorner(i))); } } } void UCubeGridTool::ApplyCornerMode(bool bDontWaitForTick) { using namespace CubeGridToolLocals; if (CurrentExtrudeAmount != 0 && IsAnyCornerSelected(CornerSelectedFlags)) { bWaitingToApplyPreview = true; bBlockUntilPreviewUpdate = true; bAdjustSelectionOnPreviewUpdate = false; if (bDontWaitForTick) { ApplyPreview(); } } CornersGeometrySet.Reset(); Mode = EMode::PushPull; GetToolManager()->DisplayMessage(PushPullModeMessage, EToolMessageLevel::UserNotification); SetToolPropertySourceEnabled(ToolActions, true); ClearViewportButtonCustomization(); Settings->bInCornerMode = false; NotifyOfPropertyChangeByTool(Settings); UpdateCornerModeLineSet(); } void UCubeGridTool::CancelCornerMode() { using namespace CubeGridToolLocals; CornersGeometrySet.Reset(); Mode = EMode::PushPull; GetToolManager()->DisplayMessage(PushPullModeMessage, EToolMessageLevel::UserNotification); SetToolPropertySourceEnabled(ToolActions, true); ClearViewportButtonCustomization(); CurrentExtrudeAmount = 0; InvalidatePreview(); Settings->bInCornerMode = false; NotifyOfPropertyChangeByTool(Settings); UpdateCornerModeLineSet(); } void UCubeGridTool::UpdateUsingMeshChange(const FDynamicMeshChange& MeshChange, bool bRevert) { MeshChange.Apply(CurrentMesh.Get(), bRevert); MeshSpatial->Build(); UpdateComputeInputs(); CurrentExtrudeAmount = 0; bPreviewMayDiffer = true; InvalidatePreview(); } bool UCubeGridTool::IsInDefaultMode() const { return Mode == EMode::PushPull; } bool UCubeGridTool::IsInCornerMode() const { return Mode == EMode::Corner; } void UCubeGridTool::RevertToDefaultMode() { if (Mode == EMode::Corner) { CancelCornerMode(); } else if (Mode == EMode::FitGrid) { //CancelFitGrid(); } } void UCubeGridTool::ClearViewportButtonCustomization() { if (IToolHostCustomizationAPI* ButtonCustomizer = IToolHostCustomizationAPI::Find(GetToolManager()).GetInterface()) { ButtonCustomizer->ClearButtonOverrides(); } } void UCubeGridTool::SetChangesMade(bool bChangesMadeIn) { bChangesMade = bChangesMadeIn; } void UCubeGridTool::SetCurrentMeshTransform(const FTransform& TransformIn) { CurrentMeshTransform = TransformIn; InvalidatePreview(); } // For use by Undo/Redo during corner mode void UCubeGridTool::SetCurrentExtrudeAmount(int32 ExtrudeAmount) { CurrentExtrudeAmount = ExtrudeAmount; InvalidatePreview(); } // For use by Undo/Redo during corner mode void UCubeGridTool::SetCornerSelection(bool CornerSelectedFlagsIn[4]) { for (int i = 0; i < 4; ++i) { CornerSelectedFlags[i] = CornerSelectedFlagsIn[i]; } InvalidatePreview(); } bool UCubeGridTool::CanCurrentlyNestedCancel() { return Mode == EMode::Corner || bHaveSelection; } bool UCubeGridTool::ExecuteNestedCancelCommand() { if (!IsInDefaultMode()) { RevertToDefaultMode(); return true; } else if (bHaveSelection) { ClearSelection(true); return true; } return false; } bool UCubeGridTool::CanCurrentlyNestedAccept() { return Mode == EMode::Corner; } bool UCubeGridTool::ExecuteNestedAcceptCommand() { if (Mode == EMode::Corner) { ApplyCornerMode(); return true; } return false; } #undef LOCTEXT_NAMESPACE