diff --git a/CHANGELOG.md b/CHANGELOG.md index 618e33fde..425e740f3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 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) +- Contour: Perforation post-processor for dashed cuts. The laser + fires for a Cut Length and is off for a Skip Length along every + contour, starting with a full cut at the start of each contour; off + by default (#525) - 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/builtin_addons/rayforge-addon-laser/laser_essentials/steps/contour_step.py b/rayforge/builtin_addons/rayforge-addon-laser/laser_essentials/steps/contour_step.py index 9eddb44c2..4783fa67e 100644 --- a/rayforge/builtin_addons/rayforge-addon-laser/laser_essentials/steps/contour_step.py +++ b/rayforge/builtin_addons/rayforge-addon-laser/laser_essentials/steps/contour_step.py @@ -260,6 +260,9 @@ def get_default_transformers_dicts(cls) -> tuple[list, list]: Smooth = transformer_registry.get("Smooth") LeadInOutTransformer = transformer_registry.get("LeadInOutTransformer") TabOpsTransformer = transformer_registry.get("TabOpsTransformer") + PerforationTransformer = transformer_registry.get( + "PerforationTransformer" + ) CropTransformer = transformer_registry.get("CropTransformer") MergeLinesTransformer = transformer_registry.get( "MergeLinesTransformer" @@ -269,6 +272,7 @@ def get_default_transformers_dicts(cls) -> tuple[list, list]: assert Smooth is not None assert LeadInOutTransformer is not None assert TabOpsTransformer is not None + assert PerforationTransformer is not None assert CropTransformer is not None assert MergeLinesTransformer is not None assert Optimize is not None @@ -280,6 +284,7 @@ def get_default_transformers_dicts(cls) -> tuple[list, list]: enabled=False, lead_in_mm=0, lead_out_mm=0, auto=True ).to_dict(), TabOpsTransformer().to_dict(), + PerforationTransformer(enabled=False).to_dict(), CropTransformer(enabled=False).to_dict(), optimize_dict, ], [ diff --git a/rayforge/builtin_addons/rayforge-addon-laser/tests/steps/test_contour_step.py b/rayforge/builtin_addons/rayforge-addon-laser/tests/steps/test_contour_step.py index 1b104c01e..eb9cf3f8d 100644 --- a/rayforge/builtin_addons/rayforge-addon-laser/tests/steps/test_contour_step.py +++ b/rayforge/builtin_addons/rayforge-addon-laser/tests/steps/test_contour_step.py @@ -37,13 +37,25 @@ def test_create(self, mock_context): step = ContourStep.create(mock_context, name="Created") assert isinstance(step, ContourStep) assert step.name == "Created" - assert len(step.per_workpiece_transformers_dicts) == 5 + assert len(step.per_workpiece_transformers_dicts) == 6 assert len(step.per_step_transformers_dicts) == 3 assert step.selected_head_uid == "test-laser-uid" def test_create_without_optimize(self, mock_context): step = ContourStep.create(mock_context, optimize=False) - assert len(step.per_workpiece_transformers_dicts) == 4 + assert len(step.per_workpiece_transformers_dicts) == 5 + + def test_create_adds_disabled_perforation_after_tabs(self, mock_context): + step = ContourStep.create(mock_context) + names = [t["name"] for t in step.per_workpiece_transformers_dicts] + assert "PerforationTransformer" in names + assert names.index("PerforationTransformer") == ( + names.index("TabOpsTransformer") + 1 + ) + perforation = step.per_workpiece_transformers_dicts[ + names.index("PerforationTransformer") + ] + assert perforation["enabled"] is False def test_serialization_includes_step_type(self): step = ContourStep(name="Test") @@ -88,6 +100,7 @@ def test_from_dict_adds_new_transformers_from_old_project(self): t["name"] for t in restored.per_workpiece_transformers_dicts ] assert "TabOpsTransformer" in wp_names + assert "PerforationTransformer" in wp_names assert "Smooth" in wp_names assert "CropTransformer" in wp_names assert "Optimize" in wp_names diff --git a/rayforge/builtin_addons/rayforge-addon-post/post_processors/transformers/__init__.py b/rayforge/builtin_addons/rayforge-addon-post/post_processors/transformers/__init__.py index 7583f8b03..9840ecb7c 100644 --- a/rayforge/builtin_addons/rayforge-addon-post/post_processors/transformers/__init__.py +++ b/rayforge/builtin_addons/rayforge-addon-post/post_processors/transformers/__init__.py @@ -12,6 +12,7 @@ from .multipass_transformer import MultiPassTransformer from .optimize_transformer import Optimize from .overscan_transformer import OverscanTransformer +from .perforation_transformer import PerforationTransformer from .smooth_transformer import Smooth from .tabs_transformer import TabOpsTransformer @@ -23,6 +24,7 @@ "MultiPassTransformer", "Optimize", "OverscanTransformer", + "PerforationTransformer", "Smooth", "TabOpsTransformer", ] diff --git a/rayforge/builtin_addons/rayforge-addon-post/post_processors/transformers/perforation_transformer.py b/rayforge/builtin_addons/rayforge-addon-post/post_processors/transformers/perforation_transformer.py new file mode 100644 index 000000000..e0a36a2a5 --- /dev/null +++ b/rayforge/builtin_addons/rayforge-addon-post/post_processors/transformers/perforation_transformer.py @@ -0,0 +1,138 @@ +from __future__ import annotations + +from collections.abc import Sequence +from gettext import gettext as _ +from typing import TYPE_CHECKING, Any + +from raygeo.geo import Matrix +from raygeo.ops.transform.tabs import TabsSpec + +from rayforge.core.workpiece import WorkPiece +from rayforge.pipeline.transformer.base import OpsTransformer + +if TYPE_CHECKING: + from raygeo.geo import Geometry + + +MIN_LENGTH = 0.01 +EPSILON = 1e-4 + + +class PerforationTransformer(OpsTransformer): + """ + Turns cut paths into a dashed cut: the laser fires for + ``cut_length`` and is off for ``skip_length``, repeated along + every contour of the workpiece. + + The gaps are placed in millimetres along each contour of the + workpiece's boundaries, starting with a full cut at the start of + every contour, and handed to the tabs gap machinery, which cuts + them out of the nearest toolpath. + """ + + DEFAULT_CUT_LENGTH = 2.0 + DEFAULT_SKIP_LENGTH = 1.0 + + def __init__( + self, + enabled: bool = True, + cut_length: float = DEFAULT_CUT_LENGTH, + skip_length: float = DEFAULT_SKIP_LENGTH, + ): + super().__init__(enabled=enabled) + self._cut_length = max(MIN_LENGTH, cut_length) + self._skip_length = max(MIN_LENGTH, skip_length) + + @property + def cut_length(self) -> float: + return self._cut_length + + @cut_length.setter + def cut_length(self, value: float) -> None: + self._cut_length = max(MIN_LENGTH, value) + self.changed.send(self) + + @property + def skip_length(self) -> float: + return self._skip_length + + @skip_length.setter + def skip_length(self, value: float) -> None: + self._skip_length = max(MIN_LENGTH, value) + self.changed.send(self) + + @property + def label(self) -> str: + return _("Perforation") + + @property + def description(self) -> str: + return _( + "Cuts in dashes: the laser fires for the cut length and " + "skips the skip length" + ) + + def _gap_centres(self, length: float) -> list[float]: + period = self._cut_length + self._skip_length + half_gap = self._skip_length / 2.0 + centres: list[float] = [] + centre = self._cut_length + half_gap + while centre + half_gap <= length + EPSILON: + centres.append(centre) + centre += period + return centres + + def _clips_for_contour( + self, contour: Geometry + ) -> list[tuple[float, float, float]]: + centres = self._gap_centres(contour.distance()) + if not centres: + return [] + positions = contour.get_positions_at_distances(centres) + return [ + (point[0], point[1], self._skip_length) + for _seg, _t, point in positions + ] + + def _generate_clips( + self, workpiece: WorkPiece + ) -> list[tuple[float, float, float]]: + boundaries = workpiece.boundaries + if boundaries is None or boundaries.is_empty(): + return [] + width, height = workpiece.size + scaled = boundaries.copy() + if width > 0 and height > 0: + scaled.transform(Matrix.scale(width, height)) + clips: list[tuple[float, float, float]] = [] + for contour in scaled.split_into_contours(): + clips.extend(self._clips_for_contour(contour)) + return clips + + def to_spec( + self, + workpiece: WorkPiece | None, + stock_geometries: Sequence[Geometry] | None, + settings: dict[str, Any] | None, + ) -> TabsSpec: + clips = self._generate_clips(workpiece) if workpiece else [] + return TabsSpec(tab_power=0.0, original_power=1.0, clips=clips) + + def to_dict(self) -> dict[str, Any]: + data = super().to_dict() + data["cut_length"] = self._cut_length + data["skip_length"] = self._skip_length + return data + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> PerforationTransformer: + if data.get("name") != cls.__name__: + raise ValueError( + f"Mismatched transformer name: expected {cls.__name__}," + f" got {data.get('name')}" + ) + return cls( + enabled=data.get("enabled", True), + cut_length=data.get("cut_length", cls.DEFAULT_CUT_LENGTH), + skip_length=data.get("skip_length", cls.DEFAULT_SKIP_LENGTH), + ) diff --git a/rayforge/builtin_addons/rayforge-addon-post/post_processors/widgets/__init__.py b/rayforge/builtin_addons/rayforge-addon-post/post_processors/widgets/__init__.py index d8facaa9a..b53c7479d 100644 --- a/rayforge/builtin_addons/rayforge-addon-post/post_processors/widgets/__init__.py +++ b/rayforge/builtin_addons/rayforge-addon-post/post_processors/widgets/__init__.py @@ -9,6 +9,7 @@ MultiPassTransformer, Optimize, OverscanTransformer, + PerforationTransformer, Smooth, ) from .crop_group import CropSettingsGroup @@ -17,6 +18,7 @@ from .multipass_group import MultiPassSettingsGroup from .optimize_group import OptimizeSettingsGroup from .overscan_group import OverscanSettingsGroup +from .perforation_group import PerforationSettingsGroup from .smooth_group import SmoothSettingsGroup TRANSFORMER_WIDGETS = { @@ -26,5 +28,6 @@ MultiPassTransformer: MultiPassSettingsGroup, Optimize: OptimizeSettingsGroup, OverscanTransformer: OverscanSettingsGroup, + PerforationTransformer: PerforationSettingsGroup, Smooth: SmoothSettingsGroup, } diff --git a/rayforge/builtin_addons/rayforge-addon-post/post_processors/widgets/perforation_group.py b/rayforge/builtin_addons/rayforge-addon-post/post_processors/widgets/perforation_group.py new file mode 100644 index 000000000..5bd1135be --- /dev/null +++ b/rayforge/builtin_addons/rayforge-addon-post/post_processors/widgets/perforation_group.py @@ -0,0 +1,70 @@ +from gettext import gettext as _ +from typing import TYPE_CHECKING + +from rayforge.shared.util.glib import DebounceMixin +from rayforge.ui_gtk.doceditor.post_processor.groups import ( + ExpanderHost, + TransformerSettingsGroup, +) +from rayforge.ui_gtk.shared.pref_rows import LengthSpinRow + +from ..transformers import PerforationTransformer +from ..transformers.perforation_transformer import MIN_LENGTH + +if TYPE_CHECKING: + from rayforge.core.step import Step + + +class PerforationSettingsGroup(DebounceMixin, TransformerSettingsGroup): + """UI for configuring the PerforationTransformer.""" + + def __init__( + self, + title: str, + transformer: PerforationTransformer, + page: ExpanderHost, + *, + step: "Step | None" = None, + **kwargs, + ): + super().__init__(title, transformer, page, step=step, **kwargs) + + self.cut_row = LengthSpinRow( + _("Cut Length"), + _("Distance the laser fires before each gap"), + lower=MIN_LENGTH, + upper=1000.0, + value_in_base=transformer.cut_length, + ) + self.cut_row.value_changed.connect( + lambda r: self._debounce(self._on_cut_changed, r) + ) + self.add(self.cut_row) + + self.skip_row = LengthSpinRow( + _("Skip Length"), + _("Distance travelled with the laser off between cuts"), + lower=MIN_LENGTH, + upper=1000.0, + value_in_base=transformer.skip_length, + ) + self.skip_row.value_changed.connect( + lambda r: self._debounce(self._on_skip_changed, r) + ) + self.add(self.skip_row) + + def _on_cut_changed(self, row: LengthSpinRow) -> None: + self.param_changed.send( + self, + key="cut_length", + value=row.get_value_in_base_units(), + name=_("Change perforation cut length"), + ) + + def _on_skip_changed(self, row: LengthSpinRow) -> None: + self.param_changed.send( + self, + key="skip_length", + value=row.get_value_in_base_units(), + name=_("Change perforation skip length"), + ) diff --git a/rayforge/builtin_addons/rayforge-addon-post/post_processors/worker.py b/rayforge/builtin_addons/rayforge-addon-post/post_processors/worker.py index 691d01aab..3a976fc9e 100644 --- a/rayforge/builtin_addons/rayforge-addon-post/post_processors/worker.py +++ b/rayforge/builtin_addons/rayforge-addon-post/post_processors/worker.py @@ -14,6 +14,7 @@ MultiPassTransformer, Optimize, OverscanTransformer, + PerforationTransformer, Smooth, TabOpsTransformer, ) @@ -33,5 +34,8 @@ def register_transformers(transformer_registry): transformer_registry.register(MultiPassTransformer, addon_name=ADDON_NAME) transformer_registry.register(Optimize, addon_name=ADDON_NAME) transformer_registry.register(OverscanTransformer, addon_name=ADDON_NAME) + transformer_registry.register( + PerforationTransformer, addon_name=ADDON_NAME + ) transformer_registry.register(Smooth, addon_name=ADDON_NAME) transformer_registry.register(TabOpsTransformer, addon_name=ADDON_NAME) diff --git a/rayforge/builtin_addons/rayforge-addon-post/tests/transformers/test_perforation_transformer.py b/rayforge/builtin_addons/rayforge-addon-post/tests/transformers/test_perforation_transformer.py new file mode 100644 index 000000000..15191561c --- /dev/null +++ b/rayforge/builtin_addons/rayforge-addon-post/tests/transformers/test_perforation_transformer.py @@ -0,0 +1,197 @@ +from unittest.mock import MagicMock + +import pytest +from post_processors.transformers import ( + PerforationTransformer, + TabOpsTransformer, +) +from raygeo.geo import Geometry +from raygeo.ops import Ops +from raygeo.ops.transform.tabs import TabsSpec +from raygeo.ops.types import CommandType, SectionType + +from rayforge.core.workpiece import WorkPiece +from rayforge.pipeline.transformer.registry import TransformerRegistry + + +def _unit_square(x0=0.0, y0=0.0, x1=1.0, y1=1.0) -> Geometry: + geo = Geometry() + geo.move_to(x0, y0) + geo.line_to(x1, y0) + geo.line_to(x1, y1) + geo.line_to(x0, y1) + geo.close_path() + return geo + + +def _workpiece(boundaries, size=(20.0, 20.0)): + wp = MagicMock(spec=WorkPiece) + wp.boundaries = boundaries + wp.size = size + return wp + + +def _square_ops(x0, y0, x1, y1) -> Ops: + ops = Ops() + ops.set_power(1.0) + ops.ops_section_start( + section_type=SectionType.VECTOR_OUTLINE, workpiece_uid="wp" + ) + ops.move_to(x0, y0, 0.0) + ops.line_to(x1, y0, 0.0) + ops.line_to(x1, y1, 0.0) + ops.line_to(x0, y1, 0.0) + ops.line_to(x0, y0, 0.0) + ops.ops_section_end(section_type=SectionType.VECTOR_OUTLINE) + return ops + + +def _cut_runs(ops: Ops) -> int: + return sum( + 1 + for i in range(ops.len()) + if ops.command_type(i) == CommandType.MOVE_TO + ) + + +class TestPerforationConfig: + def test_defaults(self): + t = PerforationTransformer() + assert t.enabled is True + assert t.cut_length == pytest.approx(2.0) + assert t.skip_length == pytest.approx(1.0) + + def test_label_and_description(self): + t = PerforationTransformer() + assert t.label + assert t.description + + def test_lengths_are_clamped_to_a_minimum(self): + t = PerforationTransformer(cut_length=0.0, skip_length=-1.0) + assert t.cut_length >= 0.01 + assert t.skip_length >= 0.01 + + def test_setter_sends_changed(self): + t = PerforationTransformer() + received = [] + + def on_changed(sender): + received.append(sender) + + t.changed.connect(on_changed) + t.cut_length = 3.0 + t.skip_length = 0.5 + assert len(received) == 2 + assert t.cut_length == pytest.approx(3.0) + assert t.skip_length == pytest.approx(0.5) + + def test_dict_round_trip(self): + t = PerforationTransformer( + enabled=False, cut_length=1.5, skip_length=0.5 + ) + data = t.to_dict() + assert data["name"] == "PerforationTransformer" + restored = PerforationTransformer.from_dict(data) + assert restored.enabled is False + assert restored.cut_length == pytest.approx(1.5) + assert restored.skip_length == pytest.approx(0.5) + + def test_from_dict_defaults(self): + restored = PerforationTransformer.from_dict( + {"name": "PerforationTransformer"} + ) + assert restored.cut_length == pytest.approx(2.0) + assert restored.skip_length == pytest.approx(1.0) + + def test_from_dict_rejects_other_name(self): + with pytest.raises(ValueError): + PerforationTransformer.from_dict({"name": "Optimize"}) + + +class TestPerforationSpec: + def test_spec_is_gap_mode_tabs(self): + t = PerforationTransformer(cut_length=1.5, skip_length=0.5) + spec = t.to_spec(_workpiece(_unit_square()), None, None) + assert isinstance(spec, TabsSpec) + assert spec.tab_power == 0.0 + + def test_no_workpiece_gives_no_clips(self): + spec = PerforationTransformer().to_spec(None, None, None) + assert spec.clips == [] + + def test_empty_boundaries_give_no_clips(self): + wp = _workpiece(Geometry()) + spec = PerforationTransformer().to_spec(wp, None, None) + assert spec.clips == [] + + def test_square_gets_one_gap_per_period(self): + t = PerforationTransformer(cut_length=1.5, skip_length=0.5) + spec = t.to_spec(_workpiece(_unit_square()), None, None) + assert len(spec.clips) == 40 + assert all(w == pytest.approx(0.5) for _x, _y, w in spec.clips) + + def test_first_gap_follows_a_full_cut(self): + t = PerforationTransformer(cut_length=1.5, skip_length=0.5) + spec = t.to_spec(_workpiece(_unit_square()), None, None) + x, y, _w = spec.clips[0] + assert x == pytest.approx(1.75) + assert y == pytest.approx(0.0) + + def test_clips_are_in_millimetres(self): + t = PerforationTransformer(cut_length=4.0, skip_length=1.0) + wp = _workpiece(_unit_square(), size=(40.0, 10.0)) + spec = t.to_spec(wp, None, None) + xs = [x for x, _y, _w in spec.clips] + ys = [y for _x, y, _w in spec.clips] + assert max(xs) <= 40.0 + 1e-6 + assert max(ys) <= 10.0 + 1e-6 + assert len(spec.clips) == 20 + + def test_gap_never_wraps_past_the_contour_end(self): + t = PerforationTransformer(cut_length=3.0, skip_length=1.0) + spec = t.to_spec(_workpiece(_unit_square()), None, None) + assert len(spec.clips) == 20 + x, y, _w = spec.clips[-1] + assert (x, y) == (pytest.approx(0.0), pytest.approx(0.5)) + + def test_pattern_restarts_for_every_contour(self): + geo = _unit_square(0.0, 0.0, 0.25, 0.25) + geo.extend(_unit_square(0.5, 0.5, 0.75, 0.75)) + t = PerforationTransformer(cut_length=1.5, skip_length=0.5) + spec = t.to_spec(_workpiece(geo, size=(40.0, 40.0)), None, None) + assert len(spec.clips) == 40 + starts = [c for c in spec.clips if c[1] == pytest.approx(20.0)] + assert starts[0][0] == pytest.approx(21.75) + + def test_last_gap_survives_rounding_of_the_contour_length(self): + geo = Geometry() + geo.move_to(0.0, 0.0) + geo.line_to(1.0 - 1e-7, 0.0) + t = PerforationTransformer(cut_length=3.0, skip_length=1.5) + spec = t.to_spec(_workpiece(geo, size=(90.0, 1.0)), None, None) + assert len(spec.clips) == 20 + + def test_contour_shorter_than_a_period_is_left_whole(self): + geo = _unit_square(0.0, 0.0, 0.01, 0.01) + t = PerforationTransformer(cut_length=1.5, skip_length=0.5) + spec = t.to_spec(_workpiece(geo), None, None) + assert spec.clips == [] + + +class TestPerforationOnOps: + def test_cut_and_skip_lengths_on_square(self): + t = PerforationTransformer(cut_length=1.5, skip_length=0.5) + spec = t.to_spec(_workpiece(_unit_square()), None, None) + ops = _square_ops(0.0, 0.0, 20.0, 20.0) + Ops.apply_transformers(ops, [spec]) + ops.preload_state() + assert ops.cut_distance() == pytest.approx(60.0, abs=1e-6) + assert _cut_runs(ops) == 40 + + +class TestPerforationProgressLabel: + def test_tabs_progress_keeps_the_tabs_label(self): + registry = TransformerRegistry() + registry.register(PerforationTransformer) + registry.register(TabOpsTransformer) + assert registry.progress_label("tabs") == TabOpsTransformer().label diff --git a/rayforge/builtin_addons/rayforge-addon-post/tests/ui_gtk/test_perforation_group.py b/rayforge/builtin_addons/rayforge-addon-post/tests/ui_gtk/test_perforation_group.py new file mode 100644 index 000000000..2006735d9 --- /dev/null +++ b/rayforge/builtin_addons/rayforge-addon-post/tests/ui_gtk/test_perforation_group.py @@ -0,0 +1,56 @@ +# flake8: noqa: E402 +"""UI tests: the Perforation settings group edits cut and skip lengths.""" + +import gi + +gi.require_version("Gtk", "4.0") +gi.require_version("Adw", "1") + +import pytest +from post_processors.transformers import PerforationTransformer +from post_processors.widgets import TRANSFORMER_WIDGETS +from post_processors.widgets.perforation_group import ( + PerforationSettingsGroup, +) + + +class _Page: + use_expanders = True + + +def _build_group(): + transformer = PerforationTransformer(cut_length=1.5, skip_length=0.5) + group = PerforationSettingsGroup("Perforation", transformer, _Page()) + return group, transformer + + +@pytest.mark.ui +def test_perforation_group_is_registered(ui_context): + assert TRANSFORMER_WIDGETS[PerforationTransformer] is ( + PerforationSettingsGroup + ) + + +@pytest.mark.ui +def test_perforation_group_shows_lengths(ui_context): + group, _transformer = _build_group() + assert group.cut_row.get_value_in_base_units() == pytest.approx(1.5) + assert group.skip_row.get_value_in_base_units() == pytest.approx(0.5) + + +@pytest.mark.ui +def test_perforation_group_reports_changes(ui_context): + group, _transformer = _build_group() + sent = [] + + def on_param(sender, key, value, name): + sent.append((key, value)) + + group.param_changed.connect(on_param) + group.cut_row.set_value_in_base_units(3.0) + group._on_cut_changed(group.cut_row) + group.skip_row.set_value_in_base_units(0.25) + group._on_skip_changed(group.skip_row) + + assert ("cut_length", pytest.approx(3.0)) in sent + assert ("skip_length", pytest.approx(0.25)) in sent diff --git a/scripts/screenshot/cli.py b/scripts/screenshot/cli.py index 025ffbe68..2ead7f2b6 100755 --- a/scripts/screenshot/cli.py +++ b/scripts/screenshot/cli.py @@ -124,6 +124,7 @@ "addons:sketcher:array:circular": "sketcher_array", "addons:sketcher:array:curve-along": "sketcher_array", "material-test": "material_test", + "operations:perforation": "perforation", "operations:wavefront": "wavefront", "recipe-editor:general": "recipe_editor_general", "recipe-editor:applicability": "recipe_editor_applicability", @@ -135,6 +136,7 @@ "step-settings:contour:general": "step_settings", "step-settings:contour:laser": "step_settings", "step-settings:contour:post": "step_settings", + "step-settings:contour:post:perforation": "step_settings_perforation", "step-settings:engrave:general:constant_power": "step_settings", "step-settings:engrave:general:dither": "step_settings", "step-settings:engrave:general:multi_pass": "step_settings", diff --git a/scripts/screenshot/perforation.py b/scripts/screenshot/perforation.py new file mode 100644 index 000000000..43504ef99 --- /dev/null +++ b/scripts/screenshot/perforation.py @@ -0,0 +1,139 @@ +""" +Screenshot: Perforation post-processor applied to a contour. + +Usage: pixi run screenshot operations:perforation +""" + +import logging +import time + +from utils import ( + clear_window_subtitle, + load_project, + restore_config, + run_on_main_thread, + set_window_size, + take_region_screenshot, + wait_for_settled, +) + +from rayforge.core.workpiece import WorkPiece +from rayforge.uiscript import app, win + +logger = logging.getLogger(__name__) + +TARGET_PX_PER_MM = 26.0 +CROP_SIZE = (1200, 800) + + +def _content_boxes() -> list[tuple[float, float, float, float]]: + items = [ + item + for layer in win.doc_editor.doc.layers + for item in layer.children + if isinstance(item, WorkPiece) + ] + items.extend(win.doc_editor.doc.stock_items) + return [item.bbox for item in items if item.size != (0.0, 0.0)] + + +def frame_closeup() -> tuple[float, float] | None: + """Zoom in on the workpiece so the dash pattern is clearly visible. + + Centers the view on a point inside the workpiece and returns the + focused world position, or None when there is nothing to frame. + """ + + def _frame() -> tuple[float, float] | None: + boxes = _content_boxes() + if not boxes: + logger.warning("No content to frame") + return None + min_x = min(b[0] for b in boxes) + min_y = min(b[1] for b in boxes) + max_x = max(b[0] + b[2] for b in boxes) + max_y = max(b[1] + b[3] for b in boxes) + + work_boxes = [ + b + for layer in win.doc_editor.doc.layers + for b in [ + item.bbox + for item in layer.children + if isinstance(item, WorkPiece) + ] + ] + if work_boxes: + box = work_boxes[0] + else: + box = (min_x, min_y, max_x - min_x, max_y - min_y) + + focus_x = box[0] + 0.79 * box[2] + focus_y = box[1] + 0.5 * box[3] + + scale_x, scale_y = win.surface.get_view_scale() + px_per_mm = max((scale_x + scale_y) / 2.0, 1e-9) + zoom = win.surface.zoom_level * TARGET_PX_PER_MM / px_per_mm + win.surface.set_zoom(max(zoom, 0.01)) + + for _pass in range(2): + pos = win.surface.view_transform.transform_point(focus_x, focus_y) + dx = win.surface.get_width() / 2.0 - pos[0] + dy = win.surface.get_height() / 2.0 - pos[1] + scale_x, scale_y = win.surface.get_view_scale() + win.surface.set_pan( + win.surface.pan_x_mm - dx / scale_x, + win.surface.pan_y_mm + dy / scale_y, + ) + return focus_x, focus_y + + return run_on_main_thread(_frame) + + +def closeup_region(focus: tuple[float, float]) -> tuple[int, int, int, int]: + """Return the main-window region around the focused point.""" + + def _region() -> tuple[int, int, int, int]: + pos = win.surface.view_transform.transform_point(*focus) + ox, oy = win.surface.translate_coordinates(win, 0, 0) + cx, cy = ox + pos[0], oy + pos[1] + w, h = CROP_SIZE + return ( + int(cx - w / 2), + int(cy - h / 2), + int(cx + w / 2), + int(cy + h / 2), + ) + + return run_on_main_thread(_region) + + +@restore_config +def main(): + set_window_size(win, 2400, 1650) + + load_project(win, "perforation.ryp") + logger.info("Waiting for document to settle...") + if not wait_for_settled(win, timeout=30): + logger.error("Document did not settle in time") + app.quit_idle() + return + logger.info("Document settled") + + focus = frame_closeup() + if focus is None: + app.quit_idle() + return + time.sleep(0.5) + + clear_window_subtitle(win) + time.sleep(0.25) + + logger.info("Taking close-up screenshot: operations-perforation.webp") + take_region_screenshot(win, closeup_region(focus)) + + time.sleep(0.25) + app.quit_idle() + + +main() diff --git a/scripts/screenshot/step_settings_perforation.py b/scripts/screenshot/step_settings_perforation.py new file mode 100644 index 000000000..b5d557785 --- /dev/null +++ b/scripts/screenshot/step_settings_perforation.py @@ -0,0 +1,109 @@ +"""Screenshot: Contour post-processing page with Perforation expanded.""" + +import logging +import time + +from gi.repository import Adw +from utils import ( + find_step_by_type, + get_target, + load_project, + open_step_settings, + restore_config, + run_on_main_thread, + set_window_size, + take_screenshot, +) + +from rayforge.uiscript import app, win + +logger = logging.getLogger(__name__) + +TARGET = get_target("step-settings:contour:post:perforation") + +TRANSFORMER_NAME = "PerforationTransformer" + + +def find_expander_row(widget, title: str) -> Adw.ExpanderRow | None: + """Find the expander row with the given title in the widget tree.""" + if isinstance(widget, Adw.ExpanderRow) and widget.get_title() == title: + return widget + child = widget.get_first_child() + while child is not None: + found = find_expander_row(child, title) + if found is not None: + return found + child = child.get_next_sibling() + return None + + +def find_post_page(widget): + """Find the post-processing page in the widget tree.""" + if hasattr(widget, "_group_dicts"): + return widget + child = widget.get_first_child() + while child is not None: + found = find_post_page(child) + if found is not None: + return found + child = child.get_next_sibling() + return None + + +def stage_perforation_group(dialog) -> bool: + """Enable the Perforation transformer and expand its settings.""" + + def _stage() -> bool: + page = find_post_page(dialog) + if page is None: + return False + for group, t_dict in page._group_dicts.items(): + if t_dict.get("name") != TRANSFORMER_NAME: + continue + title = group.get_title() + group.enable_switch.set_active(True) + expander = find_expander_row(page, title) + if expander is not None: + expander.set_expanded(True) + return True + return False + + staged = run_on_main_thread(_stage) + if not staged: + logger.error(f"No {TRANSFORMER_NAME} found on the post page") + return staged + + +@restore_config +def main(): + set_window_size(win, 2400, 1650) + + load_project(win, "allsteps.ryp") + time.sleep(0.25) + + step_type, page, _mode = "contour", "post-processing", None + logger.info(f"Target: {TARGET} -> type={step_type}, page={page}") + + found = find_step_by_type(win, step_type) + if found is None or found[0] is None: + logger.error(f"No step found with type: {step_type}") + app.quit_idle() + return + + step, step_index = found + logger.info(f"Found step: {step.name} at index {step_index}") + + dialog = open_step_settings(win, step_index=step_index, page=page) + time.sleep(0.5) + + if not stage_perforation_group(dialog): + app.quit_idle() + return + time.sleep(0.5) + + take_screenshot() + time.sleep(0.25) + app.quit_idle() + + +main() diff --git a/tests/assets/perforation.ryp b/tests/assets/perforation.ryp new file mode 100644 index 000000000..58ef01d2e Binary files /dev/null and b/tests/assets/perforation.ryp differ diff --git a/website/docs/features/operations/contour.md b/website/docs/features/operations/contour.md index b6615b6fd..d3ed5e0ee 100644 --- a/website/docs/features/operations/contour.md +++ b/website/docs/features/operations/contour.md @@ -208,6 +208,7 @@ See [Kerf](../kerf.md) for detailed guide. Contour operations support several post-processing options: - **[Smooth Path](../smooth.md)** - Reduce jagged edges in cutting paths +- **[Perforation](../perforation.md)** - Cut dashed lines for folds, tear-off lines and stitch holes - **[Holding Tabs](../holding-tabs.md)** - Keep cut pieces attached to stock material - **[Crop to Stock](../crop-to-stock.md)** - Limit cuts to material boundary - **[Path Optimization](../path-optimization.md)** - Reduce travel distance between cuts diff --git a/website/docs/features/perforation.md b/website/docs/features/perforation.md new file mode 100644 index 000000000..14cdf970b --- /dev/null +++ b/website/docs/features/perforation.md @@ -0,0 +1,65 @@ +--- +description: + "Cut dashed lines in Rayforge with the Perforation post-processor: set a cut length and a skip + length for fold lines, tear-off lines and stitch holes." +--- + +# Perforation + +Not every cut is meant to free a part from the material. Some lines are there so the material can +fold along them, tear apart cleanly, or show where the needle goes. That is what the **Perforation** +post-processor is for: it turns a continuous cut into a dashed one. Instead of keeping the laser on +from start to finish, it fires for the **Cut Length**, travels the **Skip Length** with the laser +off, and repeats that rhythm along every contour of the workpiece. What remains is a line of small +cuts that holds the material together — until you want it to give way. + +![The Perforation settings on the post-processing page of a Contour step](/screenshots/step-settings-contour-post-perforation.webp) + +## Typical Uses + +Whenever a line should weaken the material without cutting it free, a perforation cut is worth +considering. The classics: + +- **Fold lines** in card stock and corrugated cardboard fold crisply along a dashed cut — think + boxes, cards and pop-up designs. +- **Tear-off lines** let tickets, coupons and packaging open without scissors. +- **Stitch holes** in leather mark the path of the needle before sewing. +- **Dashed outlines** are a decorative effect in their own right, on wood, paper and acrylic. + +## Enabling Perforation + +Perforation lives on the **Post Processing** page of a [Contour](operations/contour) step's +settings, alongside the other post-processors. It is off by default — flip the switch and two values +appear: + +- **Cut Length** is how far the laser stays on before each gap. +- **Skip Length** is how far it travels with the laser off between two cuts. + +The pattern starts at the beginning of each contour with a full cut and restarts from there on every +contour, so all dashed lines on the workpiece begin the same way. A contour too short to fit even +one cut and one skip is simply cut in one piece. + +## The Result + +On the canvas, the dashed line replaces the solid cut outline — every gap in the toolpath is a gap +in the finished part. + +![A rectangle and a circle cut as dashed lines into a sheet of plywood](/screenshots/operations-perforation.webp) + +## Tips + +For a fold line that must not cut through, start with a skip length about as long as the cut length +and lower the power — a shallow groove folds just as well as a deep dashed cut, and the underside +stays clean. + +Very short lengths (a few tenths of a millimetre) make the laser switch on and off in rapid +succession, which lowers the effective power and can leave uneven marks. If the pattern looks +inconsistent, try longer lengths. + +Perforation plays well with the rest of the post-processing stack: [Holding Tabs](holding-tabs), +[Lead-In/Out](lead-in-out) and [Multi-Pass](multi-pass) all keep working as usual. + +## Related Pages + +- [Contour Cutting](operations/contour) — the cutting operation this post-processor belongs to +- [Holding Tabs](holding-tabs) — gaps placed by hand to keep parts attached diff --git a/website/i18n/de/docusaurus-plugin-content-docs/current/features/operations/contour.md b/website/i18n/de/docusaurus-plugin-content-docs/current/features/operations/contour.md index 59338b9fd..92a329753 100644 --- a/website/i18n/de/docusaurus-plugin-content-docs/current/features/operations/contour.md +++ b/website/i18n/de/docusaurus-plugin-content-docs/current/features/operations/contour.md @@ -211,6 +211,8 @@ Siehe [Schnittbreite](../kerf.md) für detaillierte Anleitung. Kontur-Operationen unterstützen mehrere Nachbearbeitungsoptionen: - **[Pfad-Glättung](../smooth.md)** - Gezackte Kanten in Schneidepfaden reduzieren +- **[Perforation](../perforation.md)** - Gestrichelte Schnitte für Falzlinien, Abreißlinien und + Nählöcher - **[Halte-Laschen](../holding-tabs.md)** - Geschnittene Teile am Rohmaterial befestigt halten - **[Auf Rohmaterial zuschneiden](../crop-to-stock.md)** - Schnitte auf Materialgrenze beschränken - **[Pfad-Optimierung](../path-optimization.md)** - Verfahrdistanz zwischen Schnitten reduzieren diff --git a/website/i18n/de/docusaurus-plugin-content-docs/current/features/perforation.md b/website/i18n/de/docusaurus-plugin-content-docs/current/features/perforation.md new file mode 100644 index 000000000..4405bc238 --- /dev/null +++ b/website/i18n/de/docusaurus-plugin-content-docs/current/features/perforation.md @@ -0,0 +1,69 @@ +--- +description: + "Gestrichelte Schnitte in Rayforge mit dem Perforation-Nachbearbeitungsschritt: Schnittlänge und + Auslasslänge für Falzlinien, Abreißlinien und Nählöcher festlegen." +--- + +# Perforation + +Nicht jeder Schnitt soll ein Teil aus dem Material lösen. Manche Linien sind da, damit sich das +Material entlang ihnen falten lässt, sauber abreißt oder den Weg der Nadel vorgezeichnet. Dafür gibt +es den Nachbearbeitungsschritt **Perforation**: Er macht aus einem durchgehenden Schnitt einen +gestrichelten. Statt den Laser von Anfang bis Ende eingeschaltet zu lassen, feuert er über die +**Schnittlänge**, fährt die **Auslasslänge** mit ausgeschaltetem Laser zurück und wiederholt diesen +Rhythmus entlang jeder Kontur des Werkstücks. Übrig bleibt eine Reihe kleiner Schnitte, die das +Material zusammenhalten — bis es nachgeben soll. + +![Die Perforation-Einstellungen auf der Nachbearbeitung-Seite eines Kontur-Schritts](/screenshots/step-settings-contour-post-perforation.webp) + +## Beispielhafte Anwendungsfälle + +Überall dort, wo eine Linie das Material schwächen soll, ohne es zu durchtrennen, lohnt sich ein +perforierter Schnitt. Die klassischen Fälle: + +- **Falzlinien** in Karton und Wellpappe brechen entlang einer gestrichelten Linie sauber — etwa bei + Schachteln, Karten und Pop-up-Designs. +- **Abreißlinien** öffnen Tickets, Gutscheine und Verpackungen ganz ohne Schere. +- **Nählöcher** in Leder markieren den Weg der Nadel, bevor genäht wird. +- **Gestrichelte Konturen** sind darüber hinaus ein dekorativer Effekt — auf Holz, Papier und Acryl. + +## Perforation aktivieren + +Perforation findet sich auf der Seite **Nachbearbeitung** der Einstellungen eines +[Kontur](operations/contour)-Schritts, neben den übrigen Nachbearbeitungsschritten. Sie ist +standardmäßig ausgeschaltet — sobald der Schalter umgelegt wird, erscheinen zwei Werte: + +- Die **Schnittlänge** gibt an, wie weit der Laser vor jeder Lücke eingeschaltet bleibt. +- Die **Auslasslänge** ist die Strecke, die zwischen zwei Schnitten mit ausgeschaltetem Laser + zurückgelegt wird. + +Das Muster beginnt am Anfang jeder Kontur mit einem vollen Schnitt und beginnt auf jeder Kontur neu, +sodass alle gestrichelten Linien des Werkstücks gleich ansetzen. Eine Kontur, zu klein für auch nur +einen Schnitt und eine Lücke, wird einfach am Stück geschnitten. + +## Das Ergebnis + +Auf der Arbeitsfläche ersetzt die gestrichelte Linie die durchgehende Schnittkontur — jede Lücke im +Werkzeugpfad ist auch eine Lücke im fertigen Teil. + +![Ein Rechteck und ein Kreis werden als gestrichelte Linien in eine Birkenplywood-Platte geschnitten](/screenshots/operations-perforation.webp) + +## Tipps + +Für eine Falzlinie, die nicht durchtrennen soll, beginnt man am besten mit einer Auslasslänge von +etwa der Schnittlänge und senkt die Leistung — eine flache Rille faltet genauso gut wie ein tiefer +gestrichelter Schnitt, und die Unterseite bleibt sauber. + +Sehr kurze Längen (wenige Zehntelmillimeter) lassen den Laser in schneller Folge ein- und +ausschalten, was die effektive Leistung verringert und ungleichmäßige Spuren hinterlassen kann. +Wirkt das Muster uneinheitlich, helfen längere Werte. + +Perforation lässt sich gut mit dem Rest der Nachbearbeitung kombinieren: +[Halte-Laschen](holding-tabs), [Einlauf / Auslauf](lead-in-out) und [Mehrfach-Durchgang](multi-pass) +funktionieren wie gewohnt. + +## Verwandte Seiten + +- [Kontur-Schneiden](operations/contour) — die Schneidoperation, zu der dieser + Nachbearbeitungsschritt gehört +- [Halte-Laschen](holding-tabs) — von Hand gesetzte Lücken, die Teile festhalten diff --git a/website/i18n/es/docusaurus-plugin-content-docs/current/features/operations/contour.md b/website/i18n/es/docusaurus-plugin-content-docs/current/features/operations/contour.md index b7a160a16..610a88b52 100644 --- a/website/i18n/es/docusaurus-plugin-content-docs/current/features/operations/contour.md +++ b/website/i18n/es/docusaurus-plugin-content-docs/current/features/operations/contour.md @@ -205,6 +205,8 @@ Ver [Kerf](../kerf.md) para una guía detallada. Las operaciones de contorno soportan varias opciones de post-procesamiento: - **[Suavizar Trayectoria](../smooth.md)** - Reduce bordes irregulares en trayectorias de corte +- **[Perforación](../perforation.md)** - Cortes discontinuos para pliegues, líneas de rasgado y + agujeros de costura - **[Pestañas de Sujeción](../holding-tabs.md)** - Mantienen las piezas cortadas adjuntas al material base - **[Recortar al Material](../crop-to-stock.md)** - Limita los cortes al límite del material diff --git a/website/i18n/es/docusaurus-plugin-content-docs/current/features/perforation.md b/website/i18n/es/docusaurus-plugin-content-docs/current/features/perforation.md new file mode 100644 index 000000000..26e49563a --- /dev/null +++ b/website/i18n/es/docusaurus-plugin-content-docs/current/features/perforation.md @@ -0,0 +1,70 @@ +--- +description: + "Corta líneas discontinuas en Rayforge con el posprocesador Perforación: define una longitud de + corte y una longitud de salto para pliegues, líneas de rasgado y agujeros de costura." +--- + +# Perforación + +No todos los cortes sirven para separar una pieza del material. Hay líneas que existen para que el +material se pliegue sobre ellas, se rasgue limpiamente o marque el camino de la aguja. Para eso está +el posprocesador **Perforación**: convierte un corte continuo en uno discontinuo. En lugar de +mantener el láser encendido de principio a fin, dispara durante la **Longitud de corte**, recorre la +**Longitud de salto** con el láser apagado, y repite ese ritmo a lo largo de cada contorno de la +pieza. Lo que queda es una línea de cortes pequeños que mantiene unido el material — hasta que +quieras que ceda. + +![Los ajustes de Perforación en la página de posprocesado de un paso de Contorno](/screenshots/step-settings-contour-post-perforation.webp) + +## Algunos usos típicos + +Allí donde una línea deba debilitar el material sin cortarlo por completo, vale la pena un corte +perforado. Los casos clásicos: + +- **Líneas de pliegue** en cartulina y cartón ondulado se pliegan con precisión sobre un corte + discontinuo: cajas, tarjetas y diseños pop-up, por ejemplo. +- **Líneas de rasgado** abren entradas, cupones y envases sin necesidad de tijeras. +- **Agujeros de costura** en cuero marcan el camino de la aguja antes de coser. +- **Contornos discontinuos** son además un efecto decorativo en sí mismos, sobre madera, papel o + acrílico. + +## Activar la perforación + +La perforación se encuentra en la página **Posprocesado** de los ajustes de un paso de +[Contorno](operations/contour), junto a los demás posprocesadores. Viene desactivada por defecto; al +activar el interruptor aparecen dos valores: + +- La **Longitud de corte** es cuánto permanece encendido el láser antes de cada hueco. +- La **Longitud de salto** es la distancia recorrida con el láser apagado entre dos cortes. + +El patrón empieza al inicio de cada contorno con un corte completo y vuelve a empezar en cada +contorno, de modo que todas las líneas discontinuas de la pieza comienzan igual. Un contorno +demasiado corto para un solo corte y un salto simplemente se corta entero. + +## El resultado + +En el lienzo, la línea discontinua sustituye al contorno de corte sólido: cada hueco del trazado es +también un hueco en la pieza terminada. + +![Un rectángulo y un círculo se cortan como líneas discontinuas en una plancha de madera +contrachapada](/screenshots/operations-perforation.webp) + +## Consejos + +Para una línea de pliegue que no debe atravesar el material, empieza con una longitud de salto +parecida a la de corte y baja la potencia: un surco poco profundo pliega igual de bien que un corte +discontinuo profundo, y el reverso queda limpio. + +Las longitudes muy cortas (unas décimas de milímetro) hacen que el láser se encienda y apague a toda +velocidad, lo que reduce la potencia efectiva y puede dejar marcas irregulares. Si el patrón se ve +desigual, prueba con longitudes mayores. + +La perforación se combina bien con el resto del posprocesado: las +[Pestañas de Sujeción](holding-tabs), la [Entrada / Salida](lead-in-out) y la +[Multi-Pasada](multi-pass) siguen funcionando como siempre. + +## Páginas relacionadas + +- [Corte de Contorno](operations/contour) — la operación de corte a la que pertenece este + posprocesador +- [Pestañas de Sujeción](holding-tabs) — huecos colocados a mano para mantener las piezas unidas diff --git a/website/i18n/fr/docusaurus-plugin-content-docs/current/features/operations/contour.md b/website/i18n/fr/docusaurus-plugin-content-docs/current/features/operations/contour.md index 768206b5a..d3af80318 100644 --- a/website/i18n/fr/docusaurus-plugin-content-docs/current/features/operations/contour.md +++ b/website/i18n/fr/docusaurus-plugin-content-docs/current/features/operations/contour.md @@ -210,6 +210,8 @@ Voir [Kerf](../kerf.md) pour un guide détaillé. Les opérations de contour supportent plusieurs options de post-traitement : - **[Lissage de Parcours](../smooth.md)** - Réduire les bords irréguliers dans les parcours de coupe +- **[Perforation](../perforation.md)** - Découpe en pointillés pour plis, lignes prédécoupées et + trous de couture - **[Ponts de Maintien](../holding-tabs.md)** - Maintenir les pièces coupées attachées au matériau de stock - **[Rognage au Stock](../crop-to-stock.md)** - Limiter les coupes à la limite du matériau diff --git a/website/i18n/fr/docusaurus-plugin-content-docs/current/features/perforation.md b/website/i18n/fr/docusaurus-plugin-content-docs/current/features/perforation.md new file mode 100644 index 000000000..0ba276d24 --- /dev/null +++ b/website/i18n/fr/docusaurus-plugin-content-docs/current/features/perforation.md @@ -0,0 +1,71 @@ +--- +description: + "Découpez des lignes en pointillés dans Rayforge avec le post-traitement Perforation : réglez une + longueur de coupe et une longueur de saut pour plis, lignes prédécoupées et trous de couture." +--- + +# Perforation + +Tous les traits ne servent pas à détacher une pièce du matériau. Certains existent pour que le +matériau se plie le long d'eux, se déchire proprement, ou pour marquer le passage de l'aiguille. +C'est le rôle du post-traitement **Perforation** : il transforme une coupe continue en coupe en +pointillés. Au lieu de laisser le laser allumé du début à la fin, il tire sur la **Longueur de +coupe**, parcourt la **Longueur de saut** laser éteint, et répète ce rythme le long de chaque +contour de la pièce. Il reste une ligne de petites coupes qui maintient le matériau — jusqu'à ce que +vous décidiez de le plier ou de le déchirer. + +![Les réglages de la Perforation sur la page de post-traitement d'une étape de Contour](/screenshots/step-settings-contour-post-perforation.webp) + +## Quelques cas d'usage + +Chaque fois qu'un trait doit affaiblir le matériau sans le couper, une coupe perforée a du sens. Les +grands classiques : + +- **Les lignes de pliage** dans le carton et le carton ondulé se plient net le long d'une coupe en + pointillés — boîtes, cartes et créations pop-up, par exemple. +- **Les lignes prédécoupées** ouvrent billets, bons et emballages sans ciseaux. +- **Les trous de couture** dans le cuir marquent le passage de l'aiguille avant la couture. +- **Les contours en pointillés** sont en eux-mêmes un effet décoratif, sur le bois, le papier ou + l'acrylique. + +## Activer la perforation + +La perforation se trouve sur la page **Post-traitement** des réglages d'une étape de +[Contour](operations/contour), aux côtés des autres post-traitements. Elle est désactivée par défaut +; dès que vous basculez l'interrupteur, deux valeurs apparaissent : + +- La **Longueur de coupe** est la distance pendant laquelle le laser reste allumé avant chaque + interruption. +- La **Longueur de saut** est la distance parcourue laser éteint entre deux coupes. + +Le motif recommence au début de chaque contour, toujours par une coupe complète, pour que toutes les +lignes en pointillés de la pièce démarrent de la même façon. Un contour trop court pour ne serait-ce +qu'une coupe et un saut est simplement découpé d'un seul tenant. + +## Le résultat + +Sur le canevas, la ligne en pointillés remplace le contour de coupe plein : chaque interruption du +tracé sera aussi une interruption dans la pièce finie. + +![Un rectangle et un cercle découpés en pointillés dans une planche de contreplaqué](/screenshots/operations-perforation.webp) + +## Conseils + +Pour une ligne de pliage qui ne doit pas traverser le matériau, commencez avec une longueur de saut +proche de la longueur de coupe et baissez la puissance : une rainure peu profonde plie aussi bien +qu'une coupe pointillée profonde, et l'envers reste propre. + +Les longueurs très courtes (quelques dixièmes de millimètre) font allumer et éteindre le laser à +grande cadence, ce qui réduit la puissance effective et peut laisser des traces irrégulières. Si le +motif paraît inégal, essayez des valeurs plus longues. + +La perforation se combine bien avec le reste du post-traitement : les +[Ponts de Maintien](holding-tabs), l'[Approche / Sortie](lead-in-out) et la +[Passe Multiple](multi-pass) continuent de fonctionner comme d'habitude. + +## Pages associées + +- [Coupe de Contour](operations/contour) — l'opération de coupe à laquelle appartient ce + post-traitement +- [Ponts de Maintien](holding-tabs) — interruptions placées à la main pour garder les pièces + attachées diff --git a/website/i18n/hi/docusaurus-plugin-content-docs/current/features/operations/contour.md b/website/i18n/hi/docusaurus-plugin-content-docs/current/features/operations/contour.md index 6b3031fdc..6875460cd 100644 --- a/website/i18n/hi/docusaurus-plugin-content-docs/current/features/operations/contour.md +++ b/website/i18n/hi/docusaurus-plugin-content-docs/current/features/operations/contour.md @@ -207,6 +207,7 @@ _Step Settings_ टैब का **कॉन्टूर सेटिंग् कॉन्टूर ऑपरेशन कई पोस्ट-प्रोसेसिंग विकल्प समर्थन करते हैं: - **[पाथ चिकना करें](../smooth.md)** - कटिंग पाथ में खुरदरे किनारे घटाएँ +- **[परफोरेशन](../perforation.md)** - मोड़, फाड़ने की लाइन और सिलाई के छेदों के लिए डैश वाली कटिंग - **[होल्डिंग टैब](../holding-tabs.md)** - कटे हुए पार्टों को स्टॉक सामग्री से बंधा रखें - **[स्टॉक पर क्रॉप करें](../crop-to-stock.md)** - कट सामग्री सीमा तक सीमित करें - **[पाथ अनुकूलन](../path-optimization.md)** - कटों के बीच ट्रैवल दूरी घटाएँ diff --git a/website/i18n/hi/docusaurus-plugin-content-docs/current/features/perforation.md b/website/i18n/hi/docusaurus-plugin-content-docs/current/features/perforation.md new file mode 100644 index 000000000..7c5d4ec5a --- /dev/null +++ b/website/i18n/hi/docusaurus-plugin-content-docs/current/features/perforation.md @@ -0,0 +1,63 @@ +--- +description: + "Rayforge में परफोरेशन पोस्ट-प्रोसेसर से डैश वाली लाइनें काटें: मोड़, फाड़ने की लाइन और सिलाई के + छेदों के लिए कट लंबाई और स्किप लंबाई सेट करें।" +--- + +# परफोरेशन + +हर कट का मकसद मटेरियल से पार्ट अलग करना नहीं होता। कुछ लाइनें इसलिए होती हैं कि मटेरियल उन पर मुड़े, +साफ फटे, या सुई का रास्ता पहले से निशान हो। इसी काम के लिए **परफोरेशन** पोस्ट-प्रोसेसर है: यह लगातार +कट को डैश वाले कट में बदल देता है। लेज़र को शुरू से आखिर तक जलाए रखने के बजाय यह **कट लंबाई** तक +चलता है, फिर **स्किप लंबाई** तक लेज़र बंद करके आगे बढ़ता है, और वर्कपीस के हर कंटूर पर यही लय +दोहराता है। नतीजा छोटे-छोटे कटों की एक लाइन होती है जो मटेरियल को जोड़े रखती है — जब तक आप खुद उसे +मोड़ना या फाड़ना न चाहें। + +![कंटूर स्टेप की पोस्ट-प्रोसेसिंग पेज पर परफोरेशन की सेटिंग्स](/screenshots/step-settings-contour-post-perforation.webp) + +## कुछ सामान्य उपयोग + +जहाँ भी कोई लाइन मटेरियल को काटे बिना कमज़ोर करना चाहती है, वहाँ परफोरेटेड कट काम आता है। कुछ प्रमुख +उदाहरण: + +- कार्ड स्टॉक और नालीदार गत्ते में **मोड़ की लाइनें** डैश वाले कट पर बिल्कुल साफ मुड़ती हैं — जैसे + डिब्बे, कार्ड और पॉप-अप डिज़ाइन। +- **फाड़ने की लाइनें** टिकट, कूपन और पैकेजिंग बिना कैंची खोल देती हैं। +- चमड़े में **सिलाई के छेद** सिलाई से पहले सुई का रास्ता दिखा देते हैं। +- **डैश वाली कंटूरें** अपने आप में भी एक सजावटी अंदाज़ हैं — लकड़ी, कागज़ और ऐक्रेलिक पर। + +## परफोरेशन चालू करना + +परफोरेशन [कंटूर](operations/contour) स्टेप की सेटिंग्स में **पोस्ट-प्रोसेसिंग** पेज पर मिलती है, +बाकी पोस्ट-प्रोसेसरों के साथ। यह डिफ़ॉल्ट रूप से बंद रहती है; स्विच चालू करते ही दो मान सामने आते +हैं: + +- **कट लंबाई**: हर गैप से पहले लेज़र जितनी दूरी तक जलता रहता है। +- **स्किप लंबाई**: दो कटों के बीच लेज़र बंद करके तय की गई दूरी। + +पैटर्न हर कंटूर की शुरुआत में एक पूरे कट के साथ शुरू होता है और हर कंटूर पर नए सिरे से शुरू होता है, +ताकि वर्कपीस की सारी डैश वाली लाइनें एक जैसे अंदाज़ में शुरू हों। जो कंटूर एक कट और एक स्किप के लिए +भी छोटा हो, उसे पूरा काट दिया जाता है। + +## नतीजा + +कैनवस पर डैश वाली लाइन सीधे कट की सॉलिड रूपरेखा की जगह ले लेती है — टूलपाथ का हर गैप तैयार पार्ट में +भी गैप ही होता है। + +![एक आयत और एक वृत्त को प्लाईवुड की चादर में डैश वाली लाइनों की तरह काटा गया](/screenshots/operations-perforation.webp) + +## सुझाव + +जिस मोड़ की लाइन को आर-पार नहीं काटना है, उसके लिए कट लंबाई जितनी स्किप लंबाई से शुरू करें और पावर +कम करें — उथली नाली गहरे डैश वाले कट जितनी ही अच्छी मुड़ती है, और पिछला हिस्सा साफ रहता है। + +बहुत छोटी लंबाइयाँ (मिलीमीटर के कुछ दसवें हिस्से) लेज़र को तेज़ी से चालू-बंद करवाती हैं, जिससे +प्रभावी पावर घटती है और असमान निशान पड़ सकते हैं। पैटर्न असमान लगे तो लंबी वैल्यू आज़माएँ। + +परफोरेशन बाकी पोस्ट-प्रोसेसिंग के साथ अच्छी तरह चलती है: [होल्डिंग टैब](holding-tabs), +[लीड-इन / लीड-आउट](lead-in-out) और [मल्टी-पास](multi-pass) पहले की तरह ही काम करते रहते हैं। + +## संबंधित पेज + +- [कॉन्टूर कटिंग](operations/contour) — वह कटिंग ऑपरेशन जिससे यह पोस्ट-प्रोसेसर जुड़ा है +- [होल्डिंग टैब](holding-tabs) — पार्टों को जोड़े रखने के लिए हाथ से लगाए गए गैप diff --git a/website/i18n/pt-BR/docusaurus-plugin-content-docs/current/features/operations/contour.md b/website/i18n/pt-BR/docusaurus-plugin-content-docs/current/features/operations/contour.md index ce07b0f9a..e8c42538b 100644 --- a/website/i18n/pt-BR/docusaurus-plugin-content-docs/current/features/operations/contour.md +++ b/website/i18n/pt-BR/docusaurus-plugin-content-docs/current/features/operations/contour.md @@ -207,6 +207,8 @@ Veja [Kerf](../kerf.md) para um guia detalhado. Operações de contorno suportam várias opções de pós-processamento: - **[Suavização de Caminho](../smooth.md)** - Reduz bordas irregulares em caminhos de corte +- **[Perfuração](../perforation.md)** - Cortes tracejados para dobras, linhas destacáveis e furos de + costura - **[Abas de Fixação](../holding-tabs.md)** - Mantém peças cortadas anexadas ao material de estoque - **[Cortar para Estoque](../crop-to-stock.md)** - Limita cortes ao limite do material - **[Otimização de Caminho](../path-optimization.md)** - Reduz distância de deslocamento entre diff --git a/website/i18n/pt-BR/docusaurus-plugin-content-docs/current/features/perforation.md b/website/i18n/pt-BR/docusaurus-plugin-content-docs/current/features/perforation.md new file mode 100644 index 000000000..3235e9eff --- /dev/null +++ b/website/i18n/pt-BR/docusaurus-plugin-content-docs/current/features/perforation.md @@ -0,0 +1,67 @@ +--- +description: + "Corte linhas tracejadas no Rayforge com o pós-processador Perfuração: defina um comprimento de + corte e um comprimento de salto para dobras, linhas destacáveis e furos de costura." +--- + +# Perfuração + +Nem todo corte serve para soltar uma peça do material. Algumas linhas existem para que o material +dobre sobre elas, se rasgue com limpeza ou mostre por onde a agulha vai passar. É para isso que +serve o pós-processador **Perfuração**: ele transforma um corte contínuo em um corte tracejado. Em +vez de manter o laser aceso do começo ao fim, ele dispara pelo **Comprimento de corte**, percorre o +**Comprimento de salto** com o laser desligado e repete esse ritmo ao longo de cada contorno da +peça. O que sobra é uma linha de cortes pequenos que mantém o material unido — até que você decida +dobrá-lo ou rasgá-lo. + +![As configurações da Perfuração na página de pós-processamento de uma etapa de Contorno](/screenshots/step-settings-contour-post-perforation.webp) + +## Alguns usos típicos + +Sempre que uma linha deve enfraquecer o material sem cortá-lo por completo, vale a pena um corte +perfurado. Os casos clássicos: + +- **Linhas de dobra** em cartolina e papelão ondulado dobram com precisão sobre um corte tracejado — + caixas, cartões e designs pop-up, por exemplo. +- **Linhas destacáveis** abrem ingressos, cupons e embalagens sem tesoura. +- **Furos de costura** em couro marcam o caminho da agulha antes de costurar. +- **Contornos tracejados** são, por si só, um efeito decorativo — em madeira, papel e acrílico. + +## Ativar a perfuração + +A perfuração fica na página **Pós-processamento** das configurações de uma etapa de +[Contorno](operations/contour), junto aos demais pós-processadores. Ela vem desativada por padrão; +ao ligar o interruptor, aparecem dois valores: + +- O **Comprimento de corte** é a distância em que o laser permanece aceso antes de cada intervalo. +- O **Comprimento de salto** é a distância percorrida com o laser desligado entre dois cortes. + +O padrão recomeça no início de cada contorno, sempre com um corte completo, para que todas as linhas +tracejadas da peça comecem da mesma forma. Um contorno curto demais para um único corte e um salto é +simplesmente cortado inteiro. + +## O resultado + +Na área de desenho, a linha tracejada substitui o contorno de corte sólido: cada intervalo do +caminho será também um intervalo na peça pronta. + +![Um retângulo e um círculo cortados como linhas tracejadas em uma chapa de compensado](/screenshots/operations-perforation.webp) + +## Dicas + +Para uma linha de dobra que não deve atravessar o material, comece com um comprimento de salto +parecido com o de corte e reduza a potência: um sulco raso dobra tão bem quanto um corte tracejado +profundo, e o verso fica limpo. + +Comprimentos muito curtos (alguns décimos de milímetro) fazem o laser ligar e desligar em sequência +rápida, o que reduz a potência efetiva e pode deixar marcas irregulares. Se o padrão aparecer +desigual, experimente valores maiores. + +A perfuração convive bem com o resto do pós-processamento: as [Abas de Fixação](holding-tabs), o +[Aproximação / Saída](lead-in-out) e a [Multi-Passagem](multi-pass) continuam funcionando como +sempre. + +## Páginas relacionadas + +- [Corte de Contorno](operations/contour) — a operação de corte à qual este pós-processador pertence +- [Abas de Fixação](holding-tabs) — intervalos colocados à mão para manter as peças presas diff --git a/website/i18n/uk/docusaurus-plugin-content-docs/current/features/operations/contour.md b/website/i18n/uk/docusaurus-plugin-content-docs/current/features/operations/contour.md index ed5156b72..5e1803498 100644 --- a/website/i18n/uk/docusaurus-plugin-content-docs/current/features/operations/contour.md +++ b/website/i18n/uk/docusaurus-plugin-content-docs/current/features/operations/contour.md @@ -200,6 +200,8 @@ Операції контуру підтримують кілька опцій постобробки: - **[Згладжування шляху](../smooth.md)** - Зменшення зубчастих країв у шляхах різання +- **[Перфорація](../perforation.md)** - Пунктирне різання для ліній згину, відривних ліній і отворів + для шиття - **[Утримуючі лапки](../holding-tabs.md)** - Утримання вирізаних деталей прикріпленими до матеріалу заготовки - **[Обрізка до заготовки](../crop-to-stock.md)** - Обмеження різів до межі матеріалу diff --git a/website/i18n/uk/docusaurus-plugin-content-docs/current/features/perforation.md b/website/i18n/uk/docusaurus-plugin-content-docs/current/features/perforation.md new file mode 100644 index 000000000..e04c31a1a --- /dev/null +++ b/website/i18n/uk/docusaurus-plugin-content-docs/current/features/perforation.md @@ -0,0 +1,65 @@ +--- +description: + "Різання пунктирних ліній у Rayforge за допомогою постобробки Перфорація: задайте довжину різу й + довжину пропуску для ліній згину, відривних ліній і отворів для шиття." +--- + +# Перфорація + +Не кожен різ має відділити деталь від матеріалу. Деякі лінії потрібні, щоб матеріал згинався вздовж +них, чисто відривався чи показував, куди піде голка. Для цього існує постобробка **Перфорація**: +вона перетворює суцільний різ на пунктирний. Замість того щоб тримати лазер увімкненим від початку +до кінця, він різе на **Довжині різу**, проходить **Довжину пропуску** з вимкненим лазером і +повторює цей ритм уздовж кожного контуру заготовки. У результаті лишається лінія з дрібних різів, +яка тримає матеріал докупи — доти, доки ви не захочете його зігнути чи відрвати. + +![Налаштування Перфорації на сторінці постобробки кроку Контур](/screenshots/step-settings-contour-post-perforation.webp) + +## Типові застосування + +Скрізь, де лінія має послабити матеріал, не розрізаючи його, варто спробувати перфорований різ. +Класичні приклади: + +- **Лінії згину** в картоні та гофрокартоні згинаються рівно вздовж пунктирного різу — коробки, + листівки та pop-up дизайни. +- **Відривні лінії** дають змогу відкривати квитки, купони та упаковку без ножиць. +- **Отвори для шиття** в шкірі позначають шлях голки ще до шиття. +- **Пунктирні контури** самі собою є декоративним ефектом — на дереві, папері та акрилі. + +## Увімкнення перфорації + +Перфорація живе на сторінці **Постобробка** в налаштуваннях кроку [Контур](operations/contour), +поруч з іншими постобробками. Типово вона вимкнена; щойно ви перемикаєте тумблер, з'являються два +значення: + +- **Довжина різу** — як далеко лазер лишається увімкненим перед кожним проміжком. +- **Довжина пропуску** — відстань, яку лазер проходить вимкненим між двома різами. + +Візерунок починається на початку кожного контуру з повного різу й поновлюється на кожному контурі, +тож усі пунктирні лінії заготовки починаються однаково. Контур, замалий навіть для одного різу й +одного пропуску, просто вирізається цілим. + +## Результат + +На полотні пунктирна лінія замінює суцільний контур різу — кожен проміжок у траєкторії буде +проміжком і в готовій деталі. + +![Прямокутник і коло, вирізані пунктирними лініями в аркуші фанери](/screenshots/operations-perforation.webp) + +## Поради + +Для лінії згину, яка не повинна прорізати наскрізь, почніть із довжиною пропуску, приблизно рівною +довжині різу, і зменште потужність — мілка канавка згинається так само добре, як глибокий пунктирний +різ, а виворіт лишається чистим. + +Дуже малі довжини (кілька десятих міліметра) змушують лазер швидко вмикатися й вимикатися, що +зменшує ефективну потужність і може лишати нерівні сліди. Якщо візерунок виглядає нерівномірно, +спробуйте більші значення. + +Перфорація добре поєднується з рештою постобробки: [Утримуючі лапки](holding-tabs), +[Вхід / Вихід](lead-in-out) і [Багатопрохідність](multi-pass) працюють як завжди. + +## Пов'язані сторінки + +- [Контурне різання](operations/contour) — операція різання, до якої належить ця постобробка +- [Утримуючі лапки](holding-tabs) — проміжки, розставлені вручну, щоб деталі не випадали diff --git a/website/i18n/zh-CN/docusaurus-plugin-content-docs/current/features/operations/contour.md b/website/i18n/zh-CN/docusaurus-plugin-content-docs/current/features/operations/contour.md index a31f024a4..d7c62834c 100644 --- a/website/i18n/zh-CN/docusaurus-plugin-content-docs/current/features/operations/contour.md +++ b/website/i18n/zh-CN/docusaurus-plugin-content-docs/current/features/operations/contour.md @@ -192,6 +192,7 @@ 轮廓操作支持多种后处理选项: - **[平滑路径](../smooth.md)** - 减少切割路径中的锯齿状边缘 +- **[穿孔](../perforation.md)** - 用于折线、撕线和缝线孔的虚线切割 - **[固定标签](../holding-tabs.md)** - 保持切割件与材料连接 - **[裁剪到材料](../crop-to-stock.md)** - 将切割限制在材料边界内 - **[路径优化](../path-optimization.md)** - 减少切割之间的行程距离 diff --git a/website/i18n/zh-CN/docusaurus-plugin-content-docs/current/features/perforation.md b/website/i18n/zh-CN/docusaurus-plugin-content-docs/current/features/perforation.md new file mode 100644 index 000000000..58e32fb8a --- /dev/null +++ b/website/i18n/zh-CN/docusaurus-plugin-content-docs/current/features/perforation.md @@ -0,0 +1,47 @@ +--- +description: "使用穿孔后处理器在 Rayforge 中切割虚线:为折线、撕线和缝线孔设置切割长度和跳过长度。" +--- + +# 穿孔 + +并不是每条切割线都是为了把零件从材料上取下来。有些线的意义在于让材料沿着它折叠、干净地撕开,或预先标出缝针的走向。**穿孔**后处理正是为此而生:它把连续切割变成虚线切割。激光不再从头到尾一直出光,而是按**切割长度**出光,再关闭激光走过**跳过长度**,并沿工件的每条轮廓重复这一节奏。留下的是一串小切割,它们把材料连在一起——直到你想让它折或撕开的那一刻。 + +![轮廓步骤后处理页面中的穿孔设置](/screenshots/step-settings-contour-post-perforation.webp) + +## 典型用途 + +凡是需要削弱材料而又不把它切开的场合,都值得考虑穿孔切割。常见的例子: + +- 卡纸和瓦楞纸板上的**折线**沿虚线切口干净利落地折叠——比如盒子、卡片和立体弹出设计。 +- **撕线**让门票、优惠券和包装不需要剪刀就能打开。 +- 皮革上的**缝线孔**在缝纫前标出针的走向。 +- **虚线轮廓**本身就是一种装饰效果,适合木材、纸张和亚克力。 + +## 启用穿孔 + +穿孔位于[轮廓](operations/contour)步骤设置中的**后处理**页面,与其他后处理并列。它默认关闭;打开开关后会出现两个数值: + +- **切割长度**:每个间隙前激光持续出光的距离。 +- **跳过长度**:两段切割之间关闭激光移动的距离。 + +图案从每条轮廓的起点的一段完整切割开始,并在每条轮廓上重新开始,因此工件上所有虚线都以同样的方式起头。短到连一段切割加一段跳跃都放不下的轮廓会被完整切下。 + +## 效果 + +在画布上,虚线取代了实线切割轮廓——刀具路径上的每个间隙就是成品零件上的每个间隙。 + +![矩形和圆形以虚线形式切入桦木胶合板](/screenshots/operations-perforation.webp) + +## 提示 + +对于不应切穿的折线,可以先让跳过长度与切割长度大致相同,并降低功率——浅浅的压痕和深深的虚线切口一样容易折,而且背面保持整洁。 + +非常短的长度(零点几毫米)会让激光快速开关,降低有效功率,还可能留下不均匀的痕迹。如果图案看起来不一致,试试更长的数值。 + +穿孔与后处理的其他功能配合良好:[固定标签](holding-tabs)、[引入 / 引出](lead-in-out)和 +[多次遍](multi-pass)都照常工作。 + +## 相关页面 + +- [轮廓切割](operations/contour) — 本后处理所属的切割操作 +- [固定标签](holding-tabs) — 手动放置以固定零件的间隙 diff --git a/website/sidebars.js b/website/sidebars.js index a293e104e..e2c9793c8 100644 --- a/website/sidebars.js +++ b/website/sidebars.js @@ -78,6 +78,7 @@ module.exports = { 'features/crop-to-stock', 'features/overscan', 'features/holding-tabs', + 'features/perforation', 'features/merge-lines', 'features/lead-in-out', ], diff --git a/website/static/screenshots/operations-perforation.webp b/website/static/screenshots/operations-perforation.webp new file mode 100644 index 000000000..2dc331705 Binary files /dev/null and b/website/static/screenshots/operations-perforation.webp differ diff --git a/website/static/screenshots/step-settings-contour-post-perforation.webp b/website/static/screenshots/step-settings-contour-post-perforation.webp new file mode 100644 index 000000000..8fa88c218 Binary files /dev/null and b/website/static/screenshots/step-settings-contour-post-perforation.webp differ diff --git a/website/static/screenshots/step-settings-contour-post.webp b/website/static/screenshots/step-settings-contour-post.webp index cd4621a9b..b32d952a1 100644 Binary files a/website/static/screenshots/step-settings-contour-post.webp and b/website/static/screenshots/step-settings-contour-post.webp differ