Closed fedorkotov closed 3 years ago
You could try selecting the wires and using offset2d to create the lip.
Thank you @jmwright. I forgot about offset2D.
For this experiment I modified the case shape from my previous example to make it non-convex.
def get_case_bottom(wp):
return\
split_bottom(
get_inner_volume(wp)
.union(get_inner_volume(wp.move(25,25)))\
.shell(2),
0)
Cutting lip with ridge on the outer side of case seam by removing material was easy:
def cut_inner_lip(wp, width):
return\
wp\
.faces(">Z")\
.wires()\
.item(0)\
.toPending()\
.workplane()\
.offset2D(-width)\
.cutBlind(-1)
result = \ cut_inner_lip( get_case_bottom( cq.Workplane("XY")), 0.9)
![image](https://user-images.githubusercontent.com/1106650/110837862-d38db080-82b2-11eb-846f-99041b81ef55.png)
But what if I want to add the ridge material without removing part of existing wall?
And how do I make a ridge on the inner side of the seam surface?
**For some reason I can not extrude or cut with one of the top face wires. Their 2D offsets can be cut or extruded but the wires themselves can not.** For example I tried to
- select top outer wire and extrude it in positive Z direction to create a temporary "lid"
- select new top outer wire, create 2D offset and cut it in negative Z direction to remove most of the "lid" so that only outer ridge remains
```python
def add_outer_lip(wp, width):
return\
wp\
.faces(">Z")\
.wires()\
.item(0)\
.toPending()\
.workplane()\
.extrude(1)\
.faces(">Z")\
.wires()\
.item(0)\
.toPending()\
.workplane()\
.offset2D(-width)\
.cutBlind(-1)
result = \
add_outer_lip(
get_case_bottom(
cq.Workplane("XY")),
0.9)
This code fails at extrude(1)
with error Standard_NullObject: BRep_Tool:: TopoDS_Vertex hasn't gp_Pnt
See full code of minimal example at the end of this post
For some reason if I replace extrude(1)
with twistExtrude(1, 1)
it works.
I do not need a skewed lip but this fact may may help someone in debugging.
Same thing with simpler geometry works
result = \
cq.Workplane("XY")\
.rect(50,50)\
.extrude(50)\
.faces(">Z")\
.shell(2)\
.faces(">Z")\
.wires()\
.item(1)\
.toPending()\
.workplane()\
.extrude(1)\
.faces(">Z")\
.wires()\
.item(0)\
.toPending()\
.workplane()\
.offset2D(-1)\
.cutBlind(-1)
Can you paste the minimal example with correct formatting?
Sorry. Did not notice that details tag has garbled it. minimal-example.py.txt
Your minimal example works for me and results in the following model:
Hmm. Probably this was fixed recently. I checked again on another machine and got the same error. I recreated conda environment and re-installed cadquery and cq-editor. Now it works.
And creating inner ridge works too with the following code (full source code attached)
def add_inner_lip(wp, width):
return\
wp\
.faces(">Z")\
.wires()\
.item(0)\
.toPending()\
.workplane()\
.offset2D(-width)\
.extrude(1)\
.faces(">Z[-2]")\
.wires()\
.item(2)\
.toPending()\
.workplane()\
.cutBlind(1)
result = \
add_inner_lip(
get_case_bottom(
cq.Workplane("XY")),
0.9)
Using Offset2D is better than my initial approach but I have a new related question.
How do I select the innermost or outermost wire of a non-convex face(s) of irregular shape?
Box selection will not do. Now I use
.item(..)
as you can see in code examples in this and previous posts. But this is not a robust method. There are no guaranties about wire ordering (or are there?).
This looks like a pretty cool project @fedorkotov!
I don't have time to fully read and digest your code, so I might be missing a better solution, but for:
How do I select the innermost or outermost wire of a non-convex face(s) of irregular shape?
Note that there is a method Wire.Length
that might help you.
w = (
# make 2 unioned boxes
cq.Workplane()
.box(10, 10, 2)
.union(
cq.Workplane()
.box(10, 10, 2, centered=(False, False, True))
)
# shell and remove the top face
.faces(">Z")
.shell(-0.5, "intersection")
# select the two wires from the top face
.faces(">Z")
.wires()
)
wires_sorted = w.vals()
wires_sorted.sort(key=lambda x: x.Length())
# select just the outer wire and fillet that edge
w = w.newObject([wires_sorted[1]]).fillet(0.4)
@fedorkotov's code here is another use case for #646.
Method of ordering by length proposed by @marcus7070 sounds cool and easy to implement but it is based on the following implicit assumption
For two continuous closed 2D curves L_1
, L_2
without self-intersections and not intersecting each other
((L_1 is inside L_2) or (L_2 is inside L_1)) and (Length(L_2) < Length(L_1)) ==> (L_2 is inside L_1)
(assume any sane definition of "is inside")
Obviously this is not true for all curves. Counterexample is easy to construct.
Here the longer curve is inside the sorter one.
Probably it is true for the curves of interest but it is not obvious (to me) what additional assumptions about L_1
and L_2
are required to prove it.
A better criterion would be area bounded by the curve. Proposition analogous to above for areas instead of lengths is easy to prove. But how do I calculate said areas in cadquery?
@fedorkotov
The following converts an arbitrary set of wires to a face and gets the area from that. It uses log
instead of print
because I wrote it in CQ-editor. Note that I'm stepping down to the layer below the fluent API. The other devs might think of a way to do it through the fluent API, I'm not sure.
import cadquery as cq
result = cq.Workplane().rect(10, 10)
face = cq.Face.makeFromWires(result.wires().val())
log(face.Faces()[0].Area()) # logs out 99.99999999999
@fedorkotov check Face.outerWire
and Face.innerWires
- I think it is exactly what you want.
@jmwright , @adam-urbanczyk thank you for suggestions. I will check them out over the weekend and will share my results
Face.makeFromWires(..)
proposed by @jmwright is better in this case than Face.outerWire
/Face.innerWires
proposed by @adam-urbanczyk because Face.outerWire
/Face.innerWires
can not select innermost or outermost wire when I have more than one face selected (see inner lip example earlier in this issue)
I wrote a selector that helps select wire with nth area.
class WireWithNthAreaSelector(cq.Selector):
def __init__(
self,
idx):
self._idx = idx
def filter(self, objectlist: Sequence[cq.Wire]) -> List[cq.Wire]:
if len(objectlist) == 0:
# nothing to filter
raise ValueError(
"Can not return the Nth nested wire of an empty list")
if(len(objectlist) <= self._idx or
self._idx < -len(objectlist)):
raise IndexError(
"Can not return wire with index {} from list with {} wires"\
.format(self._idx, len(objectlist)))
wires_with_areas = \
[(wire,
cq.Face.makeFromWires(wire).Area())
for wire in objectlist]
wires_sorted_by_area = \
sorted(
wires_with_areas,
key=lambda w_data: w_data[1])
selected_wire, _ = \
wires_sorted_by_area[self._idx]
return [selected_wire]
this is how I used it to add inner and outer lips
def add_inner_lip(wp, width):
return \
wp\
.faces(">Z")\
.wires(WireWithNthAreaSelector(-1))\
.toPending()\
.workplane()\
.offset2D(-width)\
.extrude(1)\
.faces(">Z[-2]")\
.wires(WireWithNthAreaSelector(0))\
.toPending()\
.workplane()\
.cutBlind(1)
def add_outer_lip(wp, width):
return\
wp\
.faces(">Z")\
.wires(WireWithNthAreaSelector(-1))\
.toPending()\
.workplane()\
.extrude(1)\
.faces(">Z")\
.wires()\
.item(0)\
.toPending()\
.workplane()\
.offset2D(-width)\
.cutBlind(-1)
Full source code is attached below
Many thanks to everyone for your help.
I think WireWithNthAreaSelector
can be useful not only for this particular task but for many other uses.
Should I create a pull-request?
Great, a PR would be nice! I will probably propose to generalize it into NthAreaSelector
, but let's discuss in the PR.
Yes it can be closed. I'm working on project now. It is a case for USB-flash like device which will use new selectors. Unfortunately it is not going as fast as I would like because of day job. But I intend to submit it to cq-contrib when it is ready.
What is the proper way of making lips on a case seam like this?
Note that there is a gap between lips of bottom and top half that is necessary to account for 3D printing inaccuracies.
Below I describe how I do it. Maybe there is a better way? I thought about sweeping lip cross section along seam boundary but did not find a way to do that without manually redrawing sweeping path which would be tedious for anything but a simple box with sharp corners. Or maybe someone knows a library that does this automatically?
My method is
(see code below)