Skip to content

Commit d04d9f5

Browse files
committed
refactor hpgl2 add-on
1 parent c2d7479 commit d04d9f5

File tree

7 files changed

+132
-116
lines changed

7 files changed

+132
-116
lines changed

src/ezdxf/addons/hpgl2/api.py

Lines changed: 15 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,16 @@
55
from ezdxf.document import Drawing
66
from ezdxf import zoom, transform
77
from ezdxf.math import Matrix44
8-
from .tokenizer import parse
8+
from .tokenizer import hpgl2_commands
99
from .plotter import Plotter
1010
from .interpreter import Interpreter
11-
from .backend import BoundingBoxDetector
11+
from .backend import Recorder
1212
from .dxf_backend import DXFBackend, ColorMode
1313
from .svg_backend import SVGBackend
1414
from .compiler import build
1515

1616

17-
def plot_to_dxf(
17+
def to_dxf(
1818
b: bytes,
1919
scale: float = 1.0,
2020
*,
@@ -23,20 +23,14 @@ def plot_to_dxf(
2323
) -> Drawing:
2424
doc = ezdxf.new()
2525
msp = doc.modelspace()
26-
commands = parse(b)
2726
plotter = Plotter(
2827
DXFBackend(
2928
msp,
3029
color_mode=color_mode,
3130
map_black_rgb_to_white_rgb=map_black_rgb_to_white_rgb,
3231
)
3332
)
34-
interpreter = Interpreter(plotter)
35-
interpreter.run(commands)
36-
if interpreter.not_implemented_commands:
37-
print(
38-
f"not implemented commands: {sorted(interpreter.not_implemented_commands)}"
39-
)
33+
Interpreter(plotter).run(hpgl2_commands(b))
4034
if plotter.bbox.has_data: # non-empty page
4135
_scale_and_zoom(msp, scale, plotter.bbox)
4236
return doc
@@ -53,24 +47,15 @@ def _scale_and_zoom(layout, scale, bbox):
5347
zoom.window(layout, extmin, extmax)
5448

5549

56-
PROLOG = (
57-
'<?xml version="1.0" encoding="UTF-8"?>\n'
58-
'<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">\n'
59-
)
60-
61-
62-
def plot_to_svg(b: bytes) -> str:
63-
# 1st pass detect extents
64-
detector = BoundingBoxDetector()
65-
plotter = Plotter(detector)
66-
commands = parse(b)
67-
interpreter = Interpreter(plotter)
68-
interpreter.run(commands)
69-
if not detector.bbox.has_data:
50+
def to_svg(b: bytes) -> str:
51+
# 1st pass records the plotting commands and detects the bounding box
52+
recorder = Recorder()
53+
plotter = Plotter(recorder)
54+
Interpreter(plotter).run(hpgl2_commands(b))
55+
if not plotter.bbox.has_data:
7056
return ""
71-
# 2nd pass plot SVG
72-
svg_backend = SVGBackend(detector.bbox)
73-
plotter = Plotter(svg_backend)
74-
interpreter = Interpreter(plotter)
75-
interpreter.run(commands)
76-
return PROLOG + svg_backend.get_string()
57+
58+
# 2nd pass replays the plotting commands to plot the SVG
59+
svg_backend = SVGBackend(plotter.bbox)
60+
recorder.replay(svg_backend)
61+
return svg_backend.get_string()

src/ezdxf/addons/hpgl2/backend.py

Lines changed: 37 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
# Copyright (c) 2023, Manfred Moitzi
22
# License: MIT License
33
from __future__ import annotations
4-
from typing import Sequence
4+
from typing import Sequence, NamedTuple, Any
55
import abc
6+
import enum
7+
68
from .deps import Vec2, Path, Bezier4P, BoundingBox2d
79
from .properties import Properties
810

@@ -16,14 +18,6 @@
1618

1719

1820
class Backend(abc.ABC):
19-
def draw_cubic_bezier(
20-
self, properties: Properties, start: Vec2, ctrl1: Vec2, ctrl2: Vec2, end: Vec2
21-
) -> None:
22-
# input coordinates are page coordinates
23-
curve = Bezier4P([start, ctrl1, ctrl2, end])
24-
# 10 plu = 0.25 mm
25-
self.draw_polyline(properties, list(curve.flattening(distance=10)))
26-
2721
@abc.abstractmethod
2822
def draw_polyline(self, properties: Properties, points: Sequence[Vec2]) -> None:
2923
# input coordinates are page coordinates
@@ -44,27 +38,48 @@ def draw_outline_polygon_buffer(
4438
# input coordinates are page coordinates
4539
...
4640

47-
class BoundingBoxDetector(Backend):
48-
def __init__(self, max_distance=10):
49-
self.max_distance = max_distance
50-
self.bbox = BoundingBox2d()
41+
class RecordType(enum.Enum):
42+
POLYLINE = enum.auto()
43+
FILLED_POLYGON = enum.auto()
44+
OUTLINE_POLYGON = enum.auto()
45+
46+
class DataRecord(NamedTuple):
47+
type: RecordType
48+
property_hash: int
49+
args: Any
50+
51+
class Recorder(Backend):
52+
def __init__(self) -> None:
53+
self.records: list[DataRecord] = []
54+
self.properties: dict[int, Properties] = {}
5155

5256
def draw_polyline(self, properties: Properties, points: Sequence[Vec2]) -> None:
53-
# input coordinates are page coordinates
54-
# argument <points> can be zero, one, two or more points.
55-
self.bbox.extend(points)
57+
self.store(RecordType.POLYLINE, properties, tuple(points))
58+
5659

5760
def draw_filled_polygon_buffer(
5861
self, properties: Properties, paths: Sequence[Path], fill_method: int
5962
) -> None:
60-
# input coordinates are page coordinates
61-
for p in paths:
62-
self.bbox.extend(p.flattening(distance=self.max_distance))
63+
self.store(RecordType.FILLED_POLYGON, properties, tuple(paths), fill_method)
6364

6465
def draw_outline_polygon_buffer(
6566
self, properties: Properties, paths: Sequence[Path]
6667
) -> None:
67-
# input coordinates are page coordinates
68-
for p in paths:
69-
self.bbox.extend(p.flattening(distance=self.max_distance))
68+
self.store(RecordType.OUTLINE_POLYGON, properties, tuple(paths))
69+
70+
def store(self, record_type: RecordType, properties: Properties, *args) -> None:
71+
prop_hash = properties.hash()
72+
if prop_hash not in self.properties:
73+
self.properties[prop_hash] = properties.copy()
74+
self.records.append(DataRecord(record_type, prop_hash, args))
7075

76+
def replay(self, backend: Backend) -> None:
77+
properties = Properties()
78+
draw = {
79+
RecordType.POLYLINE: backend.draw_polyline,
80+
RecordType.FILLED_POLYGON: backend.draw_filled_polygon_buffer,
81+
RecordType.OUTLINE_POLYGON: backend.draw_outline_polygon_buffer,
82+
}
83+
for record in self.records:
84+
properties = self.properties.get(record.property_hash, properties)
85+
draw[record.type](properties, *record.args) # type: ignore

src/ezdxf/addons/hpgl2/plotter.py

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,15 @@
44
from typing import Sequence, Iterator, Iterable
55
import math
66

7-
from .deps import Vec2, Path, NULLVEC2, ConstructionCircle, BoundingBox2d, path_bbox
7+
from .deps import (
8+
Vec2,
9+
Path,
10+
NULLVEC2,
11+
ConstructionCircle,
12+
BoundingBox2d,
13+
path_bbox,
14+
Bezier4P,
15+
)
816
from .properties import RGB, Properties
917
from .backend import Backend
1018
from .polygon_buffer import PolygonBuffer
@@ -57,7 +65,6 @@ def reset(self) -> None:
5765

5866
def setup_page(self, size_x: int, size_y: int):
5967
self.page = Page(size_x, size_y)
60-
self.properties.set_page_size(size_x, size_y)
6168

6269
def set_scaling_points(self, p1: Vec2, p2: Vec2) -> None:
6370
self.page.set_scaling_points(p1, p2)
@@ -272,8 +279,10 @@ def plot_abs_cubic_bezier(self, ctrl1: Vec2, ctrl2: Vec2, end: Vec2):
272279
ctrl1, ctrl2, end = self.page.page_points((ctrl1, ctrl2, end))
273280
# draw cubic bezier curve in absolute page coordinates:
274281
self.update_bbox((ctrl1, ctrl2, end))
275-
self.backend.draw_cubic_bezier(
276-
self.properties, current_page_location, ctrl1, ctrl2, end
282+
curve = Bezier4P([current_page_location, ctrl1, ctrl2, end])
283+
# distance of 10 plu is 0.25 mm
284+
self.backend.draw_polyline(
285+
self.properties, list(curve.flattening(distance=10))
277286
)
278287

279288
def plot_rel_cubic_bezier(self, ctrl1: Vec2, ctrl2: Vec2, end: Vec2):

src/ezdxf/addons/hpgl2/properties.py

Lines changed: 29 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from typing import NamedTuple
55
import dataclasses
66
import enum
7+
import copy
78
from .deps import NULLVEC2
89

910

@@ -32,50 +33,62 @@ class Pen:
3233

3334

3435
class Properties:
35-
DEFAULT_PEN = Pen(1, 0.35, RGB(0, 0, 0))
36+
DEFAULT_PEN = Pen(1, 0.35, RGB_NONE)
3637

3738
def __init__(self) -> None:
38-
self.max_pen_count: int = 2
39+
# hashed content
3940
self.pen_index: int = 1
4041
self.pen_color = RGB_NONE
4142
self.pen_width: float = 0.35
42-
self.pen_table: dict[int, Pen] = {}
4343
self.fill_type = FillType.SOLID
4444
self.fill_hatch_line_angle: float = 0.0 # in degrees
4545
self.fill_hatch_line_spacing: float = 40.0 # in plotter units
4646
self.fill_shading_density: float = 100.0
47-
self.page_width: int = 0 # in plotter units
48-
self.page_height: int = 0 # in plotter units
4947
self.clipping_window = (NULLVEC2, NULLVEC2) # in plotter units
48+
# not hashed content
49+
self.max_pen_count: int = 2
50+
self.pen_table: dict[int, Pen] = {}
5051
self.reset()
5152

52-
def has_clipping_window(self) -> bool:
53-
return self.clipping_window[0] is not self.clipping_window[1]
53+
def hash(self) -> int:
54+
return hash(
55+
(
56+
self.pen_index,
57+
self.pen_color,
58+
self.pen_width,
59+
self.fill_type,
60+
self.fill_hatch_line_angle,
61+
self.fill_hatch_line_spacing,
62+
self.fill_shading_density,
63+
self.clipping_window,
64+
)
65+
)
66+
67+
def copy(self) -> Properties:
68+
# the pen table is shared across all copies of Properties
69+
return copy.copy(self)
5470

5571
def reset(self) -> None:
5672
self.max_pen_count = 2
57-
self.pen_index = 1
58-
self.pen_color = RGB(0, 0, 0)
59-
self.pen_width = 0.35 # in mm
73+
self.pen_index = self.DEFAULT_PEN.index
74+
self.pen_color = self.DEFAULT_PEN.color
75+
self.pen_width = self.DEFAULT_PEN.width
6076
self.pen_table = {}
6177
self.fill_type = FillType.SOLID
6278
self.fill_hatch_line_angle = 0.0
6379
self.fill_hatch_line_spacing = 40.0
6480
self.fill_shading_density = 1.0
6581
self.reset_clipping_window()
66-
# do not reset the page size
82+
83+
def has_clipping_window(self) -> bool:
84+
return self.clipping_window[0] is not self.clipping_window[1]
6785

6886
def reset_clipping_window(self) -> None:
6987
self.clipping_window = (NULLVEC2, NULLVEC2)
7088

7189
def get_pen(self, index: int) -> Pen:
7290
return self.pen_table.get(index, self.DEFAULT_PEN)
7391

74-
def set_page_size(self, width: int, height: int) -> None:
75-
# in plotter units
76-
self.page_width = int(width)
77-
self.page_height = int(height)
78-
7992
def set_max_pen_count(self, count: int) -> None:
8093
self.max_pen_count = count
8194

0 commit comments

Comments
 (0)