Extract center curves from 3D meshes

I’m looking for a way to extract the skeleton of a mesh - the center curves of the solid mesh.
I have previously used ngon mesh skeleton component, but in the latest version this component is no longer there. (@Petras_Vestartas).

Are there any alternatives? Or suggestions for methods.
This article describes pretty well what I intend to do

Image from article:

Recently I added this method to my compas_wood plugin:

#! python3
# venv: timber_connections
# r: wood-nano==0.2.0, compas-wood==2.4.0, compas==2.8.1, wood-rui==0.2.2

import Rhino
import Rhino.Input
import Rhino.DocObjects
from Rhino.Geometry import Plane
from typing import *
from wood_rui import wood_rui_globals, process_input, add_skeleton
from compas_wood.binding_rhino import beam_skeleton, mesh_skeleton
import System


def callback(selection: dict[str, any], name: str):
    # Just axis without radii
    if not selection["simplify"]:
        for mesh in selection["meshes"]:
            polylines = mesh_skeleton(mesh)
            for polyline in polylines:
                Rhino.RhinoDoc.ActiveDoc.Objects.AddPolyline(polyline)
        return

    # Try to simplify an axis

    polylines = []
    distances = []
    meshes = []
    transforms = []

    for mesh in selection["meshes"]:
        polyline = None
        polylines = []

        polyline, distance = beam_skeleton(
            mesh,
            selection["divisions"],
            selection["nearest_neighbors"],
            selection["extend_end"],
        )
        polylines.append(polyline)
        distances.append(distance)
        meshes.append(mesh)

        # Add a small segement at the end of the polyline to provide orientation
        z_axis = polyline[polyline.Count - 1] - polyline[0]
        plane = Plane(polyline[polyline.Count - 1], z_axis)
        x_axis = plane.XAxis
        polyline.Add(polyline[polyline.Count - 1] + x_axis * 0.1)
        plane_xy = Plane.WorldXY
        plane = Plane(
            polyline[polyline.Count - 1],
            x_axis,
            Rhino.Geometry.Vector3d.CrossProduct(x_axis, z_axis),
        )
        xform = Rhino.Geometry.Transform.PlaneToPlane(plane, plane_xy)
        transforms.append(xform)

    add_skeleton(polylines, "mesh_skeleton", distances, meshes, transforms)


if __name__ == "__main__":
    input_dict = {
        "meshes": ([], list[Rhino.Geometry.Mesh]),
        "simplify": (False, bool),
        "divisions": (10, int),
        "nearest_neighbors": (10, int),
        "extend_end": (True, bool),
    }

    # Call the generalized input method with the dataset name and input dictionary
    process_input(input_dict, callback, hide_input=False)

This is method what you up to, since you dont care about straight timber beams made from scans. This method should work on your favorite editors rhino script editor, grasshopper or vscode:
from compas_wood.binding_rhino import mesh_skeleton

That is irrelated of the shape:

CGAL docs:

All the credits must be given to the smart people:

The initial implementation of this package is the result of the work of Xiang Gao during the 2013 season of the Google Summer of Code mentored by Andrea Tagliasacchi and Sébastien Loriot. It was finalized by Andreas Fabri and Sébastien Loriot.

At ibois long time ago I learnt how to wrap C++ methods to C# and Python since all the goodies are in the low level languages…

4 Likes

You are fantastic - absolutely fantastic. Thank you!

1 Like

So I tried implementing this in a python component but the result is not easy to get as great as it was in ngon.

This is my code

#!/usr/bin/env python3
# venv: timber_connections
# r: wood-nano==0.2.0, compas-wood==2.4.0, compas==2.8.1, wood-rui==0.2.2


import System
import Rhino
import Rhino.Input
import Rhino.DocObjects
from Rhino.Geometry import Plane
from typing import *
from wood_rui import wood_rui_globals, process_input, add_skeleton
from compas_wood.binding_rhino import beam_skeleton, mesh_skeleton
import System

def compute_mesh_skeleton(input_meshes: list[Rhino.Geometry.Mesh],
                          simplify: bool = True,
                          divisions: int = 100,
                          nearest_neighbors: int = 10,
                          extend_end: bool = False
                         ) -> dict[str, any]:
    if not simplify:
        # Use mesh_skeleton when not simplifying: accumulate all returned polylines.
        polylines = []
        for mesh in input_meshes:
            for poly in mesh_skeleton(mesh):
                polylines.append(poly)
        return {"polylines": polylines}
    
    # When simplifying, use beam_skeleton and adjust each polyline.
    polylines = []
    distances = []
    meshes_out = []
    transforms = []
    
    for mesh in input_meshes:
        polyline, distance = beam_skeleton(mesh, divisions, nearest_neighbors, extend_end)
        polylines.append(polyline)
        distances.append(distance)
        meshes_out.append(mesh)
        
        # Add a small segment at the end to provide orientation.
        z_axis = polyline[polyline.Count - 1] - polyline[0]
        plane = Plane(polyline[polyline.Count - 1], z_axis)
        x_axis = plane.XAxis
        polyline.Add(polyline[polyline.Count - 1] + x_axis * 0.1)
        plane_xy = Plane.WorldXY
        plane = Plane(polyline[polyline.Count - 1],
                      x_axis,
                      Rhino.Geometry.Vector3d.CrossProduct(x_axis, z_axis))
        xform = Rhino.Geometry.Transform.PlaneToPlane(plane, plane_xy)
        transforms.append(xform)
    
    return {
        "polylines": polylines,
        "distances": distances,
        "meshes": meshes_out,
        "transforms": transforms
    }

skeleton = compute_mesh_skeleton([mesh])['polylines']

And here is the grasshopper script
MeshSkeleton.gh (957.5 KB)

Dense meshes:

Skeleton:

Dont use beam_skeleton but mesh_skeleton.

Try it and let me know.

polylines = mesh_skeleton(mesh)
            for polyline in polylines:
                Rhino.RhinoDoc.ActiveDoc.Objects.AddPolyline(polyline

Distances you can find by closest point to mesh search in rhinocommon.

I see. If I use simplify = false I get the mesh_skeleton function, but what it returns to me is this:

(I have used shrinkwrap to create a mesh)
MeshSkeleton.gh (962.4 KB)

Triangulate the mesh.

This is literally one line of code:

By the way, if you are using only 3d boxes, you could just compute principal-component-analysis from opennest. The normal of a plane is your axis:

1 Like


THANK YOU!