Skip to content
Open
Show file tree
Hide file tree
Changes from 7 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
459 changes: 459 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.

234 changes: 234 additions & 0 deletions Editor/EditorCore/CutTool.Rectangle.cs
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)
Comment thread
singam96 marked this conversation as resolved.
Outdated
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)

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

{
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;

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

Hardcoding VertexTypes.NewVertex for the rectangle's corners bypasses the m_SnapToGeometry logic. If a user starts or ends the rectangle exactly on an existing edge or vertex, the cut tool will insert disconnected floating geometry instead of topologically connecting to it.

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);
Comment thread
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;

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

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;
}
}
}
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