Problem making face/workplane from wires (personal homework/growth).

21 views
Skip to first unread message

george hartzell

unread,
Nov 18, 2025, 4:33:04 PMNov 18
to CadQuery
Hi All,

I'm trying to get a better understanding of how cadquery works and make useful stuff along the way.

Lately I've been playing with cutting hexagonal arrays through solids, giving me a flexible way to do things like this:

Screenshot 2025-11-18 at 2.18.45 PM.png

(I know, not rocket science.  Think homework...)

I'd like to extend the technique to cut through non-rectangular shapes (for the above, I'm getting the bounding box of the face, subtracting a bit, then making a rectangle and extruding it and intersecting the resulting box with an array of extruded hexagons...).  
Now I'd like to do e.g., this:

Screenshot 2025-11-18 at 2.19.48 PM.png

I thought I could get a face, then get the wires, then offset2D them inward a bit, then extrude the result, but none of the gyrations I've tried have worked.

Here's the basic test harness:

from datetime import datetime

import cadquery as cq

# import dumper

log(f"Starting new run: {datetime.now()}")

r1 = cq.Workplane("XY").rect(10, 5).extrude(5)
r2 = cq.Workplane("XY").rect(4, 5).extrude(5).translate((0, 0, 5))
r = r1.union(r2)

f = r.faces("<Y")
log(f"f: {f}")
debug(f, name="f")

w = f.val().wires()
log(f"w: {w}")
debug(w, name="w")

offset_wires = w.offset2D(-1, "intersection")
log(f"offset_wires: {offset_wires}")
debug(offset_wires, name="offset_wires")

# solid = ...
# show_object(solid)


I'd like to "buy" a clue, what is a recommended way to get back up from the list of Wires to the fluent API level?

For example, this;

solid = cq.Workplane().newObject(offset_wires).toPending().extrude(2)

gives me this:

Screenshot 2025-11-18 at 2.30.43 PM.png

Thoughts, feedback, or suggestions?

Thanks!

g.

george hartzell

unread,
Nov 18, 2025, 6:43:50 PMNov 18
to CadQuery
I came at this from another angle I stumbled on, using transformGeometry.  It works, as long as the face that I'm starting with is on the XY plane, I could shift the smaller object down a bit and subtract it from the bigger one and get just the kind of border I'm looking for (or intersect the smaller object with an array of tubes/hexes/... and subtract *that* from the bigger object.

But, if I start from another face, odd things happen.  I *think* that it either has something to do with the transformation matrix (reaching back to college classwork...) I'm using or with the plane of the Workplane I use to create the "small_object" from the "smaller_face".

Here's a little example:

import cadquery as cq
from cadquery import Matrix

big1 = cq.Workplane("XY").box(30, 20, 10)
big2 = cq.Workplane("XY").box(40, 5, 10)
big_object = big1.union(big2)

big_face = big_object.faces(">Z")
debug(big_face, name="big_face")

smaller_face = big_face.val().transformGeometry(
    Matrix([[0.75, 0, 0, 0], [0, 0.75, 0, 0], [0, 0, 0, 0]])
)
debug(smaller_face, name="smaller_face")
log(f"smaller_face: {smaller_face}")

small = cq.Workplane(obj=smaller_face)
debug(small, name="small")
log(f"small: {small}")

small_object = small.extrude(10)
debug(small_object, name="small_object")
log(f"small_object: {small_object}")

show_object(big_object, options={"alpha": 0.7, "color": (0, 0, 127)})
show_object(small_object, options={"alpha": 0.7, "color": (127, 0, 127)})

which generates this:

Screenshot 2025-11-18 at 4.36.08 PM.png

Constructive criticism, color commentary, or useful feedback welcome.

george hartzell

unread,
Nov 18, 2025, 7:36:32 PMNov 18
to CadQuery
Indeed, I get a bit further if I use this matrix:

    Matrix([[0.75, 0, 0, 0], [0, 0.75, 0, 0], [0, 0, 0.75, 0]])

george hartzell

unread,
Nov 18, 2025, 7:47:48 PMNov 18
to CadQuery
With that change (fixing the z scaling value in the matrix), if I select the ">X" face, the extrude seems to happen in the wrong direction (global Z, when the new workplane that's constructed for the smaller face is (1,0,-0).

Here's the code:

import cadquery as cq
from cadquery import Matrix

big1 = cq.Workplane("XY").box(30, 20, 10)
big2 = cq.Workplane("XY").box(40, 5, 10)
big_object = big1.union(big2)

# big_face = big_object.faces(">Z")
# big_face = big_object.faces("<Y")
big_face = big_object.faces(">X")

debug(big_face, name="big_face")

smaller_face = big_face.val().transformGeometry(
    Matrix([[0.75, 0, 0, 0], [0, 0.75, 0, 0], [0, 0, 0.75, 0]])

)
debug(smaller_face, name="smaller_face")
log(f"smaller_face: {smaller_face}")

small_workplane = cq.Workplane(obj=smaller_face)
debug(small_workplane, name="small_workplane")
log(f"small: {small_workplane}")
log(f"small normal: {small_workplane.val().normalAt()}")

small_object = small_workplane.extrude(10)

debug(small_object, name="small_object")
log(f"small_object: {small_object}")

show_object(big_object, options={"alpha": 0.7, "color": (0, 0, 127)})
show_object(small_object, options={"alpha": 0.7, "color": (127, 0, 127)})

and here's what it looks like:

Screenshot 2025-11-18 at 5.44.16 PM.png

Anyone see what I'm doing wrong?

g.

george hartzell

unread,
Nov 18, 2025, 7:57:20 PMNov 18
to CadQuery
Apologies for the running commentary, but I see that while the normal for the face that's in the "small object" workplane is (1, 0, -0), the Z direction for the workplane is (0,0,1).

[00:52:49] INFO: small_workplane: Workplane object at 0x1e28bccb0:

no parent

plane: 0)):

origin: (0.0, 0.0, 0.0)

z direction: (0.0, 0.0, 1.0)

objects: [<cadquery.occ_impl.shapes.Face object at 0x1e28bd640>]

modelling context: CQContext object at 0x1e34e5670:

pendingWires: []

pendingEdges: []

tags: {}

[00:52:49] INFO: small_workplane val normal: Vector: (1.0, 0.0, -0.0)


Hence the issue.

Off to scratch my head a bit.

george hartzell

unread,
Nov 19, 2025, 4:39:06 PM (14 days ago) Nov 19
to CadQuery
Apologies for the traffic, but I figured out a way to get to where I was trying to go (after a bunch of blind alleys).  Someone here once said "Use the source, Luke", and that was what finally got me to an answer.

Here's a little demo, it:

- makes a cube with some holes through one face (to make things "interesting")
- then cuts an array of squares (or whatever) 
- but avoids cutting through the edges of the cube or the "interesting" holes.

Things I learned, or was reminded of, include:

- working with a face's outer and inner wires;
- making a Face from a set of Wires;
- getting a Plane from a Workspace with a Face;
- using that Plane to set up a new Workspace with the coordinate system that's useful;
- Workplane add() and toPending()

The helper functions in https://cadquery.readthedocs.io/en/latest/workplane.html#an-introspective-example were really useful for logging meaningful info about various things.

Here's the demo I ended up with:

Screenshot 2025-11-19 at 2.37.03 PM.png

and here's the source code for it:

from datetime import datetime

import cadquery as cq
from cadquery import Face

log(f"Starting new run at: {datetime.now()}")

face_selector = ">Y"
size = 50

# create a box with some holes drilled through from some face face.
box = cq.Workplane("XY").box(size, size, size)
c = (
    box.faces(face_selector)
    .workplane()
    .rect(15, 30, forConstruction=True)
    .vertices()
    .circle(4)
    .extrude(-size, combine=False)
)
box = box.cut(c)

# Get the "<Y" face and its wires
the_face = box.faces(face_selector)
# debug(the_face, name="the_face")

da_workplane = cq.Workplane(the_face.workplane(centerOption="ProjectedOrigin").plane)

# debug(da_workplane, name="da_workplane")

# Make a new face with modified wires
_tf = the_face.val()
ow = _tf.outerWire()
iw = _tf.innerWires()
new_ow = ow.offset2D(-2)[0]
new_iw = [w.offset2D(2)[0] for w in iw]
f2 = Face.makeFromWires(new_ow, new_iw)
# debug(f2, name="f2")

# Generate a big array of cylinders
tubes = da_workplane.rarray(7, 7, 10, 10).rect(5, 5).extrude(-size)  # Extrude the tubes
# debug(tubes, name="tubes")

mask = da_workplane.add(f2).wires().toPending().extrude(-size)
# debug(mask, name="mask")

masked_tubes = tubes.intersect(mask)
# debug(masked_tubes, name="masked tubes")

box = box.cut(masked_tubes)

show_object(box, options={"alpha": 0.5, "color": (0, 127, 127)})

george hartzell

unread,
Nov 19, 2025, 5:44:28 PM (14 days ago) Nov 19
to CadQuery

f2 = Face.makeFromWires(new_ow, new_iw)

Note that makeFromWires doesn't check whether the "inner" wires are actually contained by the outer wires.  Given that I made the outer smaller and the inner bigger, for some variations you can get weird things happening.

g.

Lorenz

unread,
Nov 21, 2025, 8:42:13 PM (11 days ago) Nov 21
to CadQuery
Thanks for sharing the demo.  You might be interested to try the Free function API.

For example to create the face f2:

from cadquery.func import *
f2b = face(new_ow, *new_iw)

# and the mask:
maskb = extrude(f2b, size * f2b.normalAt())
Reply all
Reply to author
Forward
0 new messages