Work in progress - Quick area sketching overview

Hi all,

I’m slowly picking up C# after a few years in Python-land.
One of the first scripts I decided to make is a quick GH component, that I’ll share with you guys.

It works quite well for my intent - it provides quick feedback on your areas when sketching.
I had a colleague who had a similar workflow as native GH components and in terms of speed it beat all of my python attempts, but finally I beat him with C# :wink:

But there’s a bug - sometimes when relinking geometry my rhino6 freezes and crashes - most times it doesn’t. I can’t seem to find any error in my code. I was hoping to pick up some feedback on that.

Ideas, should anyone feel like adding to it:

  • automaticly pick up geometry on sublayers to a parentlayer with an update button (or use Human).
  • color output surfaces based on which layer they are hosted by.

Areascript.3dm (8.4 MB) Areascript.gh (35.8 KB)

@sonderskovmathias

Check the attached. I broke up the logic in different methods so you can make your code scalable in the future. I am also not relying on any brep input, instead I am taking the data that is on each layer. Take note, that if you create a compiled version of this, the code will run much faster.

Concerning your ideas

1.To color the slabs, you have two options:

A. Create a list of colors for every slab from the corresponding layer. Output the colors from the component and do the normal preview routine,

B. Create a list of colors for every slab from the corresponding layer. Dont output the geometry, and instead override the display method.

  1. Determining child - parent relationships in layer can be a good homework for you to figure out. Hint… build a method, or perhaps more? to distill the logic and add this method in to what I called Foo(). Which you will, I hope… rename with a proper name which alludes to the action it is performing

Areascript.gh (38.5 KB)

 private void RunScript(object x, ref object Slabs, ref object Areas, ref object LayerNames, ref object Data)
  {

    DataTree<double> outAreas;
    DataTree<string> outLayerNames;

    System.Diagnostics.Stopwatch sw = new System.Diagnostics.Stopwatch();

    sw.Start();
    List<Brep> slabs = Foo(out outAreas, out outLayerNames);
    sw.Stop();
    Print(string.Format("Elapsed time: {0} ms", sw.ElapsedMilliseconds));

    Data = ComputeData(outAreas, outLayerNames);
    Slabs = slabs;
    Areas = outAreas;
    LayerNames = outLayerNames;
  }

// <Custom additional code> 

  /// <summary>
  /// Compute Geometrical data from Rhino Layers.
  /// This method only works on Brep Geometry
  /// </summary>
  /// <param name="outAreas"></param>
  /// <param name="outLayerNames"></param>
  /// <returns></returns>
  public List<Brep> Foo(out DataTree<double> outAreas, out DataTree<string> outLayerNames)
  {
    // This units stuff is not being used for now...
    UnitSystem unitSystem = RhinoDocument.ModelUnitSystem;
    string unitString;
    double units = DetermineUnitSystem(unitSystem, out unitString);

    DataTree<double> areas = new DataTree<double>();
    DataTree<string> layerNames = new DataTree<string>();
    List<Brep> slabGeo = new List<Brep>();


    for (int i = 0; i < RhinoDocument.Layers.Count; i++)
    {
      string layerName = RhinoDocument.Layers[i].Name;
      if (RhinoDocument.Layers[i].IsLocked) continue;
      if (!RhinoDocument.Layers[i].IsValid)
        throw new Exception(string.Format("Laeyer {0} is invalid", layerName));

      // Get RhinoObjects in current Layer
      Rhino.DocObjects.RhinoObject[] rhObjs = RhinoDocument.Objects.FindByLayer(layerName);


      if (rhObjs == null || rhObjs.Length < 1)
        throw new ArgumentNullException("You are attempting to access objects in an empty layer!");

      for (int j = 0; j < rhObjs.Length; j++)
      {
        if (rhObjs[i] == null)
          throw new ArgumentNullException(string.Format("object at index {0} in Layer {1} is null", j, layerName));

        // You might want to check if its a valid GUID
        Rhino.DocObjects.RhinoObject rhObj = RhinoDocument.Objects.Find(rhObjs[j].Id);

        // Get geometry base
        Rhino.Geometry.GeometryBase rhGeo = rhObj.Geometry;

        Brep brep = null;
        if (rhGeo.HasBrepForm) brep = Brep.TryConvertBrep(rhGeo);

        else
          throw new Exception(string.Format("Geometry object at index {0} in Layer {1}" +
            " cant be converted to a Brep", j, layerName)); // throw or continue??

        if (!brep.IsValid)
          throw new Exception(string.Format("Brep at index {0} in Layer {1} is not valid", j, layerName)); // throw or continue??

        if (!brep.IsSolid)
          throw new Exception(string.Format("Brep at index {0} in Layer {1} is not solid", j, layerName)); // throw or continue??

        if(brep != null)
        {
          Brep slab = GetLowestBrepFace(brep);
          double floorArea = slab.GetArea() * 0.0000010;// converts to square meters
          areas.Add(floorArea, new GH_Path(i));
          layerNames.Add(layerName, new GH_Path(i));
          slabGeo.Add(slab);

        }

      }

    }

    outAreas = areas;
    outLayerNames = layerNames;
    return slabGeo;
  }


  /// <summary>
  /// Display data
  /// </summary>
  /// <param name="areas"></param>
  /// <param name="layerNames"></param>
  /// <returns></returns>
  public List<string> ComputeData(DataTree<double> areas, DataTree<string> layerNames)
  {
    DataTree<double> totalAreas = new DataTree<double>();
    double totalArea = 0;
    for (int i = 0; i < areas.Branches.Count; i++)
    {
      double totalA = 0;
      for (int j = 0; j < areas.Branches[i].Count; j++)
        totalA += areas.Branches[i][j];

      totalArea += totalA;
      totalAreas.Add(totalA, new GH_Path(i));
    }

    List<string> data = new List<string>();
    for (int i = 0; i < totalAreas.Branches.Count; i++)
    {

      for (int j = 0; j < totalAreas.Branches[i].Count; j++)
      {
        data.Add(string.Format("{0,-20} \t: {1,15:n1} m2 ",
          layerNames.Branches[i][j], totalAreas.Branches[i][j]));
        data.Add(Environment.NewLine);
      }


    }

    data.Add(string.Format("Total  {0,-13} \t: {1,15:n1} m2  ", "", totalArea));

    return data;

  }


  /// <summary>
  /// Determine the current Unit system of the Rhino
  ///Doc
  /// </summary>
  /// <param name="unitSystem"></param>
  /// <param name="_sUnits"></param>
  /// <returns></returns>
  public double DetermineUnitSystem(UnitSystem unitSystem, out string _sUnits)
  {
    double units = 0;
    string sUnits = "";
    switch(unitSystem)
    {
      case UnitSystem.Millimeters: units = 1000; sUnits = "mm2"; break;
      case UnitSystem.Meters: units = 1; sUnits = "m2"; break;

      // Keep going
    }

    _sUnits = sUnits;
    return units;
  }


  /// <summary>
  /// Get Lowest Brep surface from a Brep Solid
  /// </summary>
  /// <param name="brep"></param>
  /// <returns></returns>
  public static Brep GetLowestBrepFace(Brep brep)
  {
    Brep lowest = null;
    for (int i = 0; i < brep.Faces.Count; i++)
    {
      BrepFace face = brep.Faces[i];
      Brep surface = brep.Faces.ExtractFace(i);

      Plane frame;
      if(!face.FrameAt(0.5, 0.5, out frame))

        if (frame.ZAxis.Z < 0)
          lowest = surface;
    }

    return lowest;
  }
1 Like

Thanks Nicholas Rawitscher,
This is a really good learning example for everyone diving into C#.

I believe my occasionally crash can be related to linking to GUIDs in the C# component rather than the brep itself, whereas it might not crash if using IGH_Geometry in the compiled. Will let you know.

Hi @sonderskovmathias

do you mean IGH_GeometricGoo ? this possible, but for simplicity I am just dealing with an item and not a collection, but you get my point.

 protected override void RegisterInputParams(GH_Component.GH_InputParamManager pManager)
        {
          
            pManager.AddGeometryParameter("Geo", "G", " ", GH_ParamAccess.item);
 
        }


  protected override void SolveInstance(IGH_DataAccess DA)
  {
    IGH_GeometricGoo geoGoo = null;
    DA.GetData(0, ref geoGoo);
    

    if (!geoGoo.IsReferencedGeometry)
           throw new ArgumentNullException("Geometry is not referenced! can be prone to loosing attributes!");
 

    Guid guid = geoGoo .ReferenceID;
        if (giud == Guid.Empty)
            throw new ArgumentNullException("Guid is empty!");

    ObjRef objRef = new ObjRef(guid);

    // check for null here just in case object does not exist in doc

//Extract attributes
    ObjectAttributes atts = objRef.Object().Attributes;
    string layerName =   objRef.Object().Document.Layers[atts.LayerIndex].Name;

  }

https://developer.rhino3d.com/api/RhinoCommon/html/T_Rhino_DocObjects_ObjRef.htm

But if it was me, I think it would perhaps be better to get the data from the layers themselves so users wouldn’t have to deal with Grasshopper. I am not sure if having the geometry as input is what is causing your code to fail in come situations. Or you could even just to drop a component and show all your data through a window by double clicking the component or something along those lines.

Another suggestion I can give you is once you start writing some code in VS, just think the GH template as a wrapper for your code. In other words do all the business logic, even from another C# project. In the end you can have various projects in one Solution and one of the projects will be only for your code to interface with Grasshopper, in this project you will reference the .dll from your main library. By doing this, your library will be more of a standalone thing that you could even port to other CAD platforms that support .NET. Another benefit is the separation of concerns within your code, modularity, ease for debugging and proper documentation.

1 Like

Thanks, good inputs.
I’m used to OOP and modules in Python and slowly getting used to Namespaces in C# and the whole .NET eco system. I will definately make this more scalable in time. I think the final product will be compiling this as a class/namespace and feed that into a Rhino plugin with an ETOs layertable.