From 9849c7b01359d37a668465dc63a2ccd99c43452c Mon Sep 17 00:00:00 2001 From: Gert Pellin Date: Fri, 9 Oct 2026 12:57:42 +0200 Subject: [PATCH 1/8] feat(editor): add path clean-up commands for workpieces Close open contours whose ends meet within a tolerance, join open contours into longer paths (reversing pieces where needed), remove duplicate contours and stacked duplicate workpieces, and break a workpiece apart into one workpiece per contour. The geometry work lives in rayforge.core.path_cleanup and runs in millimetres, so tolerances do not depend on the workpiece scale. Gaps are bridged with straight segments between existing end points, so the bounding box and placement stay the same. Each command writes the result through ChangePropertyCommand on _edited_boundaries and is a single undo step. Break Apart reuses SplitCmd with a new ContourSplitStrategy that, unlike the connectivity split, keeps holes and open paths as pieces of their own. Refs #516 --- rayforge/core/path_cleanup.py | 321 +++++++++++++++++++++++ rayforge/doceditor/editor.py | 2 + rayforge/doceditor/path_cleanup_cmd.py | 232 ++++++++++++++++ rayforge/doceditor/split_cmd.py | 18 +- tests/core/test_path_cleanup.py | 314 ++++++++++++++++++++++ tests/doceditor/test_path_cleanup_cmd.py | 195 ++++++++++++++ 6 files changed, 1081 insertions(+), 1 deletion(-) create mode 100644 rayforge/core/path_cleanup.py create mode 100644 rayforge/doceditor/path_cleanup_cmd.py create mode 100644 tests/core/test_path_cleanup.py create mode 100644 tests/doceditor/test_path_cleanup_cmd.py diff --git a/rayforge/core/path_cleanup.py b/rayforge/core/path_cleanup.py new file mode 100644 index 000000000..8a1c3cbcf --- /dev/null +++ b/rayforge/core/path_cleanup.py @@ -0,0 +1,321 @@ +""" +Clean-up operations for vector paths. + +These functions close, join, de-duplicate and split the contours of a +:class:`~raygeo.geo.Geometry`. They never move existing points: gaps are +bridged with a straight segment between two existing end points, so the +bounding box of the result never grows. All distances are expressed in +the units of the geometry passed in; callers that work on normalized +workpiece geometry should scale it to millimetres first. +""" + +import math +from collections.abc import Iterator + +from raygeo.geo import Geometry, Move + +Point = tuple[float, float] + +_CLOSED_EPS = 1e-6 +_DUPLICATE_SAMPLES = 32 + + +def _start_point(contour: Geometry) -> Point: + first = contour.data[0] + return first.end[0], first.end[1] + + +def _end_point(contour: Geometry) -> Point: + x, y, _z = contour.get_last_point() + return x, y + + +def _distance(a: Point, b: Point) -> float: + return math.hypot(a[0] - b[0], a[1] - b[1]) + + +def _segment_count(contour: Geometry) -> int: + return sum(1 for cmd in contour.data if not isinstance(cmd, Move)) + + +def _drawn_contours(geo: Geometry) -> list[Geometry]: + """Splits into contours, dropping those that draw nothing.""" + if geo.is_empty(): + return [] + return [c for c in geo.split_into_contours() if _segment_count(c) > 0] + + +def _is_open(contour: Geometry) -> bool: + return not contour.is_closed(_CLOSED_EPS) + + +def _close_if_near(contour: Geometry, tolerance: float) -> bool: + """ + Closes an open contour of at least two segments in place when its + start and end are within the tolerance. Returns True if it closed it. + """ + if not _is_open(contour) or _segment_count(contour) < 2: + return False + start = _start_point(contour) + if _distance(start, _end_point(contour)) > tolerance: + return False + contour.line_to(start[0], start[1]) + return True + + +def _concat(contours: list[Geometry]) -> Geometry: + result = Geometry() + for contour in contours: + result.extend(contour) + return result + + +def close_open_contours( + geo: Geometry, tolerance: float +) -> tuple[Geometry, int]: + """ + Closes every open contour whose start and end point lie within the + tolerance of each other. + + Args: + geo: The geometry to process. It is not modified. + tolerance: The largest gap that is closed. + + Returns: + A tuple of the new geometry and the number of closed contours. + """ + contours = [c.copy() for c in _drawn_contours(geo)] + closed = sum(1 for c in contours if _close_if_near(c, tolerance)) + return _concat(contours), closed + + +class _EndpointGrid: + """A spatial hash of open contour end points for neighbour lookups.""" + + def __init__(self, cell_size: float): + self._cell = max(cell_size, 1e-9) + self._cells: dict[tuple[int, int], set[tuple[int, int]]] = {} + self._points: dict[tuple[int, int], Point] = {} + + def _key(self, p: Point) -> tuple[int, int]: + return math.floor(p[0] / self._cell), math.floor(p[1] / self._cell) + + def add(self, index: int, end: int, p: Point): + self._points[(index, end)] = p + self._cells.setdefault(self._key(p), set()).add((index, end)) + + def remove_contour(self, index: int): + for end in (0, 1): + p = self._points.pop((index, end), None) + if p is not None: + self._cells[self._key(p)].discard((index, end)) + + def _neighbours(self, p: Point) -> Iterator[tuple[int, int]]: + kx, ky = self._key(p) + for dx in (-1, 0, 1): + for dy in (-1, 0, 1): + yield from self._cells.get((kx + dx, ky + dy), ()) + + def nearest(self, p: Point, tolerance: float) -> tuple[int, int] | None: + best: tuple[int, int] | None = None + best_dist = tolerance + for entry in sorted(self._neighbours(p)): + d = _distance(p, self._points[entry]) + if d <= best_dist: + best, best_dist = entry, d + return best + + +def _append_contour(chain: Geometry, piece: Geometry): + """Appends a piece to the end of a chain, bridging any gap.""" + start = _start_point(piece) + if _distance(_end_point(chain), start) > 0.0: + chain.line_to(start[0], start[1]) + chain.extend(piece.filter(set(range(1, len(piece.data))))) + + +def _grow_tail( + chain: Geometry, + contours: list[Geometry], + grid: _EndpointGrid, + tolerance: float, + consumed: set[int], +) -> int: + """ + Keeps appending the nearest open contour to the chain's end, and + records the appended contours in ``consumed``. + """ + joins = 0 + while True: + match = grid.nearest(_end_point(chain), tolerance) + if match is None: + return joins + index, end = match + grid.remove_contour(index) + consumed.add(index) + piece = contours[index].copy() + if end == 1: + piece.reverse_contour() + _append_contour(chain, piece) + joins += 1 + + +def join_open_contours( + geo: Geometry, tolerance: float +) -> tuple[Geometry, int]: + """ + Joins open contours whose end points meet within the tolerance into + longer contours, reversing contours where needed. A joined contour + whose own ends then meet within the tolerance is closed. Closed + contours are left untouched. + + Args: + geo: The geometry to process. It is not modified. + tolerance: The largest gap between two end points that is joined. + + Returns: + A tuple of the new geometry and the number of joins made. + """ + contours = _drawn_contours(geo) + grid = _EndpointGrid(tolerance) + open_indices = [i for i, c in enumerate(contours) if _is_open(c)] + for i in open_indices: + grid.add(i, 0, _start_point(contours[i])) + grid.add(i, 1, _end_point(contours[i])) + + chains: dict[int, Geometry] = {} + consumed: set[int] = set() + joins = 0 + for i in open_indices: + if i in consumed: + continue + grid.remove_contour(i) + chain = contours[i].copy() + joins += _grow_tail(chain, contours, grid, tolerance, consumed) + chain.reverse_contour() + joins += _grow_tail(chain, contours, grid, tolerance, consumed) + chain.reverse_contour() + _close_if_near(chain, tolerance) + chains[i] = chain + + result = Geometry() + for i, contour in enumerate(contours): + if i in chains: + result.extend(chains[i]) + elif i not in consumed: + result.extend(contour) + return result, joins + + +def _sample_points(contour: Geometry) -> list[Point]: + """The contour's vertices plus evenly spaced points along it.""" + points = [(cmd.end[0], cmd.end[1]) for cmd in contour.data] + length = contour.distance() + if length > 0.0: + step = length / _DUPLICATE_SAMPLES + distances = [step * k for k in range(_DUPLICATE_SAMPLES + 1)] + for _idx, _t, p in contour.get_positions_at_distances(distances): + points.append((p[0], p[1])) + return points + + +def _covers(target: Geometry, points: list[Point], tolerance: float) -> bool: + for x, y in points: + hit = target.find_closest_point(x, y) + if hit is None or _distance((x, y), hit[2]) > tolerance: + return False + return True + + +def _rects_match(a: Geometry, b: Geometry, tolerance: float) -> bool: + return all(abs(va - vb) <= tolerance for va, vb in zip(a.rect(), b.rect())) + + +def _contours_match(a: Geometry, b: Geometry, tolerance: float) -> bool: + """ + True if two contours trace the same path within the tolerance, + regardless of their direction or start point. + """ + if _is_open(a) != _is_open(b): + return False + if not _rects_match(a, b, tolerance): + return False + return _covers(b, _sample_points(a), tolerance) and _covers( + a, _sample_points(b), tolerance + ) + + +def _length_slack(tolerance: float) -> float: + return 4.0 * tolerance + 1e-9 + + +def remove_duplicate_contours( + geo: Geometry, tolerance: float +) -> tuple[Geometry, int]: + """ + Removes contours that duplicate an earlier contour within the + tolerance, regardless of direction or start point. The first + occurrence is kept and the order of the remaining contours is + preserved. + + Args: + geo: The geometry to process. It is not modified. + tolerance: The largest distance between two matching paths. + + Returns: + A tuple of the new geometry and the number of removed contours. + """ + contours = _drawn_contours(geo) + lengths = [c.distance() for c in contours] + order = sorted(range(len(contours)), key=lambda i: lengths[i]) + duplicates: set[int] = set() + slack = _length_slack(tolerance) + for pos, i in enumerate(order): + if i in duplicates: + continue + for j in order[pos + 1 :]: + if lengths[j] - lengths[i] > slack: + break + if j in duplicates: + continue + if _contours_match(contours[i], contours[j], tolerance): + duplicates.add(max(i, j)) + if max(i, j) == i: + break + kept = [c for i, c in enumerate(contours) if i not in duplicates] + return _concat(kept), len(duplicates) + + +def geometries_match(a: Geometry, b: Geometry, tolerance: float) -> bool: + """ + True if both geometries consist of the same contours within the + tolerance, in any order, direction or start point. Empty geometries + never match. + """ + contours_a = _drawn_contours(a) + contours_b = _drawn_contours(b) + if not contours_a or len(contours_a) != len(contours_b): + return False + unmatched = list(contours_b) + for contour in contours_a: + found = next( + ( + other + for other in unmatched + if _contours_match(contour, other, tolerance) + ), + None, + ) + if found is None: + return False + unmatched.remove(found) + return True + + +def split_contours(geo: Geometry) -> list[Geometry]: + """ + Splits a geometry into one geometry per contour. Unlike splitting + into connected components, holes become parts of their own and open + contours are kept. + """ + return [c.copy() for c in _drawn_contours(geo)] diff --git a/rayforge/doceditor/editor.py b/rayforge/doceditor/editor.py index 16e8c1b32..8f0788770 100644 --- a/rayforge/doceditor/editor.py +++ b/rayforge/doceditor/editor.py @@ -26,6 +26,7 @@ from .group_cmd import GroupCmd from .layer_cmd import LayerCmd from .layout_cmd import LayoutCmd +from .path_cleanup_cmd import PathCleanupCmd from .split_cmd import SplitCmd from .step_cmd import StepCmd from .stock_cmd import StockCmd @@ -151,6 +152,7 @@ def __init__( self.group = GroupCmd(self, self.task_manager) self.layer = LayerCmd(self) self.layout = LayoutCmd(self, self.task_manager) + self.path_cleanup = PathCleanupCmd(self) self.split = SplitCmd(self) self.stock = StockCmd(self) self.step = StepCmd(self) diff --git a/rayforge/doceditor/path_cleanup_cmd.py b/rayforge/doceditor/path_cleanup_cmd.py new file mode 100644 index 000000000..2fc3ef152 --- /dev/null +++ b/rayforge/doceditor/path_cleanup_cmd.py @@ -0,0 +1,232 @@ +import logging +from collections.abc import Callable +from gettext import gettext as _ +from typing import TYPE_CHECKING + +from raygeo.geo import Geometry, Matrix + +from ..core.item import DocItem +from ..core.path_cleanup import ( + close_open_contours, + geometries_match, + join_open_contours, + remove_duplicate_contours, +) +from ..core.undo import ChangePropertyCommand, ListItemCommand +from ..core.workpiece import WorkPiece +from .split_cmd import ContourSplitStrategy + +if TYPE_CHECKING: + from .editor import DocEditor + +logger = logging.getLogger(__name__) + +DEFAULT_DUPLICATE_TOLERANCE_MM = 0.01 + +GeometryOperation = Callable[[Geometry, float], tuple[Geometry, int]] + + +def _mm_scale(workpiece: WorkPiece) -> tuple[float, float]: + """ + The factors that turn the workpiece's normalized geometry into + millimetres. A degenerate axis keeps a factor of 1 so the scaling can + be undone. + """ + width, height = workpiece.size + return ( + width if width > 1e-9 else 1.0, + height if height > 1e-9 else 1.0, + ) + + +def _is_editable(workpiece: WorkPiece) -> bool: + """ + Workpieces generated from a geometry provider such as a sketch are + edited at their source, so path clean-up leaves them alone. + """ + if workpiece.geometry_provider_uid: + return False + boundaries = workpiece.boundaries + return boundaries is not None and not boundaries.is_empty() + + +def _world_geometry(workpiece: WorkPiece) -> Geometry | None: + boundaries = workpiece.boundaries + if boundaries is None or boundaries.is_empty(): + return None + geo = boundaries.copy() + geo.transform(workpiece.get_world_transform()) + return geo + + +def _rects_overlap(a: Geometry, b: Geometry, tolerance: float) -> bool: + return all(abs(va - vb) <= tolerance for va, vb in zip(a.rect(), b.rect())) + + +class PathCleanupCmd: + """ + Clean-up operations on the vector paths of workpieces: closing, + joining, removing duplicates and breaking apart. Tolerances are given + in millimetres, independent of the workpiece's scale. + """ + + def __init__(self, editor: "DocEditor"): + self._editor = editor + + def close_paths( + self, workpieces: list[WorkPiece], tolerance_mm: float + ) -> int: + """ + Closes open contours whose start and end are within the tolerance. + + Returns: + The number of closed contours. + """ + return self._apply( + workpieces, close_open_contours, tolerance_mm, _("Close paths") + ) + + def join_paths( + self, workpieces: list[WorkPiece], tolerance_mm: float + ) -> int: + """ + Joins open contours whose end points meet within the tolerance. + + Returns: + The number of joins made. + """ + return self._apply( + workpieces, join_open_contours, tolerance_mm, _("Join paths") + ) + + def delete_duplicates( + self, + workpieces: list[WorkPiece], + tolerance_mm: float = DEFAULT_DUPLICATE_TOLERANCE_MM, + ) -> int: + """ + Removes duplicate contours inside each workpiece, and removes + workpieces that are an exact copy of an earlier one in the list + lying on top of it. + + Returns: + The number of removed contours and workpieces. + """ + duplicates = self._find_duplicate_workpieces(workpieces, tolerance_mm) + remaining = [wp for wp in workpieces if wp not in duplicates] + changes = self._compute_changes( + remaining, remove_duplicate_contours, tolerance_mm + ) + if not duplicates and not changes: + return 0 + + name = _("Delete duplicates") + removed = len(duplicates) + with self._editor.history_manager.transaction(name) as t: + for workpiece in duplicates: + t.execute( + ListItemCommand( + owner_obj=workpiece.parent, + item=workpiece, + undo_command="add_child", + redo_command="remove_child", + name=name, + ) + ) + for workpiece, new_geo, count in changes: + t.execute(self._boundaries_command(workpiece, new_geo, name)) + removed += count + return removed + + def break_apart(self, workpieces: list[WorkPiece]) -> list[DocItem]: + """ + Splits each workpiece into one workpiece per contour. + + Returns: + The newly created workpieces. + """ + return self._editor.split.split_items( + workpieces, strategy=ContourSplitStrategy(), name=_("Break apart") + ) + + def _apply( + self, + workpieces: list[WorkPiece], + operation: GeometryOperation, + tolerance_mm: float, + name: str, + ) -> int: + changes = self._compute_changes(workpieces, operation, tolerance_mm) + if not changes: + return 0 + total = 0 + with self._editor.history_manager.transaction(name) as t: + for workpiece, new_geo, count in changes: + t.execute(self._boundaries_command(workpiece, new_geo, name)) + total += count + return total + + def _compute_changes( + self, + workpieces: list[WorkPiece], + operation: GeometryOperation, + tolerance_mm: float, + ) -> list[tuple[WorkPiece, Geometry, int]]: + """ + Runs the operation on each workpiece's geometry in millimetres and + returns the normalized results of the workpieces it changed. + """ + changes = [] + for workpiece in workpieces: + if not _is_editable(workpiece): + continue + boundaries = workpiece.boundaries + assert boundaries is not None + sx, sy = _mm_scale(workpiece) + geo_mm = boundaries.copy() + geo_mm.transform(Matrix.scale(sx, sy)) + new_geo, count = operation(geo_mm, tolerance_mm) + if count == 0: + continue + new_geo.transform(Matrix.scale(1.0 / sx, 1.0 / sy)) + changes.append((workpiece, new_geo, count)) + return changes + + def _boundaries_command( + self, workpiece: WorkPiece, new_geo: Geometry, name: str + ) -> ChangePropertyCommand: + def _on_changed(): + workpiece.clear_render_cache() + workpiece.updated.send(workpiece) + + return ChangePropertyCommand( + target=workpiece, + property_name="_edited_boundaries", + new_value=new_geo, + old_value=workpiece._edited_boundaries, + on_change_callback=_on_changed, + name=name, + ) + + def _find_duplicate_workpieces( + self, workpieces: list[WorkPiece], tolerance_mm: float + ) -> list[WorkPiece]: + """ + Finds workpieces whose world-space geometry matches an earlier + workpiece in the list. + """ + kept: list[tuple[WorkPiece, Geometry]] = [] + duplicates: list[WorkPiece] = [] + for workpiece in workpieces: + geo = _world_geometry(workpiece) + if geo is None or workpiece.parent is None: + continue + if any( + _rects_overlap(geo, other, tolerance_mm) + and geometries_match(geo, other, tolerance_mm) + for _wp, other in kept + ): + duplicates.append(workpiece) + else: + kept.append((workpiece, geo)) + return duplicates diff --git a/rayforge/doceditor/split_cmd.py b/rayforge/doceditor/split_cmd.py index 12f08883e..03f4b9282 100644 --- a/rayforge/doceditor/split_cmd.py +++ b/rayforge/doceditor/split_cmd.py @@ -7,6 +7,7 @@ from raygeo.geo import Geometry from ..core.item import DocItem +from ..core.path_cleanup import split_contours from ..core.undo import ListItemCommand from ..core.workpiece import WorkPiece @@ -49,6 +50,19 @@ def calculate_fragments(self, workpiece: "WorkPiece") -> list[Geometry]: return workpiece.boundaries.split_into_components() +class ContourSplitStrategy(SplitStrategy): + """ + Splits a workpiece into one fragment per contour ("Break Apart"). + Unlike the connectivity split, holes become separate pieces and open + paths are kept. + """ + + def calculate_fragments(self, workpiece: "WorkPiece") -> list[Geometry]: + if not workpiece.boundaries or workpiece.boundaries.is_empty(): + return [] + return split_contours(workpiece.boundaries) + + class SplitCmd: """Handles splitting of document items.""" @@ -59,6 +73,7 @@ def split_items( self, items: list[WorkPiece], strategy: SplitStrategy | None = None, + name: str | None = None, ) -> list[DocItem]: """ Splits the provided items into multiple fragments based on the given @@ -69,6 +84,7 @@ def split_items( items: The list of items to split. strategy: The strategy to use for calculating fragments. Defaults to splitting disjoint components. + name: The name of the undo step. Defaults to "Split item(s)". Returns: A list of the newly created items. @@ -81,7 +97,7 @@ def split_items( history = self._editor.history_manager newly_created_items = [] - with history.transaction(_("Split item(s)")) as t: + with history.transaction(name or _("Split item(s)")) as t: for item in items: # Capture the parent before any modification/removal occurs. # Executing remove_cmd may set item.parent to None. diff --git a/tests/core/test_path_cleanup.py b/tests/core/test_path_cleanup.py new file mode 100644 index 000000000..8169a244e --- /dev/null +++ b/tests/core/test_path_cleanup.py @@ -0,0 +1,314 @@ +import math + +import pytest +from raygeo.geo import Geometry + +from rayforge.core.path_cleanup import ( + close_open_contours, + geometries_match, + join_open_contours, + remove_duplicate_contours, + split_contours, +) + + +def _polyline(points, close=False) -> Geometry: + geo = Geometry() + geo.move_to(*points[0]) + for p in points[1:]: + geo.line_to(*p) + if close: + geo.line_to(*points[0]) + return geo + + +def _concat(*geos: Geometry) -> Geometry: + result = Geometry() + for geo in geos: + result.extend(geo) + return result + + +def _contours(geo: Geometry) -> list[Geometry]: + return geo.split_into_contours() + + +def _start(geo: Geometry) -> tuple[float, float]: + first = next(iter(geo.iter_typed_commands())) + return first.end[0], first.end[1] + + +def _end(geo: Geometry) -> tuple[float, float]: + x, y, _z = geo.get_last_point() + return x, y + + +def _dist(a, b) -> float: + return math.hypot(a[0] - b[0], a[1] - b[1]) + + +SQUARE = [(0, 0), (10, 0), (10, 10), (0, 10)] + + +class TestCloseOpenContours: + def test_closes_contour_within_tolerance(self): + geo = _polyline(SQUARE + [(0, 0.05)]) + result, count = close_open_contours(geo, 0.1) + assert count == 1 + contours = _contours(result) + assert len(contours) == 1 + assert contours[0].is_closed() + + def test_leaves_contour_outside_tolerance_open(self): + geo = _polyline(SQUARE + [(0, 0.5)]) + result, count = close_open_contours(geo, 0.1) + assert count == 0 + assert not _contours(result)[0].is_closed() + + def test_does_not_move_existing_points(self): + geo = _polyline(SQUARE + [(0, 0.05)]) + result, _count = close_open_contours(geo, 0.1) + assert result.rect() == pytest.approx(geo.rect()) + + def test_already_closed_contour_is_untouched(self): + geo = _polyline(SQUARE, close=True) + result, count = close_open_contours(geo, 0.1) + assert count == 0 + assert len(result.data) == len(geo.data) + + def test_handles_each_contour_separately(self): + geo = _concat( + _polyline(SQUARE + [(0, 0.05)]), + _polyline([(20, 0), (30, 0), (30, 10), (20, 3)]), + _polyline([(40, 0), (50, 0), (50, 10), (40, 0.02)]), + ) + result, count = close_open_contours(geo, 0.1) + assert count == 2 + closed = [c.is_closed() for c in _contours(result)] + assert closed == [True, False, True] + + def test_ignores_degenerate_single_segment(self): + geo = _polyline([(0, 0), (0.05, 0)]) + _result, count = close_open_contours(geo, 0.1) + assert count == 0 + + def test_empty_geometry(self): + result, count = close_open_contours(Geometry(), 0.1) + assert count == 0 + assert result.is_empty() + + +class TestJoinOpenContours: + def test_joins_two_pieces_in_order(self): + geo = _concat( + _polyline([(0, 0), (10, 0), (10, 10)]), + _polyline([(10, 10.05), (0, 10), (0, 0.05)]), + ) + result, joins = join_open_contours(geo, 0.1) + assert joins == 1 + contours = _contours(result) + assert len(contours) == 1 + assert contours[0].is_closed() + + def test_joins_reversed_piece(self): + geo = _concat( + _polyline([(0, 0), (10, 0)]), + _polyline([(20, 0), (10.02, 0)]), + ) + result, joins = join_open_contours(geo, 0.1) + assert joins == 1 + contours = _contours(result) + assert len(contours) == 1 + ends = {_start(contours[0]), _end(contours[0])} + assert ends == {(0.0, 0.0), (20.0, 0.0)} + + def test_joins_pieces_out_of_order(self): + geo = _concat( + _polyline([(0, 0), (10, 0)]), + _polyline([(20, 0), (30, 0)]), + _polyline([(10, 0), (20, 0)]), + ) + result, joins = join_open_contours(geo, 0.1) + assert joins == 2 + contours = _contours(result) + assert len(contours) == 1 + assert result.distance() == pytest.approx(30.0) + + def test_joins_at_the_start_of_a_piece(self): + geo = _concat( + _polyline([(10, 0), (20, 0)]), + _polyline([(0, 0), (10, 0)]), + ) + result, joins = join_open_contours(geo, 0.1) + assert joins == 1 + contours = _contours(result) + assert len(contours) == 1 + assert {_start(contours[0]), _end(contours[0])} == { + (0.0, 0.0), + (20.0, 0.0), + } + + def test_keeps_pieces_apart_beyond_tolerance(self): + geo = _concat( + _polyline([(0, 0), (10, 0)]), + _polyline([(11, 0), (20, 0)]), + ) + result, joins = join_open_contours(geo, 0.1) + assert joins == 0 + assert len(_contours(result)) == 2 + + def test_leaves_closed_contours_alone(self): + square = _polyline(SQUARE, close=True) + line = _polyline([(0, 0), (-10, 0)]) + geo = _concat(square, line) + result, joins = join_open_contours(geo, 0.1) + assert joins == 0 + assert len(_contours(result)) == 2 + + def test_closes_single_piece_whose_ends_meet(self): + geo = _polyline(SQUARE + [(0, 0.05)]) + result, _joins = join_open_contours(geo, 0.1) + assert _contours(result)[0].is_closed() + + def test_bridge_does_not_change_bounds(self): + geo = _concat( + _polyline([(0, 0), (10, 0), (10, 10)]), + _polyline([(10.05, 10), (0, 10), (0, 0)]), + ) + result, _joins = join_open_contours(geo, 0.1) + assert result.rect() == pytest.approx(geo.rect()) + + def test_many_segments_form_one_closed_outline(self): + pts = [ + (10 * math.cos(a), 10 * math.sin(a)) + for a in (2 * math.pi * i / 60 for i in range(60)) + ] + pieces = [] + for i in range(60): + a, b = pts[i], pts[(i + 1) % 60] + if i % 2: + a, b = b, a + pieces.append(_polyline([a, b])) + geo = _concat(*reversed(pieces)) + result, joins = join_open_contours(geo, 0.01) + assert joins == 59 + contours = _contours(result) + assert len(contours) == 1 + assert contours[0].is_closed() + + +class TestRemoveDuplicateContours: + def test_removes_identical_copy(self): + geo = _concat( + _polyline(SQUARE, close=True), _polyline(SQUARE, close=True) + ) + result, removed = remove_duplicate_contours(geo, 0.01) + assert removed == 1 + assert len(_contours(result)) == 1 + + def test_removes_reversed_copy(self): + geo = _concat( + _polyline(SQUARE, close=True), + _polyline(list(reversed(SQUARE)), close=True), + ) + _result, removed = remove_duplicate_contours(geo, 0.01) + assert removed == 1 + + def test_removes_copy_with_other_start_point(self): + shifted = SQUARE[2:] + SQUARE[:2] + geo = _concat( + _polyline(SQUARE, close=True), _polyline(shifted, close=True) + ) + _result, removed = remove_duplicate_contours(geo, 0.01) + assert removed == 1 + + def test_removes_open_reversed_copy(self): + line = [(0, 0), (5, 5), (10, 0)] + geo = _concat(_polyline(line), _polyline(list(reversed(line)))) + _result, removed = remove_duplicate_contours(geo, 0.01) + assert removed == 1 + + def test_keeps_shapes_that_only_overlap_partly(self): + geo = _concat( + _polyline(SQUARE, close=True), + _polyline([(0, 0), (10, 0), (10, 10)]), + ) + result, removed = remove_duplicate_contours(geo, 0.01) + assert removed == 0 + assert len(_contours(result)) == 2 + + def test_keeps_nested_shapes(self): + inner = [(2, 2), (8, 2), (8, 8), (2, 8)] + geo = _concat( + _polyline(SQUARE, close=True), _polyline(inner, close=True) + ) + _result, removed = remove_duplicate_contours(geo, 0.01) + assert removed == 0 + + def test_tolerance_applies(self): + nudged = [(x + 0.005, y) for x, y in SQUARE] + geo = _concat( + _polyline(SQUARE, close=True), _polyline(nudged, close=True) + ) + assert remove_duplicate_contours(geo, 0.01)[1] == 1 + assert remove_duplicate_contours(geo, 0.001)[1] == 0 + + def test_keeps_first_occurrence_and_order(self): + other = [(20, 0), (30, 0), (30, 10)] + geo = _concat( + _polyline(SQUARE, close=True), + _polyline(other), + _polyline(SQUARE, close=True), + ) + result, removed = remove_duplicate_contours(geo, 0.01) + assert removed == 1 + contours = _contours(result) + assert _start(contours[0]) == (0.0, 0.0) + assert _start(contours[1]) == (20.0, 0.0) + + def test_removes_triple_copies(self): + geo = _concat(*(_polyline(SQUARE, close=True) for _ in range(3))) + _result, removed = remove_duplicate_contours(geo, 0.01) + assert removed == 2 + + +class TestGeometriesMatch: + def test_same_shapes_in_other_order_match(self): + a = _concat( + _polyline(SQUARE, close=True), _polyline([(20, 0), (30, 0)]) + ) + b = _concat( + _polyline([(30, 0), (20, 0)]), _polyline(SQUARE, close=True) + ) + assert geometries_match(a, b, 0.01) + + def test_different_shapes_do_not_match(self): + a = _polyline(SQUARE, close=True) + b = _polyline([(0, 0), (10, 0), (10, 11), (0, 10)], close=True) + assert not geometries_match(a, b, 0.01) + + def test_extra_contour_does_not_match(self): + a = _polyline(SQUARE, close=True) + b = _concat( + _polyline(SQUARE, close=True), _polyline([(20, 0), (30, 0)]) + ) + assert not geometries_match(a, b, 0.01) + + def test_empty_geometries_do_not_match(self): + assert not geometries_match(Geometry(), Geometry(), 0.01) + + +class TestSplitContours: + def test_splits_every_contour_including_holes_and_open_paths(self): + inner = [(2, 2), (8, 2), (8, 8), (2, 8)] + geo = _concat( + _polyline(SQUARE, close=True), + _polyline(inner, close=True), + _polyline([(20, 0), (30, 0)]), + ) + parts = split_contours(geo) + assert len(parts) == 3 + assert parts[2].distance() == pytest.approx(10.0) + + def test_single_contour(self): + assert len(split_contours(_polyline(SQUARE, close=True))) == 1 diff --git a/tests/doceditor/test_path_cleanup_cmd.py b/tests/doceditor/test_path_cleanup_cmd.py new file mode 100644 index 000000000..8c8ce34c6 --- /dev/null +++ b/tests/doceditor/test_path_cleanup_cmd.py @@ -0,0 +1,195 @@ +import pytest +from raygeo.geo import Geometry, Matrix + +from rayforge.core.source_asset_segment import SourceAssetSegment +from rayforge.core.vectorization_spec import PassthroughSpec +from rayforge.core.workpiece import WorkPiece +from rayforge.doceditor.path_cleanup_cmd import PathCleanupCmd +from rayforge.doceditor.split_cmd import ContourSplitStrategy + +WIDTH_MM = 100.0 +HEIGHT_MM = 50.0 + + +@pytest.fixture +def cleanup_cmd(doc_editor): + return PathCleanupCmd(doc_editor) + + +def _mm_polyline(points_mm, close=False) -> Geometry: + """A polyline given in mm, normalized to the 100x50 mm test box.""" + geo = Geometry() + pts = [(x / WIDTH_MM, y / HEIGHT_MM) for x, y in points_mm] + geo.move_to(*pts[0]) + for p in pts[1:]: + geo.line_to(*p) + if close: + geo.line_to(*pts[0]) + return geo + + +def _concat(*geos: Geometry) -> Geometry: + result = Geometry() + for geo in geos: + result.extend(geo) + return result + + +def _add_workpiece(doc_editor, norm_geo: Geometry, name="Logo", pos=(0, 0)): + segment = SourceAssetSegment( + source_asset_uid="dummy", + pristine_geometry=norm_geo, + vectorization_spec=PassthroughSpec(), + normalization_matrix=Matrix.identity(), + ) + wp = WorkPiece(name=name, source_segment=segment) + wp.natural_width_mm = WIDTH_MM + wp.natural_height_mm = HEIGHT_MM + wp.set_size(WIDTH_MM, HEIGHT_MM) + wp.pos = pos + doc_editor.doc.active_layer.add_child(wp) + return wp + + +def _contours(wp: WorkPiece) -> list[Geometry]: + assert wp.boundaries is not None + return wp.boundaries.split_into_contours() + + +# A 0.3 mm wide shape inset into the 100x50 mm box. The bounding box +# corners at (0, 0) and (100, 50) keep the normalization stable. +FRAME = [(0, 0), (100, 0), (100, 50), (0, 50)] +OPEN_SQUARE_MM = [(20, 10), (40, 10), (40, 30), (20, 30), (20, 10.05)] + + +class TestClosePaths: + def test_tolerance_is_in_millimetres(self, doc_editor, cleanup_cmd): + geo = _concat( + _mm_polyline(FRAME, close=True), _mm_polyline(OPEN_SQUARE_MM) + ) + wp = _add_workpiece(doc_editor, geo) + + assert cleanup_cmd.close_paths([wp], tolerance_mm=0.01) == 0 + assert not _contours(wp)[1].is_closed() + + assert cleanup_cmd.close_paths([wp], tolerance_mm=0.1) == 1 + assert _contours(wp)[1].is_closed() + + def test_is_one_undo_step(self, doc_editor, cleanup_cmd): + geo = _mm_polyline(OPEN_SQUARE_MM) + wp = _add_workpiece(doc_editor, geo) + history = doc_editor.history_manager + depth = len(history.undo_stack) + + cleanup_cmd.close_paths([wp], tolerance_mm=0.1) + assert len(history.undo_stack) == depth + 1 + assert _contours(wp)[0].is_closed() + + history.undo() + assert not _contours(wp)[0].is_closed() + + def test_no_change_adds_no_undo_step(self, doc_editor, cleanup_cmd): + wp = _add_workpiece(doc_editor, _mm_polyline(FRAME, close=True)) + history = doc_editor.history_manager + depth = len(history.undo_stack) + + assert cleanup_cmd.close_paths([wp], tolerance_mm=0.1) == 0 + assert len(history.undo_stack) == depth + + def test_keeps_size_and_position(self, doc_editor, cleanup_cmd): + wp = _add_workpiece(doc_editor, _mm_polyline(OPEN_SQUARE_MM)) + size, pos = wp.size, wp.pos + cleanup_cmd.close_paths([wp], tolerance_mm=0.1) + assert wp.size == pytest.approx(size) + assert wp.pos == pytest.approx(pos) + + def test_skips_sketch_based_workpieces(self, doc_editor, cleanup_cmd): + wp = _add_workpiece(doc_editor, _mm_polyline(OPEN_SQUARE_MM)) + wp.geometry_provider_uid = "some-sketch" + assert cleanup_cmd.close_paths([wp], tolerance_mm=0.1) == 0 + assert wp._edited_boundaries is None + + +class TestJoinPaths: + def test_joins_pieces_within_tolerance(self, doc_editor, cleanup_cmd): + geo = _concat( + _mm_polyline(FRAME, close=True), + _mm_polyline([(20, 10), (40, 10), (40, 30)]), + _mm_polyline([(20, 10), (20, 30), (40, 30.05)]), + ) + wp = _add_workpiece(doc_editor, geo) + + assert cleanup_cmd.join_paths([wp], tolerance_mm=0.1) == 1 + contours = _contours(wp) + assert len(contours) == 2 + assert contours[1].is_closed() + + doc_editor.history_manager.undo() + assert len(_contours(wp)) == 3 + + +class TestDeleteDuplicates: + def test_removes_duplicate_contour(self, doc_editor, cleanup_cmd): + geo = _concat( + _mm_polyline(FRAME, close=True), + _mm_polyline(OPEN_SQUARE_MM), + _mm_polyline(list(reversed(OPEN_SQUARE_MM))), + ) + wp = _add_workpiece(doc_editor, geo) + + assert cleanup_cmd.delete_duplicates([wp]) == 1 + assert len(_contours(wp)) == 2 + + doc_editor.history_manager.undo() + assert len(_contours(wp)) == 3 + + def test_removes_stacked_copy_of_workpiece(self, doc_editor, cleanup_cmd): + layer = doc_editor.doc.active_layer + geo = _mm_polyline(FRAME, close=True) + first = _add_workpiece(doc_editor, geo, name="A", pos=(10, 10)) + copy = _add_workpiece(doc_editor, geo.copy(), name="B", pos=(10, 10)) + moved = _add_workpiece(doc_editor, geo.copy(), name="C", pos=(50, 10)) + + assert cleanup_cmd.delete_duplicates([first, copy, moved]) == 1 + assert first in layer.children + assert copy not in layer.children + assert moved in layer.children + + doc_editor.history_manager.undo() + assert copy in layer.children + + def test_nothing_to_delete(self, doc_editor, cleanup_cmd): + wp = _add_workpiece(doc_editor, _mm_polyline(FRAME, close=True)) + depth = len(doc_editor.history_manager.undo_stack) + assert cleanup_cmd.delete_duplicates([wp]) == 0 + assert len(doc_editor.history_manager.undo_stack) == depth + + +class TestBreakApart: + def test_every_contour_becomes_a_workpiece(self, doc_editor, cleanup_cmd): + layer = doc_editor.doc.active_layer + inner = [(10, 10), (90, 10), (90, 40), (10, 40)] + geo = _concat( + _mm_polyline(FRAME, close=True), + _mm_polyline(inner, close=True), + _mm_polyline([(20, 20), (80, 20)]), + ) + wp = _add_workpiece(doc_editor, geo) + + new_items = cleanup_cmd.break_apart([wp]) + assert len(new_items) == 3 + assert wp not in layer.children + sizes = sorted(round(item.size[0]) for item in new_items) + assert sizes == [60, 80, 100] + + doc_editor.history_manager.undo() + assert wp in layer.children + + def test_contour_strategy_keeps_open_paths(self, doc_editor): + geo = _concat( + _mm_polyline(FRAME, close=True), + _mm_polyline([(20, 20), (80, 20)]), + ) + wp = _add_workpiece(doc_editor, geo) + fragments = ContourSplitStrategy().calculate_fragments(wp) + assert len(fragments) == 2 From 63748b2734637ca7017e376990fdde3e65e05d01 Mon Sep 17 00:00:00 2001 From: Gert Pellin Date: Fri, 9 Oct 2026 12:57:42 +0200 Subject: [PATCH 2/8] feat(ui): expose path clean-up tools in Object and context menus MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add Break Apart, Close Paths…, Join Open Paths… and Delete Duplicates to the Object menu and a Clean Up Paths submenu to the canvas context menu. Close and join ask for a tolerance in a small dialog that remembers the last value per tool for the session; each tool reports its result in a toast. Includes translations for all shipped languages, a documentation page in every website language and a changelog entry. Closes #516 --- CHANGELOG.md | 5 + rayforge/ui_gtk/actions.py | 101 +++++++++++++++++- rayforge/ui_gtk/canvas2d/context_menu.py | 18 ++++ .../ui_gtk/doceditor/path_tolerance_dialog.py | 68 ++++++++++++ rayforge/ui_gtk/main_menu.py | 7 ++ .../doceditor/test_path_tolerance_dialog.py | 60 +++++++++++ website/docs/features/path-cleanup.md | 53 +++++++++ website/docs/ui/canvas-tools.md | 3 + .../current/features/path-cleanup.md | 51 +++++++++ .../current/ui/canvas-tools.md | 3 + .../current/features/path-cleanup.md | 50 +++++++++ .../current/ui/canvas-tools.md | 3 + .../current/features/path-cleanup.md | 51 +++++++++ .../current/ui/canvas-tools.md | 3 + .../current/features/path-cleanup.md | 47 ++++++++ .../current/ui/canvas-tools.md | 3 + .../current/features/path-cleanup.md | 51 +++++++++ .../current/ui/canvas-tools.md | 3 + .../current/features/path-cleanup.md | 48 +++++++++ .../current/ui/canvas-tools.md | 3 + .../current/features/path-cleanup.md | 34 ++++++ .../current/ui/canvas-tools.md | 2 + website/sidebars.js | 1 + 23 files changed, 667 insertions(+), 1 deletion(-) create mode 100644 rayforge/ui_gtk/doceditor/path_tolerance_dialog.py create mode 100644 tests/ui_gtk/doceditor/test_path_tolerance_dialog.py create mode 100644 website/docs/features/path-cleanup.md create mode 100644 website/i18n/de/docusaurus-plugin-content-docs/current/features/path-cleanup.md create mode 100644 website/i18n/es/docusaurus-plugin-content-docs/current/features/path-cleanup.md create mode 100644 website/i18n/fr/docusaurus-plugin-content-docs/current/features/path-cleanup.md create mode 100644 website/i18n/hi/docusaurus-plugin-content-docs/current/features/path-cleanup.md create mode 100644 website/i18n/pt-BR/docusaurus-plugin-content-docs/current/features/path-cleanup.md create mode 100644 website/i18n/uk/docusaurus-plugin-content-docs/current/features/path-cleanup.md create mode 100644 website/i18n/zh-CN/docusaurus-plugin-content-docs/current/features/path-cleanup.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 283c89fe6..b18ae19e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 the page size (#517) - Import: GIF, TIFF and WebP bitmaps, traced like PNG. Animated GIFs and multi-page TIFFs import their first frame or page (#517) +- Path clean-up tools in the Object menu and canvas context menu: + Close Paths and Join Open Paths with a tolerance in mm, Delete + Duplicates (duplicate contours inside a workpiece and stacked copies + of whole workpieces), and Break Apart (one workpiece per contour, + holes and open paths included). Each is a single undo step (#516) - Sketcher: boolean operations. Union, Difference, Intersection and Exclude bake the selected closed regions into multi-ring polygons (outer contours counter-clockwise, hole contours clockwise), so diff --git a/rayforge/ui_gtk/actions.py b/rayforge/ui_gtk/actions.py index 4327dad2d..9bd4b26ef 100644 --- a/rayforge/ui_gtk/actions.py +++ b/rayforge/ui_gtk/actions.py @@ -1,9 +1,10 @@ import logging from collections.abc import Callable from gettext import gettext as _ +from gettext import ngettext from typing import TYPE_CHECKING, cast -from gi.repository import Gio, GLib, Gtk +from gi.repository import Adw, Gio, GLib, Gtk from ..context import get_context from ..core.group import Group @@ -20,6 +21,7 @@ PointRotationArrayDialog, ) from .doceditor.add_tabs_popover import AddTabsPopover +from .doceditor.path_tolerance_dialog import PathToleranceDialog from .doceditor.stock_properties_dialog import StockPropertiesDialog from .shared.keyboard import PRIMARY_ACCEL @@ -423,6 +425,12 @@ def register_actions(self): # Split Action self._add_action("split", self.on_split_action) + # Path Clean-Up Actions + self._add_action("break-apart", self.on_break_apart) + self._add_action("close-paths", self.on_close_paths) + self._add_action("join-paths", self.on_join_paths) + self._add_action("delete-duplicates", self.on_delete_duplicates) + # Convert to Stock Action self._add_action("convert-to-stock", self.on_convert_to_stock) @@ -550,6 +558,13 @@ def update_action_states(self, *args, **kwargs): layout_info.action.set_enabled(has_workpieces) self.actions["split"].set_enabled(bool(selected_wps)) + for name in ( + "break-apart", + "close-paths", + "join-paths", + "delete-duplicates", + ): + self.actions[name].set_enabled(bool(selected_wps)) self.actions["export-object"].set_enabled(len(selected_wps) == 1) action_extension_registry.invoke_state_update_handlers(self) @@ -798,6 +813,90 @@ def on_split_action(self, action, param): if new_items: self.win.surface.select_items(new_items) + def on_break_apart(self, action, param): + """Handler for the 'break-apart' action.""" + selected_workpieces = self.win.surface.get_selected_workpieces() + if not selected_workpieces: + return + + new_items = self.editor.path_cleanup.break_apart(selected_workpieces) + if new_items: + self.win.surface.select_items(new_items) + else: + self._show_toast(_("Nothing to break apart")) + + def on_close_paths(self, action, param): + """Handler for the 'close-paths' action: asks for a tolerance.""" + workpieces = self.win.surface.get_selected_workpieces() + if not workpieces: + return + + def _apply(tolerance: float): + count = self.editor.path_cleanup.close_paths(workpieces, tolerance) + self._show_toast( + ngettext( + "Closed {count} path", + "Closed {count} paths", + count, + ).format(count=count) + ) + + PathToleranceDialog( + heading=_("Close Paths"), + body=_( + "Closes open paths whose start and end point are closer " + "than the tolerance." + ), + apply_label=_("Close Paths"), + key="close-paths", + on_apply=_apply, + ).present(self.win) + + def on_join_paths(self, action, param): + """Handler for the 'join-paths' action: asks for a tolerance.""" + workpieces = self.win.surface.get_selected_workpieces() + if not workpieces: + return + + def _apply(tolerance: float): + count = self.editor.path_cleanup.join_paths(workpieces, tolerance) + self._show_toast( + ngettext( + "Made {count} join", + "Made {count} joins", + count, + ).format(count=count) + ) + + PathToleranceDialog( + heading=_("Join Open Paths"), + body=_( + "Joins open paths whose end points are closer than the " + "tolerance. Paths whose ends then meet are closed." + ), + apply_label=_("Join Paths"), + key="join-paths", + on_apply=_apply, + ).present(self.win) + + def on_delete_duplicates(self, action, param): + """Handler for the 'delete-duplicates' action.""" + workpieces = self.win.surface.get_selected_workpieces() + if not workpieces: + return + + count = self.editor.path_cleanup.delete_duplicates(workpieces) + self._show_toast( + ngettext( + "Deleted {count} duplicate", + "Deleted {count} duplicates", + count, + ).format(count=count) + ) + + def _show_toast(self, message: str): + self.win.toast_overlay.add_toast(Adw.Toast.new(message)) + def on_convert_to_stock(self, action, param): """Handler for the 'convert-to-stock' action.""" selected_wps = self.win.surface.get_selected_workpieces() diff --git a/rayforge/ui_gtk/canvas2d/context_menu.py b/rayforge/ui_gtk/canvas2d/context_menu.py index dae5fcb0e..9f18b437d 100644 --- a/rayforge/ui_gtk/canvas2d/context_menu.py +++ b/rayforge/ui_gtk/canvas2d/context_menu.py @@ -114,6 +114,24 @@ def _populate_standard_items(menu: Gio.Menu): # Separator menu.append_section(None, Gio.Menu.new()) + cleanup_menu = Gio.Menu.new() + cleanup_menu.append_item( + Gio.MenuItem.new(_("Break Apart"), "win.break-apart") + ) + cleanup_menu.append_item( + Gio.MenuItem.new(_("Close Paths…"), "win.close-paths") + ) + cleanup_menu.append_item( + Gio.MenuItem.new(_("Join Open Paths…"), "win.join-paths") + ) + cleanup_menu.append_item( + Gio.MenuItem.new(_("Delete Duplicates"), "win.delete-duplicates") + ) + menu.append_submenu(_("Clean Up Paths"), cleanup_menu) + + # Separator + menu.append_section(None, Gio.Menu.new()) + menu.append_item( Gio.MenuItem.new(_("Convert to Stock"), "win.convert-to-stock") ) diff --git a/rayforge/ui_gtk/doceditor/path_tolerance_dialog.py b/rayforge/ui_gtk/doceditor/path_tolerance_dialog.py new file mode 100644 index 000000000..4673e6f35 --- /dev/null +++ b/rayforge/ui_gtk/doceditor/path_tolerance_dialog.py @@ -0,0 +1,68 @@ +from __future__ import annotations + +import logging +from collections.abc import Callable +from gettext import gettext as _ +from typing import ClassVar + +from gi.repository import Adw + +from ..shared.pref_rows.length_spin_row import LengthSpinRow + +logger = logging.getLogger(__name__) + +DEFAULT_TOLERANCE_MM = 0.1 + + +class PathToleranceDialog(Adw.AlertDialog): + """ + Asks for the tolerance of a path clean-up operation and runs it on + confirmation. The last value used per operation is remembered for + the rest of the session. + """ + + _last_values: ClassVar[dict[str, float]] = {} + + def __init__( + self, + heading: str, + body: str, + apply_label: str, + key: str, + on_apply: Callable[[float], None], + ): + super().__init__() + self._key = key + self._on_apply = on_apply + + self.set_heading(heading) + self.set_body(body) + self.add_response("cancel", _("Cancel")) + self.add_response("apply", apply_label) + self.set_response_appearance("apply", Adw.ResponseAppearance.SUGGESTED) + self.set_default_response("apply") + self.set_close_response("cancel") + + self.tolerance_row = LengthSpinRow( + _("Tolerance"), + lower=0.001, + upper=10.0, + step_increment=0.01, + digits=3, + value_in_base=self._last_values.get(key, DEFAULT_TOLERANCE_MM), + ) + group = Adw.PreferencesGroup() + group.add(self.tolerance_row) + self.set_extra_child(group) + + self.connect("response", self._on_response) + + def get_tolerance_mm(self) -> float: + return self.tolerance_row.get_value_in_base_units() + + def _on_response(self, _dialog, response: str): + if response != "apply": + return + tolerance = self.get_tolerance_mm() + self._last_values[self._key] = tolerance + self._on_apply(tolerance) diff --git a/rayforge/ui_gtk/main_menu.py b/rayforge/ui_gtk/main_menu.py index 2c1f4cfe2..4c9b859d1 100644 --- a/rayforge/ui_gtk/main_menu.py +++ b/rayforge/ui_gtk/main_menu.py @@ -108,9 +108,16 @@ def __init__(self): other_group = Gio.Menu() other_group.append(_("Split"), "win.split") + other_group.append(_("Break Apart"), "win.break-apart") other_group.append(_("Export Object..."), "win.export-object") object_menu.append_section(None, other_group) + cleanup_group = Gio.Menu() + cleanup_group.append(_("Close Paths…"), "win.close-paths") + cleanup_group.append(_("Join Open Paths…"), "win.join-paths") + cleanup_group.append(_("Delete Duplicates"), "win.delete-duplicates") + object_menu.append_section(None, cleanup_group) + # Addon section for Object menu self._addon_sections["object"] = Gio.Menu() object_menu.append_section(None, self._addon_sections["object"]) diff --git a/tests/ui_gtk/doceditor/test_path_tolerance_dialog.py b/tests/ui_gtk/doceditor/test_path_tolerance_dialog.py new file mode 100644 index 000000000..04d145c6a --- /dev/null +++ b/tests/ui_gtk/doceditor/test_path_tolerance_dialog.py @@ -0,0 +1,60 @@ +# flake8: noqa: E402 +import gi + +gi.require_version("Gtk", "4.0") +gi.require_version("Adw", "1") + +import pytest + +from rayforge.ui_gtk.doceditor.path_tolerance_dialog import ( + DEFAULT_TOLERANCE_MM, + PathToleranceDialog, +) + + +@pytest.fixture(autouse=True) +def _reset_remembered_values(ui_context_initializer): + PathToleranceDialog._last_values.clear() + yield + PathToleranceDialog._last_values.clear() + + +def _dialog(calls: list[float], key="close-paths") -> PathToleranceDialog: + return PathToleranceDialog( + heading="Close Paths", + body="Body", + apply_label="Close", + key=key, + on_apply=calls.append, + ) + + +@pytest.mark.ui +def test_apply_runs_callback_with_tolerance(): + calls: list[float] = [] + dialog = _dialog(calls) + assert dialog.get_tolerance_mm() == pytest.approx(DEFAULT_TOLERANCE_MM) + + dialog.tolerance_row.set_value_in_base_units(0.25) + dialog.emit("response", "apply") + + assert calls == [pytest.approx(0.25)] + + +@pytest.mark.ui +def test_cancel_does_not_run_callback(): + calls: list[float] = [] + dialog = _dialog(calls) + dialog.emit("response", "cancel") + assert calls == [] + + +@pytest.mark.ui +def test_last_value_is_remembered_per_operation(): + first = _dialog([]) + first.tolerance_row.set_value_in_base_units(0.5) + first.emit("response", "apply") + + assert _dialog([]).get_tolerance_mm() == pytest.approx(0.5) + other = _dialog([], key="join-paths") + assert other.get_tolerance_mm() == pytest.approx(DEFAULT_TOLERANCE_MM) diff --git a/website/docs/features/path-cleanup.md b/website/docs/features/path-cleanup.md new file mode 100644 index 000000000..2286ff963 --- /dev/null +++ b/website/docs/features/path-cleanup.md @@ -0,0 +1,53 @@ +--- +description: + "Clean up imported vector paths in Rayforge: close nearly closed paths, join open pieces, delete + duplicate shapes and break a workpiece apart into its contours." +--- + +# Path Clean-Up + +Logos and drawings from other programs are not always clean. An outline may stop a fraction of a +millimetre before its start point, a shape may be made of several open pieces, or the same path may +be in the file twice. Rayforge cuts such paths exactly as they are, so a shape that only looks +closed is treated as open, and a doubled path is cut twice. + +The path clean-up tools fix this on the selected workpieces. You find them in the **Object** menu +and under **Clean Up Paths** in the canvas context menu. Each tool is a single undo step. + +## Close Paths + +**Close Paths…** closes every open path whose start and end point are closer than the tolerance you +enter. The gap is bridged with a straight line between the two existing points, so the shape does +not move or change size. + +## Join Open Paths + +**Join Open Paths…** connects open paths whose end points are closer than the tolerance into longer +paths. Pieces are reversed where needed, so the order and direction in which they were drawn do not +matter. When the ends of a joined path then meet, the path is closed as well. + +Both tools remember the last tolerance you used during the session. A tolerance between 0.05 mm and +0.2 mm works for most imported files. + +## Delete Duplicates + +**Delete Duplicates** removes paths that lie exactly on top of another path in the same workpiece, +regardless of their direction or start point. When you select several workpieces, a workpiece that +is an exact copy of another selected workpiece at the same position is removed too. + +Overlapping paths that only share part of their length are left alone. The +[Merge Lines](merge-lines) post-processor takes care of those when the job is generated. + +## Break Apart + +**Break Apart** turns every path of a workpiece into a workpiece of its own, holes and open paths +included. This differs from **Split**, which keeps an island together with its holes. + +Path clean-up only works on imported and traced workpieces. Workpieces that come from a sketch are +edited in the [Sketcher](sketcher/index) instead. + +## Related Pages + +- [Merge Lines](merge-lines) - Cutting overlapping segments only once +- [Canvas Tools](../ui/canvas-tools) - Selecting and deleting individual segments +- [Importing Files](../files/importing) - Bringing designs into Rayforge diff --git a/website/docs/ui/canvas-tools.md b/website/docs/ui/canvas-tools.md index 81db70c84..73bf633c3 100644 --- a/website/docs/ui/canvas-tools.md +++ b/website/docs/ui/canvas-tools.md @@ -107,6 +107,9 @@ and delete individual vector segments of the workpiece. To exit vector editing mode, click outside the workpiece or press Escape. +To close gaps, join open pieces, delete duplicate paths or break a workpiece apart, use the +[Path Clean-Up](../features/path-cleanup) tools. + --- **Next**: [3D View →](3d-preview) diff --git a/website/i18n/de/docusaurus-plugin-content-docs/current/features/path-cleanup.md b/website/i18n/de/docusaurus-plugin-content-docs/current/features/path-cleanup.md new file mode 100644 index 000000000..8bfeff794 --- /dev/null +++ b/website/i18n/de/docusaurus-plugin-content-docs/current/features/path-cleanup.md @@ -0,0 +1,51 @@ +# Pfade bereinigen + +Logos und Zeichnungen aus anderen Programmen sind nicht immer sauber. Ein Umriss kann einen +Bruchteil eines Millimeters vor seinem Startpunkt enden, eine Form kann aus mehreren offenen Teilen +bestehen, oder derselbe Pfad steht zweimal in der Datei. Rayforge schneidet solche Pfade genau so, +wie sie sind: Eine Form, die nur geschlossen aussieht, wird als offen behandelt, und ein doppelter +Pfad wird zweimal geschnitten. + +Die Werkzeuge zum Bereinigen von Pfaden beheben das bei den ausgewählten Werkstücken. Du findest sie +im Menü **Objekt** und im Kontextmenü der Arbeitsfläche unter **Pfade bereinigen**. Jedes Werkzeug +ist ein einzelner Rückgängig-Schritt. + +## Pfade schließen + +**Pfade schließen…** schließt jeden offenen Pfad, dessen Anfangs- und Endpunkt näher beieinander +liegen als die eingegebene Toleranz. Die Lücke wird mit einer geraden Linie zwischen den beiden +vorhandenen Punkten überbrückt, sodass sich die Form weder verschiebt noch ihre Größe ändert. + +## Offene Pfade verbinden + +**Offene Pfade verbinden…** verbindet offene Pfade, deren Endpunkte näher beieinander liegen als die +Toleranz, zu längeren Pfaden. Teile werden bei Bedarf umgedreht, die Reihenfolge und Richtung, in +der sie gezeichnet wurden, spielt also keine Rolle. Treffen sich danach die Enden eines verbundenen +Pfads, wird er außerdem geschlossen. + +Beide Werkzeuge merken sich die zuletzt verwendete Toleranz für die laufende Sitzung. Für die +meisten importierten Dateien passt eine Toleranz zwischen 0,05 mm und 0,2 mm. + +## Duplikate löschen + +**Duplikate löschen** entfernt Pfade, die genau auf einem anderen Pfad im selben Werkstück liegen, +unabhängig von ihrer Richtung oder ihrem Startpunkt. Wenn du mehrere Werkstücke auswählst, wird auch +ein Werkstück entfernt, das eine exakte Kopie eines anderen ausgewählten Werkstücks an derselben +Position ist. + +Überlappende Pfade, die nur einen Teil ihrer Länge gemeinsam haben, bleiben erhalten. Um diese +kümmert sich der Postprozessor [Linien zusammenführen](merge-lines), wenn der Auftrag erzeugt wird. + +## Zerlegen + +**Zerlegen** macht aus jedem Pfad eines Werkstücks ein eigenes Werkstück, einschließlich Löchern und +offener Pfade. Das unterscheidet sich von **Teilen**, das eine Insel mit ihren Löchern zusammenhält. + +Das Bereinigen von Pfaden funktioniert nur bei importierten und nachgezeichneten Werkstücken. +Werkstücke aus einer Skizze bearbeitest du stattdessen im [Sketcher](sketcher/index). + +## Verwandte Seiten + +- [Linien zusammenführen](merge-lines) - Überlappende Segmente nur einmal schneiden +- [Canvas-Werkzeuge](../ui/canvas-tools) - Einzelne Segmente auswählen und löschen +- [Dateien importieren](../files/importing) - Designs in Rayforge laden diff --git a/website/i18n/de/docusaurus-plugin-content-docs/current/ui/canvas-tools.md b/website/i18n/de/docusaurus-plugin-content-docs/current/ui/canvas-tools.md index b8f7c4fe3..4f725fa94 100644 --- a/website/i18n/de/docusaurus-plugin-content-docs/current/ui/canvas-tools.md +++ b/website/i18n/de/docusaurus-plugin-content-docs/current/ui/canvas-tools.md @@ -166,6 +166,9 @@ In diesem Modus kannst du einzelne Vektorsegmente des Werkstücks auswählen und Um den Vektorbearbeitungsmodus zu verlassen, klicke außerhalb des Werkstücks oder drücke Escape. +Um Lücken zu schließen, offene Teile zu verbinden, doppelte Pfade zu löschen oder ein Werkstück zu +zerlegen, verwende die Werkzeuge unter [Pfade bereinigen](../features/path-cleanup). + --- **Weiter**: [3D-Vorschau →](3d-preview) diff --git a/website/i18n/es/docusaurus-plugin-content-docs/current/features/path-cleanup.md b/website/i18n/es/docusaurus-plugin-content-docs/current/features/path-cleanup.md new file mode 100644 index 000000000..5dada85f2 --- /dev/null +++ b/website/i18n/es/docusaurus-plugin-content-docs/current/features/path-cleanup.md @@ -0,0 +1,50 @@ +# Limpieza de rutas + +Los logotipos y dibujos de otros programas no siempre están limpios. Un contorno puede terminar una +fracción de milímetro antes de su punto inicial, una forma puede estar hecha de varias piezas +abiertas o la misma ruta puede estar dos veces en el archivo. Rayforge corta esas rutas tal como +son: una forma que solo parece cerrada se trata como abierta y una ruta duplicada se corta dos +veces. + +Las herramientas de limpieza de rutas corrigen esto en las piezas de trabajo seleccionadas. Las +encontrarás en el menú **Objeto** y en **Limpiar rutas** del menú contextual del lienzo. Cada +herramienta es un solo paso de deshacer. + +## Cerrar rutas + +**Cerrar rutas…** cierra cada ruta abierta cuyo punto inicial y final están más cerca que la +tolerancia que indiques. El hueco se salva con una línea recta entre los dos puntos existentes, así +que la forma no se mueve ni cambia de tamaño. + +## Unir rutas abiertas + +**Unir rutas abiertas…** une en rutas más largas las rutas abiertas cuyos extremos están más cerca +que la tolerancia. Las piezas se invierten cuando hace falta, así que el orden y la dirección en que +se dibujaron no importan. Si después los extremos de una ruta unida coinciden, la ruta también se +cierra. + +Ambas herramientas recuerdan la última tolerancia que usaste durante la sesión. Una tolerancia entre +0,05 mm y 0,2 mm funciona para la mayoría de los archivos importados. + +## Eliminar duplicados + +**Eliminar duplicados** quita las rutas que están exactamente encima de otra ruta de la misma pieza +de trabajo, sin importar su dirección o punto inicial. Si seleccionas varias piezas de trabajo, +también se quita una pieza que sea una copia exacta de otra pieza seleccionada en la misma posición. + +Las rutas que solo se superponen en parte de su longitud se mantienen. De ellas se encarga el +posprocesador [Fusionar líneas](merge-lines) al generar el trabajo. + +## Separar + +**Separar** convierte cada ruta de una pieza de trabajo en una pieza propia, incluidos los agujeros +y las rutas abiertas. Esto es distinto de **Dividir**, que mantiene una isla junto con sus agujeros. + +La limpieza de rutas solo funciona en piezas de trabajo importadas y trazadas. Las piezas que vienen +de un boceto se editan en el [Sketcher](sketcher/index). + +## Páginas relacionadas + +- [Fusionar líneas](merge-lines) - Cortar los segmentos superpuestos una sola vez +- [Herramientas del lienzo](../ui/canvas-tools) - Seleccionar y eliminar segmentos individuales +- [Importar archivos](../files/importing) - Llevar diseños a Rayforge diff --git a/website/i18n/es/docusaurus-plugin-content-docs/current/ui/canvas-tools.md b/website/i18n/es/docusaurus-plugin-content-docs/current/ui/canvas-tools.md index 5e3bf3edc..45b9a3a55 100644 --- a/website/i18n/es/docusaurus-plugin-content-docs/current/ui/canvas-tools.md +++ b/website/i18n/es/docusaurus-plugin-content-docs/current/ui/canvas-tools.md @@ -166,6 +166,9 @@ este modo, puedes seleccionar y eliminar segmentos de vector individuales de la Para salir del modo de edición de vectores, haz clic fuera de la pieza de trabajo o presiona Escape. +Para cerrar huecos, unir piezas abiertas, eliminar rutas duplicadas o separar una pieza de trabajo, +usa las herramientas de [Limpieza de rutas](../features/path-cleanup). + --- **Siguiente**: [Vista Previa 3D →](3d-preview) diff --git a/website/i18n/fr/docusaurus-plugin-content-docs/current/features/path-cleanup.md b/website/i18n/fr/docusaurus-plugin-content-docs/current/features/path-cleanup.md new file mode 100644 index 000000000..4ba9f6cd9 --- /dev/null +++ b/website/i18n/fr/docusaurus-plugin-content-docs/current/features/path-cleanup.md @@ -0,0 +1,51 @@ +# Nettoyage des chemins + +Les logos et dessins provenant d'autres logiciels ne sont pas toujours propres. Un contour peut +s'arrêter une fraction de millimètre avant son point de départ, une forme peut être composée de +plusieurs morceaux ouverts, ou le même chemin peut figurer deux fois dans le fichier. Rayforge +découpe ces chemins tels quels : une forme qui semble seulement fermée est traitée comme ouverte, et +un chemin en double est découpé deux fois. + +Les outils de nettoyage des chemins corrigent cela sur les pièces sélectionnées. Vous les trouverez +dans le menu **Objet** et sous **Nettoyer les chemins** dans le menu contextuel du canevas. Chaque +outil correspond à une seule étape d'annulation. + +## Fermer les chemins + +**Fermer les chemins…** ferme chaque chemin ouvert dont le point de départ et le point d'arrivée +sont plus proches que la tolérance saisie. L'écart est comblé par une ligne droite entre les deux +points existants : la forme ne se déplace pas et ne change pas de taille. + +## Joindre les chemins ouverts + +**Joindre les chemins ouverts…** relie en chemins plus longs les chemins ouverts dont les extrémités +sont plus proches que la tolérance. Les morceaux sont inversés si nécessaire, l'ordre et le sens +dans lesquels ils ont été dessinés n'ont donc pas d'importance. Si les extrémités d'un chemin joint +se rejoignent ensuite, le chemin est également fermé. + +Les deux outils retiennent la dernière tolérance utilisée pendant la session. Une tolérance comprise +entre 0,05 mm et 0,2 mm convient à la plupart des fichiers importés. + +## Supprimer les doublons + +**Supprimer les doublons** retire les chemins qui se trouvent exactement sur un autre chemin de la +même pièce, quels que soient leur sens ou leur point de départ. Si vous sélectionnez plusieurs +pièces, une pièce qui est la copie exacte d'une autre pièce sélectionnée à la même position est +également retirée. + +Les chemins qui ne se chevauchent que sur une partie de leur longueur sont conservés. Le +post-processeur [Fusionner les lignes](merge-lines) s'en charge lors de la génération de la tâche. + +## Décomposer + +**Décomposer** transforme chaque chemin d'une pièce en une pièce distincte, trous et chemins ouverts +compris. C'est différent de **Scinder**, qui garde un îlot avec ses trous. + +Le nettoyage des chemins ne fonctionne que sur les pièces importées et vectorisées. Les pièces +issues d'une esquisse se modifient dans le [Sketcher](sketcher/index). + +## Pages connexes + +- [Fusionner les lignes](merge-lines) - Ne découper qu'une fois les segments qui se chevauchent +- [Outils du canevas](../ui/canvas-tools) - Sélectionner et supprimer des segments individuels +- [Importer des fichiers](../files/importing) - Importer des dessins dans Rayforge diff --git a/website/i18n/fr/docusaurus-plugin-content-docs/current/ui/canvas-tools.md b/website/i18n/fr/docusaurus-plugin-content-docs/current/ui/canvas-tools.md index 1d0a10b36..4fc699ba7 100644 --- a/website/i18n/fr/docusaurus-plugin-content-docs/current/ui/canvas-tools.md +++ b/website/i18n/fr/docusaurus-plugin-content-docs/current/ui/canvas-tools.md @@ -168,6 +168,9 @@ vous pouvez sélectionner et supprimer des segments vectoriels individuels de la Pour quitter le mode d'édition vectorielle, cliquez en dehors de la pièce ou appuyez sur Échap. +Pour fermer des écarts, joindre des morceaux ouverts, supprimer des chemins en double ou décomposer +une pièce, utilisez les outils de [Nettoyage des chemins](../features/path-cleanup). + --- **Suivant** : [Prévisualisation 3D →](3d-preview) diff --git a/website/i18n/hi/docusaurus-plugin-content-docs/current/features/path-cleanup.md b/website/i18n/hi/docusaurus-plugin-content-docs/current/features/path-cleanup.md new file mode 100644 index 000000000..dae47055a --- /dev/null +++ b/website/i18n/hi/docusaurus-plugin-content-docs/current/features/path-cleanup.md @@ -0,0 +1,47 @@ +# पथ सफ़ाई + +दूसरे प्रोग्राम से आए लोगो और ड्रॉइंग हमेशा साफ़ नहीं होते. कोई आउटलाइन अपने आरंभ बिंदु से एक +मिलीमीटर के अंश पहले रुक सकती है, कोई आकृति कई खुले टुकड़ों से बनी हो सकती है, या एक ही पथ फ़ाइल में +दो बार हो सकता है. Rayforge ऐसे पथों को ठीक वैसे ही काटता है जैसे वे हैं: जो आकृति केवल बंद दिखती है +उसे खुला माना जाता है, और दोहरा पथ दो बार काटा जाता है. + +पथ सफ़ाई टूल चुने गए वर्कपीस पर इसे ठीक करते हैं. ये **ऑब्जेक्ट** मेनू में और कैनवस के संदर्भ मेनू +में **पथ साफ़ करें** के अंतर्गत मिलते हैं. हर टूल एक ही पूर्ववत चरण है. + +## पथ बंद करें + +**पथ बंद करें…** हर उस खुले पथ को बंद करता है जिसके आरंभ और अंत बिंदु आपकी दी गई सहनशीलता से अधिक +निकट हैं. अंतराल को दोनों मौजूदा बिंदुओं के बीच एक सीधी रेखा से जोड़ा जाता है, इसलिए आकृति न हिलती +है और न उसका आकार बदलता है. + +## खुले पथ जोड़ें + +**खुले पथ जोड़ें…** उन खुले पथों को लंबे पथों में जोड़ता है जिनके अंत बिंदु सहनशीलता से अधिक निकट +हैं. ज़रूरत होने पर टुकड़ों की दिशा उलट दी जाती है, इसलिए यह मायने नहीं रखता कि उन्हें किस क्रम और +दिशा में बनाया गया था. अगर जुड़े हुए पथ के सिरे फिर मिल जाते हैं, तो पथ बंद भी कर दिया जाता है. + +दोनों टूल सत्र के दौरान आपकी पिछली सहनशीलता याद रखते हैं. ज़्यादातर आयात की गई फ़ाइलों के लिए 0.05 +mm से 0.2 mm के बीच की सहनशीलता ठीक रहती है. + +## डुप्लिकेट हटाएँ + +**डुप्लिकेट हटाएँ** उन पथों को हटाता है जो उसी वर्कपीस के किसी दूसरे पथ के ठीक ऊपर हैं, चाहे उनकी +दिशा या आरंभ बिंदु कुछ भी हो. अगर आप कई वर्कपीस चुनते हैं, तो वह वर्कपीस भी हटा दिया जाता है जो उसी +स्थान पर किसी दूसरे चुने गए वर्कपीस की हूबहू प्रति है. + +जो पथ अपनी लंबाई के केवल एक हिस्से में ओवरलैप करते हैं, वे बने रहते हैं. जॉब बनाते समय +[रेखाएँ मर्ज करें](merge-lines) पोस्ट-प्रोसेसर उन्हें संभालता है. + +## अलग-अलग करें + +**अलग-अलग करें** किसी वर्कपीस के हर पथ को उसका अपना वर्कपीस बना देता है, छेद और खुले पथ भी. यह +**विभाजित करें** से अलग है, जो किसी द्वीप को उसके छेदों के साथ एक रखता है. + +पथ सफ़ाई केवल आयात किए गए और ट्रेस किए गए वर्कपीस पर काम करती है. स्केच से बने वर्कपीस को +[स्केचर](sketcher/index) में संपादित करें. + +## संबंधित पृष्ठ + +- [रेखाएँ मर्ज करें](merge-lines) - ओवरलैप होने वाले खंडों को केवल एक बार काटना +- [कैनवस उपकरण](../ui/canvas-tools) - अलग-अलग खंड चुनना और हटाना +- [फ़ाइलें आयात करना](../files/importing) - Rayforge में डिज़ाइन लाना diff --git a/website/i18n/hi/docusaurus-plugin-content-docs/current/ui/canvas-tools.md b/website/i18n/hi/docusaurus-plugin-content-docs/current/ui/canvas-tools.md index f29e4baf2..b9521d394 100644 --- a/website/i18n/hi/docusaurus-plugin-content-docs/current/ui/canvas-tools.md +++ b/website/i18n/hi/docusaurus-plugin-content-docs/current/ui/canvas-tools.md @@ -108,6 +108,9 @@ description: वेक्टर संपादन मोड से बाहर निकलने के लिए, वर्कपीस के बाहर क्लिक करें या Escape दबाएँ. +अंतराल बंद करने, खुले टुकड़ों को जोड़ने, डुप्लिकेट पथ हटाने या किसी वर्कपीस को अलग-अलग करने के लिए +[पथ सफ़ाई](../features/path-cleanup) टूल का उपयोग करें. + --- **अगला:** [3D दृश्य →](3d-preview) diff --git a/website/i18n/pt-BR/docusaurus-plugin-content-docs/current/features/path-cleanup.md b/website/i18n/pt-BR/docusaurus-plugin-content-docs/current/features/path-cleanup.md new file mode 100644 index 000000000..22771fbcb --- /dev/null +++ b/website/i18n/pt-BR/docusaurus-plugin-content-docs/current/features/path-cleanup.md @@ -0,0 +1,51 @@ +# Limpeza de caminhos + +Logotipos e desenhos vindos de outros programas nem sempre estão limpos. Um contorno pode terminar +uma fração de milímetro antes do ponto inicial, uma forma pode ser composta de várias partes +abertas, ou o mesmo caminho pode aparecer duas vezes no arquivo. O Rayforge corta esses caminhos +exatamente como estão: uma forma que só parece fechada é tratada como aberta, e um caminho duplicado +é cortado duas vezes. + +As ferramentas de limpeza de caminhos corrigem isso nas peças de trabalho selecionadas. Você as +encontra no menu **Objeto** e em **Limpar caminhos** no menu de contexto da tela. Cada ferramenta é +um único passo de desfazer. + +## Fechar caminhos + +**Fechar caminhos…** fecha cada caminho aberto cujos pontos inicial e final estão mais próximos do +que a tolerância informada. A lacuna é preenchida com uma linha reta entre os dois pontos +existentes, então a forma não se move nem muda de tamanho. + +## Unir caminhos abertos + +**Unir caminhos abertos…** junta em caminhos mais longos os caminhos abertos cujas extremidades +estão mais próximas do que a tolerância. As partes são invertidas quando necessário, então a ordem e +o sentido em que foram desenhadas não importam. Quando as extremidades de um caminho unido passam a +se encontrar, o caminho também é fechado. + +As duas ferramentas lembram a última tolerância usada durante a sessão. Uma tolerância entre 0,05 mm +e 0,2 mm funciona para a maioria dos arquivos importados. + +## Excluir duplicados + +**Excluir duplicados** remove caminhos que estão exatamente sobre outro caminho da mesma peça de +trabalho, independentemente do sentido ou do ponto inicial. Se você selecionar várias peças de +trabalho, uma peça que seja cópia exata de outra peça selecionada na mesma posição também é +removida. + +Caminhos que se sobrepõem apenas em parte do comprimento são mantidos. O pós-processador +[Mesclar linhas](merge-lines) cuida deles quando o trabalho é gerado. + +## Separar + +**Separar** transforma cada caminho de uma peça de trabalho em uma peça própria, incluindo furos e +caminhos abertos. Isso é diferente de **Dividir**, que mantém uma ilha junto com seus furos. + +A limpeza de caminhos só funciona em peças de trabalho importadas e vetorizadas. Peças que vêm de um +esboço são editadas no [Sketcher](sketcher/index). + +## Páginas relacionadas + +- [Mesclar linhas](merge-lines) - Cortar segmentos sobrepostos apenas uma vez +- [Ferramentas de tela](../ui/canvas-tools) - Selecionar e excluir segmentos individuais +- [Importando arquivos](../files/importing) - Trazer desenhos para o Rayforge diff --git a/website/i18n/pt-BR/docusaurus-plugin-content-docs/current/ui/canvas-tools.md b/website/i18n/pt-BR/docusaurus-plugin-content-docs/current/ui/canvas-tools.md index ba7d15ed7..db63200b1 100644 --- a/website/i18n/pt-BR/docusaurus-plugin-content-docs/current/ui/canvas-tools.md +++ b/website/i18n/pt-BR/docusaurus-plugin-content-docs/current/ui/canvas-tools.md @@ -166,6 +166,9 @@ modo, você pode selecionar e excluir segmentos de vetor individuais da peça de Para sair do modo de edição de vetores, clique fora da peça de trabalho ou pressione Escape. +Para fechar lacunas, unir partes abertas, excluir caminhos duplicados ou separar uma peça de +trabalho, use as ferramentas de [Limpeza de caminhos](../features/path-cleanup). + --- **Próximo**: [Visualização 3D →](3d-preview) diff --git a/website/i18n/uk/docusaurus-plugin-content-docs/current/features/path-cleanup.md b/website/i18n/uk/docusaurus-plugin-content-docs/current/features/path-cleanup.md new file mode 100644 index 000000000..087cd40a1 --- /dev/null +++ b/website/i18n/uk/docusaurus-plugin-content-docs/current/features/path-cleanup.md @@ -0,0 +1,48 @@ +# Очищення шляхів + +Логотипи й креслення з інших програм не завжди чисті. Контур може закінчуватися на частку міліметра +раніше за свою початкову точку, фігура може складатися з кількох відкритих частин, або той самий +шлях може бути у файлі двічі. Rayforge ріже такі шляхи саме такими, якими вони є: фігура, яка лише +здається замкненою, вважається відкритою, а подвоєний шлях ріжеться двічі. + +Інструменти очищення шляхів виправляють це для вибраних робочих деталей. Вони є в меню **Об'єкт** і +в контекстному меню полотна в розділі **Очистити шляхи**. Кожен інструмент — це один крок +скасування. + +## Замкнути шляхи + +**Замкнути шляхи…** замикає кожен відкритий шлях, у якого початкова й кінцева точки ближчі за +введений допуск. Проміжок з'єднується прямою лінією між двома наявними точками, тож фігура не +зсувається і не змінює розміру. + +## З'єднати відкриті шляхи + +**З'єднати відкриті шляхи…** об'єднує в довші шляхи відкриті шляхи, кінцеві точки яких ближчі за +допуск. За потреби частини розвертаються, тож порядок і напрямок, у якому їх намалювали, не мають +значення. Якщо після цього кінці з'єднаного шляху збігаються, шлях також замикається. + +Обидва інструменти пам'ятають останній використаний допуск до кінця сеансу. Для більшості +імпортованих файлів підходить допуск від 0,05 мм до 0,2 мм. + +## Видалити дублікати + +**Видалити дублікати** прибирає шляхи, що лежать точно на іншому шляху тієї самої робочої деталі, +незалежно від їхнього напрямку чи початкової точки. Якщо вибрати кілька робочих деталей, то деталь, +яка є точною копією іншої вибраної деталі в тому самому місці, також видаляється. + +Шляхи, що перекриваються лише частиною своєї довжини, залишаються. Ними опікується постпроцесор +[Об'єднання ліній](merge-lines) під час створення завдання. + +## Розібрати на частини + +**Розібрати на частини** робить із кожного шляху робочої деталі окрему деталь, включно з отворами й +відкритими шляхами. Це відрізняється від **Розділити**, яке тримає острів разом з його отворами. + +Очищення шляхів працює лише з імпортованими та трасованими робочими деталями. Деталі, створені зі +скетчу, редагуйте у [Sketcher](sketcher/index). + +## Пов'язані сторінки + +- [Об'єднання ліній](merge-lines) - Різати сегменти, що перекриваються, лише один раз +- [Інструменти полотна](../ui/canvas-tools) - Вибір і видалення окремих сегментів +- [Імпортування файлів](../files/importing) - Завантаження дизайнів у Rayforge diff --git a/website/i18n/uk/docusaurus-plugin-content-docs/current/ui/canvas-tools.md b/website/i18n/uk/docusaurus-plugin-content-docs/current/ui/canvas-tools.md index 1dcf8c81b..83cae063b 100644 --- a/website/i18n/uk/docusaurus-plugin-content-docs/current/ui/canvas-tools.md +++ b/website/i18n/uk/docusaurus-plugin-content-docs/current/ui/canvas-tools.md @@ -103,6 +103,9 @@ Щоб вийти з режиму редагування векторів, клацніть поза робочою деталлю або натисніть Escape. +Щоб закрити проміжки, з'єднати відкриті частини, видалити дублікати шляхів або розібрати робочу +деталь на частини, скористайтеся інструментами [Очищення шляхів](../features/path-cleanup). + --- **Далі**: [3D вид →](3d-preview) diff --git a/website/i18n/zh-CN/docusaurus-plugin-content-docs/current/features/path-cleanup.md b/website/i18n/zh-CN/docusaurus-plugin-content-docs/current/features/path-cleanup.md new file mode 100644 index 000000000..604717dfc --- /dev/null +++ b/website/i18n/zh-CN/docusaurus-plugin-content-docs/current/features/path-cleanup.md @@ -0,0 +1,34 @@ +# 清理路径 + +来自其他软件的标志和图纸并不总是干净的。轮廓可能在距起点不到一毫米的地方就停止了,一个形状可能由多段开放的片段组成,或者同一条路径在文件中出现了两次。Rayforge 会按原样切割这些路径:看起来闭合但实际未闭合的形状会被当作开放路径处理,重复的路径会被切割两次。 + +路径清理工具可以修复所选工件中的这些问题。您可以在**对象**菜单中,以及画布右键菜单的**清理路径**下找到它们。每个工具都是一个撤销步骤。 + +## 闭合路径 + +**闭合路径…**会闭合所有起点与终点距离小于您输入的公差的开放路径。缺口由两个现有点之间的一条直线连接,因此形状不会移动,也不会改变大小。 + +## 连接开放路径 + +**连接开放路径…**会把端点距离小于公差的开放路径连接成更长的路径。必要时会反转片段方向,因此片段的绘制顺序和方向都无关紧要。如果连接后的路径首尾相接,该路径也会被闭合。 + +两个工具都会在本次会话中记住您上次使用的公差。对大多数导入的文件来说,0.05 mm 到 0.2 +mm 之间的公差即可。 + +## 删除重复项 + +**删除重复项**会删除与同一工件中另一条路径完全重合的路径,无论其方向或起点如何。如果您选择了多个工件,与另一个所选工件位置相同且完全相同的工件也会被删除。 + +只有部分长度重叠的路径会被保留。生成作业时,[合并线条](merge-lines)后处理器会处理这些路径。 + +## 打散 + +**打散**会把工件的每条路径都变成一个独立的工件,包括孔和开放路径。这与**分割**不同,分割会把一个岛与它的孔保留在一起。 + +路径清理只适用于导入和描摹得到的工件。来自草图的工件请在[草图编辑器](sketcher/index)中编辑。 + +## 相关页面 + +- [合并线条](merge-lines) - 重叠的线段只切割一次 +- [画布工具](../ui/canvas-tools) - 选择和删除单个线段 +- [导入文件](../files/importing) - 将设计导入 Rayforge diff --git a/website/i18n/zh-CN/docusaurus-plugin-content-docs/current/ui/canvas-tools.md b/website/i18n/zh-CN/docusaurus-plugin-content-docs/current/ui/canvas-tools.md index f9fdc7b05..efa60de09 100644 --- a/website/i18n/zh-CN/docusaurus-plugin-content-docs/current/ui/canvas-tools.md +++ b/website/i18n/zh-CN/docusaurus-plugin-content-docs/current/ui/canvas-tools.md @@ -159,6 +159,8 @@ 要退出矢量编辑模式,请点击工件外部或按 Escape。 +要闭合缺口、连接开放的片段、删除重复路径或将工件打散,请使用[清理路径](../features/path-cleanup)工具。 + --- **下一步:**[3D 预览 →](3d-preview) diff --git a/website/sidebars.js b/website/sidebars.js index c1577ae78..a293e104e 100644 --- a/website/sidebars.js +++ b/website/sidebars.js @@ -102,6 +102,7 @@ module.exports = { ], }, 'features/arrays', + 'features/path-cleanup', 'features/multi-layer', 'features/stock-handling', 'features/framing-your-job', From 550e176030e2a1fb2176cacb3be866a9d657e9a0 Mon Sep 17 00:00:00 2001 From: Gert Pellin Date: Fri, 9 Oct 2026 13:12:57 +0200 Subject: [PATCH 3/8] perf(editor): index contours spatially when deleting duplicates Comparing every pair of contours with a similar length made Delete Duplicates take seconds on files with thousands of short, equally long segments (a 3,600-segment line soup took 9 s). Kept contours are now indexed by the lower-left corner of their bounding box, so only neighbouring candidates are compared (0.03 s for the same file). Refs #516 --- rayforge/core/path_cleanup.py | 57 ++++++++++++++++++++++++--------- tests/core/test_path_cleanup.py | 14 ++++++++ 2 files changed, 55 insertions(+), 16 deletions(-) diff --git a/rayforge/core/path_cleanup.py b/rayforge/core/path_cleanup.py index 8a1c3cbcf..3a41b73d0 100644 --- a/rayforge/core/path_cleanup.py +++ b/rayforge/core/path_cleanup.py @@ -249,6 +249,30 @@ def _length_slack(tolerance: float) -> float: return 4.0 * tolerance + 1e-9 +class _CornerIndex: + """ + A spatial hash of the lower-left bounding box corner of kept + contours. Duplicates have corners within the tolerance, so only + neighbouring cells need to be compared. + """ + + def __init__(self, tolerance: float): + self._cell = max(tolerance, 1e-9) + self._cells: dict[tuple[int, int], list[int]] = {} + + def _key(self, x: float, y: float) -> tuple[int, int]: + return math.floor(x / self._cell), math.floor(y / self._cell) + + def add(self, index: int, rect: tuple[float, float, float, float]): + self._cells.setdefault(self._key(rect[0], rect[1]), []).append(index) + + def near(self, rect: tuple[float, float, float, float]) -> Iterator[int]: + kx, ky = self._key(rect[0], rect[1]) + for dx in (-1, 0, 1): + for dy in (-1, 0, 1): + yield from self._cells.get((kx + dx, ky + dy), ()) + + def remove_duplicate_contours( geo: Geometry, tolerance: float ) -> tuple[Geometry, int]: @@ -266,24 +290,25 @@ def remove_duplicate_contours( A tuple of the new geometry and the number of removed contours. """ contours = _drawn_contours(geo) - lengths = [c.distance() for c in contours] - order = sorted(range(len(contours)), key=lambda i: lengths[i]) - duplicates: set[int] = set() slack = _length_slack(tolerance) - for pos, i in enumerate(order): - if i in duplicates: + index = _CornerIndex(tolerance) + lengths: list[float] = [] + kept: list[Geometry] = [] + removed = 0 + for contour in contours: + rect = contour.rect() + length = contour.distance() + if any( + abs(lengths[k] - length) <= slack + and _contours_match(kept[k], contour, tolerance) + for k in index.near(rect) + ): + removed += 1 continue - for j in order[pos + 1 :]: - if lengths[j] - lengths[i] > slack: - break - if j in duplicates: - continue - if _contours_match(contours[i], contours[j], tolerance): - duplicates.add(max(i, j)) - if max(i, j) == i: - break - kept = [c for i, c in enumerate(contours) if i not in duplicates] - return _concat(kept), len(duplicates) + index.add(len(kept), rect) + lengths.append(length) + kept.append(contour) + return _concat(kept), removed def geometries_match(a: Geometry, b: Geometry, tolerance: float) -> bool: diff --git a/tests/core/test_path_cleanup.py b/tests/core/test_path_cleanup.py index 8169a244e..9f01a9699 100644 --- a/tests/core/test_path_cleanup.py +++ b/tests/core/test_path_cleanup.py @@ -271,6 +271,20 @@ def test_removes_triple_copies(self): _result, removed = remove_duplicate_contours(geo, 0.01) assert removed == 2 + def test_many_scattered_duplicates(self): + pieces = [] + for i in range(200): + x, y = (i * 37) % 290, (i * 53) % 290 + pieces.append(_polyline([(x, y), (x + 5, y)])) + copies = [ + _polyline(list(reversed([(x, y), (x + 5, y)]))) + for x, y in (((i * 37) % 290, (i * 53) % 290) for i in range(200)) + ] + geo = _concat(*pieces, *copies) + result, removed = remove_duplicate_contours(geo, 0.01) + assert removed == 200 + assert len(_contours(result)) == 200 + class TestGeometriesMatch: def test_same_shapes_in_other_order_match(self): From c4f43af60ecc01d4f524377454ba0ff6f23d56b2 Mon Sep 17 00:00:00 2001 From: Gert Pellin Date: Sun, 11 Oct 2026 00:32:55 +0200 Subject: [PATCH 4/8] refactor(doceditor): use raygeo's path clean-up operations The geometry manipulation core moves into raygeo (barebaric/raygeo#56) as suggested in review: the editor command now calls the Geometry methods close_open_contours, join_open_contours, remove_duplicate_contours, matches and split_drawn_contours, and the core module with its unit tests moves there. The command, undo integration and UI stay here. Needs a raygeo release that contains barebaric/raygeo#56. --- rayforge/core/path_cleanup.py | 346 ------------------------- rayforge/doceditor/path_cleanup_cmd.py | 20 +- rayforge/doceditor/split_cmd.py | 3 +- tests/core/test_path_cleanup.py | 328 ----------------------- 4 files changed, 11 insertions(+), 686 deletions(-) delete mode 100644 rayforge/core/path_cleanup.py delete mode 100644 tests/core/test_path_cleanup.py diff --git a/rayforge/core/path_cleanup.py b/rayforge/core/path_cleanup.py deleted file mode 100644 index 3a41b73d0..000000000 --- a/rayforge/core/path_cleanup.py +++ /dev/null @@ -1,346 +0,0 @@ -""" -Clean-up operations for vector paths. - -These functions close, join, de-duplicate and split the contours of a -:class:`~raygeo.geo.Geometry`. They never move existing points: gaps are -bridged with a straight segment between two existing end points, so the -bounding box of the result never grows. All distances are expressed in -the units of the geometry passed in; callers that work on normalized -workpiece geometry should scale it to millimetres first. -""" - -import math -from collections.abc import Iterator - -from raygeo.geo import Geometry, Move - -Point = tuple[float, float] - -_CLOSED_EPS = 1e-6 -_DUPLICATE_SAMPLES = 32 - - -def _start_point(contour: Geometry) -> Point: - first = contour.data[0] - return first.end[0], first.end[1] - - -def _end_point(contour: Geometry) -> Point: - x, y, _z = contour.get_last_point() - return x, y - - -def _distance(a: Point, b: Point) -> float: - return math.hypot(a[0] - b[0], a[1] - b[1]) - - -def _segment_count(contour: Geometry) -> int: - return sum(1 for cmd in contour.data if not isinstance(cmd, Move)) - - -def _drawn_contours(geo: Geometry) -> list[Geometry]: - """Splits into contours, dropping those that draw nothing.""" - if geo.is_empty(): - return [] - return [c for c in geo.split_into_contours() if _segment_count(c) > 0] - - -def _is_open(contour: Geometry) -> bool: - return not contour.is_closed(_CLOSED_EPS) - - -def _close_if_near(contour: Geometry, tolerance: float) -> bool: - """ - Closes an open contour of at least two segments in place when its - start and end are within the tolerance. Returns True if it closed it. - """ - if not _is_open(contour) or _segment_count(contour) < 2: - return False - start = _start_point(contour) - if _distance(start, _end_point(contour)) > tolerance: - return False - contour.line_to(start[0], start[1]) - return True - - -def _concat(contours: list[Geometry]) -> Geometry: - result = Geometry() - for contour in contours: - result.extend(contour) - return result - - -def close_open_contours( - geo: Geometry, tolerance: float -) -> tuple[Geometry, int]: - """ - Closes every open contour whose start and end point lie within the - tolerance of each other. - - Args: - geo: The geometry to process. It is not modified. - tolerance: The largest gap that is closed. - - Returns: - A tuple of the new geometry and the number of closed contours. - """ - contours = [c.copy() for c in _drawn_contours(geo)] - closed = sum(1 for c in contours if _close_if_near(c, tolerance)) - return _concat(contours), closed - - -class _EndpointGrid: - """A spatial hash of open contour end points for neighbour lookups.""" - - def __init__(self, cell_size: float): - self._cell = max(cell_size, 1e-9) - self._cells: dict[tuple[int, int], set[tuple[int, int]]] = {} - self._points: dict[tuple[int, int], Point] = {} - - def _key(self, p: Point) -> tuple[int, int]: - return math.floor(p[0] / self._cell), math.floor(p[1] / self._cell) - - def add(self, index: int, end: int, p: Point): - self._points[(index, end)] = p - self._cells.setdefault(self._key(p), set()).add((index, end)) - - def remove_contour(self, index: int): - for end in (0, 1): - p = self._points.pop((index, end), None) - if p is not None: - self._cells[self._key(p)].discard((index, end)) - - def _neighbours(self, p: Point) -> Iterator[tuple[int, int]]: - kx, ky = self._key(p) - for dx in (-1, 0, 1): - for dy in (-1, 0, 1): - yield from self._cells.get((kx + dx, ky + dy), ()) - - def nearest(self, p: Point, tolerance: float) -> tuple[int, int] | None: - best: tuple[int, int] | None = None - best_dist = tolerance - for entry in sorted(self._neighbours(p)): - d = _distance(p, self._points[entry]) - if d <= best_dist: - best, best_dist = entry, d - return best - - -def _append_contour(chain: Geometry, piece: Geometry): - """Appends a piece to the end of a chain, bridging any gap.""" - start = _start_point(piece) - if _distance(_end_point(chain), start) > 0.0: - chain.line_to(start[0], start[1]) - chain.extend(piece.filter(set(range(1, len(piece.data))))) - - -def _grow_tail( - chain: Geometry, - contours: list[Geometry], - grid: _EndpointGrid, - tolerance: float, - consumed: set[int], -) -> int: - """ - Keeps appending the nearest open contour to the chain's end, and - records the appended contours in ``consumed``. - """ - joins = 0 - while True: - match = grid.nearest(_end_point(chain), tolerance) - if match is None: - return joins - index, end = match - grid.remove_contour(index) - consumed.add(index) - piece = contours[index].copy() - if end == 1: - piece.reverse_contour() - _append_contour(chain, piece) - joins += 1 - - -def join_open_contours( - geo: Geometry, tolerance: float -) -> tuple[Geometry, int]: - """ - Joins open contours whose end points meet within the tolerance into - longer contours, reversing contours where needed. A joined contour - whose own ends then meet within the tolerance is closed. Closed - contours are left untouched. - - Args: - geo: The geometry to process. It is not modified. - tolerance: The largest gap between two end points that is joined. - - Returns: - A tuple of the new geometry and the number of joins made. - """ - contours = _drawn_contours(geo) - grid = _EndpointGrid(tolerance) - open_indices = [i for i, c in enumerate(contours) if _is_open(c)] - for i in open_indices: - grid.add(i, 0, _start_point(contours[i])) - grid.add(i, 1, _end_point(contours[i])) - - chains: dict[int, Geometry] = {} - consumed: set[int] = set() - joins = 0 - for i in open_indices: - if i in consumed: - continue - grid.remove_contour(i) - chain = contours[i].copy() - joins += _grow_tail(chain, contours, grid, tolerance, consumed) - chain.reverse_contour() - joins += _grow_tail(chain, contours, grid, tolerance, consumed) - chain.reverse_contour() - _close_if_near(chain, tolerance) - chains[i] = chain - - result = Geometry() - for i, contour in enumerate(contours): - if i in chains: - result.extend(chains[i]) - elif i not in consumed: - result.extend(contour) - return result, joins - - -def _sample_points(contour: Geometry) -> list[Point]: - """The contour's vertices plus evenly spaced points along it.""" - points = [(cmd.end[0], cmd.end[1]) for cmd in contour.data] - length = contour.distance() - if length > 0.0: - step = length / _DUPLICATE_SAMPLES - distances = [step * k for k in range(_DUPLICATE_SAMPLES + 1)] - for _idx, _t, p in contour.get_positions_at_distances(distances): - points.append((p[0], p[1])) - return points - - -def _covers(target: Geometry, points: list[Point], tolerance: float) -> bool: - for x, y in points: - hit = target.find_closest_point(x, y) - if hit is None or _distance((x, y), hit[2]) > tolerance: - return False - return True - - -def _rects_match(a: Geometry, b: Geometry, tolerance: float) -> bool: - return all(abs(va - vb) <= tolerance for va, vb in zip(a.rect(), b.rect())) - - -def _contours_match(a: Geometry, b: Geometry, tolerance: float) -> bool: - """ - True if two contours trace the same path within the tolerance, - regardless of their direction or start point. - """ - if _is_open(a) != _is_open(b): - return False - if not _rects_match(a, b, tolerance): - return False - return _covers(b, _sample_points(a), tolerance) and _covers( - a, _sample_points(b), tolerance - ) - - -def _length_slack(tolerance: float) -> float: - return 4.0 * tolerance + 1e-9 - - -class _CornerIndex: - """ - A spatial hash of the lower-left bounding box corner of kept - contours. Duplicates have corners within the tolerance, so only - neighbouring cells need to be compared. - """ - - def __init__(self, tolerance: float): - self._cell = max(tolerance, 1e-9) - self._cells: dict[tuple[int, int], list[int]] = {} - - def _key(self, x: float, y: float) -> tuple[int, int]: - return math.floor(x / self._cell), math.floor(y / self._cell) - - def add(self, index: int, rect: tuple[float, float, float, float]): - self._cells.setdefault(self._key(rect[0], rect[1]), []).append(index) - - def near(self, rect: tuple[float, float, float, float]) -> Iterator[int]: - kx, ky = self._key(rect[0], rect[1]) - for dx in (-1, 0, 1): - for dy in (-1, 0, 1): - yield from self._cells.get((kx + dx, ky + dy), ()) - - -def remove_duplicate_contours( - geo: Geometry, tolerance: float -) -> tuple[Geometry, int]: - """ - Removes contours that duplicate an earlier contour within the - tolerance, regardless of direction or start point. The first - occurrence is kept and the order of the remaining contours is - preserved. - - Args: - geo: The geometry to process. It is not modified. - tolerance: The largest distance between two matching paths. - - Returns: - A tuple of the new geometry and the number of removed contours. - """ - contours = _drawn_contours(geo) - slack = _length_slack(tolerance) - index = _CornerIndex(tolerance) - lengths: list[float] = [] - kept: list[Geometry] = [] - removed = 0 - for contour in contours: - rect = contour.rect() - length = contour.distance() - if any( - abs(lengths[k] - length) <= slack - and _contours_match(kept[k], contour, tolerance) - for k in index.near(rect) - ): - removed += 1 - continue - index.add(len(kept), rect) - lengths.append(length) - kept.append(contour) - return _concat(kept), removed - - -def geometries_match(a: Geometry, b: Geometry, tolerance: float) -> bool: - """ - True if both geometries consist of the same contours within the - tolerance, in any order, direction or start point. Empty geometries - never match. - """ - contours_a = _drawn_contours(a) - contours_b = _drawn_contours(b) - if not contours_a or len(contours_a) != len(contours_b): - return False - unmatched = list(contours_b) - for contour in contours_a: - found = next( - ( - other - for other in unmatched - if _contours_match(contour, other, tolerance) - ), - None, - ) - if found is None: - return False - unmatched.remove(found) - return True - - -def split_contours(geo: Geometry) -> list[Geometry]: - """ - Splits a geometry into one geometry per contour. Unlike splitting - into connected components, holes become parts of their own and open - contours are kept. - """ - return [c.copy() for c in _drawn_contours(geo)] diff --git a/rayforge/doceditor/path_cleanup_cmd.py b/rayforge/doceditor/path_cleanup_cmd.py index 2fc3ef152..1ca509333 100644 --- a/rayforge/doceditor/path_cleanup_cmd.py +++ b/rayforge/doceditor/path_cleanup_cmd.py @@ -6,12 +6,6 @@ from raygeo.geo import Geometry, Matrix from ..core.item import DocItem -from ..core.path_cleanup import ( - close_open_contours, - geometries_match, - join_open_contours, - remove_duplicate_contours, -) from ..core.undo import ChangePropertyCommand, ListItemCommand from ..core.workpiece import WorkPiece from .split_cmd import ContourSplitStrategy @@ -83,7 +77,10 @@ def close_paths( The number of closed contours. """ return self._apply( - workpieces, close_open_contours, tolerance_mm, _("Close paths") + workpieces, + Geometry.close_open_contours, + tolerance_mm, + _("Close paths"), ) def join_paths( @@ -96,7 +93,10 @@ def join_paths( The number of joins made. """ return self._apply( - workpieces, join_open_contours, tolerance_mm, _("Join paths") + workpieces, + Geometry.join_open_contours, + tolerance_mm, + _("Join paths"), ) def delete_duplicates( @@ -115,7 +115,7 @@ def delete_duplicates( duplicates = self._find_duplicate_workpieces(workpieces, tolerance_mm) remaining = [wp for wp in workpieces if wp not in duplicates] changes = self._compute_changes( - remaining, remove_duplicate_contours, tolerance_mm + remaining, Geometry.remove_duplicate_contours, tolerance_mm ) if not duplicates and not changes: return 0 @@ -223,7 +223,7 @@ def _find_duplicate_workpieces( continue if any( _rects_overlap(geo, other, tolerance_mm) - and geometries_match(geo, other, tolerance_mm) + and geo.matches(other, tolerance_mm) for _wp, other in kept ): duplicates.append(workpiece) diff --git a/rayforge/doceditor/split_cmd.py b/rayforge/doceditor/split_cmd.py index 03f4b9282..884df1af0 100644 --- a/rayforge/doceditor/split_cmd.py +++ b/rayforge/doceditor/split_cmd.py @@ -7,7 +7,6 @@ from raygeo.geo import Geometry from ..core.item import DocItem -from ..core.path_cleanup import split_contours from ..core.undo import ListItemCommand from ..core.workpiece import WorkPiece @@ -60,7 +59,7 @@ class ContourSplitStrategy(SplitStrategy): def calculate_fragments(self, workpiece: "WorkPiece") -> list[Geometry]: if not workpiece.boundaries or workpiece.boundaries.is_empty(): return [] - return split_contours(workpiece.boundaries) + return workpiece.boundaries.split_drawn_contours() class SplitCmd: diff --git a/tests/core/test_path_cleanup.py b/tests/core/test_path_cleanup.py deleted file mode 100644 index 9f01a9699..000000000 --- a/tests/core/test_path_cleanup.py +++ /dev/null @@ -1,328 +0,0 @@ -import math - -import pytest -from raygeo.geo import Geometry - -from rayforge.core.path_cleanup import ( - close_open_contours, - geometries_match, - join_open_contours, - remove_duplicate_contours, - split_contours, -) - - -def _polyline(points, close=False) -> Geometry: - geo = Geometry() - geo.move_to(*points[0]) - for p in points[1:]: - geo.line_to(*p) - if close: - geo.line_to(*points[0]) - return geo - - -def _concat(*geos: Geometry) -> Geometry: - result = Geometry() - for geo in geos: - result.extend(geo) - return result - - -def _contours(geo: Geometry) -> list[Geometry]: - return geo.split_into_contours() - - -def _start(geo: Geometry) -> tuple[float, float]: - first = next(iter(geo.iter_typed_commands())) - return first.end[0], first.end[1] - - -def _end(geo: Geometry) -> tuple[float, float]: - x, y, _z = geo.get_last_point() - return x, y - - -def _dist(a, b) -> float: - return math.hypot(a[0] - b[0], a[1] - b[1]) - - -SQUARE = [(0, 0), (10, 0), (10, 10), (0, 10)] - - -class TestCloseOpenContours: - def test_closes_contour_within_tolerance(self): - geo = _polyline(SQUARE + [(0, 0.05)]) - result, count = close_open_contours(geo, 0.1) - assert count == 1 - contours = _contours(result) - assert len(contours) == 1 - assert contours[0].is_closed() - - def test_leaves_contour_outside_tolerance_open(self): - geo = _polyline(SQUARE + [(0, 0.5)]) - result, count = close_open_contours(geo, 0.1) - assert count == 0 - assert not _contours(result)[0].is_closed() - - def test_does_not_move_existing_points(self): - geo = _polyline(SQUARE + [(0, 0.05)]) - result, _count = close_open_contours(geo, 0.1) - assert result.rect() == pytest.approx(geo.rect()) - - def test_already_closed_contour_is_untouched(self): - geo = _polyline(SQUARE, close=True) - result, count = close_open_contours(geo, 0.1) - assert count == 0 - assert len(result.data) == len(geo.data) - - def test_handles_each_contour_separately(self): - geo = _concat( - _polyline(SQUARE + [(0, 0.05)]), - _polyline([(20, 0), (30, 0), (30, 10), (20, 3)]), - _polyline([(40, 0), (50, 0), (50, 10), (40, 0.02)]), - ) - result, count = close_open_contours(geo, 0.1) - assert count == 2 - closed = [c.is_closed() for c in _contours(result)] - assert closed == [True, False, True] - - def test_ignores_degenerate_single_segment(self): - geo = _polyline([(0, 0), (0.05, 0)]) - _result, count = close_open_contours(geo, 0.1) - assert count == 0 - - def test_empty_geometry(self): - result, count = close_open_contours(Geometry(), 0.1) - assert count == 0 - assert result.is_empty() - - -class TestJoinOpenContours: - def test_joins_two_pieces_in_order(self): - geo = _concat( - _polyline([(0, 0), (10, 0), (10, 10)]), - _polyline([(10, 10.05), (0, 10), (0, 0.05)]), - ) - result, joins = join_open_contours(geo, 0.1) - assert joins == 1 - contours = _contours(result) - assert len(contours) == 1 - assert contours[0].is_closed() - - def test_joins_reversed_piece(self): - geo = _concat( - _polyline([(0, 0), (10, 0)]), - _polyline([(20, 0), (10.02, 0)]), - ) - result, joins = join_open_contours(geo, 0.1) - assert joins == 1 - contours = _contours(result) - assert len(contours) == 1 - ends = {_start(contours[0]), _end(contours[0])} - assert ends == {(0.0, 0.0), (20.0, 0.0)} - - def test_joins_pieces_out_of_order(self): - geo = _concat( - _polyline([(0, 0), (10, 0)]), - _polyline([(20, 0), (30, 0)]), - _polyline([(10, 0), (20, 0)]), - ) - result, joins = join_open_contours(geo, 0.1) - assert joins == 2 - contours = _contours(result) - assert len(contours) == 1 - assert result.distance() == pytest.approx(30.0) - - def test_joins_at_the_start_of_a_piece(self): - geo = _concat( - _polyline([(10, 0), (20, 0)]), - _polyline([(0, 0), (10, 0)]), - ) - result, joins = join_open_contours(geo, 0.1) - assert joins == 1 - contours = _contours(result) - assert len(contours) == 1 - assert {_start(contours[0]), _end(contours[0])} == { - (0.0, 0.0), - (20.0, 0.0), - } - - def test_keeps_pieces_apart_beyond_tolerance(self): - geo = _concat( - _polyline([(0, 0), (10, 0)]), - _polyline([(11, 0), (20, 0)]), - ) - result, joins = join_open_contours(geo, 0.1) - assert joins == 0 - assert len(_contours(result)) == 2 - - def test_leaves_closed_contours_alone(self): - square = _polyline(SQUARE, close=True) - line = _polyline([(0, 0), (-10, 0)]) - geo = _concat(square, line) - result, joins = join_open_contours(geo, 0.1) - assert joins == 0 - assert len(_contours(result)) == 2 - - def test_closes_single_piece_whose_ends_meet(self): - geo = _polyline(SQUARE + [(0, 0.05)]) - result, _joins = join_open_contours(geo, 0.1) - assert _contours(result)[0].is_closed() - - def test_bridge_does_not_change_bounds(self): - geo = _concat( - _polyline([(0, 0), (10, 0), (10, 10)]), - _polyline([(10.05, 10), (0, 10), (0, 0)]), - ) - result, _joins = join_open_contours(geo, 0.1) - assert result.rect() == pytest.approx(geo.rect()) - - def test_many_segments_form_one_closed_outline(self): - pts = [ - (10 * math.cos(a), 10 * math.sin(a)) - for a in (2 * math.pi * i / 60 for i in range(60)) - ] - pieces = [] - for i in range(60): - a, b = pts[i], pts[(i + 1) % 60] - if i % 2: - a, b = b, a - pieces.append(_polyline([a, b])) - geo = _concat(*reversed(pieces)) - result, joins = join_open_contours(geo, 0.01) - assert joins == 59 - contours = _contours(result) - assert len(contours) == 1 - assert contours[0].is_closed() - - -class TestRemoveDuplicateContours: - def test_removes_identical_copy(self): - geo = _concat( - _polyline(SQUARE, close=True), _polyline(SQUARE, close=True) - ) - result, removed = remove_duplicate_contours(geo, 0.01) - assert removed == 1 - assert len(_contours(result)) == 1 - - def test_removes_reversed_copy(self): - geo = _concat( - _polyline(SQUARE, close=True), - _polyline(list(reversed(SQUARE)), close=True), - ) - _result, removed = remove_duplicate_contours(geo, 0.01) - assert removed == 1 - - def test_removes_copy_with_other_start_point(self): - shifted = SQUARE[2:] + SQUARE[:2] - geo = _concat( - _polyline(SQUARE, close=True), _polyline(shifted, close=True) - ) - _result, removed = remove_duplicate_contours(geo, 0.01) - assert removed == 1 - - def test_removes_open_reversed_copy(self): - line = [(0, 0), (5, 5), (10, 0)] - geo = _concat(_polyline(line), _polyline(list(reversed(line)))) - _result, removed = remove_duplicate_contours(geo, 0.01) - assert removed == 1 - - def test_keeps_shapes_that_only_overlap_partly(self): - geo = _concat( - _polyline(SQUARE, close=True), - _polyline([(0, 0), (10, 0), (10, 10)]), - ) - result, removed = remove_duplicate_contours(geo, 0.01) - assert removed == 0 - assert len(_contours(result)) == 2 - - def test_keeps_nested_shapes(self): - inner = [(2, 2), (8, 2), (8, 8), (2, 8)] - geo = _concat( - _polyline(SQUARE, close=True), _polyline(inner, close=True) - ) - _result, removed = remove_duplicate_contours(geo, 0.01) - assert removed == 0 - - def test_tolerance_applies(self): - nudged = [(x + 0.005, y) for x, y in SQUARE] - geo = _concat( - _polyline(SQUARE, close=True), _polyline(nudged, close=True) - ) - assert remove_duplicate_contours(geo, 0.01)[1] == 1 - assert remove_duplicate_contours(geo, 0.001)[1] == 0 - - def test_keeps_first_occurrence_and_order(self): - other = [(20, 0), (30, 0), (30, 10)] - geo = _concat( - _polyline(SQUARE, close=True), - _polyline(other), - _polyline(SQUARE, close=True), - ) - result, removed = remove_duplicate_contours(geo, 0.01) - assert removed == 1 - contours = _contours(result) - assert _start(contours[0]) == (0.0, 0.0) - assert _start(contours[1]) == (20.0, 0.0) - - def test_removes_triple_copies(self): - geo = _concat(*(_polyline(SQUARE, close=True) for _ in range(3))) - _result, removed = remove_duplicate_contours(geo, 0.01) - assert removed == 2 - - def test_many_scattered_duplicates(self): - pieces = [] - for i in range(200): - x, y = (i * 37) % 290, (i * 53) % 290 - pieces.append(_polyline([(x, y), (x + 5, y)])) - copies = [ - _polyline(list(reversed([(x, y), (x + 5, y)]))) - for x, y in (((i * 37) % 290, (i * 53) % 290) for i in range(200)) - ] - geo = _concat(*pieces, *copies) - result, removed = remove_duplicate_contours(geo, 0.01) - assert removed == 200 - assert len(_contours(result)) == 200 - - -class TestGeometriesMatch: - def test_same_shapes_in_other_order_match(self): - a = _concat( - _polyline(SQUARE, close=True), _polyline([(20, 0), (30, 0)]) - ) - b = _concat( - _polyline([(30, 0), (20, 0)]), _polyline(SQUARE, close=True) - ) - assert geometries_match(a, b, 0.01) - - def test_different_shapes_do_not_match(self): - a = _polyline(SQUARE, close=True) - b = _polyline([(0, 0), (10, 0), (10, 11), (0, 10)], close=True) - assert not geometries_match(a, b, 0.01) - - def test_extra_contour_does_not_match(self): - a = _polyline(SQUARE, close=True) - b = _concat( - _polyline(SQUARE, close=True), _polyline([(20, 0), (30, 0)]) - ) - assert not geometries_match(a, b, 0.01) - - def test_empty_geometries_do_not_match(self): - assert not geometries_match(Geometry(), Geometry(), 0.01) - - -class TestSplitContours: - def test_splits_every_contour_including_holes_and_open_paths(self): - inner = [(2, 2), (8, 2), (8, 8), (2, 8)] - geo = _concat( - _polyline(SQUARE, close=True), - _polyline(inner, close=True), - _polyline([(20, 0), (30, 0)]), - ) - parts = split_contours(geo) - assert len(parts) == 3 - assert parts[2].distance() == pytest.approx(10.0) - - def test_single_contour(self): - assert len(split_contours(_polyline(SQUARE, close=True))) == 1 From 8ca6a0a674688d279148d41e4f75daa39485bc3b Mon Sep 17 00:00:00 2001 From: Samuel Abels Date: Sun, 11 Oct 2026 22:20:39 +0200 Subject: [PATCH 5/8] chore(deps): bump raygeo to 1.63.0 raygeo 1.63.0 ships the path clean-up operations this PR calls (close_open_contours, join_open_contours, remove_duplicate_contours, matches, split_drawn_contours; raygeo#56), so the pins can move off 1.62.0, which predates them. --- debian/requirements-bundle.txt | 2 +- pixi.lock | 68 +++++++++++++++++----------------- pixi.toml | 2 +- requirements.txt | 2 +- 4 files changed, 37 insertions(+), 37 deletions(-) diff --git a/debian/requirements-bundle.txt b/debian/requirements-bundle.txt index 0d43efc59..3129f9356 100644 --- a/debian/requirements-bundle.txt +++ b/debian/requirements-bundle.txt @@ -1,7 +1,7 @@ asyncudp==0.11.0 pyvips==3.2.0 raydriver==0.3.0 -raygeo==1.62.0 +raygeo==1.63.0 ruida-pa==0.21.2 trimesh==5.1.1 vtracer==0.6.15 \ No newline at end of file diff --git a/pixi.lock b/pixi.lock index 7390f5939..5c83c311b 100644 --- a/pixi.lock +++ b/pixi.lock @@ -247,11 +247,11 @@ environments: - pypi: https://files.pythonhosted.org/packages/13/d4/1152d1c7ab42d8b908be64fd200ddc870dc9d4925e951198702084aa1a7d/linkify_it_py-2.2.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/16/67/1f0b582e73ce46511f2799cbdcdbdf9ca98f344f2aad42ff5ff5dde0d8fe/narwhals-2.27.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/17/fe/77f4a24264e728fd95542e53f026a91249b51d1611087b8c82d3a033ea97/asyncudp-0.11.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/22/35/285ce45186a60572472c96df5646989ca97710e060419e5c749226a057cf/raygeo-1.63.0.tar.gz - pypi: https://files.pythonhosted.org/packages/31/c4/e54512d343c4c177e92c45ec16852abe63945b924274c692a5a1867db011/zeroconf-0.151.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/38/bb/d215ee7c73b61497b28a5503f9f53523f294fcc936762b7caf90e0c1c2b5/pyparsing-3.3.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/3a/cf/9c2e4d6c179529d5bf4a64cff76fa581486569e9fbdd35bd98f51cb624bf/msgpack-1.2.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/3c/2c/c43c03eaf630435f023f1dc61ec4a4a78951ad5530a62c71cc89bde307b7/pypdf-6.19.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/3d/de/02f631997741485881fbe129aef2de75a23b4e9776f5b1c4a7afc313ae7e/raygeo-1.62.0.tar.gz - pypi: https://files.pythonhosted.org/packages/3f/99/2e119d541d596daea39643eb9312b47c7847383951300f889166938035b1/rpyc-6.0.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/41/51/8041c2e70649e5b7f2a0aedbbbd0609ac099cfaa0cbde2014279c9c05756/cairosvg-2.9.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/4f/a7/aeedb5140afa41fc74c225e9184ab96723a6e873b6ee1c9fede7283456d8/markupsafe-3.0.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl @@ -480,7 +480,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/13/d4/1152d1c7ab42d8b908be64fd200ddc870dc9d4925e951198702084aa1a7d/linkify_it_py-2.2.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/16/67/1f0b582e73ce46511f2799cbdcdbdf9ca98f344f2aad42ff5ff5dde0d8fe/narwhals-2.27.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/17/fe/77f4a24264e728fd95542e53f026a91249b51d1611087b8c82d3a033ea97/asyncudp-0.11.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/20/a5/1d1baffeb743cbdfff98c180473a9c81bbabe433dea5a72761d4c1edf1ef/raygeo-1.62.0-cp311-abi3-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/38/bb/d215ee7c73b61497b28a5503f9f53523f294fcc936762b7caf90e0c1c2b5/pyparsing-3.3.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/3c/2c/c43c03eaf630435f023f1dc61ec4a4a78951ad5530a62c71cc89bde307b7/pypdf-6.19.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/3f/99/2e119d541d596daea39643eb9312b47c7847383951300f889166938035b1/rpyc-6.0.2-py3-none-any.whl @@ -530,6 +529,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/fa/48/f36d233ce749d1bb880cb5e6f8bb171dc2effabc7435c31da604d831b3ec/gitpython-3.2.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/fb/be/35261223d9416a0751cdff1c7b4a6f881387218a12d439fe22fefebc8c04/textual-8.2.8-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fc/4b/7cf8a68f8865e04e036a5b6847276f97075157ca2360048ba7fd60cd3b88/raygeo-1.63.0-cp311-abi3-macosx_11_0_arm64.whl build: channels: - url: https://conda.anaconda.org/conda-forge/ @@ -928,12 +928,12 @@ environments: - pypi: https://files.pythonhosted.org/packages/16/67/1f0b582e73ce46511f2799cbdcdbdf9ca98f344f2aad42ff5ff5dde0d8fe/narwhals-2.27.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/16/7d/e9ffbadfbf89c93848412d04594135c4ae8c1d37d9e053b9c3ed718fabc4/python_discovery-1.6.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/17/fe/77f4a24264e728fd95542e53f026a91249b51d1611087b8c82d3a033ea97/asyncudp-0.11.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/22/35/285ce45186a60572472c96df5646989ca97710e060419e5c749226a057cf/raygeo-1.63.0.tar.gz - pypi: https://files.pythonhosted.org/packages/27/1a/1f68f9ba0c207934b35b86a8ca3aad8395a3d6dd7921c0686e23853ff5a9/mccabe-0.7.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/31/c4/e54512d343c4c177e92c45ec16852abe63945b924274c692a5a1867db011/zeroconf-0.151.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/38/bb/d215ee7c73b61497b28a5503f9f53523f294fcc936762b7caf90e0c1c2b5/pyparsing-3.3.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/3a/cf/9c2e4d6c179529d5bf4a64cff76fa581486569e9fbdd35bd98f51cb624bf/msgpack-1.2.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/3c/2c/c43c03eaf630435f023f1dc61ec4a4a78951ad5530a62c71cc89bde307b7/pypdf-6.19.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/3d/de/02f631997741485881fbe129aef2de75a23b4e9776f5b1c4a7afc313ae7e/raygeo-1.62.0.tar.gz - pypi: https://files.pythonhosted.org/packages/3f/99/2e119d541d596daea39643eb9312b47c7847383951300f889166938035b1/rpyc-6.0.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/41/51/8041c2e70649e5b7f2a0aedbbbd0609ac099cfaa0cbde2014279c9c05756/cairosvg-2.9.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/45/e2/bbb7129c9e7999a6b8ee9cca3b66486c25c423ab5a75f34071798b74ce94/pre_commit-4.6.2-py2.py3-none-any.whl @@ -1188,7 +1188,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/16/67/1f0b582e73ce46511f2799cbdcdbdf9ca98f344f2aad42ff5ff5dde0d8fe/narwhals-2.27.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/16/7d/e9ffbadfbf89c93848412d04594135c4ae8c1d37d9e053b9c3ed718fabc4/python_discovery-1.6.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/17/fe/77f4a24264e728fd95542e53f026a91249b51d1611087b8c82d3a033ea97/asyncudp-0.11.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/20/a5/1d1baffeb743cbdfff98c180473a9c81bbabe433dea5a72761d4c1edf1ef/raygeo-1.62.0-cp311-abi3-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/27/1a/1f68f9ba0c207934b35b86a8ca3aad8395a3d6dd7921c0686e23853ff5a9/mccabe-0.7.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/38/bb/d215ee7c73b61497b28a5503f9f53523f294fcc936762b7caf90e0c1c2b5/pyparsing-3.3.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/3c/2c/c43c03eaf630435f023f1dc61ec4a4a78951ad5530a62c71cc89bde307b7/pypdf-6.19.0-py3-none-any.whl @@ -1254,6 +1253,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/fa/70/fffc9613501877c0a10a1ef73a165e6ded3e53f6e7502227fef56d973933/identify-2.6.20-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/fb/be/35261223d9416a0751cdff1c7b4a6f881387218a12d439fe22fefebc8c04/textual-8.2.8-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fc/4b/7cf8a68f8865e04e036a5b6847276f97075157ca2360048ba7fd60cd3b88/raygeo-1.63.0-cp311-abi3-macosx_11_0_arm64.whl video: channels: - url: https://conda.anaconda.org/conda-forge/ @@ -9647,7 +9647,7 @@ packages: name: rayforge requires_dist: - raydriver==0.3.0 - - raygeo==1.62.0 + - raygeo==1.63.0 - aiohttp==3.14.3 - asyncudp==0.11.0 - blinker==1.9.0 @@ -9820,25 +9820,25 @@ packages: name: asyncudp version: 0.11.0 sha256: 96d859d86471e3ed30cf9f05cb38e96920156e3a9fe70ee916096ba758ea316e -- pypi: https://files.pythonhosted.org/packages/20/a5/1d1baffeb743cbdfff98c180473a9c81bbabe433dea5a72761d4c1edf1ef/raygeo-1.62.0-cp311-abi3-macosx_11_0_arm64.whl +- pypi: https://files.pythonhosted.org/packages/22/35/285ce45186a60572472c96df5646989ca97710e060419e5c749226a057cf/raygeo-1.63.0.tar.gz name: raygeo - version: 1.62.0 - sha256: 094187c356b390e729a8fa7287b97f7f657b26a280cf2014c6726e944786a4a3 + version: 1.63.0 + sha256: 6a411c28e7aecea81fa7ccb3d34c652ed1ff4bf1fa677a09323fdd00988a77c8 requires_dist: - msgpack - numpy>=1.20.0 - - matplotlib ; extra == 'cli' - - matplotlib ; extra == 'docs' - - mdformat ; extra == 'docs' - - mdformat-frontmatter ; extra == 'docs' - - mdformat-tables ; extra == 'docs' - - pillow ; extra == 'docs' - pycairo ; extra == 'test' - pytest ; extra == 'test' - pytest-mock ; extra == 'test' - streamlit ; extra == 'test' - streamlit ; extra == 'visual' - matplotlib ; extra == 'visual' + - matplotlib ; extra == 'docs' + - mdformat ; extra == 'docs' + - mdformat-frontmatter ; extra == 'docs' + - mdformat-tables ; extra == 'docs' + - pillow ; extra == 'docs' + - matplotlib ; extra == 'cli' requires_python: '>=3.10' - pypi: https://files.pythonhosted.org/packages/27/1a/1f68f9ba0c207934b35b86a8ca3aad8395a3d6dd7921c0686e23853ff5a9/mccabe-0.7.0-py2.py3-none-any.whl name: mccabe @@ -9894,26 +9894,6 @@ packages: - arabic-reshaper ; extra == 'rtl-text' - python-bidi ; extra == 'rtl-text' requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/3d/de/02f631997741485881fbe129aef2de75a23b4e9776f5b1c4a7afc313ae7e/raygeo-1.62.0.tar.gz - name: raygeo - version: 1.62.0 - sha256: 45b319ba805362aaa288532a0f0b0394b0898f39724c9b6a7ae378c463ed2346 - requires_dist: - - msgpack - - numpy>=1.20.0 - - pycairo ; extra == 'test' - - pytest ; extra == 'test' - - pytest-mock ; extra == 'test' - - streamlit ; extra == 'test' - - streamlit ; extra == 'visual' - - matplotlib ; extra == 'visual' - - matplotlib ; extra == 'docs' - - mdformat ; extra == 'docs' - - mdformat-frontmatter ; extra == 'docs' - - mdformat-tables ; extra == 'docs' - - pillow ; extra == 'docs' - - matplotlib ; extra == 'cli' - requires_python: '>=3.10' - pypi: https://files.pythonhosted.org/packages/3f/99/2e119d541d596daea39643eb9312b47c7847383951300f889166938035b1/rpyc-6.0.2-py3-none-any.whl name: rpyc version: 6.0.2 @@ -10801,6 +10781,26 @@ packages: - tree-sitter-yaml>=0.6.0 ; python_full_version >= '3.10' and extra == 'syntax' - typing-extensions>=4.4.0,<5.0.0 requires_python: '>=3.9,<4.0' +- pypi: https://files.pythonhosted.org/packages/fc/4b/7cf8a68f8865e04e036a5b6847276f97075157ca2360048ba7fd60cd3b88/raygeo-1.63.0-cp311-abi3-macosx_11_0_arm64.whl + name: raygeo + version: 1.63.0 + sha256: 80cc04f6a7c723c4c1a6b2cf5aee1491fc99a41ad499c7100623cbbe2cd96022 + requires_dist: + - msgpack + - numpy>=1.20.0 + - matplotlib ; extra == 'cli' + - matplotlib ; extra == 'docs' + - mdformat ; extra == 'docs' + - mdformat-frontmatter ; extra == 'docs' + - mdformat-tables ; extra == 'docs' + - pillow ; extra == 'docs' + - pycairo ; extra == 'test' + - pytest ; extra == 'test' + - pytest-mock ; extra == 'test' + - streamlit ; extra == 'test' + - streamlit ; extra == 'visual' + - matplotlib ; extra == 'visual' + requires_python: '>=3.10' - pypi: https://files.pythonhosted.org/packages/ff/72/b12534f2d335448bc98504610c8d57520e530dab840251871a72cbb6c227/vtracer-0.6.15-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl name: vtracer version: 0.6.15 diff --git a/pixi.toml b/pixi.toml index b9c8738b2..750d0a29c 100644 --- a/pixi.toml +++ b/pixi.toml @@ -38,7 +38,7 @@ rust = "==1.98.1" patchelf = "==0.19.2" [feature.app.pypi-dependencies] -raygeo = "==1.62.0" +raygeo = "==1.63.0" raydriver = "==0.3.0" rayforge = { editable = true, path = "." } ruida-pa = "==0.22.0" diff --git a/requirements.txt b/requirements.txt index b46963b07..db6e926a0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ raydriver==0.3.0 -raygeo==1.62.0 +raygeo==1.63.0 aiohttp==3.14.3 asyncudp==0.11.0 blinker==1.9.0 From ef13caa896aad1e6ffa9064598723ff5b4e8e1c6 Mon Sep 17 00:00:00 2001 From: Samuel Abels Date: Sun, 11 Oct 2026 23:21:09 +0200 Subject: [PATCH 6/8] fix(ui): keep the tolerance row title on one line The Close/Join Paths dialog rendered at Adw.AlertDialog's default content width of 300 px, which wrapped the spin row's title mid-word and stacked the response buttons. Give the preferences group a minimum width so the title and buttons fit. --- rayforge/ui_gtk/doceditor/path_tolerance_dialog.py | 1 + 1 file changed, 1 insertion(+) diff --git a/rayforge/ui_gtk/doceditor/path_tolerance_dialog.py b/rayforge/ui_gtk/doceditor/path_tolerance_dialog.py index 4673e6f35..b19491f6d 100644 --- a/rayforge/ui_gtk/doceditor/path_tolerance_dialog.py +++ b/rayforge/ui_gtk/doceditor/path_tolerance_dialog.py @@ -53,6 +53,7 @@ def __init__( ) group = Adw.PreferencesGroup() group.add(self.tolerance_row) + group.set_size_request(430, -1) self.set_extra_child(group) self.connect("response", self._on_response) From f3aaef07c7442861476a0298a80ed5990e9abfab Mon Sep 17 00:00:00 2001 From: Samuel Abels Date: Sun, 11 Oct 2026 23:22:02 +0200 Subject: [PATCH 7/8] feat(screenshot): add main:path-cleanup target Captures the Close Paths dialog over a selected vector workpiece for the path clean-up documentation. --- scripts/screenshot/cli.py | 1 + scripts/screenshot/path_cleanup.py | 96 ++++++++++++++++++ .../static/screenshots/main-path-cleanup.webp | Bin 0 -> 161480 bytes 3 files changed, 97 insertions(+) create mode 100644 scripts/screenshot/path_cleanup.py create mode 100644 website/static/screenshots/main-path-cleanup.webp diff --git a/scripts/screenshot/cli.py b/scripts/screenshot/cli.py index 880a53fd8..d134be3cf 100755 --- a/scripts/screenshot/cli.py +++ b/scripts/screenshot/cli.py @@ -82,6 +82,7 @@ "main:array:grid": "array_grid", "main:array:point-rotation": "array_point_rotation", "main:array:circular": "array_circular", + "main:path-cleanup": "path_cleanup", "addons:sketcher:conflicts": "sketcher_constraints", "addons:sketcher:constraints": "sketcher_editor", "addons:sketcher:constraint:angle": "sketcher_constraints", diff --git a/scripts/screenshot/path_cleanup.py b/scripts/screenshot/path_cleanup.py new file mode 100644 index 000000000..0e226ff0a --- /dev/null +++ b/scripts/screenshot/path_cleanup.py @@ -0,0 +1,96 @@ +""" +Screenshot: Close Paths tolerance dialog with a vector workpiece selected. + +Usage: pixi run screenshot main:path-cleanup +""" + +import logging +import time +from gettext import gettext as _ + +from utils import ( + clear_window_subtitle, + load_project, + restore_config, + run_on_main_thread, + set_window_size, + take_window_screenshot, + wait_for_settled, +) + +from rayforge.uiscript import app, win + +logger = logging.getLogger(__name__) + + +def _select_all_items(): + """Select all content items on the active layer.""" + win.surface.update_from_doc() + layer = win.doc_editor.doc.active_layer + items = layer.get_content_items() + if items: + win.surface.select_items(items) + return items + + +def _open_close_paths_dialog(): + from rayforge.ui_gtk.doceditor.path_tolerance_dialog import ( + PathToleranceDialog, + ) + + def _apply(tolerance: float): + pass + + dialog = PathToleranceDialog( + heading=_("Close Paths"), + body=_( + "Closes open paths whose start and end point are closer " + "than the tolerance." + ), + apply_label=_("Close Paths"), + key="close-paths", + on_apply=_apply, + ) + dialog.present(win) + return dialog + + +@restore_config +def main(): + set_window_size(win, 2000, 1400) + + load_project(win, "contour.ryp") + logger.info("Waiting for document to settle...") + if not wait_for_settled(win, timeout=10): + logger.error("Document did not settle in time") + app.quit_idle() + return + + logger.info("Document settled") + + items = run_on_main_thread(_select_all_items) + if not items: + logger.error("No items found in document") + app.quit_idle() + return + + logger.info(f"Selected {len(items)} items") + + dialog = run_on_main_thread(_open_close_paths_dialog) + time.sleep(0.5) + + clear_window_subtitle(win) + + logger.info("Taking screenshot: main-path-cleanup.webp") + take_window_screenshot(win) + + time.sleep(0.25) + + def close_dialog(): + dialog.close() + + run_on_main_thread(close_dialog) + app.quit_idle() + + +main() diff --git a/website/static/screenshots/main-path-cleanup.webp b/website/static/screenshots/main-path-cleanup.webp new file mode 100644 index 0000000000000000000000000000000000000000..85a6a0610724b58921a1380e7540de1dedff7cb9 GIT binary patch literal 161480 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 Date: Sun, 11 Oct 2026 23:22:46 +0200 Subject: [PATCH 8/8] docs: fix internal links and embed the Close Paths dialog Use relative file paths ending in .md for the internal links in the path clean-up docs and the canvas-tools cross-reference, matching the convention from 8043b6593 (.md-less links broke client-side navigation), in English and all translations. Each language's page also embeds the new screenshot. --- website/docs/features/path-cleanup.md | 12 +++++++----- website/docs/ui/canvas-tools.md | 2 +- .../current/features/path-cleanup.md | 13 ++++++++----- .../current/ui/canvas-tools.md | 2 +- .../current/features/path-cleanup.md | 12 +++++++----- .../current/ui/canvas-tools.md | 2 +- .../current/features/path-cleanup.md | 13 ++++++++----- .../current/ui/canvas-tools.md | 2 +- .../current/features/path-cleanup.md | 12 +++++++----- .../current/ui/canvas-tools.md | 2 +- .../current/features/path-cleanup.md | 12 +++++++----- .../current/ui/canvas-tools.md | 2 +- .../current/features/path-cleanup.md | 12 +++++++----- .../current/ui/canvas-tools.md | 2 +- .../current/features/path-cleanup.md | 12 +++++++----- .../current/ui/canvas-tools.md | 2 +- 16 files changed, 66 insertions(+), 48 deletions(-) diff --git a/website/docs/features/path-cleanup.md b/website/docs/features/path-cleanup.md index 2286ff963..2468c09d6 100644 --- a/website/docs/features/path-cleanup.md +++ b/website/docs/features/path-cleanup.md @@ -14,6 +14,8 @@ closed is treated as open, and a doubled path is cut twice. The path clean-up tools fix this on the selected workpieces. You find them in the **Object** menu and under **Clean Up Paths** in the canvas context menu. Each tool is a single undo step. +![The Close Paths dialog](/screenshots/main-path-cleanup.webp) + ## Close Paths **Close Paths…** closes every open path whose start and end point are closer than the tolerance you @@ -36,7 +38,7 @@ regardless of their direction or start point. When you select several workpieces is an exact copy of another selected workpiece at the same position is removed too. Overlapping paths that only share part of their length are left alone. The -[Merge Lines](merge-lines) post-processor takes care of those when the job is generated. +[Merge Lines](./merge-lines.md) post-processor takes care of those when the job is generated. ## Break Apart @@ -44,10 +46,10 @@ Overlapping paths that only share part of their length are left alone. The included. This differs from **Split**, which keeps an island together with its holes. Path clean-up only works on imported and traced workpieces. Workpieces that come from a sketch are -edited in the [Sketcher](sketcher/index) instead. +edited in the [Sketcher](./sketcher/index.md) instead. ## Related Pages -- [Merge Lines](merge-lines) - Cutting overlapping segments only once -- [Canvas Tools](../ui/canvas-tools) - Selecting and deleting individual segments -- [Importing Files](../files/importing) - Bringing designs into Rayforge +- [Merge Lines](./merge-lines.md) - Cutting overlapping segments only once +- [Canvas Tools](../ui/canvas-tools.md) - Selecting and deleting individual segments +- [Importing Files](../files/importing.md) - Bringing designs into Rayforge diff --git a/website/docs/ui/canvas-tools.md b/website/docs/ui/canvas-tools.md index 73bf633c3..5507b3afa 100644 --- a/website/docs/ui/canvas-tools.md +++ b/website/docs/ui/canvas-tools.md @@ -108,7 +108,7 @@ and delete individual vector segments of the workpiece. To exit vector editing mode, click outside the workpiece or press Escape. To close gaps, join open pieces, delete duplicate paths or break a workpiece apart, use the -[Path Clean-Up](../features/path-cleanup) tools. +[Path Clean-Up](../features/path-cleanup.md) tools. --- diff --git a/website/i18n/de/docusaurus-plugin-content-docs/current/features/path-cleanup.md b/website/i18n/de/docusaurus-plugin-content-docs/current/features/path-cleanup.md index 8bfeff794..20585be1c 100644 --- a/website/i18n/de/docusaurus-plugin-content-docs/current/features/path-cleanup.md +++ b/website/i18n/de/docusaurus-plugin-content-docs/current/features/path-cleanup.md @@ -10,6 +10,8 @@ Die Werkzeuge zum Bereinigen von Pfaden beheben das bei den ausgewählten Werkst im Menü **Objekt** und im Kontextmenü der Arbeitsfläche unter **Pfade bereinigen**. Jedes Werkzeug ist ein einzelner Rückgängig-Schritt. +![Der Dialog „Pfade schließen“](/screenshots/main-path-cleanup.webp) + ## Pfade schließen **Pfade schließen…** schließt jeden offenen Pfad, dessen Anfangs- und Endpunkt näher beieinander @@ -34,7 +36,8 @@ ein Werkstück entfernt, das eine exakte Kopie eines anderen ausgewählten Werks Position ist. Überlappende Pfade, die nur einen Teil ihrer Länge gemeinsam haben, bleiben erhalten. Um diese -kümmert sich der Postprozessor [Linien zusammenführen](merge-lines), wenn der Auftrag erzeugt wird. +kümmert sich der Postprozessor [Linien zusammenführen](./merge-lines.md), wenn der Auftrag erzeugt +wird. ## Zerlegen @@ -42,10 +45,10 @@ kümmert sich der Postprozessor [Linien zusammenführen](merge-lines), wenn der offener Pfade. Das unterscheidet sich von **Teilen**, das eine Insel mit ihren Löchern zusammenhält. Das Bereinigen von Pfaden funktioniert nur bei importierten und nachgezeichneten Werkstücken. -Werkstücke aus einer Skizze bearbeitest du stattdessen im [Sketcher](sketcher/index). +Werkstücke aus einer Skizze bearbeitest du stattdessen im [Sketcher](./sketcher/index.md). ## Verwandte Seiten -- [Linien zusammenführen](merge-lines) - Überlappende Segmente nur einmal schneiden -- [Canvas-Werkzeuge](../ui/canvas-tools) - Einzelne Segmente auswählen und löschen -- [Dateien importieren](../files/importing) - Designs in Rayforge laden +- [Linien zusammenführen](./merge-lines.md) - Überlappende Segmente nur einmal schneiden +- [Canvas-Werkzeuge](../ui/canvas-tools.md) - Einzelne Segmente auswählen und löschen +- [Dateien importieren](../files/importing.md) - Designs in Rayforge laden diff --git a/website/i18n/de/docusaurus-plugin-content-docs/current/ui/canvas-tools.md b/website/i18n/de/docusaurus-plugin-content-docs/current/ui/canvas-tools.md index 4f725fa94..467296171 100644 --- a/website/i18n/de/docusaurus-plugin-content-docs/current/ui/canvas-tools.md +++ b/website/i18n/de/docusaurus-plugin-content-docs/current/ui/canvas-tools.md @@ -167,7 +167,7 @@ Um den Vektorbearbeitungsmodus zu verlassen, klicke außerhalb des Werkstücks o Escape. Um Lücken zu schließen, offene Teile zu verbinden, doppelte Pfade zu löschen oder ein Werkstück zu -zerlegen, verwende die Werkzeuge unter [Pfade bereinigen](../features/path-cleanup). +zerlegen, verwende die Werkzeuge unter [Pfade bereinigen](../features/path-cleanup.md). --- diff --git a/website/i18n/es/docusaurus-plugin-content-docs/current/features/path-cleanup.md b/website/i18n/es/docusaurus-plugin-content-docs/current/features/path-cleanup.md index 5dada85f2..d1fcbad2c 100644 --- a/website/i18n/es/docusaurus-plugin-content-docs/current/features/path-cleanup.md +++ b/website/i18n/es/docusaurus-plugin-content-docs/current/features/path-cleanup.md @@ -10,6 +10,8 @@ Las herramientas de limpieza de rutas corrigen esto en las piezas de trabajo sel encontrarás en el menú **Objeto** y en **Limpiar rutas** del menú contextual del lienzo. Cada herramienta es un solo paso de deshacer. +![El diálogo de cerrar rutas](/screenshots/main-path-cleanup.webp) + ## Cerrar rutas **Cerrar rutas…** cierra cada ruta abierta cuyo punto inicial y final están más cerca que la @@ -33,7 +35,7 @@ de trabajo, sin importar su dirección o punto inicial. Si seleccionas varias pi también se quita una pieza que sea una copia exacta de otra pieza seleccionada en la misma posición. Las rutas que solo se superponen en parte de su longitud se mantienen. De ellas se encarga el -posprocesador [Fusionar líneas](merge-lines) al generar el trabajo. +posprocesador [Fusionar líneas](./merge-lines.md) al generar el trabajo. ## Separar @@ -41,10 +43,10 @@ posprocesador [Fusionar líneas](merge-lines) al generar el trabajo. y las rutas abiertas. Esto es distinto de **Dividir**, que mantiene una isla junto con sus agujeros. La limpieza de rutas solo funciona en piezas de trabajo importadas y trazadas. Las piezas que vienen -de un boceto se editan en el [Sketcher](sketcher/index). +de un boceto se editan en el [Sketcher](./sketcher/index.md). ## Páginas relacionadas -- [Fusionar líneas](merge-lines) - Cortar los segmentos superpuestos una sola vez -- [Herramientas del lienzo](../ui/canvas-tools) - Seleccionar y eliminar segmentos individuales -- [Importar archivos](../files/importing) - Llevar diseños a Rayforge +- [Fusionar líneas](./merge-lines.md) - Cortar los segmentos superpuestos una sola vez +- [Herramientas del lienzo](../ui/canvas-tools.md) - Seleccionar y eliminar segmentos individuales +- [Importar archivos](../files/importing.md) - Llevar diseños a Rayforge diff --git a/website/i18n/es/docusaurus-plugin-content-docs/current/ui/canvas-tools.md b/website/i18n/es/docusaurus-plugin-content-docs/current/ui/canvas-tools.md index 45b9a3a55..5e5b97d34 100644 --- a/website/i18n/es/docusaurus-plugin-content-docs/current/ui/canvas-tools.md +++ b/website/i18n/es/docusaurus-plugin-content-docs/current/ui/canvas-tools.md @@ -167,7 +167,7 @@ Para salir del modo de edición de vectores, haz clic fuera de la pieza de traba Escape. Para cerrar huecos, unir piezas abiertas, eliminar rutas duplicadas o separar una pieza de trabajo, -usa las herramientas de [Limpieza de rutas](../features/path-cleanup). +usa las herramientas de [Limpieza de rutas](../features/path-cleanup.md). --- diff --git a/website/i18n/fr/docusaurus-plugin-content-docs/current/features/path-cleanup.md b/website/i18n/fr/docusaurus-plugin-content-docs/current/features/path-cleanup.md index 4ba9f6cd9..ba547d4f6 100644 --- a/website/i18n/fr/docusaurus-plugin-content-docs/current/features/path-cleanup.md +++ b/website/i18n/fr/docusaurus-plugin-content-docs/current/features/path-cleanup.md @@ -10,6 +10,8 @@ Les outils de nettoyage des chemins corrigent cela sur les pièces sélectionné dans le menu **Objet** et sous **Nettoyer les chemins** dans le menu contextuel du canevas. Chaque outil correspond à une seule étape d'annulation. +![La boîte de dialogue Fermer les chemins](/screenshots/main-path-cleanup.webp) + ## Fermer les chemins **Fermer les chemins…** ferme chaque chemin ouvert dont le point de départ et le point d'arrivée @@ -34,7 +36,8 @@ pièces, une pièce qui est la copie exacte d'une autre pièce sélectionnée à également retirée. Les chemins qui ne se chevauchent que sur une partie de leur longueur sont conservés. Le -post-processeur [Fusionner les lignes](merge-lines) s'en charge lors de la génération de la tâche. +post-processeur [Fusionner les lignes](./merge-lines.md) s'en charge lors de la génération de la +tâche. ## Décomposer @@ -42,10 +45,10 @@ post-processeur [Fusionner les lignes](merge-lines) s'en charge lors de la gén compris. C'est différent de **Scinder**, qui garde un îlot avec ses trous. Le nettoyage des chemins ne fonctionne que sur les pièces importées et vectorisées. Les pièces -issues d'une esquisse se modifient dans le [Sketcher](sketcher/index). +issues d'une esquisse se modifient dans le [Sketcher](./sketcher/index.md). ## Pages connexes -- [Fusionner les lignes](merge-lines) - Ne découper qu'une fois les segments qui se chevauchent -- [Outils du canevas](../ui/canvas-tools) - Sélectionner et supprimer des segments individuels -- [Importer des fichiers](../files/importing) - Importer des dessins dans Rayforge +- [Fusionner les lignes](./merge-lines.md) - Ne découper qu'une fois les segments qui se chevauchent +- [Outils du canevas](../ui/canvas-tools.md) - Sélectionner et supprimer des segments individuels +- [Importer des fichiers](../files/importing.md) - Importer des dessins dans Rayforge diff --git a/website/i18n/fr/docusaurus-plugin-content-docs/current/ui/canvas-tools.md b/website/i18n/fr/docusaurus-plugin-content-docs/current/ui/canvas-tools.md index 4fc699ba7..bca5b7210 100644 --- a/website/i18n/fr/docusaurus-plugin-content-docs/current/ui/canvas-tools.md +++ b/website/i18n/fr/docusaurus-plugin-content-docs/current/ui/canvas-tools.md @@ -169,7 +169,7 @@ Pour quitter le mode d'édition vectorielle, cliquez en dehors de la pièce ou a Échap. Pour fermer des écarts, joindre des morceaux ouverts, supprimer des chemins en double ou décomposer -une pièce, utilisez les outils de [Nettoyage des chemins](../features/path-cleanup). +une pièce, utilisez les outils de [Nettoyage des chemins](../features/path-cleanup.md). --- diff --git a/website/i18n/hi/docusaurus-plugin-content-docs/current/features/path-cleanup.md b/website/i18n/hi/docusaurus-plugin-content-docs/current/features/path-cleanup.md index dae47055a..803b48b45 100644 --- a/website/i18n/hi/docusaurus-plugin-content-docs/current/features/path-cleanup.md +++ b/website/i18n/hi/docusaurus-plugin-content-docs/current/features/path-cleanup.md @@ -8,6 +8,8 @@ पथ सफ़ाई टूल चुने गए वर्कपीस पर इसे ठीक करते हैं. ये **ऑब्जेक्ट** मेनू में और कैनवस के संदर्भ मेनू में **पथ साफ़ करें** के अंतर्गत मिलते हैं. हर टूल एक ही पूर्ववत चरण है. +![पथ बंद करें संवाद](/screenshots/main-path-cleanup.webp) + ## पथ बंद करें **पथ बंद करें…** हर उस खुले पथ को बंद करता है जिसके आरंभ और अंत बिंदु आपकी दी गई सहनशीलता से अधिक @@ -30,7 +32,7 @@ mm से 0.2 mm के बीच की सहनशीलता ठीक र स्थान पर किसी दूसरे चुने गए वर्कपीस की हूबहू प्रति है. जो पथ अपनी लंबाई के केवल एक हिस्से में ओवरलैप करते हैं, वे बने रहते हैं. जॉब बनाते समय -[रेखाएँ मर्ज करें](merge-lines) पोस्ट-प्रोसेसर उन्हें संभालता है. +[रेखाएँ मर्ज करें](./merge-lines.md) पोस्ट-प्रोसेसर उन्हें संभालता है. ## अलग-अलग करें @@ -38,10 +40,10 @@ mm से 0.2 mm के बीच की सहनशीलता ठीक र **विभाजित करें** से अलग है, जो किसी द्वीप को उसके छेदों के साथ एक रखता है. पथ सफ़ाई केवल आयात किए गए और ट्रेस किए गए वर्कपीस पर काम करती है. स्केच से बने वर्कपीस को -[स्केचर](sketcher/index) में संपादित करें. +[स्केचर](./sketcher/index.md) में संपादित करें. ## संबंधित पृष्ठ -- [रेखाएँ मर्ज करें](merge-lines) - ओवरलैप होने वाले खंडों को केवल एक बार काटना -- [कैनवस उपकरण](../ui/canvas-tools) - अलग-अलग खंड चुनना और हटाना -- [फ़ाइलें आयात करना](../files/importing) - Rayforge में डिज़ाइन लाना +- [रेखाएँ मर्ज करें](./merge-lines.md) - ओवरलैप होने वाले खंडों को केवल एक बार काटना +- [कैनवस उपकरण](../ui/canvas-tools.md) - अलग-अलग खंड चुनना और हटाना +- [फ़ाइलें आयात करना](../files/importing.md) - Rayforge में डिज़ाइन लाना diff --git a/website/i18n/hi/docusaurus-plugin-content-docs/current/ui/canvas-tools.md b/website/i18n/hi/docusaurus-plugin-content-docs/current/ui/canvas-tools.md index b9521d394..718774312 100644 --- a/website/i18n/hi/docusaurus-plugin-content-docs/current/ui/canvas-tools.md +++ b/website/i18n/hi/docusaurus-plugin-content-docs/current/ui/canvas-tools.md @@ -109,7 +109,7 @@ description: वेक्टर संपादन मोड से बाहर निकलने के लिए, वर्कपीस के बाहर क्लिक करें या Escape दबाएँ. अंतराल बंद करने, खुले टुकड़ों को जोड़ने, डुप्लिकेट पथ हटाने या किसी वर्कपीस को अलग-अलग करने के लिए -[पथ सफ़ाई](../features/path-cleanup) टूल का उपयोग करें. +[पथ सफ़ाई](../features/path-cleanup.md) टूल का उपयोग करें. --- diff --git a/website/i18n/pt-BR/docusaurus-plugin-content-docs/current/features/path-cleanup.md b/website/i18n/pt-BR/docusaurus-plugin-content-docs/current/features/path-cleanup.md index 22771fbcb..9e0893efc 100644 --- a/website/i18n/pt-BR/docusaurus-plugin-content-docs/current/features/path-cleanup.md +++ b/website/i18n/pt-BR/docusaurus-plugin-content-docs/current/features/path-cleanup.md @@ -10,6 +10,8 @@ As ferramentas de limpeza de caminhos corrigem isso nas peças de trabalho selec encontra no menu **Objeto** e em **Limpar caminhos** no menu de contexto da tela. Cada ferramenta é um único passo de desfazer. +![O diálogo de fechar caminhos](/screenshots/main-path-cleanup.webp) + ## Fechar caminhos **Fechar caminhos…** fecha cada caminho aberto cujos pontos inicial e final estão mais próximos do @@ -34,7 +36,7 @@ trabalho, uma peça que seja cópia exata de outra peça selecionada na mesma po removida. Caminhos que se sobrepõem apenas em parte do comprimento são mantidos. O pós-processador -[Mesclar linhas](merge-lines) cuida deles quando o trabalho é gerado. +[Mesclar linhas](./merge-lines.md) cuida deles quando o trabalho é gerado. ## Separar @@ -42,10 +44,10 @@ Caminhos que se sobrepõem apenas em parte do comprimento são mantidos. O pós- caminhos abertos. Isso é diferente de **Dividir**, que mantém uma ilha junto com seus furos. A limpeza de caminhos só funciona em peças de trabalho importadas e vetorizadas. Peças que vêm de um -esboço são editadas no [Sketcher](sketcher/index). +esboço são editadas no [Sketcher](./sketcher/index.md). ## Páginas relacionadas -- [Mesclar linhas](merge-lines) - Cortar segmentos sobrepostos apenas uma vez -- [Ferramentas de tela](../ui/canvas-tools) - Selecionar e excluir segmentos individuais -- [Importando arquivos](../files/importing) - Trazer desenhos para o Rayforge +- [Mesclar linhas](./merge-lines.md) - Cortar segmentos sobrepostos apenas uma vez +- [Ferramentas de tela](../ui/canvas-tools.md) - Selecionar e excluir segmentos individuais +- [Importando arquivos](../files/importing.md) - Trazer desenhos para o Rayforge diff --git a/website/i18n/pt-BR/docusaurus-plugin-content-docs/current/ui/canvas-tools.md b/website/i18n/pt-BR/docusaurus-plugin-content-docs/current/ui/canvas-tools.md index db63200b1..10245e011 100644 --- a/website/i18n/pt-BR/docusaurus-plugin-content-docs/current/ui/canvas-tools.md +++ b/website/i18n/pt-BR/docusaurus-plugin-content-docs/current/ui/canvas-tools.md @@ -167,7 +167,7 @@ Para sair do modo de edição de vetores, clique fora da peça de trabalho ou pr Escape. Para fechar lacunas, unir partes abertas, excluir caminhos duplicados ou separar uma peça de -trabalho, use as ferramentas de [Limpeza de caminhos](../features/path-cleanup). +trabalho, use as ferramentas de [Limpeza de caminhos](../features/path-cleanup.md). --- diff --git a/website/i18n/uk/docusaurus-plugin-content-docs/current/features/path-cleanup.md b/website/i18n/uk/docusaurus-plugin-content-docs/current/features/path-cleanup.md index 087cd40a1..a87fc20a9 100644 --- a/website/i18n/uk/docusaurus-plugin-content-docs/current/features/path-cleanup.md +++ b/website/i18n/uk/docusaurus-plugin-content-docs/current/features/path-cleanup.md @@ -9,6 +9,8 @@ в контекстному меню полотна в розділі **Очистити шляхи**. Кожен інструмент — це один крок скасування. +![Діалогове вікно «Замкнути шляхи»](/screenshots/main-path-cleanup.webp) + ## Замкнути шляхи **Замкнути шляхи…** замикає кожен відкритий шлях, у якого початкова й кінцева точки ближчі за @@ -31,7 +33,7 @@ яка є точною копією іншої вибраної деталі в тому самому місці, також видаляється. Шляхи, що перекриваються лише частиною своєї довжини, залишаються. Ними опікується постпроцесор -[Об'єднання ліній](merge-lines) під час створення завдання. +[Об'єднання ліній](./merge-lines.md) під час створення завдання. ## Розібрати на частини @@ -39,10 +41,10 @@ відкритими шляхами. Це відрізняється від **Розділити**, яке тримає острів разом з його отворами. Очищення шляхів працює лише з імпортованими та трасованими робочими деталями. Деталі, створені зі -скетчу, редагуйте у [Sketcher](sketcher/index). +скетчу, редагуйте у [Sketcher](./sketcher/index.md). ## Пов'язані сторінки -- [Об'єднання ліній](merge-lines) - Різати сегменти, що перекриваються, лише один раз -- [Інструменти полотна](../ui/canvas-tools) - Вибір і видалення окремих сегментів -- [Імпортування файлів](../files/importing) - Завантаження дизайнів у Rayforge +- [Об'єднання ліній](./merge-lines.md) - Різати сегменти, що перекриваються, лише один раз +- [Інструменти полотна](../ui/canvas-tools.md) - Вибір і видалення окремих сегментів +- [Імпортування файлів](../files/importing.md) - Завантаження дизайнів у Rayforge diff --git a/website/i18n/uk/docusaurus-plugin-content-docs/current/ui/canvas-tools.md b/website/i18n/uk/docusaurus-plugin-content-docs/current/ui/canvas-tools.md index 83cae063b..fd5f6050a 100644 --- a/website/i18n/uk/docusaurus-plugin-content-docs/current/ui/canvas-tools.md +++ b/website/i18n/uk/docusaurus-plugin-content-docs/current/ui/canvas-tools.md @@ -104,7 +104,7 @@ Escape. Щоб закрити проміжки, з'єднати відкриті частини, видалити дублікати шляхів або розібрати робочу -деталь на частини, скористайтеся інструментами [Очищення шляхів](../features/path-cleanup). +деталь на частини, скористайтеся інструментами [Очищення шляхів](../features/path-cleanup.md). --- diff --git a/website/i18n/zh-CN/docusaurus-plugin-content-docs/current/features/path-cleanup.md b/website/i18n/zh-CN/docusaurus-plugin-content-docs/current/features/path-cleanup.md index 604717dfc..0bd793333 100644 --- a/website/i18n/zh-CN/docusaurus-plugin-content-docs/current/features/path-cleanup.md +++ b/website/i18n/zh-CN/docusaurus-plugin-content-docs/current/features/path-cleanup.md @@ -4,6 +4,8 @@ 路径清理工具可以修复所选工件中的这些问题。您可以在**对象**菜单中,以及画布右键菜单的**清理路径**下找到它们。每个工具都是一个撤销步骤。 +![闭合路径对话框](/screenshots/main-path-cleanup.webp) + ## 闭合路径 **闭合路径…**会闭合所有起点与终点距离小于您输入的公差的开放路径。缺口由两个现有点之间的一条直线连接,因此形状不会移动,也不会改变大小。 @@ -19,16 +21,16 @@ mm 之间的公差即可。 **删除重复项**会删除与同一工件中另一条路径完全重合的路径,无论其方向或起点如何。如果您选择了多个工件,与另一个所选工件位置相同且完全相同的工件也会被删除。 -只有部分长度重叠的路径会被保留。生成作业时,[合并线条](merge-lines)后处理器会处理这些路径。 +只有部分长度重叠的路径会被保留。生成作业时,[合并线条](./merge-lines.md)后处理器会处理这些路径。 ## 打散 **打散**会把工件的每条路径都变成一个独立的工件,包括孔和开放路径。这与**分割**不同,分割会把一个岛与它的孔保留在一起。 -路径清理只适用于导入和描摹得到的工件。来自草图的工件请在[草图编辑器](sketcher/index)中编辑。 +路径清理只适用于导入和描摹得到的工件。来自草图的工件请在[草图编辑器](./sketcher/index.md)中编辑。 ## 相关页面 -- [合并线条](merge-lines) - 重叠的线段只切割一次 -- [画布工具](../ui/canvas-tools) - 选择和删除单个线段 -- [导入文件](../files/importing) - 将设计导入 Rayforge +- [合并线条](./merge-lines.md) - 重叠的线段只切割一次 +- [画布工具](../ui/canvas-tools.md) - 选择和删除单个线段 +- [导入文件](../files/importing.md) - 将设计导入 Rayforge diff --git a/website/i18n/zh-CN/docusaurus-plugin-content-docs/current/ui/canvas-tools.md b/website/i18n/zh-CN/docusaurus-plugin-content-docs/current/ui/canvas-tools.md index efa60de09..16a451793 100644 --- a/website/i18n/zh-CN/docusaurus-plugin-content-docs/current/ui/canvas-tools.md +++ b/website/i18n/zh-CN/docusaurus-plugin-content-docs/current/ui/canvas-tools.md @@ -159,7 +159,7 @@ 要退出矢量编辑模式,请点击工件外部或按 Escape。 -要闭合缺口、连接开放的片段、删除重复路径或将工件打散,请使用[清理路径](../features/path-cleanup)工具。 +要闭合缺口、连接开放的片段、删除重复路径或将工件打散,请使用[清理路径](../features/path-cleanup.md)工具。 ---