summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rwxr-xr-xgerber/excellon.py160
-rw-r--r--gerber/excellon_statements.py159
-rw-r--r--gerber/primitives.py36
-rw-r--r--gerber/render/cairo_backend.py14
-rw-r--r--gerber/render/excellon_backend.py130
-rw-r--r--gerber/render/render.py5
-rw-r--r--gerber/rs274x.py8
7 files changed, 467 insertions, 45 deletions
diff --git a/gerber/excellon.py b/gerber/excellon.py
index 0637b23..72cf75c 100755
--- a/gerber/excellon.py
+++ b/gerber/excellon.py
@@ -34,7 +34,7 @@ except(ImportError):
from .excellon_statements import *
from .excellon_tool import ExcellonToolDefinitionParser
from .cam import CamFile, FileSettings
-from .primitives import Drill
+from .primitives import Drill, Slot
from .utils import inch, metric
@@ -93,6 +93,55 @@ class DrillHit(object):
if self.tool.units == 'inch':
self.tool.to_metric()
self.position = tuple(map(metric, self.position))
+
+ @property
+ def bounding_box(self):
+ position = self.position
+ radius = self.tool.diameter / 2.
+
+ min_x = position[0] - radius
+ max_x = position[0] + radius
+ min_y = position[1] - radius
+ max_y = position[1] + radius
+ return ((min_x, max_x), (min_y, max_y))
+
+
+class DrillSlot(object):
+ """
+ A slot is created between two points. The way the slot is created depends on the statement used to create it
+ """
+
+ TYPE_ROUT = 1
+ TYPE_G85 = 2
+
+ def __init__(self, tool, start, end, slot_type):
+ self.tool = tool
+ self.start = start
+ self.end = end
+ self.slot_type = slot_type
+
+ def to_inch(self):
+ if self.tool.units == 'metric':
+ self.tool.to_inch()
+ self.start = tuple(map(inch, self.start))
+ self.end = tuple(map(inch, self.end))
+
+ def to_metric(self):
+ if self.tool.units == 'inch':
+ self.tool.to_metric()
+ self.start = tuple(map(metric, self.start))
+ self.end = tuple(map(metric, self.end))
+
+ @property
+ def bounding_box(self):
+ start = self.start
+ end = self.end
+ radius = self.tool.diameter / 2.
+ min_x = min(start[0], end[0]) - radius
+ max_x = max(start[0], end[0]) + radius
+ min_y = min(start[1], end[1]) - radius
+ max_y = max(start[1], end[1]) + radius
+ return ((min_x, max_x), (min_y, max_y))
class ExcellonFile(CamFile):
@@ -131,7 +180,17 @@ class ExcellonFile(CamFile):
@property
def primitives(self):
- return [Drill(hit.position, hit.tool.diameter, hit, units=self.settings.units) for hit in self.hits]
+
+ primitives = []
+ for hit in self.hits:
+ if isinstance(hit, DrillHit):
+ primitives.append(Drill(hit.position, hit.tool.diameter, hit, units=self.settings.units))
+ elif isinstance(hit, DrillSlot):
+ primitives.append(Slot(hit.start, hit.end, hit.tool.diameter, hit, units=self.settings.units))
+ else:
+ raise ValueError('Unknown hit type')
+
+ return primitives
@property
@@ -139,12 +198,11 @@ class ExcellonFile(CamFile):
xmin = ymin = 100000000000
xmax = ymax = -100000000000
for hit in self.hits:
- radius = hit.tool.diameter / 2.
- x, y = hit.position
- xmin = min(x - radius, xmin)
- xmax = max(x + radius, xmax)
- ymin = min(y - radius, ymin)
- ymax = max(y + radius, ymax)
+ bbox = hit.bounding_box
+ xmin = min(bbox[0][0], xmin)
+ xmax = max(bbox[0][1], xmax)
+ ymin = min(bbox[1][0], ymin)
+ ymax = max(bbox[1][1], ymax)
return ((xmin, xmax), (ymin, ymax))
def report(self, filename=None):
@@ -291,6 +349,7 @@ class ExcellonParser(object):
self.hits = []
self.active_tool = None
self.pos = [0., 0.]
+ self.drill_down = False
# Default for lated is None, which means we don't know
self.plated = ExcellonTool.PLATED_UNKNOWN
if settings is not None:
@@ -399,12 +458,15 @@ class ExcellonParser(object):
elif line[:3] == 'M15':
self.statements.append(ZAxisRoutPositionStmt())
+ self.drill_down = True
elif line[:3] == 'M16':
self.statements.append(RetractWithClampingStmt())
+ self.drill_down = False
elif line[:3] == 'M17':
self.statements.append(RetractWithoutClampingStmt())
+ self.drill_down = False
elif line[:3] == 'M30':
stmt = EndOfProgramStmt.from_excellon(line, self._settings())
@@ -437,6 +499,9 @@ class ExcellonParser(object):
stmt = CoordinateStmt.from_excellon(line[3:], self._settings())
stmt.mode = self.state
+
+ # The start position is where we were before the rout command
+ start = (self.pos[0], self.pos[1])
x = stmt.x
y = stmt.y
@@ -451,9 +516,20 @@ class ExcellonParser(object):
self.pos[0] += x
if y is not None:
self.pos[1] += y
-
+
+ # Our ending position
+ end = (self.pos[0], self.pos[1])
+
+ if self.drill_down:
+ if not self.active_tool:
+ self.active_tool = self._get_tool(1)
+
+ self.hits.append(DrillSlot(self.active_tool, start, end, DrillSlot.TYPE_ROUT))
+ self.active_tool._hit()
+
elif line[:3] == 'G05':
self.statements.append(DrillModeStmt())
+ self.drill_down = False
self.state = 'DRILL'
elif 'INCH' in line or 'METRIC' in line:
@@ -545,26 +621,54 @@ class ExcellonParser(object):
self.active_tool._hit()
elif line[0] in ['X', 'Y']:
- stmt = CoordinateStmt.from_excellon(line, self._settings())
- x = stmt.x
- y = stmt.y
- self.statements.append(stmt)
- if self.notation == 'absolute':
- if x is not None:
- self.pos[0] = x
- if y is not None:
- self.pos[1] = y
- else:
- if x is not None:
- self.pos[0] += x
- if y is not None:
- self.pos[1] += y
- if self.state == 'DRILL':
- if not self.active_tool:
- self.active_tool = self._get_tool(1)
+ if 'G85' in line:
+ stmt = SlotStmt.from_excellon(line, self._settings())
- self.hits.append(DrillHit(self.active_tool, tuple(self.pos)))
- self.active_tool._hit()
+ # I don't know if this is actually correct, but it makes sense that this is where the tool would end
+ x = stmt.x_end
+ y = stmt.y_end
+
+ self.statements.append(stmt)
+
+ if self.notation == 'absolute':
+ if x is not None:
+ self.pos[0] = x
+ if y is not None:
+ self.pos[1] = y
+ else:
+ if x is not None:
+ self.pos[0] += x
+ if y is not None:
+ self.pos[1] += y
+
+ if self.state == 'DRILL':
+ if not self.active_tool:
+ self.active_tool = self._get_tool(1)
+
+ self.hits.append(DrillSlot(self.active_tool, (stmt.x_start, stmt.y_start), (stmt.x_end, stmt.y_end), DrillSlot.TYPE_G85))
+ self.active_tool._hit()
+ else:
+ stmt = CoordinateStmt.from_excellon(line, self._settings())
+ x = stmt.x
+ y = stmt.y
+ self.statements.append(stmt)
+ if self.notation == 'absolute':
+ if x is not None:
+ self.pos[0] = x
+ if y is not None:
+ self.pos[1] = y
+ else:
+ if x is not None:
+ self.pos[0] += x
+ if y is not None:
+ self.pos[1] += y
+
+ if self.state == 'DRILL':
+ if not self.active_tool:
+ self.active_tool = self._get_tool(1)
+
+ self.hits.append(DrillHit(self.active_tool, tuple(self.pos)))
+ self.active_tool._hit()
else:
self.statements.append(UnknownStmt.from_excellon(line))
diff --git a/gerber/excellon_statements.py b/gerber/excellon_statements.py
index 18eaea1..c9367b4 100644
--- a/gerber/excellon_statements.py
+++ b/gerber/excellon_statements.py
@@ -37,7 +37,7 @@ __all__ = ['ExcellonTool', 'ToolSelectionStmt', 'CoordinateStmt',
'RetractWithClampingStmt', 'RetractWithoutClampingStmt',
'CutterCompensationOffStmt', 'CutterCompensationLeftStmt',
'CutterCompensationRightStmt', 'ZAxisInfeedRateStmt',
- 'NextToolSelectionStmt']
+ 'NextToolSelectionStmt', 'SlotStmt']
class ExcellonStatement(object):
@@ -116,6 +116,21 @@ class ExcellonTool(ExcellonStatement):
PLATED_YES = 'plated'
PLATED_NO = 'nonplated'
PLATED_OPTIONAL = 'optional'
+
+ @classmethod
+ def from_tool(cls, tool):
+ args = {}
+
+ args['depth_offset'] = tool.depth_offset
+ args['diameter'] = tool.diameter
+ args['feed_rate'] = tool.feed_rate
+ args['max_hit_count'] = tool.max_hit_count
+ args['number'] = tool.number
+ args['plated'] = tool.plated
+ args['retract_rate'] = tool.retract_rate
+ args['rpm'] = tool.rpm
+
+ return cls(None, **args)
@classmethod
def from_excellon(cls, line, settings, id=None, plated=None):
@@ -196,8 +211,10 @@ class ExcellonTool(ExcellonStatement):
self.hit_count = 0
def to_excellon(self, settings=None):
- fmt = self.settings.format
- zs = self.settings.zero_suppression
+ if self.settings and not settings:
+ settings = self.settings
+ fmt = settings.format
+ zs = settings.zero_suppression
stmt = 'T%02d' % self.number
if self.retract_rate is not None:
stmt += 'B%s' % write_gerber_value(self.retract_rate, fmt, zs)
@@ -350,8 +367,12 @@ class ZAxisInfeedRateStmt(ExcellonStatement):
class CoordinateStmt(ExcellonStatement):
@classmethod
- def from_point(cls, point):
- return cls(point[0], point[1])
+ def from_point(cls, point, mode=None):
+
+ stmt = cls(point[0], point[1])
+ if mode:
+ stmt.mode = mode
+ return stmt
@classmethod
def from_excellon(cls, line, settings, **kwargs):
@@ -628,6 +649,12 @@ class EndOfProgramStmt(ExcellonStatement):
self.y += y_offset
class UnitStmt(ExcellonStatement):
+
+ @classmethod
+ def from_settings(cls, settings):
+ """Create the unit statement from the FileSettings"""
+
+ return cls(settings.units, settings.zeros)
@classmethod
def from_excellon(cls, line, **kwargs):
@@ -810,6 +837,128 @@ class UnknownStmt(ExcellonStatement):
return "<Unknown Statement: %s>" % self.stmt
+class SlotStmt(ExcellonStatement):
+ """
+ G85 statement. Defines a slot created by multiple drills between two specified points.
+
+ Format is two coordinates, split by G85in the middle, for example, XnY0nG85XnYn
+ """
+
+ @classmethod
+ def from_points(cls, start, end):
+
+ return cls(start[0], start[1], end[0], end[1])
+
+ @classmethod
+ def from_excellon(cls, line, settings, **kwargs):
+ # Split the line based on the G85 separator
+ sub_coords = line.split('G85')
+ (x_start_coord, y_start_coord) = SlotStmt.parse_sub_coords(sub_coords[0], settings)
+ (x_end_coord, y_end_coord) = SlotStmt.parse_sub_coords(sub_coords[1], settings)
+
+
+ c = cls(x_start_coord, y_start_coord, x_end_coord, y_end_coord, **kwargs)
+ c.units = settings.units
+ return c
+
+ @staticmethod
+ def parse_sub_coords(line, settings):
+
+ x_coord = None
+ y_coord = None
+
+ if line[0] == 'X':
+ splitline = line.strip('X').split('Y')
+ x_coord = parse_gerber_value(splitline[0], settings.format,
+ settings.zero_suppression)
+ if len(splitline) == 2:
+ y_coord = parse_gerber_value(splitline[1], settings.format,
+ settings.zero_suppression)
+ else:
+ y_coord = parse_gerber_value(line.strip(' Y'), settings.format,
+ settings.zero_suppression)
+
+ return (x_coord, y_coord)
+
+
+ def __init__(self, x_start=None, y_start=None, x_end=None, y_end=None, **kwargs):
+ super(SlotStmt, self).__init__(**kwargs)
+ self.x_start = x_start
+ self.y_start = y_start
+ self.x_end = x_end
+ self.y_end = y_end
+ self.mode = None
+
+ def to_excellon(self, settings):
+ stmt = ''
+
+ if self.x_start is not None:
+ stmt += 'X%s' % write_gerber_value(self.x_start, settings.format,
+ settings.zero_suppression)
+ if self.y_start is not None:
+ stmt += 'Y%s' % write_gerber_value(self.y_start, settings.format,
+ settings.zero_suppression)
+
+ stmt += 'G85'
+
+ if self.x_end is not None:
+ stmt += 'X%s' % write_gerber_value(self.x_end, settings.format,
+ settings.zero_suppression)
+ if self.y_end is not None:
+ stmt += 'Y%s' % write_gerber_value(self.y_end, settings.format,
+ settings.zero_suppression)
+
+ return stmt
+
+ def to_inch(self):
+ if self.units == 'metric':
+ self.units = 'inch'
+ if self.x_start is not None:
+ self.x_start = inch(self.x_start)
+ if self.y_start is not None:
+ self.y_start = inch(self.y_start)
+ if self.x_end is not None:
+ self.x_end = inch(self.x_end)
+ if self.y_end is not None:
+ self.y_end = inch(self.y_end)
+
+ def to_metric(self):
+ if self.units == 'inch':
+ self.units = 'metric'
+ if self.x_start is not None:
+ self.x_start = metric(self.x_start)
+ if self.y_start is not None:
+ self.y_start = metric(self.y_start)
+ if self.x_end is not None:
+ self.x_end = metric(self.x_end)
+ if self.y_end is not None:
+ self.y_end = metric(self.y_end)
+
+ def offset(self, x_offset=0, y_offset=0):
+ if self.x_start is not None:
+ self.x_start += x_offset
+ if self.y_start is not None:
+ self.y_start += y_offset
+ if self.x_end is not None:
+ self.x_end += x_offset
+ if self.y_end is not None:
+ self.y_end += y_offset
+
+ def __str__(self):
+ start_str = ''
+ if self.x_start is not None:
+ start_str += 'X: %g ' % self.x_start
+ if self.y_start is not None:
+ start_str += 'Y: %g ' % self.y_start
+
+ end_str = ''
+ if self.x_end is not None:
+ end_str += 'X: %g ' % self.x_end
+ if self.y_end is not None:
+ end_str += 'Y: %g ' % self.y_end
+
+ return '<Slot Statement: %s to %s>' % (start_str, end_str)
+
def pairwise(iterator):
""" Iterate over list taking two elements at a time.
diff --git a/gerber/primitives.py b/gerber/primitives.py
index e5ff35f..3ecf0db 100644
--- a/gerber/primitives.py
+++ b/gerber/primitives.py
@@ -1109,6 +1109,42 @@ class Drill(Primitive):
def offset(self, x_offset=0, y_offset=0):
self.position = tuple(map(add, self.position, (x_offset, y_offset)))
+
+
+class Slot(Primitive):
+ """ A drilled slot
+ """
+ def __init__(self, start, end, diameter, hit, **kwargs):
+ super(Slot, self).__init__('dark', **kwargs)
+ validate_coordinates(start)
+ validate_coordinates(end)
+ self.start = start
+ self.end = end
+ self.diameter = diameter
+ self.hit = hit
+ self._to_convert = ['start', 'end', 'diameter']
+
+ @property
+ def flashed(self):
+ return False
+
+ @property
+ def radius(self):
+ return self.diameter / 2.
+
+ @property
+ def bounding_box(self):
+ radius = self.radius
+ min_x = min(self.start[0], self.end[0]) - radius
+ max_x = max(self.start[0], self.end[0]) + radius
+ min_y = min(self.start[1], self.end[1]) - radius
+ max_y = max(self.start[1], self.end[1]) + radius
+ return ((min_x, max_x), (min_y, max_y))
+
+ def offset(self, x_offset=0, y_offset=0):
+ self.start = tuple(map(add, self.start, (x_offset, y_offset)))
+ self.end = tuple(map(add, self.end, (x_offset, y_offset)))
+
class TestRecord(Primitive):
""" Netlist Test record
diff --git a/gerber/render/cairo_backend.py b/gerber/render/cairo_backend.py
index 7be7e6a..d895e5c 100644
--- a/gerber/render/cairo_backend.py
+++ b/gerber/render/cairo_backend.py
@@ -173,6 +173,20 @@ class GerberCairoContext(GerberContext):
def _render_drill(self, circle, color):
self._render_circle(circle, color)
+ def _render_slot(self, slot, color):
+ start = map(mul, slot.start, self.scale)
+ end = map(mul, slot.end, self.scale)
+
+ width = slot.diameter
+
+ self.ctx.set_source_rgba(color[0], color[1], color[2], self.alpha)
+ self.ctx.set_operator(cairo.OPERATOR_OVER if (slot.level_polarity == "dark" and not self.invert) else cairo.OPERATOR_CLEAR)
+ self.ctx.set_line_width(width * self.scale[0])
+ self.ctx.set_line_cap(cairo.LINE_CAP_ROUND)
+ self.ctx.move_to(*start)
+ self.ctx.line_to(*end)
+ self.ctx.stroke()
+
def _render_amgroup(self, amgroup, color):
for primitive in amgroup.primitives:
self.render(primitive)
diff --git a/gerber/render/excellon_backend.py b/gerber/render/excellon_backend.py
index bec8367..b2c213f 100644
--- a/gerber/render/excellon_backend.py
+++ b/gerber/render/excellon_backend.py
@@ -1,31 +1,62 @@
from .render import GerberContext
+from ..excellon import DrillSlot
from ..excellon_statements import *
class ExcellonContext(GerberContext):
+ MODE_DRILL = 1
+ MODE_SLOT =2
+
def __init__(self, settings):
GerberContext.__init__(self)
+
+ # Statements that we write
self.comments = []
self.header = []
self.tool_def = []
+ self.body_start = [RewindStopStmt()]
self.body = []
- self.end = [EndOfProgramStmt()]
+ self.start = [HeaderBeginStmt()]
+ # Current tool and position
self.handled_tools = set()
self.cur_tool = None
- self.pos = (None, None)
+ self.drill_mode = ExcellonContext.MODE_DRILL
+ self.drill_down = False
+ self._pos = (None, None)
self.settings = settings
- self._start_header(settings)
+ self._start_header()
+ self._start_comments()
- def _start_header(self, settings):
- pass
+ def _start_header(self):
+ """Create the header from the settings"""
+
+ self.header.append(UnitStmt.from_settings(self.settings))
+
+ def _start_comments(self):
+
+ # Write the digits used - this isn't valid Excellon statement, so we write as a comment
+ self.comments.append(CommentStmt('FILE_FORMAT=%d:%d' % (self.settings.format[0], self.settings.format[1])))
+
+ def _get_end(self):
+ """How we end depends on our mode"""
+
+ end = []
+
+ if self.drill_down:
+ end.append(RetractWithClampingStmt())
+ end.append(RetractWithoutClampingStmt())
+
+ end.append(EndOfProgramStmt())
+
+ return end
@property
def statements(self):
- return self.comments + self.header + self.body + self.end
+ return self.start + self.comments + self.header + self.body_start + self.body + self._get_end()
def set_bounds(self, bounds):
pass
@@ -61,15 +92,92 @@ class ExcellonContext(GerberContext):
def _render_drill(self, drill, color):
- if not drill in self.handled_tools:
- self.tool_def.append(drill.tool)
+ if self.drill_mode != ExcellonContext.MODE_DRILL:
+ self._start_drill_mode()
+
+ tool = drill.hit.tool
+ if not tool in self.handled_tools:
+ self.handled_tools.add(tool)
+ self.header.append(ExcellonTool.from_tool(tool))
- if drill.tool != self.cur_tool:
- self.body.append(ToolSelectionStmt(drill.tool.number))
+ if tool != self.cur_tool:
+ self.body.append(ToolSelectionStmt(tool.number))
+ self.cur_tool = tool
point = self._simplify_point(drill.position)
self._pos = drill.position
- self.body.append(CoordinateStmt.from_point())
+ self.body.append(CoordinateStmt.from_point(point))
+
+ def _start_drill_mode(self):
+ """
+ If we are not in drill mode, then end the ROUT so we can do basic drilling
+ """
+
+ if self.drill_mode == ExcellonContext.MODE_SLOT:
+
+ # Make sure we are retracted before changing modes
+ last_cmd = self.body[-1]
+ if self.drill_down:
+ self.body.append(RetractWithClampingStmt())
+ self.body.append(RetractWithoutClampingStmt())
+ self.drill_down = False
+
+ # Switch to drill mode
+ self.body.append(DrillModeStmt())
+ self.drill_mode = ExcellonContext.MODE_DRILL
+
+ else:
+ raise ValueError('Should be in slot mode')
+
+ def _render_slot(self, slot, color):
+
+ # Set the tool first, before we might go into drill mode
+ tool = slot.hit.tool
+ if not tool in self.handled_tools:
+ self.handled_tools.add(tool)
+ self.header.append(ExcellonTool.from_tool(tool))
+
+ if tool != self.cur_tool:
+ self.body.append(ToolSelectionStmt(tool.number))
+ self.cur_tool = tool
+
+ # Two types of drilling - normal drill and slots
+ if slot.hit.slot_type == DrillSlot.TYPE_ROUT:
+
+ # For ROUT, setting the mode is part of the actual command.
+
+ # Are we in the right position?
+ if slot.start != self._pos:
+ if self.drill_down:
+ # We need to move into the right position, so retract
+ self.body.append(RetractWithClampingStmt())
+ self.drill_down = False
+
+ # Move to the right spot
+ point = self._simplify_point(slot.start)
+ self._pos = slot.start
+ self.body.append(CoordinateStmt.from_point(point, mode="ROUT"))
+
+ # Now we are in the right spot, so drill down
+ if not self.drill_down:
+ self.body.append(ZAxisRoutPositionStmt())
+ self.drill_down = True
+
+ # Do a linear move from our current position to the end position
+ point = self._simplify_point(slot.end)
+ self._pos = slot.end
+ self.body.append(CoordinateStmt.from_point(point, mode="LINEAR"))
+
+ self.drill_down = ExcellonContext.MODE_SLOT
+
+ else:
+ # This is a G85 slot, so do this in normally drilling mode
+ if self.drill_mode != ExcellonContext.MODE_DRILL:
+ self._start_drill_mode()
+
+ # Slots don't use simplified points
+ self._pos = slot.end
+ self.body.append(SlotStmt.from_points(slot.start, slot.end))
def _render_inverted_layer(self):
pass
diff --git a/gerber/render/render.py b/gerber/render/render.py
index b518385..a5ae38e 100644
--- a/gerber/render/render.py
+++ b/gerber/render/render.py
@@ -150,6 +150,8 @@ class GerberContext(object):
self._render_polygon(primitive, color)
elif isinstance(primitive, Drill):
self._render_drill(primitive, self.drill_color)
+ elif isinstance(primitive, Slot):
+ self._render_slot(primitive, self.drill_color)
elif isinstance(primitive, AMGroup):
self._render_amgroup(primitive, color)
elif isinstance(primitive, Outline):
@@ -183,6 +185,9 @@ class GerberContext(object):
def _render_drill(self, primitive, color):
pass
+ def _render_slot(self, primitive, color):
+ pass
+
def _render_amgroup(self, primitive, color):
pass
diff --git a/gerber/rs274x.py b/gerber/rs274x.py
index 742fddd..7b3a3b9 100644
--- a/gerber/rs274x.py
+++ b/gerber/rs274x.py
@@ -540,6 +540,7 @@ class GerberParser(object):
# from gerber spec revision J3, Section 4.5, page 55:
# The segments are not graphics objects in themselves; segments are part of region which is the graphics object. The segments have no thickness.
# The current aperture is associated with the region. This has no graphical effect, but allows all its attributes to be applied to the region.
+
if self.current_region is None:
self.current_region = [Line(start, end, self.apertures.get(self.aperture, Circle((0,0), 0)), level_polarity=self.level_polarity, units=self.settings.units),]
else:
@@ -557,7 +558,12 @@ class GerberParser(object):
self.current_region.append(Arc(start, end, center, self.direction, self.apertures.get(self.aperture, Circle((0,0), 0)), quadrant_mode=self.quadrant_mode, level_polarity=self.level_polarity, units=self.settings.units))
elif self.op == "D02" or self.op == "D2":
- pass
+
+ if self.region_mode == "on":
+ # D02 in the middle of a region finishes that region and starts a new one
+ if self.current_region and len(self.current_region) > 1:
+ self.primitives.append(Region(self.current_region, level_polarity=self.level_polarity, units=self.settings.units))
+ self.current_region = None
elif self.op == "D03" or self.op == "D3":
primitive = copy.deepcopy(self.apertures[self.aperture])