-
Notifications
You must be signed in to change notification settings - Fork 92
Enhance cut tool functionality and code refactor #669
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from 7 commits
df84778
93988b9
6109554
b8d9edf
2bbf078
9919da7
418dcf3
7df978a
472f697
7a2a64f
0ad2382
8c68e31
df2a0ee
1bfe346
b72e564
1229bc4
6c407e9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,234 @@ | ||
| using System; | ||
| using System.Collections.Generic; | ||
| using System.Linq; | ||
| using UnityEditor.EditorTools; | ||
| using UnityEngine; | ||
| using UnityEngine.ProBuilder; | ||
| using UnityEngine.ProBuilder.MeshOperations; | ||
| using UnityEngine.UIElements; | ||
| using Cursor = UnityEngine.Cursor; | ||
| using Edge = UnityEngine.ProBuilder.Edge; | ||
| using Math = UnityEngine.ProBuilder.Math; | ||
| using UObject = UnityEngine.Object; | ||
| using RaycastHit = UnityEngine.ProBuilder.RaycastHit; | ||
| using UHandleUtility = UnityEditor.HandleUtility; | ||
|
|
||
| using ToolManager = UnityEditor.EditorTools.ToolManager; | ||
| using Vertex = UnityEngine.ProBuilder.Vertex; | ||
|
|
||
| namespace UnityEditor.ProBuilder | ||
| { | ||
| partial class CutTool | ||
| { | ||
| /// <summary> | ||
| /// Rectangle mode: click and drag to define a rectangular cut on the face. | ||
| /// On mouse up, auto-places the 4 corners and executes the cut. | ||
| /// </summary> | ||
| void DoRectanglePlacement(EditorWindow window) | ||
| { | ||
| Event evt = Event.current; | ||
| EventType evtType = evt.type; | ||
|
|
||
| m_SnappingPoint = m_SnapToGeometry || (evt.modifiers & EventModifiers.Control) != 0; | ||
| m_ModifyingPoint = false; | ||
|
|
||
| bool hasHitPosition = UpdateHitPosition(); | ||
|
|
||
| // Visual helpers | ||
| if (evtType == EventType.Repaint) | ||
| { | ||
| if (hasHitPosition && IsCursorInSceneView(window)) | ||
| { | ||
| m_CurrentCutCursor = m_CutCursorTexture; | ||
| m_CurrentHandleColor = k_HandleColorAddNewVertex; | ||
| } | ||
| else | ||
| { | ||
| m_CurrentCutCursor = null; | ||
| m_CurrentPosition = Vector3.positiveInfinity; | ||
| } | ||
| } | ||
|
|
||
| // Mouse down: start rectangle drag | ||
| if (hasHitPosition | ||
| && evtType == EventType.MouseDown && evt.button == 0 | ||
| && HandleUtility.nearestControl == m_ControlId | ||
| && !m_RectDragging) | ||
| { | ||
| m_RectDragging = true; | ||
| m_RectStartPoint = m_CurrentPosition; | ||
| m_RectEndPoint = m_CurrentPosition; | ||
| m_TargetFace = m_CurrentFace; | ||
|
|
||
| var edges = m_TargetFace.edges; | ||
| m_SelectedVertices = edges.Select(e => e.a).ToArray(); | ||
| m_SelectedEdges = edges.ToArray(); | ||
|
|
||
| m_CutPath.Clear(); | ||
| m_MeshConnections.Clear(); | ||
| evt.Use(); | ||
| } | ||
|
|
||
| // Mouse drag: update rectangle end point | ||
| if (m_RectDragging && evtType == EventType.MouseDrag && evt.button == 0) | ||
| { | ||
| if (hasHitPosition && m_CurrentFace == m_TargetFace) | ||
| { | ||
| m_RectEndPoint = m_CurrentPosition; | ||
| } | ||
| evt.Use(); | ||
| } | ||
|
|
||
| // Mouse up: finalize the rectangle and execute cut | ||
| if (m_RectDragging | ||
| && (evtType == EventType.MouseUp && evt.button == 0)) | ||
| { | ||
| m_RectDragging = false; | ||
|
|
||
| // Project start/end onto the face plane to compute the other 2 corners | ||
| Vector3 start = m_RectStartPoint; | ||
| Vector3 end = m_RectEndPoint; | ||
|
|
||
| // Compute face normal for projection | ||
| Vector3 faceNormal = Math.Normal(m_Mesh, m_TargetFace); | ||
|
|
||
| // Create a local coordinate system on the face plane | ||
| Vector3 faceRight, faceUp; | ||
| if (Mathf.Abs(Vector3.Dot(faceNormal, Vector3.up)) > 0.99f) | ||
| faceRight = Vector3.Cross(faceNormal, Vector3.forward).normalized; | ||
| else | ||
| faceRight = Vector3.Cross(faceNormal, Vector3.up).normalized; | ||
| faceUp = Vector3.Cross(faceNormal, faceRight).normalized; | ||
|
|
||
| // Decompose rect diagonals in face space | ||
| Vector3 diagonal = end - start; | ||
| float rightDot = Vector3.Dot(diagonal, faceRight); | ||
| float upDot = Vector3.Dot(diagonal, faceUp); | ||
|
|
||
| // Compute the 4 rectangle corners in world space | ||
| Vector3 corner0 = start; | ||
| Vector3 corner1 = start + faceRight * rightDot; | ||
| Vector3 corner2 = end; | ||
| Vector3 corner3 = start + faceUp * upDot; | ||
|
|
||
| // Snap all corners to grid if enabled | ||
| if (m_SnapToGrid) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Applying grid snapping independently to all four corners will distort the cut into an irregular, non-planar quadrilateral if the target face isn't perfectly axis-aligned. Additionally, because Have you considered deriving 🤖 Helpful? 👍/👎 by bug_hunter |
||
| { | ||
| corner0 = ProBuilderSnapping.Snap(corner0, EditorSnapping.activeMoveSnapValue); | ||
| corner1 = ProBuilderSnapping.Snap(corner1, EditorSnapping.activeMoveSnapValue); | ||
| corner2 = ProBuilderSnapping.Snap(corner2, EditorSnapping.activeMoveSnapValue); | ||
| corner3 = ProBuilderSnapping.Snap(corner3, EditorSnapping.activeMoveSnapValue); | ||
| } | ||
|
|
||
| if (HasSignificantRectangle(corner0, corner2)) | ||
| { | ||
| // Build cut path: 4 corners + close back to start to form a loop | ||
| UndoUtility.RecordObject(this, "Rectangle Cut"); | ||
|
|
||
| m_CurrentPosition = corner0; | ||
| m_CurrentPositionNormal = faceNormal; | ||
| m_CurrentVertexTypes = VertexTypes.NewVertex; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hardcoding Have you considered updating the point state dynamically before adding it to the path, similar to point mode? For example: m_CurrentPosition = corner0;
m_CurrentPositionNormal = faceNormal;
m_CurrentFace = m_TargetFace;
CheckPointInMesh();
corner0 = m_CurrentPosition; // Save potentially snapped position for closing the loop
AddCurrentPositionToPath(false);🤖 Helpful? 👍/👎 by bug_hunter |
||
| m_CurrentFace = m_TargetFace; | ||
| AddCurrentPositionToPath(false); | ||
|
|
||
| m_CurrentPosition = corner1; | ||
| m_CurrentPositionNormal = faceNormal; | ||
| m_CurrentVertexTypes = VertexTypes.NewVertex; | ||
| AddCurrentPositionToPath(false); | ||
|
|
||
| m_CurrentPosition = corner2; | ||
| m_CurrentPositionNormal = faceNormal; | ||
| m_CurrentVertexTypes = VertexTypes.NewVertex; | ||
| AddCurrentPositionToPath(false); | ||
|
|
||
| m_CurrentPosition = corner3; | ||
| m_CurrentPositionNormal = faceNormal; | ||
| m_CurrentVertexTypes = VertexTypes.NewVertex; | ||
| AddCurrentPositionToPath(false); | ||
|
|
||
| // Close the loop by returning to the start corner | ||
| m_CurrentPosition = corner0; | ||
| m_CurrentPositionNormal = faceNormal; | ||
| m_CurrentVertexTypes = VertexTypes.VertexInShape; | ||
| m_CurrentFace = m_TargetFace; | ||
| AddCurrentPositionToPath(false); | ||
|
|
||
| // Don't auto-execute—let user click Complete button like point mode | ||
| RebuildCutShape(false); | ||
| } | ||
|
|
||
| m_RectStartPoint = Vector3.positiveInfinity; | ||
| m_RectEndPoint = Vector3.positiveInfinity; | ||
| evt.Use(); | ||
| } | ||
|
|
||
| if (TryPassThroughSelection(window, hasHitPosition)) | ||
| return; | ||
| } | ||
|
|
||
| bool HasSignificantRectangle(Vector3 start, Vector3 end) | ||
| { | ||
| return Vector3.Distance(start, end) > 0.001f; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Draw the rectangle preview during a drag operation. | ||
| /// </summary> | ||
| void DoRectanglePreview() | ||
| { | ||
| if (!m_RectDragging || m_Mesh == null || m_TargetFace == null) | ||
| return; | ||
|
|
||
| Transform trs = m_Mesh.transform; | ||
| Vector3 startW = trs.TransformPoint(m_RectStartPoint); | ||
|
singam96 marked this conversation as resolved.
Outdated
|
||
| Vector3 endW = trs.TransformPoint(m_RectEndPoint); | ||
|
|
||
| // Compute face plane | ||
| Vector3 faceNormal = Math.Normal(m_Mesh, m_TargetFace); | ||
|
|
||
| Vector3 faceRight, faceUp; | ||
| if (Mathf.Abs(Vector3.Dot(faceNormal, Vector3.up)) > 0.99f) | ||
| faceRight = Vector3.Cross(faceNormal, Vector3.forward).normalized; | ||
| else | ||
| faceRight = Vector3.Cross(faceNormal, Vector3.up).normalized; | ||
| faceUp = Vector3.Cross(faceNormal, faceRight).normalized; | ||
|
|
||
| Vector3 diagonal = endW - startW; | ||
| float rightDot = Vector3.Dot(diagonal, faceRight); | ||
| float upDot = Vector3.Dot(diagonal, faceUp); | ||
|
|
||
| Vector3 c0 = startW; | ||
| Vector3 c1 = startW + faceRight * rightDot; | ||
| Vector3 c2 = endW; | ||
| Vector3 c3 = startW + faceUp * upDot; | ||
|
|
||
| // Draw filled rectangle | ||
| Handles.color = k_RectPreviewColor; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Have you considered avoiding allocation of new You can declare reusable fields on the class level to avoid allocation completely: private readonly Vector3[] m_RectConvexPolygon = new Vector3[4];
private readonly Vector3[] m_RectPreviewPath = new Vector3[5];And then populate and pass them to the Handles methods. 🤖 Helpful? 👍/👎 by guardian |
||
| Handles.DrawAAConvexPolygon(new Vector3[] { c0, c1, c2, c3 }); | ||
|
|
||
| // Draw outline | ||
| Handles.color = k_RectOutlineColor; | ||
| Handles.DrawAAPolyLine(2f, new Vector3[] { c0, c1, c2, c3, c0 }); | ||
| } | ||
|
|
||
| bool TryPassThroughSelection(EditorWindow window, bool hasHitPosition) | ||
| { | ||
| if (m_CutPath.Count != 0 | ||
| || hasHitPosition | ||
| || HandleUtility.nearestControl != m_ControlId) | ||
| { | ||
| return false; | ||
| } | ||
|
|
||
| SceneView sceneView = window as SceneView; | ||
| if (sceneView == null) | ||
| sceneView = SceneView.lastActiveSceneView; | ||
|
|
||
| if (sceneView == null || ProBuilderEditor.instance == null) | ||
| return false; | ||
|
|
||
| ProBuilderEditor.instance.HandleMouseEvent(sceneView, m_ControlId); | ||
| return true; | ||
| } | ||
| } | ||
| } | ||
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Uh oh!
There was an error while loading. Please reload this page.