Skip to content

Commit

Permalink
Add virtual machine based on shapely library (#311)
Browse files Browse the repository at this point in the history
This pull request introduces new implementation of `VirtualMachine`
interface, `ShapelyVirtualMachine` utilizing `shapely` library for
generation of complex geometry. Alongside the implementation, unit and
e2e tests are included. Currently `ShapelyResult` allows only SVG
export.
Overall performance of implementation is above expectations, seems to be
very close to what `pillow` based implementation offers. On Ryzen 9
7950X rendering of
[A64-OlinuXino_Rev_G-B_Cu.gbr](https://github.com/Argmaster/pygerber/blob/main/test/assets/gerberx3/A64_OLinuXino_rev_G/A64-OlinuXino_Rev_G-B_Cu.gbr)
takes around 20s, including parsing and compilation to RVMC. However I
must admit that this is still like 10x slower than Reference Gerber
Viewer, no idea what kind of black magic they are using to render this
so quickly 😄

Here is sample output:

<p align="center">
<img width="500"
src=https://github.com/user-attachments/assets/a5a99999-59b2-46bc-befe-2eeee82bc6ad
/>
</p>

Also, output is much cleaner than with masking approach used in drawsvg
based rendering in PyGerber 2.4.1. Still, drawsvg backend will be likely
implemented, since it is so easy to do so.

This virtual machine is not enabled by default in PyGerber to avoid
depending on shapely and GEOS. It can be installed with `shapely`
extras:

```bash
pip install pygerber[shapely]
```

Also, it is sick how easy it is to implement new virtual machines, VM
code itself is like 370 lines.
  • Loading branch information
Argmaster authored Oct 5, 2024
2 parents df499b9 + 60e789a commit 7da5b8b
Show file tree
Hide file tree
Showing 18 changed files with 1,781 additions and 117 deletions.
63 changes: 61 additions & 2 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ lsprotocol = { version = "^2023.0.0a3", optional = true }
drawsvg = { version = "^2.3.0", optional = true }
typing-extensions = "^4.12.2"
pygments = { version = "^2.18.0", optional = true }
shapely = {version = "^2.0.6", optional = true}

[tool.poetry.group.dev.dependencies]
mypy = "^1.6.1"
Expand Down Expand Up @@ -102,7 +103,8 @@ black = "^24.4.0"
language_server = ["pygls", "lsprotocol"]
svg = ["drawsvg"]
pygments = ["pygments"]
all = ["pygls", "lsprotocol", "drawsvg", "pygments"]
shapely = ["shapely"]
all = ["pygls", "lsprotocol", "drawsvg", "pygments", "shapely"]

[build-system]
requires = ["poetry-core"]
Expand Down
2 changes: 1 addition & 1 deletion src/pygerber/gerber/language_server/status.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ def is_language_server_available() -> bool:
_spec_lsprotocol = importlib.util.find_spec("lsprotocol")

except (ImportError, ValueError):
return False
_IS_LANGUAGE_SERVER_AVAILABLE = False

else:
_IS_LANGUAGE_SERVER_AVAILABLE = (_spec_pygls is not None) and (
Expand Down
9 changes: 3 additions & 6 deletions src/pygerber/gerber/pygments.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,13 @@ def is_pygments_available() -> bool:

if _IS_PYGMENTS_AVAILABLE is None:
try:
_spec_pygls = importlib.util.find_spec("pygls")
_spec_lsprotocol = importlib.util.find_spec("lsprotocol")
_spec_pygments = importlib.util.find_spec("pygments")

except (ImportError, ValueError):
return False
_IS_PYGMENTS_AVAILABLE = False

else:
_IS_PYGMENTS_AVAILABLE = (_spec_pygls is not None) and (
_spec_lsprotocol is not None
)
_IS_PYGMENTS_AVAILABLE = _spec_pygments is not None

return _IS_PYGMENTS_AVAILABLE

Expand Down
7 changes: 6 additions & 1 deletion src/pygerber/vm/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,18 @@


def render(
rvmc: RVMC, *, backend: Literal["pillow"] = "pillow", **options: Any
rvmc: RVMC, *, backend: Literal["pillow", "shapely"] = "pillow", **options: Any
) -> Result:
"""Render RVMC code using given builder."""
if backend == "pillow":
from pygerber.vm.pillow import PillowVirtualMachine

return PillowVirtualMachine(**options).run(rvmc)

if backend == "shapely":
from pygerber.vm.shapely import ShapelyVirtualMachine

return ShapelyVirtualMachine(**options).run(rvmc)

msg = f"Backend '{backend}' is not supported." # type: ignore[unreachable]
raise NotImplementedError(msg)
24 changes: 15 additions & 9 deletions src/pygerber/vm/pillow/vm.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,14 @@
from pygerber.vm.commands import Arc, Line, PasteLayer, Shape
from pygerber.vm.pillow.errors import DPMMTooSmallError
from pygerber.vm.rvmc import RVMC
from pygerber.vm.types.box import Box
from pygerber.vm.types.errors import NoMainLayerError, PasteDeferredLayerNotAllowedError
from pygerber.vm.types.layer_id import LayerID
from pygerber.vm.types.style import Style
from pygerber.vm.types.vector import Vector
from pygerber.vm.types import (
Box,
LayerID,
NoMainLayerError,
PasteDeferredLayerNotAllowedError,
Style,
Vector,
)
from pygerber.vm.vm import (
DeferredLayer,
DrawCmdT,
Expand All @@ -36,7 +39,9 @@


class PillowResult(Result):
"""Result of drawing commands."""
"""The `PillowResult` class is a wrapper around items returned
`PillowVirtualMachine` class as a result of executing rendering instruction.
"""

def __init__(self, main_box: Box, image: Optional[Image.Image]) -> None:
super().__init__(main_box)
Expand Down Expand Up @@ -138,13 +143,15 @@ def __init__(


class PillowVirtualMachine(VirtualMachine):
"""Execute drawing commands using Pillow library."""
"""The `PillowVirtualMachine` class is a concrete implementation of
`VirtualMachine` which uses Shapely library for rendering commands.
"""

def __init__(self, dpmm: int) -> None:
super().__init__()
self.dpmm = dpmm
self.angle_length_to_segment_count = lambda angle_length: (
segment_count
int(segment_count)
if (segment_count := angle_length * 2) > MIN_SEGMENT_COUNT
else MIN_SEGMENT_COUNT
)
Expand Down Expand Up @@ -223,7 +230,6 @@ def _calculate_arc_points(
raise DPMMTooSmallError(self.dpmm)

angle_delta = angle_delta / segment_count

assert angle_delta > 0

angle_generator: Generator[float, None, None]
Expand Down
25 changes: 25 additions & 0 deletions src/pygerber/vm/shapely/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
"""The `shapely` package contains concrete implementation of `VirtualMachine` using
Shapely library.
"""

from __future__ import annotations

from pygerber.vm.shapely.errors import (
ShapelyNotInstalledError,
ShapelyVirtualMachineError,
)
from pygerber.vm.shapely.vm import (
ShapelyDeferredLayer,
ShapelyEagerLayer,
ShapelyResult,
ShapelyVirtualMachine,
)

__all__ = [
"ShapelyVirtualMachine",
"ShapelyEagerLayer",
"ShapelyDeferredLayer",
"ShapelyResult",
"ShapelyNotInstalledError",
"ShapelyVirtualMachineError",
]
21 changes: 21 additions & 0 deletions src/pygerber/vm/shapely/errors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
"""`errors` module aggregates all exceptions related to the `ShapelyVirtualMachine`."""

from __future__ import annotations

from pygerber.vm.types.errors import VirtualMachineError


class ShapelyVirtualMachineError(VirtualMachineError):
"""Base class for all exceptions in the `ShapelyVirtualMachine`."""


class ShapelyNotInstalledError(ShapelyVirtualMachineError):
"""Raised when shapely package or other dependencies of `ShapelyVirtualMachine`
are not installed.
To install all dependencies of `ShapelyVirtualMachine`, run:
```bash
pip install pygerber[shapely]
```
"""
Loading

0 comments on commit 7da5b8b

Please sign in to comment.