Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
473 changes: 473 additions & 0 deletions Editor/EditorCore/CutTool.Geometry.cs

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions Editor/EditorCore/CutTool.Geometry.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

263 changes: 263 additions & 0 deletions Editor/EditorCore/CutTool.Rectangle.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,263 @@
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>
/// Create a local coordinate system on the face plane.
/// </summary>
static void GetFacePlaneAxes(Vector3 faceNormal, out Vector3 faceRight, out Vector3 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;
}

/// <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);

Vector3 faceRight, faceUp;
GetFacePlaneAxes(faceNormal, out faceRight, out faceUp);

// Decompose rect diagonals in face space
Vector3 diagonal = end - start;
float rightDot = Vector3.Dot(diagonal, faceRight);
float upDot = Vector3.Dot(diagonal, faceUp);

// Compute all 4 corners strictly on the face plane (coplanar)
Vector3 corner0 = start;
Vector3 corner1 = start + faceRight * rightDot;
Vector3 corner2 = start + faceRight * rightDot + faceUp * upDot;
Vector3 corner3 = start + faceUp * upDot;

// Snap only start point to grid, then project back onto face plane
if (m_SnapToGrid)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

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 end (m_RectEndPoint) was grid-snapped during the drag interaction, it might possess an elevation offset relative to the face's plane. Using corner2 = end incorporates this offset, making corner2 non-coplanar with the other three corners.

Have you considered deriving corner2 strictly on the plane (e.g., start + faceRight * rightDot + faceUp * upDot) and removing this secondary grid-snap block so the resulting shape remains a valid, planar rectangle?

🤖 Helpful? 👍/👎 by bug_hunter

{
Vector3 snapped = ProBuilderSnapping.Snap(corner0, EditorSnapping.activeMoveSnapValue);
Plane facePlane = new Plane(faceNormal, corner0);
corner0 = facePlane.ClosestPointOnPlane(snapped);
corner1 = corner0 + faceRight * rightDot;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

While corner1, corner2, and corner3 are derived properly to form a rectangle, corner0 is snapped to the 3D grid here without being projected back onto the face.

Because all other corners are offset from corner0, the entire rectangle will be translated off the face plane by the 3D snapping offset (unless the face is perfectly grid-aligned). This will cause ProBuilder to generate skewed, non-planar connection polygons.

Have you considered projecting corner0 back onto the face plane after snapping?

if (m_SnapToGrid)
{
    Vector3 snapped = ProBuilderSnapping.Snap(corner0, EditorSnapping.activeMoveSnapValue);
    Plane facePlane = new Plane(faceNormal, corner0);
    corner0 = facePlane.ClosestPointOnPlane(snapped);

    corner1 = corner0 + faceRight * rightDot;
    corner2 = corner0 + faceRight * rightDot + faceUp * upDot;
    corner3 = corner0 + faceUp * upDot;
}

🤖 Helpful? 👍/👎 by bug_hunter

corner2 = corner0 + faceRight * rightDot + faceUp * upDot;
corner3 = corner0 + faceUp * upDot;
}

if (HasSignificantRectangle(corner0, corner2))
{
// Build cut path: 4 corners + close back to start to form a loop
UndoUtility.RecordObject(this, "Rectangle Cut");

m_CurrentPositionNormal = faceNormal;
m_CurrentFace = m_TargetFace;

// Corner 0: run geometry snap so it connects to existing vertices/edges
m_CurrentPosition = corner0;
m_CurrentVertexTypes = VertexTypes.None;
if (m_SnapToGeometry)
CheckPointInMesh();
if (m_CurrentVertexTypes == VertexTypes.None)
m_CurrentVertexTypes = VertexTypes.NewVertex;
corner0 = m_CurrentPosition;
AddCurrentPositionToPath(false);

// Corner 1
m_CurrentPosition = corner1;
m_CurrentVertexTypes = VertexTypes.None;
if (m_SnapToGeometry)
CheckPointInMesh();
if (m_CurrentVertexTypes == VertexTypes.None)
m_CurrentVertexTypes = VertexTypes.NewVertex;
AddCurrentPositionToPath(false);

// Corner 2
m_CurrentPosition = corner2;
m_CurrentVertexTypes = VertexTypes.None;
if (m_SnapToGeometry)
CheckPointInMesh();
if (m_CurrentVertexTypes == VertexTypes.None)
m_CurrentVertexTypes = VertexTypes.NewVertex;
AddCurrentPositionToPath(false);

// Corner 3
m_CurrentPosition = corner3;
m_CurrentVertexTypes = VertexTypes.None;
if (m_SnapToGeometry)
CheckPointInMesh();
if (m_CurrentVertexTypes == VertexTypes.None)
m_CurrentVertexTypes = VertexTypes.NewVertex;
AddCurrentPositionToPath(false);

// Close the loop by returning to the start corner
m_CurrentPosition = corner0;
m_CurrentVertexTypes = VertexTypes.VertexInShape;
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;

// Compute the 4 corners in local space
Vector3 faceNormal = Math.Normal(m_Mesh, m_TargetFace);

Vector3 faceRight, faceUp;
GetFacePlaneAxes(faceNormal, out faceRight, out faceUp);

Vector3 diagonal = m_RectEndPoint - m_RectStartPoint;
float rightDot = Vector3.Dot(diagonal, faceRight);
float upDot = Vector3.Dot(diagonal, faceUp);

Vector3 c0 = trs.TransformPoint(m_RectStartPoint);
Vector3 c1 = trs.TransformPoint(m_RectStartPoint + faceRight * rightDot);
Vector3 c2 = trs.TransformPoint(m_RectEndPoint);
Vector3 c3 = trs.TransformPoint(m_RectStartPoint + faceUp * upDot);

// Draw filled rectangle (reuse cached arrays to avoid per-frame allocation)
Handles.color = k_RectPreviewColor;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Have you considered avoiding allocation of new Vector3[] arrays inside DoRectanglePreview()? Since this method is executed on every Repaint event during drag operations, allocating new arrays per frame generates garbage that can lead to garbage collection stuttering in the Unity editor.

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

m_RectConvexPolygon[0] = c0;
m_RectConvexPolygon[1] = c1;
m_RectConvexPolygon[2] = c2;
m_RectConvexPolygon[3] = c3;
Handles.DrawAAConvexPolygon(m_RectConvexPolygon);

// Draw outline
Handles.color = k_RectOutlineColor;
m_RectPreviewPath[0] = c0;
m_RectPreviewPath[1] = c1;
m_RectPreviewPath[2] = c2;
m_RectPreviewPath[3] = c3;
m_RectPreviewPath[4] = c0;
Handles.DrawAAPolyLine(2f, m_RectPreviewPath);
}

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;
}
}
}
2 changes: 2 additions & 0 deletions Editor/EditorCore/CutTool.Rectangle.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading