Dynamic Silhouette - Instance Definitions - Almost there... Help Needed

Hello,

I have the following code that uses DrawOverlay in a Display Conduit to draw Silhouette Boundary Curves on selected objects.

This works great on Breps, Surfaces, Etc. I’m struggling to figure out how to get it to work on Blocks aka Instance Definitions or Instance Definition References

I tested by getting a little hack in my ExplodeBlockHelper function to get the block geometry, join said block geometry, and compute the silhouette of said geometry, however, this is not the result I’m looking for and is of course verrrry slow as it’s looping through all the geometry and such…

Visually with this script I am just trying to achieve what the “Hover Silhouette Highlight” is doing as the example in this snip showcases (But instead of a Hover calling this, I want to be able to call it as a function/module from other scripts for “real-time” visual highlighting and also for “one-time” silhouette use to get the curve objects of the boundary curves (for example on a static, visual diagram):

image

Here is the result from what I have currently… (Note it did the silhouette on EVERY geometry instead of the “whole”):
image

Here is the Rhino highlight on a brep:

Here is my version of the highlight on a brep (This is what I want and working correctly):

@menno or @dale is this something either of you would be able to assist me on?
Specifically how to use the Silhouette.Compute method on an Instance Definition to get the overall boundary silhouette instead of the nested objects?

Thank you so much!

Here is the full code in Python 3:

import Rhino
import scriptcontext as sc
import rhinoscriptsyntax as rs
from System.Drawing import Color

class PreviewAttributes:
    def __init__(self):
        self.ObjectColor = Color.Black
        self.ColorSource = Rhino.DocObjects.ObjectColorSource.ColorFromObject
        self.WireDensity = 1
        self.LayerIndex = -1
        self.CurveThickness = 5  # Default thickness for non-silhouette
        self.PointStyle = Rhino.Display.PointStyle.ControlPoint
        self.PointPixels = 3
        self.TextColor = Color.White

    @staticmethod
    def Selected():
        attr = PreviewAttributes()
        attr.ObjectColor = Color.BlueViolet  # Default is Color.Yellow
        attr.ColorSource = Rhino.DocObjects.ObjectColorSource.ColorFromObject
        attr.TextColor = Color.Black
        return attr

    @staticmethod
    def New():
        attr = PreviewAttributes()
        attr.ColorSource = Rhino.DocObjects.ObjectColorSource.ColorFromLayer
        attr.LayerIndex = sc.doc.Layers.CurrentLayer.LayerIndex
        return attr

    @staticmethod
    def Warning():
        attr = PreviewAttributes()
        attr.ObjectColor = Color.Red
        attr.ColorSource = Rhino.DocObjects.ObjectColorSource.ColorFromObject
        attr.TextColor = Color.White
        return attr

class PreviewDisplayConduit(Rhino.Display.DisplayConduit):
    def __init__(self):
        super(PreviewDisplayConduit, self).__init__()
        self._objects = {}
        self._hidden_objects = []
        self._exploded_geometries = []
        self.CreateSilhouette = False
        self.Silhouettes = []

    def AddObject(self, obj, attributes):
        if obj:
            self._objects[obj] = attributes

    def HideObjects(self, objs_to_hide):
        self._hidden_objects = objs_to_hide
        rs.HideObjects(self._hidden_objects)

    def ClearObjects(self):
        self._objects.clear()
        self._exploded_geometries.clear()
        self.ShowHiddenObjects()

    def ShowHiddenObjects(self):
        for obj_id in self._hidden_objects:
            if rs.IsObjectHidden(obj_id):
                rs.ShowObject(obj_id)
        self._hidden_objects.clear()

    def DrawOverlay(self, e):
        self._exploded_geometries.clear()

        for obj, attr in self._objects.items():
            color = attr.ObjectColor
            material = Rhino.Display.DisplayMaterial(color)
            if attr.ColorSource == Rhino.DocObjects.ObjectColorSource.ColorFromLayer:
                layer = sc.doc.Layers[attr.LayerIndex]
                color = layer.Color

            if self.CreateSilhouette:
                if isinstance(obj, Rhino.Geometry.InstanceReferenceGeometry):
                    # Compute silhouette for the entire block instance
                    self.ComputeBlockSilhouette(e, obj, attr)
                else:
                    self.Silhouettes = ComputeSilhouette([obj])
                    for silhouette in self.Silhouettes:
                        e.Display.DrawCurve(silhouette, color, 2)  # Silhouette curve thickness
            else:
                if isinstance(obj, Rhino.Geometry.Curve):
                    e.Display.DrawCurve(obj, color, attr.CurveThickness)
                elif isinstance(obj, Rhino.Geometry.Brep):
                    e.Display.DrawBrepShaded(obj, material)
                elif isinstance(obj, Rhino.Geometry.Point):
                    e.Display.DrawPoint(obj.Location, attr.PointStyle, attr.PointPixels, color)
                elif isinstance(obj, Rhino.Geometry.Mesh):
                    e.Display.DrawMeshShaded(obj, material)
                elif isinstance(obj, Rhino.Geometry.TextDot):
                    e.Display.DrawDot(obj.Point, obj.Text, color, attr.TextColor)
                elif isinstance(obj, Rhino.Geometry.InstanceReferenceGeometry):
                    # Handle block instance differently
                    self.ComputeBlockSilhouette(e, obj, attr)
                else:
                    Rhino.RhinoApp.WriteLine("Obj Type Is: " + str(obj))

        # Draw collected silhouettes
        if self.CreateSilhouette:
            for silhouette in self.Silhouettes:
                e.Display.DrawCurve(silhouette, Color.Red, 2)  # Draw silhouette with specific color and thickness

    def ComputeBlockSilhouette(self, e, iref, attr):
        Rhino.RhinoApp.WriteLine("Compute Block Silhouette Call")
        idef = sc.doc.InstanceDefinitions.FindId(iref.ParentIdefId)
        if idef is None:
            return

        # Transform and collect all nested geometries
        transformed_geometries = self.TransformInstanceGeometries(iref)

        # Create a combined geometry (Brep or Mesh) for the entire block
        combined_geom = None
        if transformed_geometries:
            breps_and_meshes = [g for g in transformed_geometries if isinstance(g, (Rhino.Geometry.Brep, Rhino.Geometry.Mesh))]
            if breps_and_meshes:
                if all(isinstance(g, Rhino.Geometry.Brep) for g in breps_and_meshes):
                    combined_geom = Rhino.Geometry.Brep.JoinBreps(breps_and_meshes, sc.doc.ModelAbsoluteTolerance)
                elif all(isinstance(g, Rhino.Geometry.Mesh) for g in breps_and_meshes):
                    combined_geom = Rhino.Geometry.Mesh()
                    for mesh in breps_and_meshes:
                        combined_geom.Append(mesh)

        if combined_geom:
            self.Silhouettes.extend(ComputeSilhouette([combined_geom]))
            Rhino.RhinoApp.WriteLine(f"Silhouettes: {self.Silhouettes}")

    def TransformInstanceGeometries(self, iref):
        idef = sc.doc.InstanceDefinitions.FindId(iref.ParentIdefId)
        if idef is None:
            return []

        xform = iref.Xform
        objects = idef.GetObjects()
        transformed_geometries = []

        for obj in objects:
            if obj is None:
                continue

            geom = obj.Geometry.Duplicate()
            if isinstance(geom, Rhino.Geometry.InstanceReferenceGeometry):
                nested_geometries = self.TransformInstanceGeometries(geom)
                for nested_geom in nested_geometries:
                    if not nested_geom.Transform(xform):
                        continue
                    transformed_geometries.append(nested_geom)
            else:
                if xform.IsValid and not xform.Equals(Rhino.Geometry.Transform.Identity):
                    if not geom.Transform(xform):
                        continue
                transformed_geometries.append(geom)

        return transformed_geometries

    def CalculateBoundingBox(self, e):
        bbox = Rhino.Geometry.BoundingBox()
        for obj in self._objects.keys():
            bbox.Union(obj.GetBoundingBox(True))
        e.IncludeBoundingBox(bbox)

class PreviewManager:
    def __init__(self):
        self.conduit = PreviewDisplayConduit()

    def preview_objects(self, objects, attr, activate=True, silhouette=False):
        if not objects:
            return

        self.conduit.CreateSilhouette = silhouette

        if not silhouette:
            self.conduit.HideObjects(objects)

        for obj_id in objects:
            rhino_obj = rs.coercegeometry(obj_id)
            self.conduit.AddObject(rhino_obj, attr)

        self.conduit.Enabled = activate
        sc.doc.Views.Redraw()

    def clear_preview(self):
        self.conduit.Enabled = False
        self.conduit.ClearObjects()
        self.conduit.ShowHiddenObjects()
        sc.doc.Views.Redraw()

def ComputeSilhouette(objs):
    Rhino.RhinoApp.WriteLine("Compute Silhouette Call")
    # Get the active view
    vp = Rhino.RhinoDoc.ActiveDoc.Views.ActiveView

    # Get the camera location from the active view
    cam_loc = vp.ActiveViewport.CameraLocation

    # Define other parameters
    st = Rhino.Geometry.SilhouetteType.Boundary
    tol = Rhino.RhinoDoc.ActiveDoc.ModelAbsoluteTolerance
    a_tol = Rhino.RhinoDoc.ActiveDoc.ModelAngleToleranceRadians

    # Initialize a list to store the curves
    outline_crvs = []

    for obj in objs:
        rhino_obj = rs.coercegeometry(obj)
        if rhino_obj is not None:
            obj_outline = Rhino.Geometry.Silhouette.Compute(rhino_obj, st, cam_loc, tol, a_tol)
            for silhouette in obj_outline:
                crv = silhouette.Curve
                outline_crvs.append(crv)

    # Join the curves
    if outline_crvs:
        joined_crvs = Rhino.Geometry.Curve.JoinCurves(outline_crvs)
        if joined_crvs:
            return joined_crvs

    return outline_crvs

def PreviewObjects():
    selected_objects = rs.GetObjects("Select objects to preview")
    if not selected_objects:
        return

    manager = PreviewManager()
    attr = PreviewAttributes.Selected()

    manager.preview_objects(selected_objects, attr, activate=True, silhouette=True)
    rs.GetString("press enter or escape to end preview")
    manager.clear_preview()

if __name__ == "__main__":
    PreviewObjects()


3 Likes

Hi @michaelvollrath,

Does Rhino’s Silhouette command work with blocks?

— Dale

Hi @dale ,

No I guess it does not according to this and in further testing:

However, the hover silhouette highlighting does of course work on the blocks as the orange highlight in my screen snips shows.

I guess this must be a different method entirely? Is this actually Fake2D that I mistook for Silhouette.Compute?

Thanks for your help!

Hi @michaelvollrath

maybe this Method is better for your task?
https://developer.rhino3d.com/api/rhinocommon/rhino.geometry.mesh/getoutlines

Jess

1 Like

Thanks @Jess ,

Unfortunately this also requires exploding the block instance reference first to get the mesh objects.

I want to be able to provide geometry to a script and get this Silhouette Highlight on a per object basis so that I can set the color and stroke width dynamically and also call it as DrawOverlay or not depending on if I want to see it or not through other objects.

I am trying to achieve the exact functionality of the Silhouette Highlight OnMouseHover that can be set from Rhino Options with the following settings:

Rhino.Options.ChooseOneObject.HighlightColor (sets this silhouette color)
Rhino.Options.General.MouseOverHighlight = True (enables the mouse hover highlighting though this is not Silhouette specific... and I don't need this functionality from the script)
Rhino.Options.General.SilhouetteHighlighting = True (this enables the Silhouette highlight)

I am trying to apply this effect on a per object basis specifically on Blocks as a simplified means to highlighting them for the user experience.

EDIT:

It seems that in Technical/Pen Display Modes, it behaves much like the Silhouette.Compute method where we see each individual object being highlighted:

image
image

However in a Rendered Display Mode, it behaves exactly how I want it to where we ONLY see the Silhouette Boundary Curves.

image
image

Other than the method you shared @Jess and Silhoutte.Compute that I share in my script above, I cannot find any possible API method that works on the block instances without exploding everything first and doing a bunch of boolean/curve boolean logic which becomes very slow in complex blocks.

Is there any Fake2D code or examples that can be shared for this kind of use in a DisplayConduit?

If not can I make that a feature request for Fake2D API access @Gijs ?

Thank you all for your help!

Hi @michaelvollrath,

I do not have any experience with that exact issue. So hopefully someone will correct me if I’m wrong with my assumptions.

If I’d need meshes from geometry in a block then I would create or extract it from the source block objects and apply the instance transformation to it. To get a single outline from multiple objects “join” these to one source mesh before getting the outline.

I think it is straight forward and offers good control over the various options. But maybe there is a better way.

Jess

Thanks @Jess, I did go this route thinking the same would work but it seems that even with a JoinedMesh it computes the Silhouette of each mesh as if it were never joined.

I tried MeshUnion as well but no luck there as it still computes a separate silhouette for all the objects that “aren’t touching” as a result of the union not merging those objects.

I think maybe I need to get the silhouettes and then perform a CurveBoolean on said silhouette array in the local camera viewport plans to then get the Outline only… But this also seems like it would not be very performant.

Whatever is being used for the Object Silhouette highlighting is very performant and what I need to replicate.

I appreciate your answers and ideas!

Hi @michaelvollrath,

I think you just have to make one list of meshes using the append method:
https://developer.rhino3d.com/api/rhinocommon/rhino.geometry.mesh/append

Here an example:

Jess

1 Like

Okay thank you @Jess ,

I’ll give this a try and report back!

This is a new feature in Rhino 8 and I’m not sure if it is in the state already that we can provide access to for it, but I’ll ask the developer.
It’s in fact already on the list:
RH-82065 SDK access of Fake2D

I added this thread

1 Like

Wonderful, thank you so much @Gijs

In the mean time, it may be worthwhile to take the Mesh.GetOutlines(ViewPort) route. Please find below an example. I’ve done this one in the ScriptEditor, but you can lift out the conduit and re-use it as you see fit.

using System;
using Rhino;
using Rhino.Geometry;
using Rhino.Display;
using Rhino.DocObjects;
using Rhino.Input;
using Rhino.Input.Custom;
using Rhino.Commands;


class BlockOutlineConduit : DisplayConduit
{
    Rhino.Geometry.InstanceReferenceGeometry _block;
    Rhino.Geometry.Mesh _overall;

    public BlockOutlineConduit(RhinoDoc doc, InstanceReferenceGeometry block)
    {
        _block = block;
        InstanceDefinition d = doc.InstanceDefinitions.Find(block.ParentIdefId, true);
        if (null == d) throw new ArgumentNullException();

        _overall = new Mesh();
        Guid[] ids = d.GetObjectIds();
        foreach(Guid id in ids)
        {
            MeshingParameters mp = MeshingParameters.FastRenderMesh;
            RhinoObject obj = doc.Objects.Find(id);
            switch(obj.ObjectType)
            {
                case ObjectType.Extrusion:
                {
                    Extrusion x = obj.Geometry as Extrusion;
                    _overall.Append(Mesh.CreateFromExtrusion(x, mp));
                    break;
                }
                case ObjectType.Brep:
                {
                    Brep b = obj.Geometry as Brep;
                    _overall.Append(Mesh.CreateFromBrep(b, mp));
                    break;  
                }
                case ObjectType.Mesh:
                {
                    _overall.Append(obj.Geometry as Mesh);
                    break;
                }
            }
        }
        _overall.Transform(_block.Xform);
    }

    protected override void DrawForeground(DrawEventArgs e)
    {
        if (null == _overall) return;
        Polyline[] pls = _overall.GetOutlines(e.Viewport);
        if (null == pls) return;

        foreach(var pl in pls)
        {
            e.Display.DrawPolyline(pl, System.Drawing.Color.Orange, 4);
        }
    }
}


RhinoDoc doc = RhinoDoc.ActiveDoc;
var res = RhinoGet.GetOneObject("Select block", false, ObjectType.InstanceReference, out var bRef);
if (res != Result.Success) return;

InstanceReferenceGeometry g = bRef.Geometry() as InstanceReferenceGeometry;

BlockOutlineConduit c = new BlockOutlineConduit(doc, g);
c.Enabled = true;

string s = string.Empty;
res = RhinoGet.GetString("Press enter to finish", true, ref s);
c.Enabled = false;
2 Likes

Thank you @menno,

I’ll give this a go and report back!

Thanks for providing the example code, very helpful!