summaryrefslogtreecommitdiff
path: root/gerbonara/gerber/panelize/dxf_path.py
diff options
context:
space:
mode:
authorjaseg <git@jaseg.de>2021-06-13 15:00:17 +0200
committerjaseg <git@jaseg.de>2021-06-13 15:00:17 +0200
commit4eb0e063bcd34c21b737023aa6ed5baed80658d1 (patch)
tree3a56ef7d05f4f64cde930f2432119986e4aab49d /gerbonara/gerber/panelize/dxf_path.py
parent889ea37d9b66cbfb7a61795c7750b9f4311faa3f (diff)
downloadgerbonara-4eb0e063bcd34c21b737023aa6ed5baed80658d1.tar.gz
gerbonara-4eb0e063bcd34c21b737023aa6ed5baed80658d1.tar.bz2
gerbonara-4eb0e063bcd34c21b737023aa6ed5baed80658d1.zip
Repo re-org, make gerberex tests run
Diffstat (limited to 'gerbonara/gerber/panelize/dxf_path.py')
-rw-r--r--gerbonara/gerber/panelize/dxf_path.py412
1 files changed, 412 insertions, 0 deletions
diff --git a/gerbonara/gerber/panelize/dxf_path.py b/gerbonara/gerber/panelize/dxf_path.py
new file mode 100644
index 0000000..201dcff
--- /dev/null
+++ b/gerbonara/gerber/panelize/dxf_path.py
@@ -0,0 +1,412 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# Copyright 2019 Hiroshi Murayama <opiopan@gmail.com>
+
+from ..utils import inch, metric, write_gerber_value
+from ..cam import FileSettings
+from .utility import is_equal_point, is_equal_value, normalize_vec2d, dot_vec2d
+from .excellon import CoordinateStmtEx
+
+class DxfPath(object):
+ def __init__(self, statements, error_range=0):
+ self.statements = statements
+ self.error_range = error_range
+ self.bounding_box = statements[0].bounding_box
+ self.containers = []
+ for statement in statements[1:]:
+ self._merge_bounding_box(statement.bounding_box)
+
+ @property
+ def start(self):
+ return self.statements[0].start
+
+ @property
+ def end(self):
+ return self.statements[-1].end
+
+ @property
+ def is_closed(self):
+ if len(self.statements) == 1:
+ return self.statements[0].is_closed
+ else:
+ return is_equal_point(self.start, self.end, self.error_range)
+
+ def is_equal_to(self, target, error_range=0):
+ if not isinstance(target, DxfPath):
+ return False
+ if len(self.statements) != len(target.statements):
+ return False
+ if is_equal_point(self.start, target.start, error_range) and \
+ is_equal_point(self.end, target.end, error_range):
+ for i in range(0, len(self.statements)):
+ if not self.statements[i].is_equal_to(target.statements[i], error_range):
+ return False
+ return True
+ elif is_equal_point(self.start, target.end, error_range) and \
+ is_equal_point(self.end, target.start, error_range):
+ for i in range(0, len(self.statements)):
+ if not self.statements[i].is_equal_to(target.statements[-1 - i], error_range):
+ return False
+ return True
+ return False
+
+ def contain(self, target, error_range=0):
+ for statement in self.statements:
+ if statement.is_equal_to(target, error_range):
+ return True
+ else:
+ return False
+
+ def to_inch(self):
+ self.error_range = inch(self.error_range)
+ for statement in self.statements:
+ statement.to_inch()
+
+ def to_metric(self):
+ self.error_range = metric(self.error_range)
+ for statement in self.statements:
+ statement.to_metric()
+
+ def offset(self, offset_x, offset_y):
+ for statement in self.statements:
+ statement.offset(offset_x, offset_y)
+
+ def rotate(self, angle, center=(0, 0)):
+ for statement in self.statements:
+ statement.rotate(angle, center)
+
+ def reverse(self):
+ rlist = []
+ for statement in reversed(self.statements):
+ statement.reverse()
+ rlist.append(statement)
+ self.statements = rlist
+
+ def merge(self, element, error_range=0):
+ if self.is_closed or element.is_closed:
+ return False
+ if not error_range:
+ error_range = self.error_range
+ if is_equal_point(self.end, element.start, error_range):
+ return self._append_at_end(element, error_range)
+ elif is_equal_point(self.end, element.end, error_range):
+ element.reverse()
+ return self._append_at_end(element, error_range)
+ elif is_equal_point(self.start, element.end, error_range):
+ return self._insert_on_top(element, error_range)
+ elif is_equal_point(self.start, element.start, error_range):
+ element.reverse()
+ return self._insert_on_top(element, error_range)
+ else:
+ return False
+
+ def _append_at_end(self, element, error_range=0):
+ if isinstance(element, DxfPath):
+ if self.is_equal_to(element, error_range):
+ return False
+ for i in range(0, min(len(self.statements), len(element.statements))):
+ if not self.statements[-1 - i].is_equal_to(element.statements[i]):
+ break
+ for j in range(0, min(len(self.statements), len(element.statements))):
+ if not self.statements[j].is_equal_to(element.statements[-1 - j]):
+ break
+ if i + j >= len(element.statements):
+ return False
+ mergee = list(element.statements)
+ if i > 0:
+ del mergee[0:i]
+ del self.statements[-i]
+ if j > 0:
+ del mergee[-j]
+ del self.statements[0:j]
+ for statement in mergee:
+ self._merge_bounding_box(statement.bounding_box)
+ self.statements.extend(mergee)
+ return True
+ else:
+ if self.statements[-1].is_equal_to(element, error_range) or \
+ self.statements[0].is_equal_to(element, error_range):
+ return False
+ self._merge_bounding_box(element.bounding_box)
+ self.statements.appen(element)
+ return True
+
+ def _insert_on_top(self, element, error_range=0):
+ if isinstance(element, DxfPath):
+ if self.is_equal_to(element, error_range):
+ return False
+ for i in range(0, min(len(self.statements), len(element.statements))):
+ if not self.statements[-1 - i].is_equal_to(element.statements[i]):
+ break
+ for j in range(0, min(len(self.statements), len(element.statements))):
+ if not self.statements[j].is_equal_to(element.statements[-1 - j]):
+ break
+ if i + j >= len(element.statements):
+ return False
+ mergee = list(element.statements)
+ if i > 0:
+ del mergee[0:i]
+ del self.statements[-i]
+ if j > 0:
+ del mergee[-j]
+ del self.statements[0:j]
+ self.statements[0:0] = mergee
+ return True
+ else:
+ if self.statements[-1].is_equal_to(element, error_range) or \
+ self.statements[0].is_equal_to(element, error_range):
+ return False
+ self.statements.insert(0, element)
+ return True
+
+ def _merge_bounding_box(self, box):
+ self.bounding_box = (min(self.bounding_box[0], box[0]),
+ min(self.bounding_box[1], box[1]),
+ max(self.bounding_box[2], box[2]),
+ max(self.bounding_box[3], box[3]))
+
+ def may_be_in_collision(self, path):
+ if self.bounding_box[0] >= path.bounding_box[2] or \
+ self.bounding_box[1] >= path.bounding_box[3] or \
+ self.bounding_box[2] <= path.bounding_box[0] or \
+ self.bounding_box[3] <= path.bounding_box[1]:
+ return False
+ else:
+ return True
+
+ def to_gerber(self, settings=FileSettings(), pitch=0, width=0):
+ from .dxf import DxfArcStatement
+ if pitch == 0:
+ x0, y0 = self.statements[0].start
+ gerber = 'G01*\nX{0}Y{1}D02*\nG75*'.format(
+ write_gerber_value(x0, settings.format,
+ settings.zero_suppression),
+ write_gerber_value(y0, settings.format,
+ settings.zero_suppression),
+ )
+
+ for statement in self.statements:
+ x0, y0 = statement.start
+ x1, y1 = statement.end
+ if isinstance(statement, DxfArcStatement):
+ xc, yc = statement.center
+ gerber += '\nG{0}*\nX{1}Y{2}I{3}J{4}D01*'.format(
+ '03' if statement.end_angle > statement.start_angle else '02',
+ write_gerber_value(x1, settings.format,
+ settings.zero_suppression),
+ write_gerber_value(y1, settings.format,
+ settings.zero_suppression),
+ write_gerber_value(xc - x0, settings.format,
+ settings.zero_suppression),
+ write_gerber_value(yc - y0, settings.format,
+ settings.zero_suppression)
+ )
+ else:
+ gerber += '\nG01*\nX{0}Y{1}D01*'.format(
+ write_gerber_value(x1, settings.format,
+ settings.zero_suppression),
+ write_gerber_value(y1, settings.format,
+ settings.zero_suppression),
+ )
+ else:
+ def ploter(x, y):
+ return 'X{0}Y{1}D03*\n'.format(
+ write_gerber_value(x, settings.format,
+ settings.zero_suppression),
+ write_gerber_value(y, settings.format,
+ settings.zero_suppression),
+ )
+ gerber = self._plot_dots(pitch, width, ploter)
+
+ return gerber
+
+ def to_excellon(self, settings=FileSettings(), pitch=0, width=0):
+ from .dxf import DxfArcStatement
+ if pitch == 0:
+ x0, y0 = self.statements[0].start
+ excellon = 'G00{0}\nM15\n'.format(
+ CoordinateStmtEx(x=x0, y=y0).to_excellon(settings))
+
+ for statement in self.statements:
+ x0, y0 = statement.start
+ x1, y1 = statement.end
+ if isinstance(statement, DxfArcStatement):
+ i = statement.center[0] - x0
+ j = statement.center[1] - y0
+ excellon += '{0}{1}\n'.format(
+ 'G03' if statement.end_angle > statement.start_angle else 'G02',
+ CoordinateStmtEx(x=x1, y=y1, i=i, j=j).to_excellon(settings))
+ else:
+ excellon += 'G01{0}\n'.format(
+ CoordinateStmtEx(x=x1, y=y1).to_excellon(settings))
+
+ excellon += 'M16\nG05\n'
+ else:
+ def ploter(x, y):
+ return CoordinateStmtEx(x=x, y=y).to_excellon(settings) + '\n'
+ excellon = self._plot_dots(pitch, width, ploter)
+
+ return excellon
+
+ def _plot_dots(self, pitch, width, ploter):
+ out = ''
+ offset = 0
+ for idx in range(0, len(self.statements)):
+ statement = self.statements[idx]
+ if offset < 0:
+ offset += pitch
+ for dot, offset in statement.dots(pitch, width, offset):
+ if dot is None:
+ break
+ if offset > 0 and (statement.is_closed or idx != len(self.statements) - 1):
+ break
+ #if idx == len(self.statements) - 1 and statement.is_closed and offset > -pitch:
+ # break
+ out += ploter(dot[0], dot[1])
+ return out
+
+ def intersections_with_halfline(self, point_from, point_to, error_range=0):
+ def calculator(statement):
+ return statement.intersections_with_halfline(point_from, point_to, error_range)
+ def validator(pt, statement, idx):
+ if is_equal_point(pt, statement.end, error_range) and \
+ not self._judge_cross(point_from, point_to, idx, error_range):
+ return False
+ return True
+ return self._collect_intersections(calculator, validator, error_range)
+
+ def intersections_with_arc(self, center, radius, angle_regions, error_range=0):
+ def calculator(statement):
+ return statement.intersections_with_arc(center, radius, angle_regions, error_range)
+ return self._collect_intersections(calculator, None, error_range)
+
+ def _collect_intersections(self, calculator, validator, error_range):
+ allpts = []
+ last = allpts
+ for i in range(0, len(self.statements)):
+ statement = self.statements[i]
+ cur = calculator(statement)
+ if cur:
+ for pt in cur:
+ for dest in allpts:
+ if is_equal_point(pt, dest, error_range):
+ break
+ else:
+ if validator is not None and not validator(pt, statement, i):
+ continue
+ allpts.append(pt)
+ last = cur
+ return allpts
+
+ def _judge_cross(self, from_pt, to_pt, index, error_range):
+ standard = normalize_vec2d((to_pt[0] - from_pt[0], to_pt[1] - from_pt[1]))
+ normal = (standard[1], -standard[0])
+ def statements():
+ for i in range(index, len(self.statements)):
+ yield self.statements[i]
+ for i in range(0, index):
+ yield self.statements[i]
+ dot_standard = None
+ for statement in statements():
+ tstart = statement.start
+ tend = statement.end
+ target = normalize_vec2d((tend[0] - tstart[0], tend[1] - tstart[1]))
+ dot= dot_vec2d(normal, target)
+ if dot_standard is None:
+ dot_standard = dot
+ continue
+ if is_equal_point(standard, target, error_range):
+ continue
+ return (dot_standard > 0 and dot > 0) or (dot_standard < 0 and dot < 0)
+ raise Exception('inconsistensy is detected while cross judgement between paths')
+
+def generate_paths(statements, error_range=0):
+ from .dxf import DxfPolylineStatement
+
+ paths = []
+ for statement in filter(lambda s: isinstance(s, DxfPolylineStatement), statements):
+ units = [unit for unit in statement.disassemble()]
+ paths.append(DxfPath(units, error_range))
+
+ unique_statements = []
+ redundant = 0
+ for statement in filter(lambda s: not isinstance(s, DxfPolylineStatement), statements):
+ for path in paths:
+ if path.contain(statement):
+ redundant += 1
+ break
+ else:
+ for target in unique_statements:
+ if statement.is_equal_to(target, error_range):
+ redundant += 1
+ break
+ else:
+ unique_statements.append(statement)
+
+ paths.extend([DxfPath([s], error_range) for s in unique_statements])
+
+ prev_paths_num = 0
+ while prev_paths_num != len(paths):
+ working = []
+ for i in range(len(paths)):
+ mergee = paths[i]
+ for j in range(i + 1, len(paths)):
+ target = paths[j]
+ if target.merge(mergee, error_range):
+ break
+ else:
+ working.append(mergee)
+ prev_paths_num = len(paths)
+ paths = working
+
+ closed_path = list(filter(lambda p: p.is_closed, paths))
+ open_path = list(filter(lambda p: not p.is_closed, paths))
+ return (closed_path, open_path)
+
+def judge_containment(path1, path2, error_range=0):
+ from .dxf import DxfArcStatement, DxfLineStatement
+
+ nocontainment = (None, None)
+ if not path1.may_be_in_collision(path2):
+ return nocontainment
+
+ def is_in_line_segment(point_from, point_to, point):
+ dx = point_to[0] - point_from[0]
+ ratio = (point[0] - point_from[0]) / dx if dx != 0 else \
+ (point[1] - point_from[1]) / (point_to[1] - point_from[1])
+ return ratio >= 0 and ratio <= 1
+
+ def contain_in_path(statement, path):
+ if isinstance(statement, DxfLineStatement):
+ segment = (statement.start, statement.end)
+ elif isinstance(statement, DxfArcStatement):
+ if statement.start == statement.end:
+ segment = (statement.start, statement.center)
+ else:
+ segment = (statement.start, statement.end)
+ else:
+ raise Exception('invalid dxf statement type')
+ pts = path.intersections_with_halfline(segment[0], segment[1], error_range)
+ if len(pts) % 2 == 0:
+ return False
+ for pt in pts:
+ if is_in_line_segment(segment[0], segment[1], pt):
+ return False
+ if isinstance(statement, DxfArcStatement):
+ pts = path.intersections_with_arc(
+ statement.center, statement.radius, statement.angle_regions, error_range)
+ if len(pts) > 0:
+ return False
+ return True
+
+ if contain_in_path(path1.statements[0], path2):
+ containment = [path1, path2]
+ elif contain_in_path(path2.statements[0], path1):
+ containment = [path2, path1]
+ else:
+ return nocontainment
+ for i in range(1, len(containment[0].statements)):
+ if not contain_in_path(containment[0].statements[i], containment[1]):
+ return nocontainment
+ return containment