// Copyright Epic Games, Inc. All Rights Reserved. #pragma once #include "CoreMinimal.h" #include "BaseBehaviors/BehaviorTargetInterfaces.h" #include "ModelingOperators.h" //IDynamicMeshOperatorFactory #include "InteractiveTool.h" //UInteractiveToolPropertySet #include "InteractiveToolActivity.h" #include "InteractiveToolBuilder.h" //UInteractiveToolBuilder #include "InteractiveToolChange.h" //FToolCommandChange #include "MeshOpPreviewHelpers.h" //FDynamicMeshOpResult #include "Selection/GroupTopologySelector.h" #include "SingleSelectionTool.h" #include "ToolDataVisualizer.h" #include "PolyEditInsertEdgeLoopActivity.generated.h" PREDECLARE_USE_GEOMETRY_CLASS(FDynamicMeshChange); class UPolyEditActivityContext; UENUM() enum class EEdgeLoopPositioningMode { /** Edge loops will be evenly centered within a group. Allows for multiple insertions at a time. */ Even, /** Edge loops will fall at the same length proportion at each edge they intersect (e.g., a quarter way down). */ ProportionOffset, /** Edge loops will fall a constant distance away from the start of each edge they intersect (e.g., 20 units down). Clamps to end if edge is too short. */ DistanceOffset }; UENUM() enum class EEdgeLoopInsertionMode { /** Existing groups will be deleted and new triangles will be created for the new groups. Keeps topology simple but breaks non-planar groups. */ Retriangulate, /** Keeps existing triangles and cuts them to create a new path. May result in fragmented triangles over time.*/ PlaneCut }; UCLASS() class MESHMODELINGTOOLS_API UEdgeLoopInsertionProperties : public UInteractiveToolPropertySet { GENERATED_BODY() public: /** Determines how edge loops position themselves vertically relative to loop direction. */ UPROPERTY(EditAnywhere, Category = InsertEdgeLoop) EEdgeLoopPositioningMode PositionMode = EEdgeLoopPositioningMode::ProportionOffset; /** Determines how edge loops are added to the geometry */ UPROPERTY(EditAnywhere, Category = InsertEdgeLoop) EEdgeLoopInsertionMode InsertionMode = EEdgeLoopInsertionMode::PlaneCut; /** How many loops to insert at a time. Only used with "even" positioning mode. */ UPROPERTY(EditAnywhere, Category = InsertEdgeLoop, meta = (UIMin = "0", UIMax = "20", ClampMin = "0", ClampMax = "500", EditCondition = "PositionMode == EEdgeLoopPositioningMode::Even", EditConditionHides)) int32 NumLoops = 1; UPROPERTY(EditAnywhere, Category = InsertEdgeLoop, AdvancedDisplay, meta = (UIMin = "0", UIMax = "1.0", ClampMin = "0", ClampMax = "1.0", EditCondition = "PositionMode == EEdgeLoopPositioningMode::ProportionOffset && !bInteractive", EditConditionHides)) double ProportionOffset = 0.5; UPROPERTY(EditAnywhere, Category = InsertEdgeLoop, AdvancedDisplay, meta = (UIMin = "0", ClampMin = "0", EditCondition = "PositionMode == EEdgeLoopPositioningMode::DistanceOffset && !bInteractive", EditConditionHides)) double DistanceOffset = 10.0; /** When false, the distance/proportion offset is numerically specified, and mouse clicks just choose the edge. */ UPROPERTY(EditAnywhere, Category = InsertEdgeLoop, AdvancedDisplay, meta = ( EditCondition = "PositionMode != EEdgeLoopPositioningMode::Even", EditConditionHides)) bool bInteractive = true; /** Measure the distance offset from the opposite side of the edges. */ UPROPERTY(EditAnywhere, Category = InsertEdgeLoop, meta = ( EditCondition = "PositionMode == EEdgeLoopPositioningMode::DistanceOffset", EditConditionHides)) bool bFlipOffsetDirection = false; /** When true, non-quad-like groups that stop the loop will be highlighted, with X's marking the corners. */ UPROPERTY(EditAnywhere, Category = InsertEdgeLoop) bool bHighlightProblemGroups = true; /** How close a new loop edge needs to pass next to an existing vertex to use that vertex rather than creating a new one. */ UPROPERTY(EditAnywhere, Category = InsertEdgeLoop, AdvancedDisplay, meta = (UIMin = "0", UIMax = "0.01", ClampMin = "0", ClampMax = "10")) double VertexTolerance = 0.001; }; /** Interactive activity for inserting (group) edge loops into a mesh. */ UCLASS() class MESHMODELINGTOOLS_API UPolyEditInsertEdgeLoopActivity : public UInteractiveToolActivity, public UE::Geometry::IDynamicMeshOperatorFactory, public IHoverBehaviorTarget, public IClickBehaviorTarget { GENERATED_BODY() public: friend class FEdgeLoopInsertionChange; UPolyEditInsertEdgeLoopActivity() {}; virtual void OnPropertyModified(UObject* PropertySet, FProperty* Property); // IInteractiveToolActivity virtual void Setup(UInteractiveTool* ParentTool) override; virtual void Shutdown(EToolShutdownType ShutdownType) override; virtual bool CanStart() const override; virtual EToolActivityStartResult Start() override; virtual bool IsRunning() const override { return bIsRunning; } virtual bool CanAccept() const override; virtual EToolActivityEndResult End(EToolShutdownType) override; virtual void Render(IToolsContextRenderAPI* RenderAPI) override; virtual void Tick(float DeltaTime) override; // IDynamicMeshOperatorFactory virtual TUniquePtr MakeNewOperator() override; // IClickBehaviorTarget virtual FInputRayHit IsHitByClick(const FInputDeviceRay& ClickPos) override; virtual void OnClicked(const FInputDeviceRay& ClickPos) override; // IHoverBehaviorTarget virtual FInputRayHit BeginHoverSequenceHitTest(const FInputDeviceRay& PressPos) override; virtual void OnBeginHover(const FInputDeviceRay& DevicePos) override {} virtual bool OnUpdateHover(const FInputDeviceRay& DevicePos) override; virtual void OnEndHover() override; protected: UPROPERTY() TObjectPtr Settings = nullptr; UPROPERTY() TObjectPtr ActivityContext; bool bIsRunning = false; FTransform TargetTransform; TSharedPtr TopologySelector; TArray> PreviewEdges; // Used to highlight problematic topology (non-quad groups) when it stops a loop. TArray> ProblemTopologyEdges; TArray ProblemTopologyVerts; FViewCameraState CameraState; FToolDataVisualizer PreviewEdgeRenderer; FToolDataVisualizer ProblemTopologyRenderer; FGroupTopologySelector::FSelectionSettings TopologySelectorSettings; float ProblemVertTickWidth = 8; void SetupPreview(); FInputRayHit HitTest(const FRay& WorldRay); bool UpdateHoveredItem(const FRay& WorldRay); // Safe inputs for the background compute to use, untouched by undo/redo/other CurrentMesh updates. TSharedPtr ComputeStartMesh; TSharedPtr ComputeStartTopology; void UpdateComputeInputs(); void ConditionallyUpdatePreview(int32 NewGroupID, double* NewInputLength = nullptr); void ClearPreview(); // Taken from user interaction, read as inputs by the op factory int32 InputGroupEdgeID = FDynamicMesh3::InvalidID; double InteractiveInputLength = 0; // On valid clicks, we wait to finish the background op and apply it before taking more input. // Gets reset OnTick when the result is ready. bool bWaitingForInsertionCompletion = false; // Copied over on op completion bool bLastComputeSucceeded = false; TSharedPtr LatestOpTopologyResult; TSharedPtr, ESPMode::ThreadSafe> LatestOpChangedTids; };