CadQuery / cadquery

A python parametric CAD scripting framework based on OCCT
https://cadquery.readthedocs.io
Other
3.26k stars 295 forks source link

how to iteratively loft wires in cadquery 2? #319

Open lalebarde opened 4 years ago

lalebarde commented 4 years ago

Same post as here. I can loft a 2D shape from one plane to another like this:

import cadquery as cq
from jupyter_cadquery.cadquery import show
result = cq.Workplane("XY").rect(1,2).workplane()  \
     .transformed(offset=cq.Vector(0, -0.5, 1.0),rotate=cq.Vector(10, 0, 0)) \
     .rect(1,2).loft(combine=True)
show(result)

image image

I would like to repeat the operation several times like this:

import cadquery as cq
from jupyter_cadquery.cadquery import show

wp = cq.Workplane("XY").rect(1,2).workplane()
result = None
for i in range(0,5):
    wp2 = wp.transformed(offset=cq.Vector(0, -0.5, 1.0),rotate=cq.Vector(10, 0, 0))
    if result == None:
        result = wp2.rect(1,2).loft(combine=True)
    else:
        nextpart = wp2.rect(1,2).loft(combine=True)
        result = result.union(nextpart)
    wp = wp2
show(result)

I have the following error:

---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-54-37e88c00eca8> in <module>
      9         result = wp2.rect(1,2).loft(combine=True)
     10     else:
---> 11         nextpart = wp2.rect(1,2).loft(combine=True)
     12         result = result.union(nextpart)
     13     wp = wp2

~/anaconda3/envs/cq-jl/lib/python3.7/site-packages/cadquery/cq.py in loft(self, filled, ruled, combine)
   2897         self.ctx.pendingWires = []
   2898 
-> 2899         r = Solid.makeLoft(wiresToLoft, ruled)
   2900 
   2901         if combine:

~/anaconda3/envs/cq-jl/lib/python3.7/site-packages/cadquery/occ_impl/shapes.py in makeLoft(cls, listOfWire, ruled)
   1594         # the True flag requests building a solid instead of a shell.
   1595         if len(listOfWire) < 2:
-> 1596             raise ValueError("More than one wire is required")
   1597         loft_builder = BRepOffsetAPI_ThruSections(True, ruled)
   1598 

ValueError: More than one wire is required

At least, If I suppress the for loop and keep the same variable decomposition, it works:

import cadquery as cq
from jupyter_cadquery.cadquery import show

wp = cq.Workplane("XY").rect(1,2).workplane()
wp2 = wp.transformed(offset=cq.Vector(0, -0.5, 1.0),rotate=cq.Vector(10, 0, 0))
result = wp2.rect(1,2).loft(combine=True)
wp = wp2
show(result)

I think I am close to the result, but I cannot find where I mistake myself. The problem occurs when I come in the loop the second time. I think I have accumulated already two wires in wp2. I should keep only the last one before adding a new one, but I don't know how to do it.

lalebarde commented 4 years ago

I have tried wp = cq.copyWorkplane(wp2) instead of wp = wp2, but I get the following error:

AttributeError: module 'cadquery' has no attribute 'copyWorkplane'

I have supposed it is right from the documentation here:

class cadquery.CQ(obj)
copyWorkplane(obj)[source]

Copies the workplane from obj.

Parameters

    obj (a CQ object) – an object to copy the workplane from
Returns

    a CQ object with obj’s workplane
adam-urbanczyk commented 4 years ago

Why do you want to actually loft? There is a sweep function that can even take different sections.

lalebarde commented 4 years ago

I need control on the section orientation: yaw, roll and pitch

lalebarde commented 4 years ago

Here is an example with sweep:

import cadquery as cq
from math import sin, cos, pi
SEGMENTS = 48

def sinusoidal_ring(rad=25, segments=SEGMENTS):
    outline = []
    for i in range(segments):
        angle = i * 2 * pi / segments
        x = rad * (cos(angle) + i/segments)
        y = rad * sin(angle)
        z = 2 * rad * (sin(angle) / 5 + i / segments)
        outline.append((x, y, z))
    return outline

def triangle(base = 15, ratio = 2):
    return [(-base/2, 0), (base/2, 0), (0, base*ratio)]

def extrude_example():
    shape = triangle() 
    path = sinusoidal_ring(rad=50)
    p= cq.Workplane("XY").spline(path,includeCurrent=True)
    s = cq.Workplane("YZ").polyline(shape).close().sweep(p)
    return s

result = extrude_example()
show_object(result)

The section starts with a triangle oriented towards the Z axes, and does not stay as this along the path.

lalebarde commented 4 years ago

image image

adam-urbanczyk commented 4 years ago

Is this what you want to get?

obraz

I used raw OCC features, in the end it will be implemented in CQ in relation to #97

import cadquery as cq
from math import sin, cos, pi
SEGMENTS = 148

def sinusoidal_ring(rad=250, segments=SEGMENTS, N = 1):
    outline = []
    for i in range(segments):
        angle = N*i * 2 * pi / segments
        x = rad * (cos(angle) + i/segments)
        y = rad * sin(angle)
        z = 2 * rad * (sin(angle) / 5 + i / segments)
        outline.append((x, y, z))
    return outline

def triangle(base = 15, ratio = 2):
    return [(-base/2, 0), (base/2, 0), (0, base*ratio)]

def extrude_example():
    shape = triangle() 

    p= cq.Workplane("XY",origin=(0,0,30)).spline(path,includeCurrent=True)
    s = cq.Workplane("YZ").polyline(shape).close().sweep(p,isFrenet=True)
    return s,p

path = sinusoidal_ring(rad=50)

p1 = cq.Workplane("XY",origin=(0,0,30)).spline(path,includeCurrent=True)
p2 = cq.Workplane("XY",origin=(0,0,0)).spline(path,includeCurrent=True)
t = cq.Workplane("YZ").polyline(triangle()).close()

result,p = extrude_example()

import OCC

ps = OCC.Core.BRepOffsetAPI.BRepOffsetAPI_MakePipeShell(p1.wire().val().wrapped)
ps.Add(t.val().wrapped)
ps.SetMode(p2.wire().val().wrapped,True)
ps.Build()
ps.MakeSolid()

res = cq.Shape(ps.Shape())

show_object(res)
show_object(p1)
show_object(p2)
lalebarde commented 4 years ago

Oh yes, thank you very much Adam! I cannot use OCC directly at this time, so, you have saved me. Very nice to ear from you it will be in CQ.

lalebarde commented 4 years ago

Adam, there is something strange: I understand the spline starts at the origin because of includeCurrent=True in the spline command. If I put false, I obtain this:

image

image

lalebarde commented 4 years ago

I would like also to explore the different OCC options:

ps.SetMode(p2.wire().val().wrapped,True,OCC.Core.BRepOffsetAPI.BRepFill_ContactOnBorder)

raises the error:

AttributeError: module 'OCC.Core.BRepOffsetAPI' has no attribute 'BRepFill_ContactOnBorder'

lalebarde commented 4 years ago

Adam, I have made a more realistic test and unfortunatly, what brings ps.SetMode(p2.wire().val().wrapped,True) is not what I am looking for.

import cadquery as cq
from math import sin, cos, pi
SEGMENTS = 10

def test(l=20):
    return[(0,0,0),(l,0,0),(2*l,0,l/2),(3*l,0,4*l),(4*l,l,5*l),(4*l,2*l,6*l),(3*l,4*l,6*l)]

def triangle(base = 15, ratio = 2):
    return [(-base/2, 0), (base/2, 0), (0, base*ratio)]

path = test()

p1 = cq.Workplane("XY",origin=(0,0,40)).polyline(path)
p2 = cq.Workplane("XY",origin=(0,0,0)).polyline(path)
t = cq.Workplane("YZ").polyline(triangle()).close()

import OCC

ps = OCC.Core.BRepOffsetAPI.BRepOffsetAPI_MakePipeShell(p1.wire().val().wrapped)
ps.Add(t.val().wrapped)
ps.SetMode(p2.wire().val().wrapped,True)
ps.Build()
ps.MakeSolid()

res = cq.Shape(ps.Shape())

show_object(res)
show_object(p1)
show_object(p2)

image image

As you can see from the bottom view, the pattern don't rotate and the section of the volume becomes a line. Think of my need as a stair handrail. In my post here, my use case is (1, 1, 0) no yaw, but the section can roll when the handrail turns right or left, and can pitch when it climbs.

OCC is very hard to learn because the documentation is light and one has to test thoroughly to figure out what it can perform, or review the code if possible - and testing is hard because it lacks examples. Probably there is a way to use BRepOffsetAPI_MakePipeShell to fit ma case, but I cannot figure it out. So, maybe the best solution for me would be to going on with loft, where I can master what I want. loft enables to control precisely the location of every sections and I can calculate them. But I have failed above to perform it cf ValueError: More than one wire is required

lalebarde commented 4 years ago

Here is the working code:

import cadquery as cq
wp = cq.Workplane("XY").rect(1,2).workplane()
result = None
for i in range(0,5):
    wp2 = wp.transformed(offset=cq.Vector(0, -0.5, 1.0),rotate=cq.Vector(10, 0, 0)).rect(1,2).workplane()
    if result == None:
        result = wp2.loft(combine=True)
    else:
        nextpart = wp2.loft(combine=True)
        result = result.union(nextpart)
    wp = wp.transformed(offset=cq.Vector(0, -0.5, 1.0),rotate=cq.Vector(10, 0, 0)).rect(1,2).workplane()
show_object(result, options=dict(alpha=0.5,color='red'))

image

The point is to push back the last wire in the CQ stack, what a simple python assigment could not do (wp = wp2). So I changed it to wp = wp.transformed(offset=cq.Vector(0, -0.5, 1.0),rotate=cq.Vector(10, 0, 0)).rect(1,2).workplane() to make work the CQ kernel.