Files
UnrealEngine/Engine/Plugins/Runtime/MeshModelingToolset/Source/ModelingComponents/Public/Selection/MeshTopologySelectionMechanic.h
2025-05-18 13:04:45 +08:00

527 lines
19 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "Drawing/PreviewGeometryActor.h"
#include "Drawing/TriangleSetComponent.h"
#include "InputBehavior.h"
#include "InteractiveTool.h"
#include "Mechanics/RectangleMarqueeMechanic.h"
#include "Components/DynamicMeshComponent.h"
#include "Selection/GroupTopologySelector.h"
#include "TransformTypes.h"
#include "ToolDataVisualizer.h"
#include "InteractionMechanic.h"
#include "MeshTopologySelectionMechanic.generated.h"
#define UE_API MODELINGCOMPONENTS_API
class FMeshTopologySelectionMechanicSelectionChange;
class UPersistentMeshSelection;
class UMouseHoverBehavior;
class UMeshTopologySelectionMechanic;
class URectangleMarqueeMechanic;
class USingleClickOrDragInputBehavior;
using UE::Geometry::FDynamicMeshAABBTree3;
PREDECLARE_USE_GEOMETRY_CLASS(FCompactMaps);
UCLASS(MinimalAPI)
class UMeshTopologySelectionMechanicProperties : public UInteractiveToolPropertySet
{
GENERATED_BODY()
public:
UPROPERTY(EditAnywhere, Category = SelectionFilter)
bool bSelectVertices = true;
UPROPERTY(EditAnywhere, Category = SelectionFilter)
bool bSelectEdges = true;
UPROPERTY(EditAnywhere, Category = SelectionFilter)
bool bSelectFaces = true;
/** When true, will select edge loops. Edge loops are either paths through vertices with 4 edges, or boundaries of holes. */
UPROPERTY(EditAnywhere, Category = SelectionFilter)
bool bSelectEdgeLoops = false;
/** When set, will select rings of edges that are opposite each other across a quad face. */
UPROPERTY(EditAnywhere, Category = SelectionFilter)
bool bSelectEdgeRings = false;
/** When set, faces that face away from the camera are ignored in selection and occlusion. Useful for working with inside-out meshes. */
UPROPERTY(EditAnywhere, Category = AdditionalSelectionOptions, AdvancedDisplay)
bool bHitBackFaces = true;
UPROPERTY(EditAnywhere, Category = AdditionalSelectionOptions, AdvancedDisplay)
bool bEnableMarquee = true;
/** Determines whether vertices should be checked for occlusion in marquee select (Note: marquee select currently only works with edges and vertices) */
UPROPERTY(EditAnywhere, Category = AdditionalSelectionOptions, meta = (EditCondition = "bEnableMarquee", EditConditionHides))
bool bMarqueeIgnoreOcclusion = true;
// The following were originally in their own category, all marked as AdvancedDisplay. However, since there wasn't a non-AdvancedDisplay
// property in the category, they started out as expanded and could not be collapsed.
// The alternative approach, used below, is to have them in a nested category, which starts out as collapsed. This works nicely.
/** Prefer to select an edge projected to a point rather than the point, or a face projected to an edge rather than the edge. */
UPROPERTY(EditAnywhere, Category = "AdditionalSelectionOptions|Ortho Viewport Behavior")
bool bPreferProjectedElement = true;
/** If the closest element is valid, select other elements behind it that are aligned with it. */
UPROPERTY(EditAnywhere, Category = "AdditionalSelectionOptions|Ortho Viewport Behavior")
bool bSelectDownRay = true;
/** Do not check whether the closest element is occluded from the current view. */
UPROPERTY(EditAnywhere, Category = "AdditionalSelectionOptions|Ortho Viewport Behavior")
bool bIgnoreOcclusion = false;
// Used to avoid showing some of the selection filter buttons in triedit (in the detail customization)
bool bDisplayPolygroupReliantControls = true;
// Whether to enable the different selection modes (in the detail customization)
bool bCanSelectVertices = true;
bool bCanSelectEdges = true;
bool bCanSelectFaces = true;
/** Invert current selection. If selection is empty, has same effect as Select All, and is similarly dependent on selection filter. */
UFUNCTION(CallInEditor, Category = SelectionActions)
UE_API void InvertSelection();
/** Select all elements. Depends on selection filter, where vertices are preferred to edges to faces. */
UFUNCTION(CallInEditor, Category = SelectionActions)
UE_API void SelectAll();
void Initialize(UMeshTopologySelectionMechanic* MechanicIn)
{
Mechanic = MechanicIn;
}
protected:
TWeakObjectPtr<UMeshTopologySelectionMechanic> Mechanic;
};
/*
* Selection update type when the marquee rectangle has changed.
*/
UENUM()
enum class EMarqueeSelectionUpdateType
{
OnDrag,
OnTickAndRelease,
OnRelease
};
/**
* Base class mechanic for selecting a subset of mesh elements (edge loops, groups, corners, etc.)
* Internally it relies on an FMeshTopologySelector to define which type of mesh topology is selectable.
*
* NOTE: Users should not use this class directly, but rather subclass it and specify a particular FMeshTopologySelector to use.
*/
UCLASS(MinimalAPI, Abstract)
class UMeshTopologySelectionMechanic : public UInteractionMechanic, public IClickBehaviorTarget, public IHoverBehaviorTarget
{
GENERATED_BODY()
using FFrame3d = UE::Geometry::FFrame3d;
using FAxisAlignedBox3d = UE::Geometry::FAxisAlignedBox3d;
public:
UE_API virtual ~UMeshTopologySelectionMechanic();
// configuration variables that must be set before bSetup is called
bool bAddSelectionFilterPropertiesToParentTool = true;
UE_API void Initialize(const FDynamicMesh3* MeshIn,
FTransform3d TargetTransformIn,
UWorld* WorldIn,
TFunction<FDynamicMeshAABBTree3* ()> GetSpatialSourceFuncIn);
UE_API virtual void Setup(UInteractiveTool* ParentTool) override;
UE_API virtual void Shutdown() override;
UE_API virtual void Render(IToolsContextRenderAPI* RenderAPI) override;
UE_API virtual void DrawHUD(FCanvas* Canvas, IToolsContextRenderAPI* RenderAPI);
UE_API virtual void Tick(float DeltaTime) override;
/**
* Removes the mechanic's own click/hover handlers, which means that the parent tool
* will need to call UpdateSelection(), UpdateHighlight(), ClearHighlight(), and
* ClearSelection() from its own hover/click handlers.
*
* Must be called during tool Setup() after calling Setup() on the mechanic to have an effect.
*
* @param ParentToolIn The parent tool, needed to be able to remove the behaviors.
*/
UE_API void DisableBehaviors(UInteractiveTool* ParentToolIn);
/**
* Enable/disable the selection behaviors in the mechanic without permanently removing them or
* shutting the mechanic down.
* @param bBehaviorsEnabled Determines whether the mechanic selection behaviors are active
* @param bRenderTopology Determines whether the topology is rendered, which is independent
* of the behaviors being active.
*/
UE_API void SetIsEnabled(bool bBehaviorsEnabled, bool bRenderTopology = true);
/**
* Update the location of the mesh after Initialize()
*/
UE_API void SetTransform(const FTransform3d& InTargetTransform);
/**
* Sets how/when the selection updates are handled.
*/
UE_API void SetMarqueeSelectionUpdateType(EMarqueeSelectionUpdateType InType);
/**
* Sets the base priority so that tools can make sure that their own behaviors are higher
* priority. The mechanic will not use any priority value higher than this, but it may use
* lower if it needs to stagger the priorities of behaviors it uses.
* Can be called before or after Setup().
*/
UE_API void SetBasePriority(const FInputCapturePriority& Priority);
/**
* Gets the current priority range used by behaviors in the mechanic. The returned pair will
* have the base (highest) priority as the key, and the lowest priority as the value.
*/
UE_API TPair<FInputCapturePriority, FInputCapturePriority> GetPriorityRange() const;
void SetShouldSelectEdgeLoopsFunc(TFunction<bool(void)> Func)
{
ShouldSelectEdgeLoopsFunc = Func;
}
void SetShouldSelectEdgeRingsFunc(TFunction<bool(void)> Func)
{
ShouldSelectEdgeRingsFunc = Func;
}
/**
* By default, the shift key will cause new clicks to add to the selection. However, this
* can be changed by supplying a different function to check here.
*/
void SetShouldAddToSelectionFunc(TFunction<bool(void)> Func)
{
ShouldAddToSelectionFunc = Func;
}
/**
* By default, the Ctrl key will cause new clicks to remove from the existing selection.
* However, this can be changed by supplying a different function to check here.
*/
void SetShouldRemoveFromSelectionFunc(TFunction<bool(void)> Func)
{
ShouldRemoveFromSelectionFunc = Func;
}
/**
* Notify internal data structures that the associated MeshComponent has been modified.
* @param bTopologyModified if true, the underlying mesh topology has been changed. This clears the current selection.
*/
UE_API void NotifyMeshChanged(bool bTopologyModified);
/**
* Perform a hit test on the topology using the current selection settings. Note that to work properly,
* the mechanic requires that Render() be called on it, as it needs to cache camera state.
* In cases of hitting edges and corners, OutHit contains the following:
* OutHit.FaceIndex: edge or corner id in the topology
* OutHit.ImpactPoint: closest point on the ray to the hit element (Note: not a point on the element!)
* OutHit.Distance: distance along the ray to ImpactPoint
* OutHit.Item: if hit item was an edge, index of the segment within the edge polyline. Otherwise undefined.
*
* @param bUseOrthoSettings If true, the ortho-relevant settings for selection are used (selecting down the view ray, etc)
*/
UE_API bool TopologyHitTest(const FRay& WorldRay, FHitResult& OutHit, FGroupTopologySelection& OutSelection, bool bUseOrthoSettings = false);
UE_API bool TopologyHitTest(const FRay& WorldRay, FHitResult& OutHit, bool bUseOrthoSettings = false);
TSharedPtr<FMeshTopologySelector, ESPMode::ThreadSafe> GetTopologySelector() { return TopoSelector; }
//
// Hover API
//
/**
* Update the hover highlight based on the hit elements at the given World Ray
* @return true if something was hit and is now being hovered
*/
UE_API virtual bool UpdateHighlight(const FRay& WorldRay) PURE_VIRTUAL(UMeshTopologySelectionMechanic::UpdateHighlight, return false; );
/**
* Clear current hover-highlight
*/
UE_API void ClearHighlight();
//
// Selection API
//
/**
* Intersect the ray with the mesh and update the selection based on the hit element, modifier states, etc
* @return true if selection was modified
*/
UE_API virtual bool UpdateSelection(const FRay& WorldRay, FVector3d& LocalHitPositionOut, FVector3d& LocalHitNormalOut) PURE_VIRTUAL(UMeshTopologySelectionMechanic::UpdateSelection, return false; );
/**
* Replace the current selection with an external selection.
* @warning does not check that the selection is valid!
*
* @param bBroadcast If true, issues an OnSelectionChanged delegate broadcast.
*/
UE_API void SetSelection(const FGroupTopologySelection& Selection, bool bBroadcast = true);
/**
* Clear the current selection.
*/
UE_API void ClearSelection();
UE_API void InvertSelection();
UE_API void SelectAll();
UE_DEPRECATED(5.5, "Calling this function on the base class is not supported. It can be called on UPolygonSelectionMechanic.")
void GrowSelection() {};
UE_DEPRECATED(5.5, "Calling this function on the base class is not supported. It can be called on UPolygonSelectionMechanic.")
void ShrinkSelection() {};
UE_DEPRECATED(5.5, "Calling this function on the base class is not supported. It can be called on UPolygonSelectionMechanic.")
void FloodSelection() {};
UE_DEPRECATED(5.5, "Calling this function on the base class is not supported. It can be called on UPolygonSelectionMechanic.")
void ConvertSelectionToBorder() {};
/**
* @return true if the current selection is non-empty
*/
UE_API bool HasSelection() const;
/**
* @return the current selection
*/
const FGroupTopologySelection& GetActiveSelection() const { return PersistentSelection; }
/**
* Can be used by in an OnSelectionChanged event to inspect the clicked location (i.e., the
* values returned by the UpdateSelection() function when the click happened).
*/
UE_API void GetClickedHitPosition(FVector3d& PositionOut, FVector3d& NormalOut) const;
/**
* @return The best-guess 3D frame for the current select
* @param bWorld if true, local-to-world transform of the target MeshComponent is applied to the frame
*/
UE_API FFrame3d GetSelectionFrame(bool bWorld, FFrame3d* InitialLocalFrame = nullptr) const;
/**
* @return Bounding box for the current selection
* @param bWorld if true, the box is in world space, otherwise it is in local space of the target MeshComponent
*/
UE_API FAxisAlignedBox3d GetSelectionBounds(bool bWorld) const;
UE_API void SetShowSelectableCorners(bool bShowCorners);
//
// Change Tracking
//
/**
* Begin a change record. Internally creates a FCommandChange and initializes it with current state
*/
UE_API void BeginChange();
/**
* End the active change and return it. Returns empty change if the selection was not modified!
*/
UE_API TUniquePtr<FToolCommandChange> EndChange();
/**
* Ends the active change and emits it via the parent tool, if the selection has been modified.
*/
UE_API bool EndChangeAndEmitIfModified();
// IClickBehaviorTarget implementation
UE_API virtual FInputRayHit IsHitByClick(const FInputDeviceRay& ClickPos) override;
UE_API virtual void OnClicked(const FInputDeviceRay& ClickPos) override;
// IHoverBehaviorTarget implementation
UE_API virtual FInputRayHit BeginHoverSequenceHitTest(const FInputDeviceRay& PressPos) override;
UE_API virtual void OnBeginHover(const FInputDeviceRay& DevicePos) override;
UE_API virtual bool OnUpdateHover(const FInputDeviceRay& DevicePos) override;
UE_API virtual void OnEndHover() override;
UE_API virtual void OnUpdateModifierState(int ModifierID, bool bIsOn) override;
protected:
// These get bound to marquee mechanic delegates.
UE_API virtual void OnDragRectangleStarted();
UE_API virtual void OnDragRectangleChanged(const FCameraRectangle& CurrentRectangle);
UE_API virtual void OnDragRectangleFinished(const FCameraRectangle& Rectangle, bool bCancelled);
UE_API virtual void UpdateMarqueeEnabled();
public:
/**
* OnSelectionChanged is broadcast whenever the selection is modified (including by FChanges, which
* means that called functions should not issue undo transactions.
*/
FSimpleMulticastDelegate OnSelectionChanged;
/**
* This is broadcast during marquee rectangle dragging if selected faces change, to allow user to
* dynamically update face highlighting if desired (needed because OnSelectionChanged is only
* fired once the rectangle is completed, not while it is being updated).
*/
FSimpleMulticastDelegate OnFaceSelectionPreviewChanged;
/**
* When true, the selection mechanic is currently tracking a marquee rectangle drag, and acting
* on the selection may be unwise until it is over (and an OnSelectionChanged event is fired).
*/
bool IsCurrentlyMarqueeDragging() { return bCurrentlyMarqueeDragging; }
/**
* Render only the MarqueeMechanic, without rendering the current selection
*/
UE_API void RenderMarquee(IToolsContextRenderAPI* RenderAPI);
/**
* Toggle rendering of edges
*/
void SetShowEdges(const bool bRenderEdges) { bShowEdges = bRenderEdges; };
// TODO: Is it worth issuing separate callbacks in normal selection changes and in FChange ones, to
// allow the user to bundle in some FChanges into the normal callback?
UPROPERTY()
TObjectPtr<UMeshTopologySelectionMechanicProperties> Properties;
protected:
//
// Subclass should initialize this with a concrete subclass of FMeshTopologySelector
//
TSharedPtr<FMeshTopologySelector, ESPMode::ThreadSafe> TopoSelector;
bool bIsEnabled = true;
const FDynamicMesh3* Mesh;
TFunction<FDynamicMeshAABBTree3*()> GetSpatialFunc;
UPROPERTY()
TObjectPtr<UMouseHoverBehavior> HoverBehavior;
UPROPERTY()
TObjectPtr<USingleClickOrDragInputBehavior> ClickOrDragBehavior;
UPROPERTY()
TObjectPtr<URectangleMarqueeMechanic> MarqueeMechanic;
/**
* Selection update type (default is OnDrag) as it may not need to be triggered for every rectangle change
* This can drastically improve the responsiveness of the UI for meshes high density meshes.
* - OnDrag: calls HandleRectangleChanged when dragging
* - OnTick: stores a PendingSelection function when dragging and calls it when ticking and on release (if any)
* - OnRelease: stores a PendingSelection function when dragging and calls it on release (if any)
*/
UPROPERTY()
EMarqueeSelectionUpdateType MarqueeSelectionUpdateType = EMarqueeSelectionUpdateType::OnDrag;
FInputCapturePriority BasePriority = FInputCapturePriority(FInputCapturePriority::DEFAULT_TOOL_PRIORITY);
// When bSelectEdgeLoops is true, this function is tested to see if we should select edge loops,
// to allow edge loop selection to be toggled with some key (setting bSelectEdgeLoops to
// false overrides this function).
TFunction<bool(void)> ShouldSelectEdgeLoopsFunc = []() {return true; };
// When bSelectEdgeRings is true, this function is tested to see if we should select edge rings,
// to allow edge ring selection to be toggled with some key (setting bSelectEdgeRings to
// false overrides this function).
TFunction<bool(void)> ShouldSelectEdgeRingsFunc = []() {return true; };
TFunction<bool(void)> ShouldAddToSelectionFunc = [this]() {return bShiftToggle; };
TFunction<bool(void)> ShouldRemoveFromSelectionFunc = [this]() {return bCtrlToggle; };;
FTransform3d TargetTransform;
/** Pending selection function to be called if the selection is deferred to tick/release */
TFunction<void()> PendingSelectionFunction;
/** Calls actual selection using the input marquee rectangle. **/
UE_API void HandleRectangleChanged(const FCameraRectangle& InRectangle);
/**
* Get the topology selector settings to use given the current selection settings.
*
* @param bUseOrthoSettings If true, the topology selector will be configured to use ortho settings,
* which are generally different to allow for selection of projected elements, etc.
*/
UE_API FGroupTopologySelector::FSelectionSettings GetTopoSelectorSettings(bool bUseOrthoSettings = false);
FGroupTopologySelection HilightSelection;
FGroupTopologySelection PersistentSelection;
int32 SelectionTimestamp = 0;
TUniquePtr<FMeshTopologySelectionMechanicSelectionChange> ActiveChange;
// Used for box selection
FGroupTopologySelection PreDragPersistentSelection;
FGroupTopologySelection LastUpdateRectangleSelection;
FGroupTopologySelector::FSelectionSettings PreDragTopoSelectorSettings;
TMap<int32, bool> TriIsOccludedCache;
bool bCurrentlyMarqueeDragging = false;
FVector3d LastClickedHitPosition;
FVector3d LastClickedHitNormal;
/** The actor we create internally to own the DrawnTriangleSetComponent */
UPROPERTY()
TObjectPtr<APreviewGeometryActor> PreviewGeometryActor;
UPROPERTY()
TObjectPtr<UTriangleSetComponent> DrawnTriangleSetComponent;
TSet<int> CurrentlyHighlightedGroups;
UPROPERTY()
TObjectPtr<UMaterialInterface> HighlightedFaceMaterial;
FViewCameraState CameraState;
bool bShiftToggle = false;
bool bCtrlToggle = false;
static const int32 ShiftModifierID = 1;
static const int32 CtrlModifierID = 2;
bool bShowSelectableCorners = true;
bool bShowEdges = true;
public:
FToolDataVisualizer PolyEdgesRenderer;
FToolDataVisualizer HilightRenderer;
FToolDataVisualizer SelectionRenderer;
friend class FMeshTopologySelectionMechanicSelectionChange;
private:
bool bRenderTopology = true;
};
class FMeshTopologySelectionMechanicSelectionChange : public FToolCommandChange
{
public:
FGroupTopologySelection Before;
FGroupTopologySelection After;
int32 Timestamp = 0;
UE_API virtual void Apply(UObject* Object) override;
UE_API virtual void Revert(UObject* Object) override;
UE_API virtual FString ToString() const override;
};
#undef UE_API