Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Observe precision in interpolation and clipper #48

Merged
merged 1 commit into from
May 12, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/cq_cam/fluent.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ def __init__(
plunge_feed: float = None,
rapid_height: float = None,
op_safe_height: float = None,
gcode_precision: int = 3,
precision: int = 3,
unit: Unit = Unit.METRIC,
plane: WorkPlane = WorkPlane.XY,
coordinate: WorkOffset = WorkOffset.OFFSET_1,
Expand All @@ -91,7 +91,7 @@ def __init__(
if op_safe_height is None
else op_safe_height
)
self.gcode_precision = gcode_precision
self.precision = precision
self.unit = unit
self.plane = plane
self.coordinate = coordinate
Expand Down
18 changes: 11 additions & 7 deletions src/cq_cam/operations/pocket.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,8 @@ def pocket(
if engine == "clipper":
return pocket_clipper(
job,
[PathFace.from_cq_face(face) for face in op_areas],
[PathFace.from_cq_face(face) for face in avoid_areas],
[PathFace.from_cq_face(face, job.precision) for face in op_areas],
[PathFace.from_cq_face(face, job.precision) for face in avoid_areas],
outer_offset,
inner_offset,
avoid_outer_offset,
Expand Down Expand Up @@ -187,15 +187,17 @@ def build_pocket_ops(
return pocket_ops


def fill_pocket_contour_shrink(pocket: PathFace, step: float) -> list[list[PathFace]]:
def fill_pocket_contour_shrink(
pocket: PathFace, step: float, precision: int
) -> list[list[PathFace]]:
tree = Tree(PathFace(pocket.outer, [], depth=pocket.depth))
i = 0

# TODO sanify the variable names here
try:
while True:
node = tree.next_unlocked
next_outer_candidates = offset_path(node.obj.outer, -step)
next_outer_candidates = offset_path(node.obj.outer, -step, precision)
if next_outer_candidates and pocket.inners:
next_outers = [
face.outer
Expand Down Expand Up @@ -237,11 +239,13 @@ def pocket_clipper(
offset_avoid_areas: list[PathFace] = []

for face in op_areas:
offset_op_areas += offset_polyface(face, outer_offset, inner_offset)
offset_op_areas += offset_polyface(
face, outer_offset, inner_offset, job.precision
)

for face in avoid_areas:
offset_avoid_areas += offset_polyface(
face, avoid_outer_offset, avoid_inner_offset
face, avoid_outer_offset, avoid_inner_offset, job.precision
)

pocket_ops = build_pocket_ops(offset_op_areas, offset_avoid_areas)
Expand All @@ -250,7 +254,7 @@ def pocket_clipper(
sequences: list[list[PathFace]] = []
shallower_pocket_ops = []
for pocket_op in pocket_ops:
fill_sequences = fill_pocket_contour_shrink(pocket_op, stepover)
fill_sequences = fill_pocket_contour_shrink(pocket_op, stepover, job.precision)
if stepdown:
stepdown_start_depth = determine_stepdown_start_depth(
pocket_op, shallower_pocket_ops
Expand Down
23 changes: 16 additions & 7 deletions src/cq_cam/routers.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
Rapid,
)
from cq_cam.utils.geometry_op import Path, PathFace, distance_to_path
from cq_cam.utils.interpolation import edge_interpolation_count
from cq_cam.utils.utils import (
edge_end_param,
edge_end_point,
Expand Down Expand Up @@ -113,7 +114,12 @@ def shift_edges(


def route_edge(
edge: cq.Edge, start_p=None, end_p=None, arrow=False, feed: float | None = None
edge: cq.Edge,
precision: int,
start_p=None,
end_p=None,
arrow=False,
feed: float | None = None,
) -> tuple[list[MotionCommand], cq.Vector]:
commands = []
geom_type = edge.geomType()
Expand Down Expand Up @@ -163,8 +169,7 @@ def route_edge(
)

elif geom_type == "BSPLINE" or geom_type == "SPLINE" or geom_type == "OFFSET":
# TODO precision
n = max(int(edge.Length() / 0.1), 2)
n = edge_interpolation_count(edge, precision)

orientation = edge.wrapped.Orientation()
if orientation == TopAbs_REVERSED:
Expand Down Expand Up @@ -232,17 +237,21 @@ def route_wires(job: "Job", wires: list[Union[cq.Wire, cq.Edge]], stepover=None)
if edge_i == 0:
if param is not None:
new_commands, end = route_edge(
edge, start_p=param, arrow=True, feed=job.feed
edge, job.precision, start_p=param, arrow=True, feed=job.feed
)
commands += new_commands
else:
new_commands, end = route_edge(edge, arrow=True, feed=job.feed)
new_commands, end = route_edge(
edge, job.precision, arrow=True, feed=job.feed
)
commands += new_commands
else:
new_commands, end = route_edge(edge, feed=job.feed)
new_commands, end = route_edge(edge, job.precision, feed=job.feed)
commands += new_commands
if param:
new_commands, end = route_edge(edges[0], end_p=param, feed=job.feed)
new_commands, end = route_edge(
edges[0], job.precision, end_p=param, feed=job.feed
)
commands += new_commands

previous_wire = wire
Expand Down
39 changes: 25 additions & 14 deletions src/cq_cam/utils/geometry_op.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import logging
import math
from functools import cached_property
from itertools import pairwise
from math import sqrt
Expand Down Expand Up @@ -36,14 +37,14 @@ def __init__(self, outer: Path, inners: list[Path], depth: float):
self.depth = depth

@classmethod
def from_cq_face(cls, cq_face: cq.Face):
def from_cq_face(cls, cq_face: cq.Face, precision: int):
bbox = cq_face.BoundingBox()
if round(bbox.zmin, 3) != round(bbox.zmax, 3):
raise ValueError("PolyFace supports only flat faces")

return cls(
wire_to_path(cq_face.outerWire()),
[wire_to_path(wire) for wire in cq_face.innerWires()],
wire_to_path(cq_face.outerWire(), precision),
[wire_to_path(wire, precision) for wire in cq_face.innerWires()],
bbox.zmax,
)

Expand Down Expand Up @@ -98,34 +99,44 @@ def offset_wire(
return validated_wires


def offset_path(path: ClosedPath, offset: float) -> list[Path]:
def offset_path(path: ClosedPath, offset: float, precision: int) -> list[Path]:
# noinspection PyArgumentList
scaled_path = pc.scale_to_clipper(path)

# noinspection PyArgumentList
scaled_offset = pc.scale_to_clipper(offset)
# TODO precision?

"""
Calculating fixed length segments would work like this, but
is it overkill? Probably..

radius = abs(offset)
circumference = 2 * math.pi * radius
steps_for_precision = circumference * 10**precision
arc_tolerance = radius * (1 - math.cos(math.pi / steps_for_precision))
"""

# To get a little more leeway, we divide the precision by two
# noinspection PyArgumentList
precision = pc.scale_to_clipper(0.01)
scaled_precision = pc.scale_to_clipper(10**-precision / 2)

# Use a scaled miter limit of 2. This is the default
# noinspection PyArgumentList
pco = pc.PyclipperOffset(pc.scale_to_clipper(2.0), precision)
pco = pc.PyclipperOffset(pc.scale_to_clipper(2.0), scaled_precision)
pco.AddPath(scaled_path, pc.JT_ROUND, pc.ET_CLOSEDPOLYGON)

# TODO determine good value for the CleanPolygons
offset_paths = [
close_path(pc.scale_from_clipper(offsetted_path))
for offsetted_path in pc.CleanPolygons(
pco.Execute(scaled_offset), precision / 100
pco.Execute(scaled_offset), scaled_precision
)
if offsetted_path
]
return offset_paths


def wire_to_path(wire: cq.Wire) -> Path:
return vectors_to_2d_tuples(wire_to_vectors(wire))
def wire_to_path(wire: cq.Wire, precision: int) -> Path:
return vectors_to_2d_tuples(wire_to_vectors(wire, precision))


def path_to_wire(path: Path, reference: cq.Wire | float) -> cq.Wire:
Expand Down Expand Up @@ -187,12 +198,12 @@ def make_polyfaces(


def offset_polyface(
polyface: PathFace, outer_offset: float, inner_offset: float
polyface: PathFace, outer_offset: float, inner_offset: float, precision: int
) -> list[PathFace]:
outers = offset_path(polyface.outer, outer_offset)
outers = offset_path(polyface.outer, outer_offset, precision)
if polyface.inners:
inners = flatten_list(
[offset_path(inner, inner_offset) for inner in polyface.inners]
[offset_path(inner, inner_offset, precision) for inner in polyface.inners]
)
return make_polyfaces(outers, inners, polyface.depth)
else:
Expand Down
59 changes: 6 additions & 53 deletions src/cq_cam/utils/interpolation.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,9 @@ def get_underlying_geom_type(edge: cq.Edge):
return geom_LUT_CURVE[curve.__class__]


def interpolate_edge_to_vectors(
edge: cq.Edge, precision: float = 0.1
) -> list[cq.Vector]:
def interpolate_edge_to_vectors(edge: cq.Edge, precision: int) -> list[cq.Vector]:
# Interpolation must have at least two edges
n = max(int(edge.Length() / precision), 2)
n = edge_interpolation_count(edge, precision)

orientation = edge.wrapped.Orientation()
if orientation == TopAbs_REVERSED:
Expand All @@ -52,47 +50,11 @@ def interpolate_edge_to_vectors(
return interpolations


def interpolate_edge(edge: cq.Edge, precision: float = 0.1) -> list[cq.Edge]:
interpolations = interpolate_edge_to_vectors(edge, precision)

result = []
for a, b in zip(interpolations, interpolations[1:]):
result.append(cq.Edge.makeLine(a, b))

return result


def interpolate_edges_with_unstable_curves(
edges: list[cq.Edge], precision: float = 0.1
):
"""
It appears some curves do not offset nicely with OCCT. BSPLINE is an example.
These curves unfortunately need to be interpolated to ensure stable offset performance.
:param edges:
:param precision:
:return:
"""
result = []
interpolated = False
for edge in edges:
geom_type = edge.geomType()
if geom_type == "OFFSET":
geom_type = get_underlying_geom_type(edge)

if geom_type == "BSPLINE":
edges = interpolate_edge(edge, precision)
result += edges
interpolated = True
else:
result.append(edge)
return result, interpolated


def vectors_to_2d_tuples(vectors: list[cq.Vector]) -> list[tuple[float, float]]:
return [(vector.x, vector.y) for vector in vectors]


def edge_to_vectors(edge: cq.Edge, precision: float = 0.01) -> list[cq.Vector]:
def edge_to_vectors(edge: cq.Edge, precision: int) -> list[cq.Vector]:
geom_type = edge.geomType()
if geom_type == "OFFSET":
geom_type = get_underlying_geom_type(edge)
Expand All @@ -103,9 +65,7 @@ def edge_to_vectors(edge: cq.Edge, precision: float = 0.01) -> list[cq.Vector]:
return interpolate_edge_to_vectors(edge, precision)


def wire_to_vectors(
wire: cq.Wire, precision: float = 0.01, close=True
) -> list[cq.Vector]:
def wire_to_vectors(wire: cq.Wire, precision: int, close=True) -> list[cq.Vector]:
edges = wire_to_ordered_edges(wire)

if not edges:
Expand All @@ -128,12 +88,5 @@ def wire_to_vectors(
return vectors


def interpolate_wire_with_unstable_edges(
wire: cq.Wire, precision: float = 0.1
) -> cq.Wire:
edges, interpolated = interpolate_edges_with_unstable_curves(
wire.Edges(), precision
)
if not interpolated:
return wire
return cq.Wire.assembleEdges(edges)
def edge_interpolation_count(edge: cq.Edge, precision: int):
return max(int(edge.Length() * 10**precision), 2)
4 changes: 2 additions & 2 deletions src/cq_cam/utils/tests/test_polyface.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ def test_make_polyface():
inners = []
faces: list[cq.Face] = wp.objects
for face in faces:
outers.append(wire_to_path(face.outerWire()))
inners += [wire_to_path(inner) for inner in face.innerWires()]
outers.append(wire_to_path(face.outerWire(), 1))
inners += [wire_to_path(inner, 1) for inner in face.innerWires()]

polyfaces = make_polyfaces(outers, inners, 0)
assert len(polyfaces) == 2
Expand Down
Loading