Filleting a polyline path

65 views
Skip to first unread message

Joe Schroeder

unread,
Mar 10, 2024, 9:05:57 PMMar 10
to CadQuery
Hi,

I am new to CQ and this group, although I have been doing 3D modeling for about 30 years.

My current project requires that I model bent tube geometry. My typical workflow in  CAD, for example Rhino, is to create a polyline using the intersecion points of the tube's straight segments, then fillet the corners of that 3D polyline using the bend centerline radius before sweeping a circle along that path.

Rhino does that easily, with its "Fillet corners" command. I was hoping that CQ would too, but obviously not.  I have created my polyline, but without fillets it does not represent the tube shape.

I have also tried using a spline, but that doesn't work either, at least not with the sparse control points given by the intersections.

I have considered using 2D operations on workplanes that I can create at the intersection points and in the plane of each bend, but that seems like a lot of work to get something that seems so simple. 

I am also considering to use Sketch entities rather than Workplanes. Might that be a better option? 

Here is code that creates a sharp edged polyline or spline based sweep:

import cadquery as cq

# Create a tube (actually a solid wire)
# using intersection points
# dimensions are inches

rotated_points = [
 [ 0.000,0.000,0.000],
 [-0.287,1.183,-0.592],
 [-1.404,4.113,-2.787],
 [-1.332,1.522,0.553],
 [ 7.062,0.433,-0.097],
 [ 8.539,-0.000,-0.000]]

diameter = 0.25
fillet_radius = 0.560

pts_tuple = []
for i in range(len(rotated_points)):
    pt = tuple(rotated_points[i])
    pts_tuple.append(pt)

path = cq.Workplane("XY").polyline(pts_tuple).wire()
normal_plane = cq.Plane((0,0,0), None, pts_tuple[1])
wire = cq.Workplane(normal_plane).circle(diameter).sweep(path)
show_object(wire)

# try with spline:
# path2 = cq.Workplane("XY").spline(pts_tuple).wire()
# wire2 = cq.Workplane(normal_plane).circle(diameter).sweep(path2)
# show_object(wire2)

Thanks for any insights.

Adhip

unread,
Mar 14, 2024, 2:19:37 AMMar 14
to CadQuery
Hello

I was also figuring out the same thing few days ago. Controlling spline for tight turns is a trouble for me too and I don't know how to give control points for it. 
While using Polyline, in the sweep command you can give transition as round. This gives outer radius but I don't know how to control this radius. Hopefully someone will guide you on this.

Meanwhile you may try this:

import cadquery as cq

# Create a tube (actually a solid wire)
# using intersection points
# dimensions are inches

rotated_points = [
 [ 0.000,0.000,0.000],
 [-0.287,1.183,-0.592],
 [-1.404,4.113,-2.787],
 [-1.332,1.522,0.553],
 [ 7.062,0.433,-0.097],
 [ 8.539,-0.000,-0.000]]

diameter = 0.25
fillet_radius = 0.560

pts_tuple = []
for i in range(len(rotated_points)):
    pt = tuple(rotated_points[i])
    pts_tuple.append(pt)

path = cq.Workplane("XY").polyline(pts_tuple).wire()
normal_plane = cq.Plane((0,0,0), None, pts_tuple[1])
wire = cq.Workplane(normal_plane).circle(diameter).sweep(path, transition="round")
show_object(wire)

Adam Urbanczyk

unread,
Mar 14, 2024, 3:09:59 AMMar 14
to CadQuery
Such an operation is not implemented in CQ and AFAICT not in OCCT. You'll have to implement it yourself iterating over the edges and using something like this https://dev.opencascade.org/doc/refman/html/class_ch_fi2d___fillet_a_p_i.html PRs are welcome

Dov Grobgeld

unread,
Mar 14, 2024, 4:11:59 AMMar 14
to Adam Urbanczyk, CadQuery
Thanks Adam for the tip. I picked you up on your idea and created a proof of concept of how to use ChFi2d_FilletAPI with the OCP interface.

The result is in:


Here's a screenshot from cq-editor:

fs.png
It should be straightforward to convert this example so that it works on a polygon (or wire) input and returns a wire with filleted corners.

Regards,

--
cadquery home: https://github.com/CadQuery/cadquery
post issues at https://github.com/CadQuery/cadquery/issues
run it at home at : https://github.com/CadQuery/CQ-editor
---
You received this message because you are subscribed to the Google Groups "CadQuery" group.
To unsubscribe from this group and stop receiving emails from it, send an email to cadquery+u...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/cadquery/1bbd1ee2-7ed7-472c-95ff-0439e34a1a1en%40googlegroups.com.

Dov Grobgeld

unread,
Mar 16, 2024, 1:51:37 PMMar 16
to CadQuery, Joe Schroeder

Hello,

I found this a nice challenge. Below is my implementation of Joe’s original question.

Adam, what kind of API would you for this? Just a method to the Wire class?

Regards,

#!/usr/bin/python

######################################################################
#  An implementation of Joe Schroeder's question on the cadquery list
#  of how to fillet an open wire.
#
#  This code is free software; you can redistribute it and/or
#  modify it under the terms of the GNU Lesser General Public
#  License as published by the Free Software Foundation; either
#  version 2.1 of the License, or (at your option) any later version.
#
#  2024-03-16 Sat
#  Dov Grobgeld <dov.gr...@gmail.com>
######################################################################

import cadquery as cq
from cadquery import exporters
from OCP.gp import gp_Pln
from OCP.ChFi2d import ChFi2d_FilletAPI

def filletWire(wire: cq.Wire, radius: float) -> cq.Wire:
    """Fillets a wire in 2d or 3d

    :param wire: The input wire to fillet. Currently only open wires are supported
    :param radius: the radius of the fillet, must be > zero

    :return: A wire with filleted corners
    """
    edges = list(wire)
    newEdges = []
    currentEdge = edges[0]

    for i in range(len(edges) - 1):
        nextEdge = edges[i + 1]

        # Create a plane that is spanned by currentEdge and nextEdge
        currentDir = currentEdge.tangentAt(1)
        nextDir = nextEdge.tangentAt(0)
        normalDir = currentDir.cross(nextDir)

        # Check if they are parallell and if so, skip adding a fillet
        if normalDir.Length == 0:
            newEdges.append(currentEdge)
            currentEdge = nextEdge
            continue

        # Prepare for using ChFi2d_FilletAPI
        pointInPlane = currentEdge.Center().toPnt()
        cornerPlane = gp_Pln(pointInPlane, normalDir.toDir())

        filletMaker = ChFi2d_FilletAPI(
            currentEdge.wrapped, nextEdge.wrapped, cornerPlane
        )

        ok = filletMaker.Perform(radius)
        if not ok:
            raise RuntimeError(f"Failed fillet at vertex {i+1}!")

        # Get the result of the fillet operation
        thePoint = next(iter(nextEdge)).Center().toPnt()
        res_arc = filletMaker.Result(thePoint, currentEdge.wrapped, nextEdge.wrapped)

        newEdges.append(currentEdge)
        newEdges.append(cq.Edge(res_arc))

        currentEdge = nextEdge

    # Add the last edge
    newEdges.append(currentEdge)

    return cq.Wire.assembleEdges(newEdges)

if __name__ == "__main__":
    # Here is Joe Schroeder original question.


    # Create a tube (actually a solid wire)
    # using intersection points
    # dimensions are inches

    rotated_points = [
        [0.000, 0.000, 0.000],
        [-0.287, 1.183, -0.592],
        [-1.404, 4.113, -2.787],
        [-1.332, 1.522, 0.553],
        [7.062, 0.433, -0.097
],
        [8.539, -0.000, -0.000],
    ]

    diameter = 0.25
    fillet_radius = 0.560

    pts_tuple = rotated_points

    # Turn the polyline into a wire
    wire = cq.Wire.makePolygon(rotated_points, close=False)

    # Fillet the wire
    wire = filletWire(wire, fillet_radius)

    # The initial direction of the wire
    normal_plane = cq.Plane(rotated_points[0], normal=wire.tangentAt(0))

    result = cq.Workplane(normal_plane).circle(radius=diameter / 2).sweep(wire)

    exporters.export(result, "filling-a-polyline-path.stl")
    print("ok")

Dov Grobgeld

unread,
Mar 16, 2024, 1:56:40 PMMar 16
to CadQuery, Joe Schroeder
I forgot the obligatory cq-editor screenshot. :-)

cq-screenshot.png

Adam Urbanczyk

unread,
Mar 24, 2024, 4:48:36 AMMar 24
to CadQuery
A PR would be nice. I'd indeed start with a cq.Wire.fillet(self, radius) method. Would be also nice to have an overload that allows to specify which vertices to fillet, i.e. something like this cq.Wire.fillet(self, vertices, radius)
Message has been deleted

Dov Grobgeld

unread,
Apr 6, 2024, 1:36:09 PMApr 6
to Joe Schroeder, CadQuery
Hi Joe,

Glad you like it! Could you expand on what your idea is I'm curious? :-)

Note that my POC gist from this thread doesn't require you to do any initial installations. You should be able to run it in cq-editor (or whatever environment you are running) just as before. Of course, on the next release of cadquery, you'll have to update your code to use the new cq.Wire interface

On Sat, Apr 6, 2024 at 3:31 AM Joe Schroeder <josephjs...@gmail.com> wrote:
Thank you so much for this! It is the key to my app!

But I'm not getting pyinstaller to work - I keep getting:

File "cadquery\occ_impl\geom.py", line 5, in <module>
ModuleNotFoundError: No module named 'OCP'

I've tried adding search paths to my .spec file, but no success...

Any ideas?

Thanks,

Joe

Reply all
Reply to author
Forward
0 new messages