// 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 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 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 GetPriorityRange() const; void SetShouldSelectEdgeLoopsFunc(TFunction Func) { ShouldSelectEdgeLoopsFunc = Func; } void SetShouldSelectEdgeRingsFunc(TFunction 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 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 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 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 */ 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 */ 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 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 Properties; protected: // // Subclass should initialize this with a concrete subclass of FMeshTopologySelector // TSharedPtr TopoSelector; bool bIsEnabled = true; const FDynamicMesh3* Mesh; TFunction GetSpatialFunc; UPROPERTY() TObjectPtr HoverBehavior; UPROPERTY() TObjectPtr ClickOrDragBehavior; UPROPERTY() TObjectPtr 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 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 ShouldSelectEdgeRingsFunc = []() {return true; }; TFunction ShouldAddToSelectionFunc = [this]() {return bShiftToggle; }; TFunction ShouldRemoveFromSelectionFunc = [this]() {return bCtrlToggle; };; FTransform3d TargetTransform; /** Pending selection function to be called if the selection is deferred to tick/release */ TFunction 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 ActiveChange; // Used for box selection FGroupTopologySelection PreDragPersistentSelection; FGroupTopologySelection LastUpdateRectangleSelection; FGroupTopologySelector::FSelectionSettings PreDragTopoSelectorSettings; TMap TriIsOccludedCache; bool bCurrentlyMarqueeDragging = false; FVector3d LastClickedHitPosition; FVector3d LastClickedHitNormal; /** The actor we create internally to own the DrawnTriangleSetComponent */ UPROPERTY() TObjectPtr PreviewGeometryActor; UPROPERTY() TObjectPtr DrawnTriangleSetComponent; TSet CurrentlyHighlightedGroups; UPROPERTY() TObjectPtr 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