using System.Collections.Generic; namespace UnityEngine.ProBuilder { /// /// Functions for picking mesh elements in a view. Can either render a texture to test, or cast a ray. Prefer this over calling SelectionPickerRenderer directly. /// public static class SelectionPicker { /// /// Pick the vertex indexes contained within a rect. /// /// /// Rect is in GUI space, where 0,0 is top left of screen, width = cam.pixelWidth / pointsPerPixel. /// The objects to hit test. /// Culling options. /// Scale the render texture to match rect coordinates. Generally you'll just pass in EditorGUIUtility.pointsPerPixel. /// A dictionary of ProBuilderMesh and sharedIndexes that are in the selection rect. To get triangle indexes access the pb.sharedIndexes[index] array. public static Dictionary> PickVerticesInRect( Camera cam, Rect rect, IList selectable, PickerOptions options, float pixelsPerPoint = 1f) { if (options.depthTest) { return SelectionPickerRenderer.PickVerticesInRect( cam, rect, selectable, true, (int)(cam.pixelWidth / pixelsPerPoint), (int)(cam.pixelHeight / pixelsPerPoint)); } // while the selectionpicker render path supports no depth test picking, it's usually faster to skip // the render. also avoids issues with vertex billboards obscuring one another. var selected = new Dictionary>(); foreach (var pb in selectable) { if (!pb.selectable) continue; SharedVertex[] sharedIndexes = pb.sharedVerticesInternal; HashSet inRect = new HashSet(); Vector3[] positions = pb.positionsInternal; var trs = pb.transform; float pixelHeight = cam.pixelHeight; for (int n = 0; n < sharedIndexes.Length; n++) { Vector3 v = trs.TransformPoint(positions[sharedIndexes[n][0]]); Vector3 p = cam.WorldToScreenPoint(v); if (p.z < cam.nearClipPlane) continue; p.x /= pixelsPerPoint; p.y = (pixelHeight - p.y) / pixelsPerPoint; if (rect.Contains(p)) inRect.Add(n); } selected.Add(pb, inRect); } return selected; } /// /// Pick faces contained within rect. /// /// /// Rect is in GUI space, where 0,0 is top left of screen, width = cam.pixelWidth / pointsPerPixel. /// /// /// Scale the render texture to match rect coordinates. Generally you'll just pass in EditorGUIUtility.pixelsPerPoint. /// public static Dictionary> PickFacesInRect( Camera cam, Rect rect, IList selectable, PickerOptions options, float pixelsPerPoint = 1f) { if (options.depthTest && options.rectSelectMode == RectSelectMode.Partial) { return SelectionPickerRenderer.PickFacesInRect( cam, rect, selectable, (int)(cam.pixelWidth / pixelsPerPoint), (int)(cam.pixelHeight / pixelsPerPoint)); } var selected = new Dictionary>(); foreach (var pb in selectable) { if (!pb.selectable) continue; HashSet selectedFaces = new HashSet(); Transform trs = pb.transform; Vector3[] positions = pb.positionsInternal; Vector3[] screenPoints = new Vector3[pb.vertexCount]; for (int nn = 0; nn < pb.vertexCount; nn++) screenPoints[nn] = cam.ScreenToGuiPoint(cam.WorldToScreenPoint(trs.TransformPoint(positions[nn])), pixelsPerPoint); for (int n = 0; n < pb.facesInternal.Length; n++) { Face face = pb.facesInternal[n]; // rect select = complete if (options.rectSelectMode == RectSelectMode.Complete) { // face is behind the camera if (screenPoints[face.indexesInternal[0]].z < cam.nearClipPlane) continue; // only check the first index per quad, and if it checks out, then check every other point if (rect.Contains(screenPoints[face.indexesInternal[0]])) { bool nope = false; for (int q = 1; q < face.distinctIndexesInternal.Length; q++) { int index = face.distinctIndexesInternal[q]; if (screenPoints[index].z < cam.nearClipPlane || !rect.Contains(screenPoints[index])) { nope = true; break; } } if (!nope) { if (!options.depthTest || !HandleUtility.PointIsOccluded(cam, pb, trs.TransformPoint(Math.Average(positions, face.distinctIndexesInternal)))) { selectedFaces.Add(face); } } } } // rect select = partial else { Bounds2D poly = new Bounds2D(screenPoints, face.edgesInternal); bool overlaps = false; if (poly.Intersects(rect)) { // if rect contains one point of polygon, it overlaps for (int nn = 0; nn < face.distinctIndexesInternal.Length && !overlaps; nn++) { Vector3 p = screenPoints[face.distinctIndexesInternal[nn]]; overlaps = p.z > cam.nearClipPlane && rect.Contains(p); } // if polygon contains one point of rect, it overlaps. otherwise check for edge intersections if (!overlaps) { Vector2 tl = new Vector2(rect.xMin, rect.yMax); Vector2 tr = new Vector2(rect.xMax, rect.yMax); Vector2 bl = new Vector2(rect.xMin, rect.yMin); Vector2 br = new Vector2(rect.xMax, rect.yMin); overlaps = Math.PointInPolygon(screenPoints, poly, face.edgesInternal, tl); if (!overlaps) overlaps = Math.PointInPolygon(screenPoints, poly, face.edgesInternal, tr); if (!overlaps) overlaps = Math.PointInPolygon(screenPoints, poly, face.edgesInternal, br); if (!overlaps) overlaps = Math.PointInPolygon(screenPoints, poly, face.edgesInternal, bl); // if any polygon edge intersects rect for (int nn = 0; nn < face.edgesInternal.Length && !overlaps; nn++) { if (Math.GetLineSegmentIntersect(tr, tl, screenPoints[face.edgesInternal[nn].a], screenPoints[face.edgesInternal[nn].b])) overlaps = true; else if (Math.GetLineSegmentIntersect(tl, bl, screenPoints[face.edgesInternal[nn].a], screenPoints[face.edgesInternal[nn].b])) overlaps = true; else if (Math.GetLineSegmentIntersect(bl, br, screenPoints[face.edgesInternal[nn].a], screenPoints[face.edgesInternal[nn].b])) overlaps = true; else if (Math.GetLineSegmentIntersect(br, tl, screenPoints[face.edgesInternal[nn].a], screenPoints[face.edgesInternal[nn].b])) overlaps = true; } } } // don't test occlusion since that case is handled special if (overlaps) selectedFaces.Add(face); } } selected.Add(pb, selectedFaces); } return selected; } /// /// Pick the edges contained within a rect. /// /// /// Rect is in GUI space, where 0,0 is top left of screen, width = cam.pixelWidth / pointsPerPixel. /// The ProBuilder mesh objects to consider when hit testing. /// Culling options. /// Scale the render texture to match rect coordinates. Generally you'll just pass in EditorGUIUtility.pointsPerPixel. /// A dictionary of ProBuilderMesh and edges that are in the selection rect. public static Dictionary> PickEdgesInRect( Camera cam, Rect rect, IList selectable, PickerOptions options, float pixelsPerPoint = 1f) { if (options.depthTest && options.rectSelectMode == RectSelectMode.Partial) { return SelectionPickerRenderer.PickEdgesInRect( cam, rect, selectable, true, (int)(cam.pixelWidth / pixelsPerPoint), (int)(cam.pixelHeight / pixelsPerPoint)); } var selected = new Dictionary>(); foreach (var pb in selectable) { if (!pb.selectable) continue; Transform trs = pb.transform; var selectedEdges = new HashSet(); for (int i = 0, fc = pb.faceCount; i < fc; i++) { var edges = pb.facesInternal[i].edgesInternal; for (int n = 0, ec = edges.Length; n < ec; n++) { var edge = edges[n]; var posA = trs.TransformPoint(pb.positionsInternal[edge.a]); var posB = trs.TransformPoint(pb.positionsInternal[edge.b]); Vector3 a = cam.ScreenToGuiPoint(cam.WorldToScreenPoint(posA), pixelsPerPoint); Vector3 b = cam.ScreenToGuiPoint(cam.WorldToScreenPoint(posB), pixelsPerPoint); switch (options.rectSelectMode) { case RectSelectMode.Complete: { // if either of the positions are clipped by the camera we cannot possibly select both, skip it if ((a.z < cam.nearClipPlane || b.z < cam.nearClipPlane)) continue; if (rect.Contains(a) && rect.Contains(b)) { if (!options.depthTest || !HandleUtility.PointIsOccluded(cam, pb, (posA + posB) * .5f)) selectedEdges.Add(edge); } break; } case RectSelectMode.Partial: { // partial + depth test is covered earlier if (Math.RectIntersectsLineSegment(rect, a, b)) selectedEdges.Add(edge); break; } } } } selected.Add(pb, selectedEdges); } return selected; } /// /// Returns the first hit face on a ProBuilder mesh given a screen position and camera. /// /// The camera to use when calculating the raycast. /// The screen position to use when calculating the raycast. /// The ProBuilderMesh to test for ray/face intersection. /// A Face if successful, null if the hit test failed. public static Face PickFace(Camera camera, Vector3 mousePosition, ProBuilderMesh pickable) { Ray ray = camera.ScreenPointToRay(mousePosition); RaycastHit hit; if (HandleUtility.FaceRaycast(ray, pickable, out hit, Mathf.Infinity, CullingMode.Back)) return pickable.facesInternal[hit.face]; return null; } } }