Skip to content

Commit

Permalink
Add region handling of boundary points from arc (#61)
Browse files Browse the repository at this point in the history
Inspired by @jekoie issue
[#222](curtacircuitos/pcb-tools#222) from
[pcb-tools](https://github.com/curtacircuitos/pcb-tools) repository

> Below gerber file, why pcb-tools draw it in a wrong way?
> 
> ```
> G04 This file illustrates how to use levels to create holes*
> %FSLAX25Y25*%
> %MOMM*%
> G01*
> G04 First level: big square - dark polarity*
> %LPD*%
> G36*
> X250000Y250000D02*
> X1750000D01*
> Y1750000D01*
> X250000D01*
> Y250000D01*
> G37*
> G04 Second level: big circle - clear polarity*
> %LPC*%
> G36*
> G75*
> X500000Y1000000D02*
> G03*
> X500000Y1000000I500000J0D01*
> G37*
> G04 Third level: small square - dark polarity*
> %LPD*%
> G36*
> X750000Y750000D02*
> X1250000D01*
> Y1250000D01*
> X750000D01*
> Y750000D01*
> G37*
> G04 Fourth level: small circle - clear polarity*
> %LPC*%
> G36*
> G75*
> X1150000Y1000000D02*
> G03*
> X1150000Y1000000I250000J0D01*
> G37*
> M02*
> ```
> 
> It should be this: 
>
![image](https://user-images.githubusercontent.com/6836400/172787712-5ddd91e5-4682-44b9-943a-ffee0fd6e45c.png)
> 
> pcb-tools draw this: 
>
![image](https://user-images.githubusercontent.com/6836400/172787860-53f069e2-cfad-451a-b649-dacf4ab74673.png)

Above code mostly works, but required additional `G01` in before
rectangle region creation:

```
G04 Third level: small square - dark polarity*
G01*
```

Additionally, during development of this feature I might have revealed
hidden issue with centering images with debug features enabled, further
investigation is needed.
  • Loading branch information
Argmaster authored Sep 12, 2023
2 parents d5c3f25 + edffdf0 commit d29c8d1
Show file tree
Hide file tree
Showing 7 changed files with 186 additions and 67 deletions.
38 changes: 38 additions & 0 deletions src/pygerber/backend/abstract/draw_commands/draw_arc.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
"""Base class for creating components for aperture creation."""
from __future__ import annotations

import math
from functools import cached_property
from typing import Generator

from pygerber.backend.abstract.backend_cls import Backend
from pygerber.backend.abstract.draw_commands.draw_command import DrawCommand
Expand Down Expand Up @@ -79,3 +81,39 @@ def _bounding_box(self) -> BoundingBox:
return (vertex_box + (self.arc_center_absolute + radius)) + (
vertex_box + (self.arc_center_absolute - radius)
)

def _calculate_angles(self) -> tuple[float, float]:
angle_start = self.arc_space_start_position.angle_between_clockwise(
Vector2D.UNIT_Y,
)
angle_end = self.arc_space_end_position.angle_between_clockwise(Vector2D.UNIT_Y)

if self.is_multi_quadrant and angle_start == angle_end:
angle_start = 0
angle_end = 360

elif self.is_clockwise:
angle_start, angle_end = angle_end, angle_start

return angle_start, angle_end

def calculate_arc_points(self) -> Generator[Vector2D, None, None]:
"""Calculate points on arc."""
angle_start, angle_end = self._calculate_angles()

angle_step = 1
angle_min = min(angle_start, angle_end)
angle_max = max(angle_start, angle_end)

angle_current = angle_min

yield self.arc_center_absolute

while angle_current < (angle_max + angle_step):
yield self.arc_center_absolute + Vector2D(
x=self.arc_radius * math.cos(math.radians(angle_current)),
y=self.arc_radius * math.sin(math.radians(angle_current)),
)
angle_current += angle_step

yield self.arc_center_absolute
13 changes: 1 addition & 12 deletions src/pygerber/backend/rasterized_2d/draw_commands/draw_arc.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
from pygerber.backend.abstract.draw_commands.draw_arc import DrawArc
from pygerber.backend.abstract.drawing_target import DrawingTarget
from pygerber.backend.rasterized_2d.drawing_target import Rasterized2DDrawingTarget
from pygerber.gerberx3.math.vector_2d import Vector2D

if TYPE_CHECKING:
from pygerber.backend.rasterized_2d.backend_cls import Rasterized2DBackend
Expand All @@ -27,17 +26,7 @@ def draw(self, target: DrawingTarget) -> None:
bbox = self.get_bounding_box() - target.coordinate_origin
pixel_box = bbox.as_pixel_box(self.backend.dpi)

angle_start = self.arc_space_start_position.angle_between_clockwise(
Vector2D.UNIT_Y,
)
angle_end = self.arc_space_end_position.angle_between_clockwise(Vector2D.UNIT_Y)

if self.is_multi_quadrant and angle_start == angle_end:
angle_start = 0
angle_end = 360

elif self.is_clockwise:
angle_start, angle_end = angle_end, angle_start
angle_start, angle_end = self._calculate_angles()

width = self.width.as_pixels(self.backend.dpi)

Expand Down
151 changes: 96 additions & 55 deletions src/pygerber/gerberx3/tokenizer/tokens/d01_draw.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
"""Wrapper for plot operation token."""
from __future__ import annotations

from typing import TYPE_CHECKING, Any, Iterable, Tuple
from typing import TYPE_CHECKING, Any, Generator, Iterable, Tuple

from pygerber.gerberx3.math.offset import Offset
from pygerber.gerberx3.math.vector_2d import Vector2D
from pygerber.gerberx3.state_enums import DrawMode
from pygerber.gerberx3.state_enums import DrawMode, Polarity
from pygerber.gerberx3.tokenizer.tokens.coordinate import Coordinate, CoordinateType
from pygerber.gerberx3.tokenizer.tokens.token import Token

Expand Down Expand Up @@ -55,39 +56,108 @@ def update_drawing_state(
start_position = state.current_position

draw_commands: list[DrawCommand] = []
current_aperture = backend.get_private_aperture_handle(
state.get_current_aperture(),
)

if state.is_region:
polarity = state.polarity.to_region_variant()
else:
polarity = state.polarity

draw_commands.append(
backend.get_draw_paste_cls()(
backend=backend,
polarity=polarity,
center_position=start_position,
other=current_aperture.drawing_target,
if not state.is_region or backend.options.draw_region_outlines:
draw_commands.extend(
self._create_draw_commands(
state,
backend,
end_position,
start_position,
polarity,
),
)

if state.is_region:
self._create_region_points(
state,
backend,
end_position,
start_position,
polarity,
)

return (
state.model_copy(
update={
"current_position": end_position,
},
),
draw_commands,
)

def _create_region_points( # noqa: PLR0913
self,
state: State,
backend: Backend,
end_position: Vector2D,
start_position: Vector2D,
polarity: Polarity,
) -> None:
if state.draw_mode == DrawMode.Linear:
if state.is_region:
state.region_boundary_points.append(start_position)
state.region_boundary_points.append(end_position)
state.region_boundary_points.append(start_position)
state.region_boundary_points.append(end_position)

draw_commands.append(
backend.get_draw_vector_line_cls()(
elif state.draw_mode in (
DrawMode.ClockwiseCircular,
DrawMode.CounterclockwiseCircular,
):
i = state.parse_coordinate(self.i)
j = state.parse_coordinate(self.j)

center_offset = Vector2D(x=i, y=j)

state.region_boundary_points.extend(
backend.get_draw_arc_cls()(
backend=backend,
polarity=polarity,
start_position=start_position,
dx_dy_center=center_offset,
end_position=end_position,
width=current_aperture.get_line_width(),
),
width=Offset.NULL,
is_clockwise=(state.draw_mode == DrawMode.ClockwiseCircular),
# Will require tweaking if support for single quadrant mode
# will be desired.
is_multi_quadrant=True,
).calculate_arc_points(),
)

else:
raise NotImplementedError(state.draw_mode)

def _create_draw_commands( # noqa: PLR0913
self,
state: State,
backend: Backend,
end_position: Vector2D,
start_position: Vector2D,
polarity: Polarity,
) -> Generator[DrawCommand, None, None]:
current_aperture = backend.get_private_aperture_handle(
state.get_current_aperture(),
)
yield backend.get_draw_paste_cls()(
backend=backend,
polarity=polarity,
center_position=start_position,
other=current_aperture.drawing_target,
)

if state.draw_mode == DrawMode.Linear:
if not state.is_region or backend.options.draw_region_outlines:
yield backend.get_draw_vector_line_cls()(
backend=backend,
polarity=polarity,
start_position=start_position,
end_position=end_position,
width=current_aperture.get_line_width(),
)

elif state.draw_mode in (
DrawMode.ClockwiseCircular,
DrawMode.CounterclockwiseCircular,
Expand All @@ -96,15 +166,8 @@ def update_drawing_state(
j = state.parse_coordinate(self.j)

center_offset = Vector2D(x=i, y=j)

if state.is_region:
state.region_boundary_points.append(start_position)
# TODO([email protected]): Add region boundary points for region
# https://github.com/Argmaster/pygerber/issues/29
state.region_boundary_points.append(end_position)

draw_commands.append(
backend.get_draw_arc_cls()(
if not state.is_region or backend.options.draw_region_outlines:
yield backend.get_draw_arc_cls()(
backend=backend,
polarity=polarity,
start_position=start_position,
Expand All @@ -115,38 +178,16 @@ def update_drawing_state(
# Will require tweaking if support for single quadrant mode
# will be desired.
is_multi_quadrant=True,
),
)
)

else:
raise NotImplementedError(state.draw_mode)

draw_commands.append(
backend.get_draw_paste_cls()(
backend=backend,
polarity=polarity,
center_position=end_position,
other=current_aperture.drawing_target,
),
)

if not state.is_region or backend.options.draw_region_outlines:
return (
state.model_copy(
update={
"current_position": end_position,
},
),
draw_commands,
)

return (
state.model_copy(
update={
"current_position": end_position,
},
),
(),
yield backend.get_draw_paste_cls()(
backend=backend,
polarity=polarity,
center_position=end_position,
other=current_aperture.drawing_target,
)

def __str__(self) -> str:
Expand Down
40 changes: 40 additions & 0 deletions test/assets/gerberx3/pcb_tools_issues/222/main.grb
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
G04 This file illustrates how to use levels to create holes*
%FSLAX25Y25*%
%MOMM*%
G01*
G04 First level: big square - dark polarity*
%LPD*%
G36*
X250000Y250000D02*
X1750000D01*
Y1750000D01*
X250000D01*
Y250000D01*
G37*
G04 Second level: big circle - clear polarity*
%LPC*%
G36*
G75*
X500000Y1000000D02*
G03*
X500000Y1000000I500000J0D01*
G37*
G04 Third level: small square - dark polarity*
G01*
%LPD*%
G36*
X750000Y750000D02*
X1250000D01*
Y1250000D01*
X750000D01*
Y750000D01*
G37*
G04 Fourth level: small circle - clear polarity*
%LPC*%
G36*
G75*
X1150000Y1000000D02*
G03*
X1150000Y1000000I250000J0D01*
G37*
M02*
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
11 changes: 11 additions & 0 deletions test/gerberx3/test_rasterized_2d/test_pcb_tools_issues.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
"""Rasterized2D tests based on KicadGerberX2 board."""

from __future__ import annotations

from test.gerberx3.test_rasterized_2d.common import make_rasterized_2d_test

test_sample = make_rasterized_2d_test(
__file__,
"test/assets/gerberx3/pcb_tools_issues",
dpi=1000,
)

0 comments on commit d29c8d1

Please sign in to comment.