Welcome to the Onshape forum! Ask questions and join in the discussions about everything Onshape.

First time visiting? Here are some places to start:
  1. Looking for a certain topic? Check out the categories filter or use Search (upper right).
  2. Need support? Ask a question to our Community Support category.
  3. Please submit support tickets for bugs but you can request improvements in the Product Feedback category.
  4. Be respectful, on topic and if you see a problem, Flag it.

If you would like to contact our Community Manager personally, feel free to send a private message or an email.

NEW Custom Features for Mesh

EvanReeseEvanReese Member, Mentor Posts: 2,847 PRO
edited May 2025 in General

Hi everyone, I'm excited to announce a number of custom features I've recently written to help with mesh workflows. I've also got a new video tutorial up showing them each and demonstrating them in a workflow. You can get the features here.

Spline On Mesh lets you draw curves on a mesh

Copy and Split Mesh let's you extract a section of a mesh without modifying the original

Mesh Intersection Curve works like the native Intersection Curve feature, but accepts meshes

Mesh Projected Curve works like the native Projected Curve feature, but accepts meshes

Extract Mesh Points converts all mesh vertices into points. Useful for making box selections with Constrained Surface to diagnose why it's failing

Copy Mesh Face copies a mesh face, so you can focus on just that area and hide other clutter

Keep Face works like an inverted Delete Face. Not only for meshes, but handy. It's in a separate document, and you can get it here.

Evan Reese
The Onsherpa | Reach peak Onshape productivity
www.theonsherpa.com
Tagged:

Comments

  • romeograhamromeograham Member, csevp Posts: 744 PRO

    Nice work as always!

  • glen_dewsburyglen_dewsbury Member Posts: 1,287 PRO

    I watched the video and like it and the tools. Very nice set of tools.

  • MichaelPascoeMichaelPascoe Member Posts: 2,800 PRO
    edited May 2025

    This is like a loot box with a ton of gold in it! 😃
    These are some sweet workflows. Thanks for sharing!


    RENDERCAD
    rendercad.ai - Photorealistic product rendering.

    ▚▞▚▞▚▞▚▞▚
    ________________________________________________________________________
  • MDesignMDesign Member Posts: 1,323 PRO
    catshocked.gif

    How the ___ is Evan not employed by Onshape? Seriously that is just awesome conceptually and works in a practical way.

    I thought you were going to go a different route with it when you demo'd the 3D points w/plane offsets. I was thinking slice the mesh and create curves from the edge of the mesh for the setup of loft profiles. But that works nicely as well.

    Well done. Bravo!!

  • EvanReeseEvanReese Member, Mentor Posts: 2,847 PRO

    Thanks, Y'all!

    Evan Reese
    The Onsherpa | Reach peak Onshape productivity
    www.theonsherpa.com
  • EvanReeseEvanReese Member, Mentor Posts: 2,847 PRO

    I also tried it with Lofts and it can work too, but would need to dial that in a bit before making a video so you don't have to watch me bumble through it. I know it can be done though because I've done it. No need to slice the mesh either since I modified Intersection Curve to accept meshes. All of the planes can be made with 1 3D points feature, and all of the curves with one Mesh Intersection Curve feature, then I'd use Freeform Spline or Routing Curve to make guide curves and loft it. I'm still looking into ways to automate that more into a single feature.

    Evan Reese
    The Onsherpa | Reach peak Onshape productivity
    www.theonsherpa.com
  • MDesignMDesign Member Posts: 1,323 PRO

    Crazy good stuff. Yeah I figured the intersection curve would work instead of split after watching the complete video. That's amazingly good stuff and will save gobs of time creating "usable" geometry from meshes. So much more advanced than when I first started working with meshes.

  • Derek_Van_Allen_BDDerek_Van_Allen_BD Member Posts: 781 PRO

    Perfect timing on dropping these, I was about to have to segment out this gross scan of a clay sculpture by hand in some other software and I simply refuse to learn Blender.

    image.png
  • EvanReeseEvanReese Member, Mentor Posts: 2,847 PRO

    haha well good. You may still benefit from smoothing and trimming it up somewhere else though.

    Evan Reese
    The Onsherpa | Reach peak Onshape productivity
    www.theonsherpa.com
  • adrian_vlzkzadrian_vlzkz Member, pcbaevp Posts: 304 PRO

    Bruh! 🤯

    Adrian V. | Onshape Ambassador
    CAD Engineering Manager
  • bryan_lagrangebryan_lagrange Member, User Group Leader Posts: 982 ✭✭✭✭✭

    Excellent video and features.

    Bryan Lagrange
    Twitter: @BryanLAGdesign

  • romeograhamromeograham Member, csevp Posts: 744 PRO
    edited July 2025

    @EvanReese do you think it would be possible to add certain types of primitives or geometric instructions as "weights" or something to the tools that create a surface from a mesh?

    Let's say you have scanned an object that you know is a revolved shape. However, you want to use the mesh to inform a kind of "average" revolved surface that is a good approximation of the real mesh, but is still geometrically a revolve. This could maybe work with prisms, spheres, planes etc. In the case of a revolve, I could imagine having a "click this to find the best axis for revolve" button and a "click this to specify an axis" button, too.

  • rafael_telgmannrafael_telgmann Member Posts: 138 ✭✭✭

    @EvanReese, great video, great work, great tools! In the video, you mention how to extend the ends of a curve tangentially. I was wondering why you didn't just use the native trim curve feature. Is there a reason for this?

    image.png
  • EvanReeseEvanReese Member, Mentor Posts: 2,847 PRO

    Hmm, I could see room for more custom features for this. For example if you have a part that you know should be a revolve, it could create 12 sample splines on the mesh at "clock" locations in the revolve and average them then revolve it. I don't have enough personal use for it to write it on my own time, but I'm available for hire if you need it often.

    Evan Reese
    The Onsherpa | Reach peak Onshape productivity
    www.theonsherpa.com
  • EvanReeseEvanReese Member, Mentor Posts: 2,847 PRO
    edited July 2025

    Hmm, I don't remember why, but it's possible that I totally spaced out on that. Thanks @rafael_telgmann !

    Evan Reese
    The Onsherpa | Reach peak Onshape productivity
    www.theonsherpa.com
  • Derek_Van_Allen_BDDerek_Van_Allen_BD Member Posts: 781 PRO

    @romeograham in other software packages these kinds of approximation functions tend to run on face normals and angles and not vertex clouds. Featurescript only has mesh vertices exposed to it and not individual mesh triangles and face normals to my knowledge, so this would be more difficult to achieve with purely vertex logic alone. I've seen others suggest reconstructing the triangles from the vertex data but I would expect that to be challenging because a group of vertices has many different ways it could be triangulated.

  • DeXxikDeXxik Member Posts: 2

    Great job!!

  • EvanReeseEvanReese Member, Mentor Posts: 2,847 PRO

    Thanks, @DeXxik

    Evan Reese
    The Onsherpa | Reach peak Onshape productivity
    www.theonsherpa.com
  • armandoRRarmandoRR Member, pcbaevp Posts: 33 PRO

    @EvanReese, as always, your FS are so useful. Thank you! I expanded your mesh points to return normals as well to help guide Constrained Surface more.

    FeatureScript 2892;
    import(path : "onshape/std/common.fs", version : "2892.0");
    
    icon::import(path : "ed643723202f2e05793e2a3d", version : "380086994ea933e56d2cb4d7");
    
    annotation { "Feature Type Name" : "Extract mesh points (normals)",
            "Feature Type Description" : "Extract mesh vertices as points or mate connectors with normals",
            "Icon" : icon::BLOB_DATA }
    export const extrachMeshVertices = defineFeature(function(context is Context, id is Id, definition is map)
        precondition
        {
            annotation { "Name" : "Mesh bodies and faces", "Filter" : (EntityType.BODY && !AllowMeshGeometry.NO) || (EntityType.FACE && !AllowMeshGeometry.NO) }
            definition.meshes is Query;
    
            annotation { "Name" : "Mesh points", "Filter" : EntityType.VERTEX && AllowMeshGeometry.YES }
            definition.points is Query;
    
            annotation { "Name" : "Output type" }
            definition.outputType is OutputType;
    
            if (definition.outputType == OutputType.MATE_CONNECTOR)
            {
                annotation { "Name" : "Flip normals" }
                definition.flipNormals is boolean;
    
                annotation { "Name" : "Owner body", "Filter" : EntityType.BODY, "MaxNumberOfPicks" : 1 }
                definition.ownerBody is Query;
            }
        }
        {
            // Collect points from mesh bodies/faces (only if meshes are selected)
            var meshFacePoints = [];
            if (size(evaluateQuery(context, definition.meshes)) > 0)
            {
                meshFacePoints = evMeshPoints(context, {
                        "meshes" : definition.meshes
                    });
            }
    
            // Collect individually selected points
            var individualPoints = [];
            for (var point in evaluateQuery(context, definition.points))
            {
                individualPoints = append(individualPoints, evVertexPoint(context, { "vertex" : point }));
            }
    
            // Collect mesh faces for normal estimation (from meshes selector AND from source meshes of individual points)
            var meshFaceQuery = qEntityFilter(definition.meshes, EntityType.FACE);
            var meshBodyQuery = qEntityFilter(definition.meshes, EntityType.BODY);
            var sourceMeshQuery = qSourceMesh(definition.points);
            var allFacesQuery = qUnion([meshFaceQuery, qOwnedByBody(meshBodyQuery, EntityType.FACE), qOwnedByBody(sourceMeshQuery, EntityType.FACE)]);
    
            if (definition.outputType == OutputType.POINT)
            {
                // Original behavior: create construction points
                processPoints(context, id + "meshPts", meshFacePoints);
                processPoints(context, id + "indivPts", individualPoints);
            }
            else if (definition.outputType == OutputType.MATE_CONNECTOR)
            {
                var flipSign = definition.flipNormals ? -1 : 1;
                var owner = definition.ownerBody;
    
                // Create mate connectors for mesh body/face points
                processMateConnectors(context, id + "meshMC", meshFacePoints, allFacesQuery, flipSign, owner);
    
                // Create mate connectors for individually selected points
                processMateConnectors(context, id + "indivMC", individualPoints, allFacesQuery, flipSign, owner);
            }
    
            reportFeatureInfo(context, id, size(meshFacePoints) + size(individualPoints) ~ " points processed.");
        }, { "outputType" : OutputType.POINT, "flipNormals" : false });
    
    /** @internal */
    export enum OutputType
    {
        annotation { "Name" : "Points" }
        POINT,
        annotation { "Name" : "Mate connectors" }
        MATE_CONNECTOR
    }
    
    /**
     * Create construction points from an array of 3D positions.
     */
    function processPoints(context is Context, id is Id, points is array)
    {
        for (var i = 0; i < size(points); i += 1)
        {
            opPoint(context, id + ("pt" ~ i), { "point" : points[i] });
            addDebugPoint(context, points[i], DebugColor.BLACK);
        }
    }
    
    /**
     * Estimate vertex normal by finding the closest point on the mesh faces
     * and using the surface normal there, then create a mate connector.
     */
    function processMateConnectors(context is Context, id is Id, points is array,
        allFacesQuery is Query, flipSign is number, owner is Query)
    {
        for (var i = 0; i < size(points); i += 1)
        {
            var point = points[i];
            var normal = estimateNormalAtPoint(context, point, allFacesQuery);
    
            normal = normalize(normal) * flipSign;
    
            // Build coordinate system: Z = normal, X = arbitrary perpendicular
            var cSys = buildCoordSystem(point, normal);
    
            opMateConnector(context, id + ("mc" ~ i), {
                    "coordSystem" : cSys,
                    "owner" : owner
            });
        }
    }
    
    /**
     * Estimate the surface normal at a given point by finding the closest
     * point on the mesh faces and evaluating the tangent plane there.
     *
     * Uses evDistance to find the closest face, then evFaceTangentPlane
     * at the parameter of that closest point.
     */
    function estimateNormalAtPoint(context is Context, point is Vector, allFacesQuery is Query) returns Vector
    {
        try silent
        {
            // Find the closest point on the mesh faces to our vertex
            var distResult = evDistance(context, {
                    "side0" : point,
                    "side1" : allFacesQuery,
                    "maximum" : 1
            });
    
            if (size(distResult) > 0)
            {
                var closest = distResult[0];
                // closest.sides[1].index gives the face index in the query
                // closest.sides[1].parameter gives the UV parameter on that face
                var faceIndex = closest.sides[1].index;
    
                // Get the specific face from the query
                var face = qNthElement(allFacesQuery, faceIndex);
    
                // Evaluate the tangent plane at the closest parameter
                var tangentPlane = evFaceTangentPlane(context, {
                        "face" : face,
                        "parameter" : closest.sides[1].parameter
                });
    
                // Extract a clean unitless direction vector
                var n = tangentPlane.normal;
                return vector(n[0], n[1], n[2]);
            }
        }
    
        // Fallback: return +Z if we couldn't determine the normal
        return vector(0, 0, 1);
    }
    
    /**
     * Build a CoordSystem with the Z-axis along the given normal
     * and an arbitrary perpendicular X-axis.
     */
    function buildCoordSystem(origin is Vector, normal is Vector) returns CoordSystem
    {
        // Choose a reference vector that isn't parallel to the normal
        var ref = vector(1, 0, 0);
        if (abs(dot(normal, ref)) > 0.9)
        {
            ref = vector(0, 1, 0);
        }
    
        var perpendicular = normalize(cross(ref, normal));
    
        // coordSystem(origin, xAxis, zAxis)
        // With (origin, perpendicular, normal): red axis showed the normal
        // So third arg = red axis. To get normal on blue axis, put it second.
        return coordSystem(origin, normal, perpendicular);
    }
    
  • EvanReeseEvanReese Member, Mentor Posts: 2,847 PRO

    Thanks @armandoRR That's a good idea and I'd love to fold that change into the feature! However I'm seeing this when using it. Shouldn't the MC z axis be normal to the mesh?

    image.png

    I also updated the input type to a horizontal enum at the top and made it possible to pick meshes as owner bodies for the MCs

    image.png
    Evan Reese
    The Onsherpa | Reach peak Onshape productivity
    www.theonsherpa.com
  • armandoRRarmandoRR Member, pcbaevp Posts: 33 PRO

    @EvanReese Strange, when I test it, it is normal to mesh, but I've not tried it on closed mesh bodies. Will have to test this out. I'll switch to horizontal enum as well.

    normal to mesh.png
  • EvanReeseEvanReese Member, Mentor Posts: 2,847 PRO

    Those all look vertical to me in the screenshot fwiw. Test by transforming the mesh and rotating at an odd angle and trying it again. I really like the idea though!

    Evan Reese
    The Onsherpa | Reach peak Onshape productivity
    www.theonsherpa.com
Sign In or Register to comment.