2024-07-06 16:41:13 -07:00
|
|
|
"""
|
|
|
|
Marking utilities for `Cq.Workplane`
|
|
|
|
|
|
|
|
Adds the functions to `Cq.Workplane`:
|
|
|
|
1. `tagPoint`
|
|
|
|
2. `tagPlane`
|
|
|
|
"""
|
2024-07-11 16:02:54 -07:00
|
|
|
import math
|
2024-07-14 17:56:02 -07:00
|
|
|
import functools
|
2024-07-06 16:41:13 -07:00
|
|
|
import cadquery as Cq
|
2024-07-12 23:16:04 -07:00
|
|
|
from nhf import Role
|
2024-07-14 17:56:02 -07:00
|
|
|
from typing import Union, Tuple, cast
|
|
|
|
|
|
|
|
# Bug fixes
|
|
|
|
def _subloc(self, name: str) -> Tuple[Cq.Location, str]:
|
|
|
|
"""
|
|
|
|
Calculate relative location of an object in a subassembly.
|
|
|
|
|
|
|
|
Returns the relative positions as well as the name of the top assembly.
|
|
|
|
"""
|
|
|
|
|
|
|
|
rv = Cq.Location()
|
|
|
|
obj = self.objects[name]
|
|
|
|
name_out = name
|
|
|
|
|
|
|
|
if obj not in self.children and obj is not self:
|
|
|
|
locs = []
|
|
|
|
while not obj.parent is self:
|
|
|
|
locs.append(obj.loc)
|
|
|
|
obj = cast(Cq.Assembly, obj.parent)
|
|
|
|
name_out = obj.name
|
|
|
|
|
|
|
|
rv = functools.reduce(lambda l1, l2: l2 * l1, locs)
|
|
|
|
|
|
|
|
return (rv, name_out)
|
|
|
|
Cq.Assembly._subloc = _subloc
|
2024-07-06 16:41:13 -07:00
|
|
|
|
2024-07-17 13:09:46 -07:00
|
|
|
### Vector arithmetic
|
|
|
|
|
|
|
|
def location_sub(self: Cq.Location, rhs: Cq.Location) -> Cq.Vector:
|
|
|
|
(x1, y1, z1), _ = self.toTuple()
|
|
|
|
(x2, y2, z2), _ = rhs.toTuple()
|
|
|
|
return Cq.Vector(x1 - x2, y1 - y2, z1 - z2)
|
|
|
|
Cq.Location.__sub__ = location_sub
|
|
|
|
|
2024-07-17 14:47:34 -07:00
|
|
|
def from2d(x: float, y: float, rotate: float=0.0) -> Cq.Location:
|
|
|
|
return Cq.Location((x, y, 0), (0, 0, 1), rotate)
|
|
|
|
Cq.Location.from2d = from2d
|
|
|
|
|
|
|
|
def is2d(self: Cq.Location) -> bool:
|
|
|
|
(_, _, z), (rx, ry, _) = self.toTuple()
|
|
|
|
return z == 0 and rx == 0 and ry == 0
|
|
|
|
Cq.Location.is2d = is2d
|
|
|
|
|
|
|
|
def to2d(self: Cq.Location) -> Tuple[Tuple[float, float], float]:
|
|
|
|
"""
|
|
|
|
Returns position and angle
|
|
|
|
"""
|
|
|
|
(x, y, z), (rx, ry, rz) = self.toTuple()
|
|
|
|
assert z == 0
|
|
|
|
assert rx == 0
|
|
|
|
assert ry == 0
|
|
|
|
return (x, y), rz
|
|
|
|
Cq.Location.to2d = to2d
|
|
|
|
|
2024-07-17 13:09:46 -07:00
|
|
|
### Tags
|
2024-07-06 16:41:13 -07:00
|
|
|
|
|
|
|
def tagPoint(self, tag: str):
|
|
|
|
"""
|
|
|
|
Adds a vertex that can be used in `Point` constraints.
|
|
|
|
"""
|
|
|
|
vertex = Cq.Vertex.makeVertex(0, 0, 0)
|
|
|
|
self.eachpoint(vertex.moved, useLocalCoordinates=True).tag(tag)
|
|
|
|
|
|
|
|
Cq.Workplane.tagPoint = tagPoint
|
|
|
|
|
2024-07-07 21:01:40 -07:00
|
|
|
def tagPlane(self, tag: str,
|
|
|
|
direction: Union[str, Cq.Vector, Tuple[float, float, float]] = '+Z'):
|
2024-07-06 16:41:13 -07:00
|
|
|
"""
|
|
|
|
Adds a phantom `Cq.Edge` in the given location which can be referenced in a
|
|
|
|
`Axis`, `Point`, or `Plane` constraint.
|
|
|
|
"""
|
2024-07-07 21:01:40 -07:00
|
|
|
if isinstance(direction, str):
|
|
|
|
x, y, z = 0, 0, 0
|
|
|
|
assert len(direction) == 2
|
|
|
|
sign, axis = direction
|
|
|
|
if axis in ('z', 'Z'):
|
|
|
|
z = 1
|
|
|
|
elif axis in ('y', 'Y'):
|
|
|
|
y = 1
|
|
|
|
elif axis in ('x', 'X'):
|
|
|
|
x = 1
|
|
|
|
else:
|
|
|
|
assert False, "Axis must be one of x,y,z"
|
|
|
|
if sign == '+':
|
|
|
|
sign = 1
|
|
|
|
elif sign == '-':
|
|
|
|
sign = -1
|
|
|
|
else:
|
|
|
|
assert False, "Sign must be one of +/-"
|
|
|
|
v = Cq.Vector(x, y, z) * sign
|
2024-07-06 16:41:13 -07:00
|
|
|
else:
|
2024-07-07 21:01:40 -07:00
|
|
|
v = Cq.Vector(direction)
|
|
|
|
edge = Cq.Edge.makeLine(v * (-1), v)
|
2024-07-13 16:19:17 -07:00
|
|
|
return self.eachpoint(edge.located, useLocalCoordinates=True).tag(tag)
|
2024-07-06 16:41:13 -07:00
|
|
|
|
|
|
|
Cq.Workplane.tagPlane = tagPlane
|
2024-07-11 16:02:54 -07:00
|
|
|
|
2024-07-12 23:16:04 -07:00
|
|
|
def make_sphere(r: float = 2) -> Cq.Solid:
|
|
|
|
"""
|
|
|
|
Makes a full sphere. The default function makes a hemisphere
|
|
|
|
"""
|
|
|
|
return Cq.Solid.makeSphere(r, angleDegrees1=-90)
|
|
|
|
def make_arrow(size: float = 2) -> Cq.Workplane:
|
|
|
|
cone = Cq.Solid.makeCone(
|
|
|
|
radius1 = size,
|
|
|
|
radius2 = 0,
|
|
|
|
height=size)
|
|
|
|
result = (
|
|
|
|
Cq.Workplane("XY")
|
|
|
|
.cylinder(radius=size / 2, height=size, centered=(True, True, False))
|
|
|
|
.union(cone.located(Cq.Location((0, 0, size))))
|
|
|
|
)
|
|
|
|
result.faces("<Z").tag("dir_rev")
|
|
|
|
return result
|
|
|
|
|
2024-07-13 12:57:17 -07:00
|
|
|
def to_marker_name(tag: str) -> str:
|
|
|
|
return tag.replace("?", "__T").replace("/", "__Z") + "_marker"
|
|
|
|
|
2024-07-17 13:09:46 -07:00
|
|
|
COLOR_MARKER = Cq.Color(0, 1, 1, 1)
|
|
|
|
|
2024-07-12 23:16:04 -07:00
|
|
|
def mark_point(self: Cq.Assembly,
|
|
|
|
tag: str,
|
|
|
|
size: float = 2,
|
2024-07-16 11:55:38 -07:00
|
|
|
color: Cq.Color = COLOR_MARKER) -> Cq.Assembly:
|
2024-07-12 23:16:04 -07:00
|
|
|
"""
|
|
|
|
Adds a marker to make a point visible
|
|
|
|
"""
|
2024-07-13 12:57:17 -07:00
|
|
|
name = to_marker_name(tag)
|
2024-07-12 23:16:04 -07:00
|
|
|
return (
|
|
|
|
self
|
|
|
|
.add(make_sphere(size), name=name, color=color)
|
|
|
|
.constrain(tag, name, "Point")
|
|
|
|
)
|
|
|
|
|
|
|
|
Cq.Assembly.markPoint = mark_point
|
|
|
|
|
|
|
|
def mark_plane(self: Cq.Assembly,
|
|
|
|
tag: str,
|
|
|
|
size: float = 2,
|
2024-07-16 11:55:38 -07:00
|
|
|
color: Cq.Color = COLOR_MARKER) -> Cq.Assembly:
|
2024-07-12 23:16:04 -07:00
|
|
|
"""
|
|
|
|
Adds a marker to make a plane visible
|
|
|
|
"""
|
2024-07-13 12:57:17 -07:00
|
|
|
name = to_marker_name(tag)
|
2024-07-12 23:16:04 -07:00
|
|
|
return (
|
|
|
|
self
|
|
|
|
.add(make_arrow(size), name=name, color=color)
|
|
|
|
.constrain(tag, f"{name}?dir_rev", "Plane", param=180)
|
|
|
|
)
|
|
|
|
|
|
|
|
Cq.Assembly.markPlane = mark_plane
|
|
|
|
|
2024-07-17 13:09:46 -07:00
|
|
|
def get_abs_location(self: Cq.Assembly,
|
|
|
|
tag: str) -> Cq.Location:
|
|
|
|
name, shape = self._query(tag)
|
|
|
|
loc_self = shape.location()
|
|
|
|
loc_parent, _ = self._subloc(name)
|
|
|
|
loc = loc_parent * loc_self
|
|
|
|
return loc
|
|
|
|
|
|
|
|
Cq.Assembly.get_abs_location = get_abs_location
|
|
|
|
|
2024-07-12 23:16:04 -07:00
|
|
|
|
2024-07-11 16:02:54 -07:00
|
|
|
def extrude_with_markers(sketch: Cq.Sketch,
|
|
|
|
thickness: float,
|
|
|
|
tags: list[Tuple[str, Tuple[float, float], float]],
|
|
|
|
reverse: bool = False):
|
|
|
|
"""
|
|
|
|
Extrudes a sketch and place tags on the sketch for mating.
|
|
|
|
|
|
|
|
Each tag is of the format `(name, (x, y), angle)`, where the angle is
|
|
|
|
specifies in degrees counterclockwise from +X. Two marks are generated for
|
|
|
|
each `name`, "{name}" for the location (with normal) and "{name}_dir" for
|
|
|
|
the directrix specified by the angle.
|
|
|
|
|
|
|
|
This simulates a process of laser cutting and bonding (for wood and acrylic)
|
|
|
|
"""
|
|
|
|
result = (
|
|
|
|
Cq.Workplane('XY')
|
|
|
|
.placeSketch(sketch)
|
|
|
|
.extrude(thickness)
|
|
|
|
)
|
|
|
|
plane = result.faces("<Z" if reverse else ">Z").workplane()
|
|
|
|
sign = -1 if reverse else 1
|
2024-07-12 23:16:04 -07:00
|
|
|
for tag, (px, py), angle in tags:
|
2024-07-11 16:02:54 -07:00
|
|
|
theta = sign * math.radians(angle)
|
|
|
|
direction = (math.cos(theta), math.sin(theta), 0)
|
|
|
|
plane.moveTo(px, sign * py).tagPlane(tag)
|
|
|
|
plane.moveTo(px, sign * py).tagPlane(f"{tag}_dir", direction)
|
|
|
|
return result
|