TBC Macros and Extensions

 View Only
Expand all | Collapse all

how to retrieve the location where an entity was selected on-screen using a rectangle/polygon selection - in a 3DView

  • 1.  how to retrieve the location where an entity was selected on-screen using a rectangle/polygon selection - in a 3DView

    Posted 21 days ago

    Using a Wpf.Memberselection it works in a 2D-View with MostRecentPickIntersections.

    That gives back a list with the entity serials numbers and a sub-list with all the intersection points where the selection polygon crossed the entity. As Point3D's, without elevation information, but that's ok for my purpose.

    But that one fails completely in a 3D-View. The whole list is empty. So, how to achieve the same in a 3D-View?

    I'd like to polygon-select multiple lines in 3D-View and find out which end of each line was crossed, to know on which end the user i.e. wants to extend them.

    A Wpf.SelectEntity has PickPointProjected, that gives back a 3D location, even in 3D View, but is only useful for one entity at a time.



    ------------------------------
    Ronny Schneider
    ------------------------------


  • 2.  RE: how to retrieve the location where an entity was selected on-screen using a rectangle/polygon selection - in a 3DView

    Posted 19 days ago
    Why MostRecentPickIntersections fails in 3D

    Looking at the source code in SelectWithMousePolicy.cs (line 539-573), the intersection logic works by:
    1. Getting the PickAperature and calling aperture.ComputePolySeg() to get the selection fence as a PolySeg
    2. Transforming it to UCS via sendingView.WorldToUCS
    3. For each selected entity that implements IPolyseg, calling pickwindowpolyseg.Intersect(entityPolyseg, true, ints)
    The problem: The HOOPS 3D PickAperature (Trimble.Vce.Hoops.PickAperature) does not implement IPolyseg and has no ComputePolySeg() method. So in a 3D view, the aperture cannot produce a polyseg, and the intersection list stays empty.
    However, the selection itself still works - the 3D view does return the serial numbers of selected entities through GlobalSelection.SelectedMembers. Only the intersection point data is missing.

    Implementation Strategy
    After the MemberSelection completes in 3D, you have:
    • ✅ The list of selected entity serial numbers
    • ✅ Access to each entity's geometry via IPolyseg.ComputePolySeg()
    • ✅ The user's fence polygon vertices (from the PickAperature.ModelAperatureLocation or stored pick points)
    • ❌ No intersection points (empty list)z
    You can replicate the intersection logic yourself:
    using Trimble.Vce.Geometry;
    using Trimble.Vce.Geometry.PolySeg;
    using Trimble.Vce.Interfaces.SnapIn;
    
    /// <summary>
    /// Computes fence-polygon crossings for selected entities in a 3D view,
    /// replicating what MostRecentPickIntersections does in 2D.
    /// </summary>
    public class FenceCrossingCalculator
    {
        private readonly IProject _project;
    
        public FenceCrossingCalculator(IProject project)
        {
            _project = project;
        }
    
        /// <summary>
        /// Given the user's fence polygon points and the selected entity serial numbers,
        /// computes which end of each line entity was crossed by the fence.
        /// </summary>
        /// <param name="fencePoints">
        /// The user's selection polygon vertices in model/world coordinates.
        /// Collect these from the MemberSelection's pick points or the PickAperature.
        /// </param>
        /// <param name="selectedSerials">
        /// Serial numbers of entities selected by the MemberSelection.
        /// </param>
        /// <returns>
        /// For each entity: its serial number, the intersection points, and which end was crossed.
        /// </returns>
        public IEnumerable<EntityCrossingResult> ComputeCrossings(
            IList<Point3D> fencePoints,
            IEnumerable<uint> selectedSerials)
        {
            // 1. Build a PolySeg from the fence polygon
            var fencePolySeg = BuildFencePolySeg(fencePoints);
            if (fencePolySeg == null)
                yield break;
    
            // 2. For each selected entity, compute intersections
            foreach (var serial in selectedSerials)
            {
                var snapIn = _project.Concordance.Lookup(serial);
                if (snapIn is IPolyseg polysegProvider == false)
                    continue;
    
                var entityPolySeg = polysegProvider.ComputePolySeg();
                if (entityPolySeg == null)
                    continue;
    
                // 3. Compute intersections between the fence and the entity
                var intersections = new Intersections();
                fencePolySeg.Intersect(entityPolySeg, true, intersections);
    
                if (intersections.Count == 0)
                    continue;
    
                // 4. Determine which end was crossed
                var startPoint = entityPolySeg.StartPoint;
                var endPoint = entityPolySeg.EndPoint;
    
                var crossedEnd = DetermineClosestEnd(intersections, startPoint, endPoint);
    
                yield return new EntityCrossingResult
                {
                    SerialNumber = serial,
                    Intersections = intersections,
                    CrossedEnd = crossedEnd,
                    ClosestIntersectionPoint = GetClosestIntersection(intersections, crossedEnd, startPoint, endPoint)
                };
            }
        }
    
        /// <summary>
        /// Builds a closed PolySeg from the fence polygon points.
        /// </summary>
        private PolySeg BuildFencePolySeg(IList<Point3D> fencePoints)
        {
            if (fencePoints == null || fencePoints.Count < 3)
                return null;
    
            var polyseg = new PolySeg();
    
            // Add the polygon points. Project to 2D (XY plane) for intersection math
            // since PolySeg.Intersect works in 2D.
            foreach (var pt in fencePoints)
            {
                polyseg.Add(new Point3D(pt.X, pt.Y, 0.0));
            }
    
            // Close the polygon by adding the first point again
            if (fencePoints[0].X != fencePoints[fencePoints.Count - 1].X ||
                fencePoints[0].Y != fencePoints[fencePoints.Count - 1].Y)
            {
                polyseg.Add(new Point3D(fencePoints[0].X, fencePoints[0].Y, 0.0));
            }
    
            return polyseg;
        }
    
        /// <summary>
        /// Determines which end of the entity is closest to the first intersection point.
        /// </summary>
        private CrossedEndType DetermineClosestEnd(
            Intersections intersections,
            Point3D startPoint,
            Point3D endPoint)
        {
            // Use the first intersection point
            var firstIntersection = intersections[0].Point;
    
            var distToStart = Distance2D(firstIntersection, startPoint);
            var distToEnd = Distance2D(firstIntersection, endPoint);
    
            return distToStart <= distToEnd ? CrossedEndType.Start : CrossedEndType.End;
        }
    
        /// <summary>
        /// Gets the intersection point closest to the crossed end.
        /// </summary>
        private Point3D GetClosestIntersection(
            Intersections intersections,
            CrossedEndType crossedEnd,
            Point3D startPoint,
            Point3D endPoint)
        {
            var referencePoint = crossedEnd == CrossedEndType.Start ? startPoint : endPoint;
    
            Point3D closest = intersections[0].Point;
            double minDist = Distance2D(closest, referencePoint);
    
            for (int i = 1; i < intersections.Count; i++)
            {
                var dist = Distance2D(intersections[i].Point, referencePoint);
                if (dist < minDist)
                {
                    minDist = dist;
                    closest = intersections[i].Point;
                }
            }
    
            return closest;
        }
    
        private static double Distance2D(Point3D a, Point3D b)
        {
            var dx = a.X - b.X;
            var dy = a.Y - b.Y;
            return Math.Sqrt(dx * dx + dy * dy);
        }
    }
    
    public enum CrossedEndType
    {
        Start,
        End
    }
    
    public class EntityCrossingResult
    {
        public uint SerialNumber { get; set; }
        public Intersections Intersections { get; set; }
        public CrossedEndType CrossedEnd { get; set; }
        public Point3D ClosestIntersectionPoint { get; set; }
    }

    Usage in your command

    // In your command after MemberSelection completes in a 3D view:
    
    // 1. Check if MostRecentPickIntersections is empty (3D view case)
    var pickIntersections = memberSelection.MostRecentPickIntersections;
    if (pickIntersections == null || pickIntersections.Any() == false)
    {
        // 3D view fallback: compute crossings manually
        var selectedMembers = GlobalSelection.SelectedMembers(project);
        var serials = selectedMembers.OfType<ISnapIn>().Select(s => s.SerialNumber);
    
        // Get the fence points from the pick aperture or stored mouse picks
        var fencePoints = GetFencePointsFromSelection(); // your stored polygon points
    
        var calculator = new FenceCrossingCalculator(project);
        var crossings = calculator.ComputeCrossings(fencePoints, serials);
    
        foreach (var crossing in crossings)
        {
            // crossing.SerialNumber - the entity
            // crossing.CrossedEnd   - Start or End
            // crossing.ClosestIntersectionPoint - where the fence crossed near that end
            
            // Use this to determine extension direction
            ProcessEntityExtension(crossing.SerialNumber, crossing.CrossedEnd);
        }
    }
    else
    {
        // 2D view: use MostRecentPickIntersections directly (existing code)
        foreach (var (serial, intersections) in pickIntersections)
        {
            // ... existing logic ...
        }
    }
    Key Points
    1. PolySeg.Intersect is a 2D operation - it projects to XY. For 3D views, you'll want to flatten the Z coordinate before intersecting, which is essentially what WorldToUCS does in the 2D path (line 550-551 in SelectWithMousePolicy.cs). If your lines are tilted in 3D, consider applying sendingView.WorldToUCS to both the fence and entity polysegs before intersecting.
    2. Collecting the fence polygon points - you need to store the user's fence/polygon pick points yourself (e.g., from MouseDownInView events), or extract them from the PickAperature.ModelAperatureLocation (for rectangular selections, gives you ptMin/ptMax which defines the 4 corners).
    3. IPolyseg.ComputePolySeg() is available on any entity that has geometry (lines, arcs, polylines, etc.), so the approach works for any line-type entity you'd want to extend.

    Let me know if this helps


    ------------------------------
    Bryce Haire
    ------------------------------



  • 3.  RE: how to retrieve the location where an entity was selected on-screen using a rectangle/polygon selection - in a 3DView

    Posted 9 days ago

    Hi Bryce,

    thanks for that, I forgot to answer, been too busy.

    What you've got as simple one-liner

    var fencePoints = GetFencePointsFromSelection(); // your stored polygon points

    is the big issue.

    Looking into the scene in 3D, the ray from your mouse click is basically going into infinity. You never gonna get a polygon that you can use to compute intersection in a standard top view xyz way.

    Eventually, and with a lot of help from Claude Code it works now. But we had to explore a lot of different ways and work around exceptions, keep track of mouse button presses and movements, extensive debug dumps, transform the selected elements into the camera plane, compute the intersections there, and then you know which end was crossed, plus race conditions....... can't remember all of it anymore.

    Will be in my public GitHub repo soon.



    ------------------------------
    Ronny Schneider
    ------------------------------