diff --git a/klippy/kinematics/cartesian.py b/klippy/kinematics/cartesian.py index 5db93b2dc..12fe1355c 100644 --- a/klippy/kinematics/cartesian.py +++ b/klippy/kinematics/cartesian.py @@ -5,7 +5,7 @@ # This file may be distributed under the terms of the GNU GPLv3 license. from klippy import stepper from . import idex_modes - +from .forbidden_zones import ForbiddenZones class CartKinematics: def __init__(self, toolhead, config): @@ -24,6 +24,8 @@ def __init__(self, toolhead, config): self.axes_max = toolhead.Coord(*[r[1] for r in ranges], e=0.0) self.dc_module = None self.supports_dual_carriage = True + self.forbidden_zones = ForbiddenZones(config) + if config.has_section("dual_carriage"): dc_config = config.getsection("dual_carriage") dc_axis = dc_config.getchoice("axis", ["x", "y"]) @@ -144,6 +146,7 @@ def check_move(self, move): or ypos > limits[1][1] ): self._check_endstops(move) + self.forbidden_zones.check_move(move) if not move.axes_d[2]: # Normal XY move - use defaults return diff --git a/klippy/kinematics/forbidden_zones.py b/klippy/kinematics/forbidden_zones.py new file mode 100644 index 000000000..ed5ed7978 --- /dev/null +++ b/klippy/kinematics/forbidden_zones.py @@ -0,0 +1,35 @@ +# Code for handling forbidden zones of movements +# +# Copyright (C) 2025 Daniel Berlin +# +# This file may be distributed under the terms of the GNU GPLv3 license. + +from shapely import from_wkt, STRtree, LineString +import logging + +class ForbiddenZones: + # On init we just read in the shapes and prepare the STRtree + def __init__(self, config): + self.forbidden_zone_tree = None + if config.has_section("forbidden_zones"): + section = config.getsection("forbidden_zones") + forbidden_shapes_wkt = section.getlists("shapes", seps=("\n")) + logging.info(f"Forbidden zone shapes (WKT):{forbidden_shapes_wkt}") + forbidden_shapes_wkt = [shape.strip() for shape in forbidden_shapes_wkt if len(shape.strip()) != 0] + forbidden_shapes = [from_wkt(shape) for shape in forbidden_shapes_wkt] + logging.info(f"Forbidden zone shapes (parsed):{forbidden_shapes}") + self.forbidden_zone_tree = STRtree(forbidden_shapes) + + # Check whether a proposed line move intersects any forbidden zone shapes + # Touching the shape counts (So a point that is on the border of a shape is not allowed) + def check_move(self, move): + if self.forbidden_zone_tree: + # Transform the move into a shapely LineString, get candidates from the STRtree + # and check each candidate for intersection + x_y_line = LineString([move.start_pos[:2], move.end_pos[:2]]) + candidates = self.forbidden_zone_tree.query(x_y_line, predicate='intersects') + if len(candidates) > 0: + for geom_index in candidates: + geom = self.forbidden_zone_tree.geometries.take(geom_index) + if x_y_line.intersects(geom): + raise move.move_error(f"Move line {x_y_line} intersects forbidden zone shape {geom}") diff --git a/pyproject.toml b/pyproject.toml index 759487b79..24af00380 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,7 +14,8 @@ dependencies = [ "numpy~=2.2 ; python_version>='3.10'", "pyserial==3.4", "python-can~=4.5", - "setuptools==75.8.2 ; python_version >= '3.12'", # required by python-can < 4.3 + "setuptools==75.8.2 ; python_version >= '3.12'", # required by python-can < 4.3 + "shapely>=2.0.7", ] [tool.uv]