Has anyone implemented revision clouds around selected elements on the sheet?

Has anyone implemented revision clouds around selected elements on the sheet?

baleti3266
Advocate Advocate
313 Views
3 Replies
Message 1 of 4

Has anyone implemented revision clouds around selected elements on the sheet?

baleti3266
Advocate
Advocate

I'm trying to write a command that draws revision clouds around selected elements. It works fine when revision clouds are drawn in the view, but when I'm trying to draw them on the sheet matching their corresponding locations of selected elements in view they don't land in correct locations. They get shifted in either direction by small amounts. Does anyone know why?

baleti3266_0-1747385232774.png

 



using Autodesk.Revit.Attributes;
using Autodesk.Revit.DB;
using Autodesk.Revit.UI;
using System;
using System.Collections.Generic;
using System.Linq;

[Transaction(TransactionMode.Manual)]
public class CloudsToSheetParametric : IExternalCommand
{
    public Result Execute(
        ExternalCommandData cd,
        ref string message,
        ElementSet elements)
    {
        UIDocument uiDoc = cd.Application.ActiveUIDocument;
        Document   doc   = uiDoc.Document;
        View       view  = doc.ActiveView;

        ICollection<ElementId> selIds = uiDoc.Selection.GetElementIds();
        if (!selIds.Any())
        {
            message = "Select one or more elements first.";
            return Result.Failed;
        }

        // ─── locate the viewport that shows the active view ───
        ViewSheet sheet = null;
        Viewport  vp    = null;

        foreach (ViewSheet sh in new FilteredElementCollector(doc)
                                 .OfClass(typeof(ViewSheet)))
        {
            foreach (ElementId vpid in sh.GetAllViewports())
            {
                vp = doc.GetElement(vpid) as Viewport;
                if (vp != null && vp.ViewId == view.Id)
                {
                    sheet = sh;
                    break;
                }
            }
            if (sheet != null) break;
        }
        if (sheet == null)
        {
            message = "Active view is not placed on a sheet.";
            return Result.Failed;
        }

        // ─── build parametric mapper ───
        ParamMapper map;
        try
        {
            map = new ParamMapper(view, vp);
        }
        catch (InvalidOperationException ex)
        {
            message = ex.Message;
            return Result.Failed;
        }

        // ─── choose (or create) a revision ───
        Revision rev = GetLatestVisibleRevision(doc);
        if (rev == null)
        {
            using (Transaction t = new Transaction(doc, "Create Revision"))
            {
                t.Start();
                rev = Revision.Create(doc);
                t.Commit();
            }
        }

        const double OFFSET = 0.2;         // ft – halo

        // ─── build sheet-space rectangles ───
        var curves = new List<Curve>();

        foreach (ElementId id in selIds)
        {
            Element el = doc.GetElement(id);
            BoundingBoxXYZ bb = el.get_BoundingBox(view);
            if (bb == null) continue;

            // model-space bbox (expanded)
            XYZ p1 = bb.Transform.OfPoint(bb.Min);
            XYZ p2 = bb.Transform.OfPoint(bb.Max);

            XYZ min = new XYZ(
                Math.Min(p1.X, p2.X) - OFFSET,
                Math.Min(p1.Y, p2.Y) - OFFSET, 0);
            XYZ max = new XYZ(
                Math.Max(p1.X, p2.X) + OFFSET,
                Math.Max(p1.Y, p2.Y) + OFFSET, 0);

            // sheet points via param mapper
            XYZ bl = map.ToSheet(min);                       // bottom-left
            XYZ tl = map.ToSheet(new XYZ(min.X, max.Y, 0));  // top-left
            XYZ tr = map.ToSheet(max);                       // top-right
            XYZ br = map.ToSheet(new XYZ(max.X, min.Y, 0));  // bottom-right

            curves.Add(Line.CreateBound(bl, tl));
            curves.Add(Line.CreateBound(tl, tr));
            curves.Add(Line.CreateBound(tr, br));
            curves.Add(Line.CreateBound(br, bl));
        }

        if (!curves.Any())
        {
            message = "No bounding boxes found.";
            return Result.Failed;
        }

        // ─── create the cloud on the sheet ───
        using (Transaction t = new Transaction(doc, "Revision Clouds"))
        {
            t.Start();
            RevisionCloud.Create(doc, sheet, rev.Id, curves);
            t.Commit();
        }

        return Result.Succeeded;
    }

    // ----------------------------------------------------------
    //  Helper: model → sheet via parametric crop-box coordinates
    // ----------------------------------------------------------
    private sealed class ParamMapper
    {
        private readonly Transform _toLocal;   // model → crop-local
        private readonly XYZ       _minLoc;    // crop local min
        private readonly double    _wLoc;      // crop width  (local)
        private readonly double    _hLoc;      // crop height "
        private readonly Outline   _vpOutline; // viewport box on sheet
        private readonly ViewportRotation _rot;

        public ParamMapper(View v, Viewport vp)
        {
            BoundingBoxXYZ crop = v.CropBox;
            if (crop == null || !v.CropBoxActive)
                throw new InvalidOperationException("View lacks an active crop box.");

            _toLocal  = crop.Transform.Inverse;
            _minLoc   = crop.Min;
            _wLoc     = crop.Max.X - crop.Min.X;
            _hLoc     = crop.Max.Y - crop.Min.Y;
            _vpOutline = vp.GetBoxOutline();
            _rot       = vp.Rotation;
        }

        public XYZ ToSheet(XYZ modelPt)
        {
            // 1) model → local crop coordinates
            XYZ loc = _toLocal.OfPoint(modelPt);

            // 2) parametric position inside crop
            double u = (loc.X - _minLoc.X) / _wLoc;   // 0 … 1
            double v = (loc.Y - _minLoc.Y) / _hLoc;

            // 3) viewport size
            double wSheet = _vpOutline.MaximumPoint.X - _vpOutline.MinimumPoint.X;
            double hSheet = _vpOutline.MaximumPoint.Y - _vpOutline.MinimumPoint.Y;

            // 4) map (u,v) to sheet, honour 90° rotations
            double sx, sy;
            switch (_rot)
            {
                case ViewportRotation.None:
                    sx = u * wSheet;
                    sy = v * hSheet;
                    break;

                case ViewportRotation.Clockwise:
                    sx = v * wSheet;
                    sy = (1.0 - u) * hSheet;
                    break;

                case ViewportRotation.Counterclockwise:
                    sx = (1.0 - v) * wSheet;
                    sy = u * hSheet;
                    break;

                default:
                    throw new InvalidOperationException("Unsupported viewport rotation.");
            }

            return new XYZ(
                _vpOutline.MinimumPoint.X + sx,
                _vpOutline.MinimumPoint.Y + sy,
                0);
        }
    }

    private static Revision GetLatestVisibleRevision(Document doc)
    {
        return new FilteredElementCollector(doc)
               .OfClass(typeof(Revision))
               .Cast<Revision>()
               .Where(r => r.Visibility != RevisionVisibility.Hidden)
               .OrderByDescending(r => r.SequenceNumber)
               .FirstOrDefault();
    }
}



 

0 Likes
Accepted solutions (1)
314 Views
3 Replies
Replies (3)
Message 2 of 4

rcrdzmmrmnn
Advocate
Advocate
Accepted solution

Viewports have a method called GetProjectionToSheetTransform()

you can use this to transform the location to sheet coordinates. Maybe this is your case...

0 Likes
Message 3 of 4

baleti3266
Advocate
Advocate

holy corn @rcrdzmmrmnn you are right, this updated code works now:

// CloudsToSheetTransform.cs
//
// Draws a revision cloud on the sheet around every element
// currently selected in the active view.
//
// Revit 2023+ (needs View.GetModelToProjectionTransforms and
//              Viewport.GetProjectionToSheetTransform)

using Autodesk.Revit.Attributes;
using Autodesk.Revit.DB;
using Autodesk.Revit.UI;
using System;
using System.Collections.Generic;
using System.Linq;

[Transaction(TransactionMode.Manual)]
public class CloudsToSheetTransform : IExternalCommand
{
  private const double OFFSET_MODEL = 0.20;   // ft – halo added in MODEL space

  public Result Execute(
    ExternalCommandData cd,
    ref string message,
    ElementSet elements)
  {
    UIDocument uiDoc = cd.Application.ActiveUIDocument;
    Document   doc   = uiDoc.Document;
    View       view  = doc.ActiveView;

    // -------------------------------------------------------------
    // 1) Ensure something is selected and the view is placed
    // -------------------------------------------------------------
    ICollection<ElementId> selIds = uiDoc.Selection.GetElementIds();
    if (!selIds.Any())
    {
      message = "Select one or more elements first.";
      return Result.Failed;
    }

    ViewSheet sheet = null;
    Viewport  vp    = null;

    foreach (ViewSheet sh in new FilteredElementCollector(doc)
                               .OfClass(typeof(ViewSheet)))
    {
      foreach (ElementId vpid in sh.GetAllViewports())
      {
        vp = doc.GetElement(vpid) as Viewport;
        if (vp != null && vp.ViewId == view.Id)
        {
          sheet = sh;
          break;
        }
      }
      if (sheet != null) break;
    }

    if (sheet == null)
    {
      message = "The active view is not placed on any sheet.";
      return Result.Failed;
    }

    // -------------------------------------------------------------
    // 2) Model → Sheet transform (native, loss-free)
    // -------------------------------------------------------------
    IList<TransformWithBoundary> tList = view.GetModelToProjectionTransforms();
    if (tList.Count == 0)
    {
      message = "View has no model-to-projection transform.";
      return Result.Failed;
    }

    // Use the METHOD – not a property
    Transform modelToProj  = tList[0].GetModelToProjectionTransform();
    Transform projToSheet  = vp.GetProjectionToSheetTransform();
    Transform modelToSheet = projToSheet.Multiply(modelToProj);

    // -------------------------------------------------------------
    // 3) Pick (or create) a visible revision
    // -------------------------------------------------------------
    Revision rev = new FilteredElementCollector(doc)
                  .OfClass(typeof(Revision))
                  .Cast<Revision>()
                  .Where(r => r.Visibility != RevisionVisibility.Hidden)
                  .OrderByDescending(r => r.SequenceNumber)
                  .FirstOrDefault();

    if (rev == null)
    {
      using (Transaction t = new Transaction(doc, "Create Revision"))
      {
        t.Start();
        rev = Revision.Create(doc);
        t.Commit();
      }
    }

    // -------------------------------------------------------------
    // 4) Build sheet-space rectangles around each selected element
    // -------------------------------------------------------------
    var sheetCurves = new List<Curve>();

    foreach (ElementId id in selIds)
    {
      Element el = doc.GetElement(id);
      BoundingBoxXYZ bb = el.get_BoundingBox(view);
      if (bb == null) continue;

      // Expand model-space bounding box by OFFSET_MODEL
      XYZ min = bb.Min;
      XYZ max = bb.Max;
      min = new XYZ(min.X - OFFSET_MODEL, min.Y - OFFSET_MODEL, min.Z);
      max = new XYZ(max.X + OFFSET_MODEL, max.Y + OFFSET_MODEL, max.Z);

      // Four model-space corners (z irrelevant for 2-D projection)
      XYZ[] modelCorners =
      {
        bb.Transform.OfPoint(new XYZ(min.X, min.Y, min.Z)),
        bb.Transform.OfPoint(new XYZ(min.X, max.Y, min.Z)),
        bb.Transform.OfPoint(new XYZ(max.X, max.Y, min.Z)),
        bb.Transform.OfPoint(new XYZ(max.X, min.Y, min.Z))
      };

      // Transform to sheet and collect min/max in sheet coords
      double sxMin = double.PositiveInfinity, syMin = double.PositiveInfinity;
      double sxMax = double.NegativeInfinity, syMax = double.NegativeInfinity;

      foreach (XYZ mc in modelCorners)
      {
        XYZ sp = modelToSheet.OfPoint(mc);
        sxMin = Math.Min(sxMin, sp.X);
        syMin = Math.Min(syMin, sp.Y);
        sxMax = Math.Max(sxMax, sp.X);
        syMax = Math.Max(syMax, sp.Y);
      }

      // Four sheet-space rectangle lines (Z must be 0)
      XYZ bl = new XYZ(sxMin, syMin, 0);
      XYZ tl = new XYZ(sxMin, syMax, 0);
      XYZ tr = new XYZ(sxMax, syMax, 0);
      XYZ br = new XYZ(sxMax, syMin, 0);

      sheetCurves.Add(Line.CreateBound(bl, tl));
      sheetCurves.Add(Line.CreateBound(tl, tr));
      sheetCurves.Add(Line.CreateBound(tr, br));
      sheetCurves.Add(Line.CreateBound(br, bl));
    }

    if (!sheetCurves.Any())
    {
      message = "No bounding boxes found for the selection.";
      return Result.Failed;
    }

    // -------------------------------------------------------------
    // 5) Create the revision cloud on the sheet
    // -------------------------------------------------------------
    using (Transaction t = new Transaction(doc, "Revision Clouds"))
    {
      t.Start();
      RevisionCloud.Create(doc, sheet, rev.Id, sheetCurves);
      t.Commit();
    }

    return Result.Succeeded;
  }
}



thanks a lot 🌽

0 Likes
Message 4 of 4

baleti3266
Advocate
Advocate

I further refined the command to handle elevations/sections, prompt user to choose sheets if selected elements are not view-dependent or automatically choose views/sheets if they are and to prompt user to specify revision of these clouds:
revit-scripts/revit-scripts/DrawRevisionCloudAroundSelectedElements.cs at main · baleti/revit-script...

however, I couldn't get drafting views and legends yet to work, looks like they don't work with GetProjectionToSheetTransform and throw exception "The viewport does not have transforms."  @rcrdzmmrmnn if you had any suggestions how to handle them that would be again most appreciated

0 Likes