diff options
-rw-r--r-- | gerbonara/gerber/panelize/composition.py | 192 | ||||
-rw-r--r-- | gerbonara/gerber/panelize/dxf.py | 796 | ||||
-rw-r--r-- | gerbonara/gerber/panelize/dxf_path.py | 412 | ||||
-rw-r--r-- | gerbonara/gerber/panelize/gerber_statements.py | 49 | ||||
-rw-r--r-- | gerbonara/gerber/panelize/utility.py | 20 | ||||
-rw-r--r-- | gerbonara/gerber/render/excellon_backend.py | 188 | ||||
-rw-r--r-- | gerbonara/gerber/render/render.py | 246 | ||||
-rw-r--r-- | gerbonara/gerber/render/rs274x_backend.py | 510 | ||||
-rw-r--r-- | gerbonara/gerber/render/theme.py | 112 |
9 files changed, 0 insertions, 2525 deletions
diff --git a/gerbonara/gerber/panelize/composition.py b/gerbonara/gerber/panelize/composition.py deleted file mode 100644 index a30f959..0000000 --- a/gerbonara/gerber/panelize/composition.py +++ /dev/null @@ -1,192 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -# Copyright 2019 Hiroshi Murayama <opiopan@gmail.com> -import os -from functools import reduce -from ..cam import FileSettings -from ..gerber_statements import EofStmt -from ..excellon_statements import * -from ..excellon import DrillSlot, DrillHit -from .. import rs274x -from . import excellon -# from . import dxf - -class Composition(object): - def __init__(self, settings = None, comments = None): - self.settings = settings - self.comments = comments if comments != None else [] - -class GerberComposition(Composition): - APERTURE_ID_BIAS = 10 - - def __init__(self, settings=None, comments=None): - super(GerberComposition, self).__init__(settings, comments) - self.aperture_macros = {} - self.apertures = [] - self.drawings = [] - - def merge(self, file): - if isinstance(file, rs274x.GerberFile): - self._merge_gerber(file) -# elif isinstance(file, dxf.DxfFile): -# self._merge_dxf(file) - else: - raise Exception('unsupported file type') - - def dump(self, path): - def statements(): - for k in self.aperture_macros: - yield self.aperture_macros[k] - for s in self.apertures: - yield s - for s in self.drawings: - yield s - yield EofStmt() - self.settings.notation = 'absolute' - self.settings.zeros = 'trailing' - with open(path, 'w') as f: - rs274x.write_gerber_header(f, self.settings) - for statement in statements(): - f.write(statement.to_gerber(self.settings) + '\n') - - def _merge_gerber(self, file): - aperture_macro_map = {} - aperture_map = {} - - if self.settings: - if self.settings.units == 'metric': - file.to_metric() - else: - file.to_inch() - - for macro in file.aperture_macros: - statement = file.aperture_macros[macro] - name = statement.name - newname = self._register_aperture_macro(statement) - aperture_macro_map[name] = newname - - for statement in file.aperture_defs: - if statement.param == 'AD': - if statement.shape in aperture_macro_map: - statement.shape = aperture_macro_map[statement.shape] - dnum = statement.d - newdnum = self._register_aperture(statement) - aperture_map[dnum] = newdnum - - for statement in file.main_statements: - if statement.type == 'APERTURE': - statement.d = aperture_map[statement.d] - self.drawings.append(statement) - - if not self.settings: - self.settings = file.context - - def _merge_dxf(self, file): - if self.settings: - if self.settings.units == 'metric': - file.to_metric() - else: - file.to_inch() - - file.dcode = self._register_aperture(file.aperture) - self.drawings.append(file.statements) - - if not self.settings: - self.settings = file.settings - - - def _register_aperture_macro(self, statement): - name = statement.name - newname = name - offset = 0 - while newname in self.aperture_macros: - offset += 1 - newname = '%s_%d' % (name, offset) - statement.name = newname - self.aperture_macros[newname] = statement - return newname - - def _register_aperture(self, statement): - statement.d = len(self.apertures) + self.APERTURE_ID_BIAS - self.apertures.append(statement) - return statement.d - -class DrillComposition(Composition): - def __init__(self, settings=None, comments=None): - super(DrillComposition, self).__init__(settings, comments) - self.tools = [] - self.hits = [] - self.dxf_statements = [] - - def merge(self, file): - if isinstance(file, excellon.ExcellonFileEx): - self._merge_excellon(file) - elif isinstance(file, DxfFile): - self._merge_dxf(file) - else: - raise Exception('unsupported file type') - - def dump(self, path): - def statements(): - for t in self.tools: - yield ToolSelectionStmt(t.number).to_excellon(self.settings) - for h in self.hits: - if h.tool.number == t.number: - yield h.to_excellon(self.settings) - for num, statement in self.dxf_statements: - if num == t.number: - yield statement.to_excellon(self.settings) - yield EndOfProgramStmt().to_excellon() - - self.settings.notation = 'absolute' - self.settings.zeros = 'trailing' - with open(path, 'w') as f: - excellon.write_excellon_header(f, self.settings, self.tools) - for statement in statements(): - f.write(statement + '\n') - - def _merge_excellon(self, file): - tool_map = {} - - if not self.settings: - self.settings = file.settings - else: - if self.settings.units == 'metric': - file.to_metric() - else: - file.to_inch() - - for tool in iter(file.tools.values()): - num = tool.number - tool_map[num] = self._register_tool(tool) - - for hit in file.hits: - hit.tool = tool_map[hit.tool.number] - self.hits.append(hit) - - def _merge_dxf(self, file): - if not self.settings: - self.settings = file.settings - else: - if self.settings.units == 'metric': - file.to_metric() - else: - file.to_inch() - - tool = self._register_tool(ExcellonTool(self.settings, number=1, diameter=file.width)) - self.dxf_statements.append((tool.number, file.statements)) - - def _register_tool(self, tool): - for existing in self.tools: - if existing.equivalent(tool): - return existing - new_tool = ExcellonTool.from_tool(tool) - new_tool.settings = self.settings - def toolnums(): - for tool in self.tools: - yield tool.number - max_num = reduce(lambda x, y: x if x > y else y, toolnums(), 0) - new_tool.number = max_num + 1 - self.tools.append(new_tool) - return new_tool diff --git a/gerbonara/gerber/panelize/dxf.py b/gerbonara/gerber/panelize/dxf.py deleted file mode 100644 index 9eb9217..0000000 --- a/gerbonara/gerber/panelize/dxf.py +++ /dev/null @@ -1,796 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -# Copyright 2019 Hiroshi Murayama <opiopan@gmail.com> - -import io, sys -from math import pi, cos, sin, tan, atan, atan2, acos, asin, sqrt -import dxfgrabber -from ..cam import CamFile, FileSettings -from ..utils import inch, metric, write_gerber_value, rotate_point -from ..gerber_statements import ADParamStmt -from ..excellon_statements import ExcellonTool -from ..excellon_statements import CoordinateStmt -from .utility import is_equal_point, is_equal_value -from .dxf_path import generate_paths, judge_containment -from .excellon import write_excellon_header -from .rs274x import write_gerber_header - -ACCEPTABLE_ERROR = 0.001 - -def _normalize_angle(start_angle, end_angle): - angle = end_angle - start_angle - if angle > 0: - start = start_angle % 360 - else: - angle = -angle - start = end_angle % 360 - angle = min(angle, 360) - start = start - 360 if start > 180 else start - - regions = [] - while angle > 0: - end = start + angle - if end <= 180: - regions.append((start * pi / 180, end * pi / 180)) - angle = 0 - else: - regions.append((start * pi / 180, pi)) - angle = end - 180 - start = -180 - return regions - -def _intersections_of_line_and_circle(start, end, center, radius, error_range): - x1 = start[0] - center[0] - y1 = start[1] - center[1] - x2 = end[0] - center[0] - y2 = end[1] - center[1] - - dx = x2 - x1 - dy = y2 - y1 - dr = sqrt(dx * dx + dy * dy) - D = x1 * y2 - x2 * y1 - - distance = abs(dy * x1 - dx * y1) / dr - - D2 = D * D - dr2 = dr * dr - r2 = radius * radius - delta = r2 * dr2 - D2 - if distance > radius - error_range and distance < radius + error_range: - delta = 0 - if delta < 0: - return None - - sqrt_D = sqrt(delta) - E_x = -dx * sqrt_D if dy < 0 else dx * sqrt_D - E_y = abs(dy) * sqrt_D - - p1_x = (D * dy + E_x) / dr2 - p2_x = (D * dy - E_x) / dr2 - p1_y = (-D * dx + E_y) / dr2 - p2_y = (-D * dx - E_y) / dr2 - - p1_angle = atan2(p1_y, p1_x) - p2_angle = atan2(p2_y, p2_x) - if dx == 0: - p1_t = (p1_y - y1) / dy - p2_t = (p2_y - y1) / dy - else: - p1_t = (p1_x - x1) / dx - p2_t = (p2_x - x1) / dx - - if delta == 0: - return ( - (p1_x + center[0], p1_y + center[1]), - None, - p1_angle, None, - p1_t, None - ) - else: - return ( - (p1_x + center[0], p1_y + center[1]), - (p2_x + center[0], p2_y + center[1]), - p1_angle, p2_angle, - p1_t, p2_t - ) - -class DxfStatement(object): - def __init__(self, entity): - self.entity = entity - self.start = None - self.end = None - self.is_closed = False - - def to_inch(self): - pass - - def to_metric(self): - pass - - def is_equal_to(self, target, error_range=0): - return False - - def reverse(self): - raise Exception('Not implemented') - - def offset(self, offset_x, offset_y): - raise Exception('Not supported') - - def rotate(self, angle, center=(0, 0)): - raise Exception('Not supported') - - -class DxfLineStatement(DxfStatement): - @classmethod - def from_entity(cls, entity): - start = (entity.start[0], entity.start[1]) - end = (entity.end[0], entity.end[1]) - return cls(entity, start, end) - - @property - def bounding_box(self): - return (min(self.start[0], self.end[0]), - min(self.start[1], self.end[1]), - max(self.start[0], self.end[0]), - max(self.start[1], self.end[1])) - - def __init__(self, entity, start, end): - super(DxfLineStatement, self).__init__(entity) - self.start = start - self.end = end - - def to_inch(self): - self.start = ( - inch(self.start[0]), inch(self.start[1])) - self.end = ( - inch(self.end[0]), inch(self.end[1])) - - def to_metric(self): - self.start = ( - metric(self.start[0]), metric(self.start[1])) - self.end = ( - metric(self.end[0]), metric(self.end[1])) - - def is_equal_to(self, target, error_range=0): - if not isinstance(target, DxfLineStatement): - return False - return (is_equal_point(self.start, target.start, error_range) and \ - is_equal_point(self.end, target.end, error_range)) or \ - (is_equal_point(self.start, target.end, error_range) and \ - is_equal_point(self.end, target.start, error_range)) - - def reverse(self): - pt = self.start - self.start = self.end - self.end = pt - - def dots(self, pitch, width, offset=0): - x0, y0 = self.start - x1, y1 = self.end - y1 = self.end[1] - xp = x1 - x0 - yp = y1 - y0 - l = sqrt(xp * xp + yp * yp) - xd = xp * pitch / l - yd = yp * pitch / l - x0 += xp * offset / l - y0 += yp * offset / l - - if offset > l + width / 2: - return (None, offset - l) - else: - d = offset; - while d < l + width / 2: - yield ((x0, y0), d - l) - x0 += xd - y0 += yd - d += pitch - - def offset(self, offset_x, offset_y): - self.start = (self.start[0] + offset_x, self.start[1] + offset_y) - self.end = (self.end[0] + offset_x, self.end[1] + offset_y) - - def rotate(self, angle, center=(0, 0)): - self.start = rotate_point(self.start, angle, center) - self.end = rotate_point(self.end, angle, center) - - def intersections_with_halfline(self, point_from, point_to, error_range): - denominator = (self.end[0] - self.start[0]) * (point_to[1] - point_from[1]) - \ - (self.end[1] - self.start[1]) * (point_to[0] - point_from[0]) - de = error_range * error_range - if denominator >= -de and denominator <= de: - return [] - from_dx = point_from[0] - self.start[0] - from_dy = point_from[1] - self.start[1] - r = ((point_to[1] - point_from[1]) * from_dx - - (point_to[0] - point_from[0]) * from_dy) / denominator - s = ((self.end[1] - self.start[1]) * from_dx - - (self.end[0] - self.start[0]) * from_dy) / denominator - dx = (self.end[0] - self.start[0]) - dy = (self.end[1] - self.start[1]) - le = error_range / sqrt(dx * dx + dy * dy) - if s < 0 or r < -le or r > 1 + le: - return [] - - pt = (self.start[0] + (self.end[0] - self.start[0]) * r, - self.start[1] + (self.end[1] - self.start[1]) * r) - if is_equal_point(pt, self.start, error_range): - return [] - else: - return [pt] - - def intersections_with_arc(self, center, radius, angle_regions, error_range): - intersection = \ - _intersections_of_line_and_circle(self.start, self.end, center, radius, error_range) - if intersection is None: - return [] - else: - p1, p2, p1_angle, p2_angle, p1_t, p2_t = intersection - - pts = [] - if p1_t >= 0 and p1_t <= 1: - for region in angle_regions: - if p1_angle >= region[0] and p1_angle <= region[1]: - pts.append(p1) - break - if p2 is not None and p2_t >= 0 and p2_t <= 1: - for region in angle_regions: - if p2_angle >= region[0] and p2_angle <= region[1]: - pts.append(p2) - break - - return pts - -class DxfArcStatement(DxfStatement): - def __init__(self, entity): - super(DxfArcStatement, self).__init__(entity) - if entity.dxftype == 'CIRCLE': - self.radius = self.entity.radius - self.center = (self.entity.center[0], self.entity.center[1]) - self.start = (self.center[0] + self.radius, self.center[1]) - self.end = self.start - self.start_angle = 0 - self.end_angle = 360 - self.is_closed = True - elif entity.dxftype == 'ARC': - self.start_angle = self.entity.start_angle - self.end_angle = self.entity.end_angle - self.radius = self.entity.radius - self.center = (self.entity.center[0], self.entity.center[1]) - self.start = ( - self.center[0] + self.radius * cos(self.start_angle / 180. * pi), - self.center[1] + self.radius * sin(self.start_angle / 180. * pi), - ) - self.end = ( - self.center[0] + self.radius * cos(self.end_angle / 180. * pi), - self.center[1] + self.radius * sin(self.end_angle / 180. * pi), - ) - angle = self.end_angle - self.start_angle - self.is_closed = angle >= 360 or angle <= -360 - else: - raise Exception('invalid DXF type was specified') - self.angle_regions = _normalize_angle(self.start_angle, self.end_angle) - - @property - def bounding_box(self): - return (self.center[0] - self.radius, self.center[1] - self.radius, - self.center[0] + self.radius, self.center[1] + self.radius) - - def to_inch(self): - self.radius = inch(self.radius) - self.center = (inch(self.center[0]), inch(self.center[1])) - self.start = (inch(self.start[0]), inch(self.start[1])) - self.end = (inch(self.end[0]), inch(self.end[1])) - - def to_metric(self): - self.radius = metric(self.radius) - self.center = (metric(self.center[0]), metric(self.center[1])) - self.start = (metric(self.start[0]), metric(self.start[1])) - self.end = (metric(self.end[0]), metric(self.end[1])) - - def is_equal_to(self, target, error_range=0): - if not isinstance(target, DxfArcStatement): - return False - aerror_range = error_range / pi * self.radius * 180 - return is_equal_point(self.center, target.center, error_range) and \ - is_equal_value(self.radius, target.radius, error_range) and \ - ((is_equal_value(self.start_angle, target.start_angle, aerror_range) and - is_equal_value(self.end_angle, target.end_angle, aerror_range)) or - (is_equal_value(self.start_angle, target.end_angle, aerror_range) and - is_equal_value(self.end_angle, target.end_angle, aerror_range))) - - def reverse(self): - tmp = self.start_angle - self.start_angle = self.end_angle - self.end_angle = tmp - tmp = self.start - self.start = self.end - self.end = tmp - - def dots(self, pitch, width, offset=0): - angle = self.end_angle - self.start_angle - afactor = 1 if angle > 0 else -1 - aangle = angle * afactor - L = 2 * pi * self.radius - l = L * aangle / 360 - pangle = pitch / L * 360 - wangle = width / L * 360 - oangle = offset / L * 360 - - if offset > l + width / 2: - yield (None, offset - l) - else: - da = oangle - while da < aangle + wangle / 2: - cangle = self.start_angle + da * afactor - x = self.radius * cos(cangle / 180 * pi) + self.center[0] - y = self.radius * sin(cangle / 180 * pi) + self.center[1] - remain = (da - aangle) / 360 * L - yield((x, y), remain) - da += pangle - - def offset(self, offset_x, offset_y): - self.center = (self.center[0] + offset_x, self.center[1] + offset_y) - self.start = (self.start[0] + offset_x, self.start[1] + offset_y) - self.end = (self.end[0] + offset_x, self.end[1] + offset_y) - - def rotate(self, angle, center=(0, 0)): - self.start_angle += angle - self.end_angle += angle - self.center = rotate_point(self.center, angle, center) - self.start = rotate_point(self.start, angle, center) - self.end = rotate_point(self.end, angle, center) - self.angle_regions = _normalize_angle(self.start_angle, self.end_angle) - - def intersections_with_halfline(self, point_from, point_to, error_range): - intersection = \ - _intersections_of_line_and_circle( - point_from, point_to, self.center, self.radius, error_range) - if intersection is None: - return [] - else: - p1, p2, p1_angle, p2_angle, p1_t, p2_t = intersection - - if is_equal_point(p1, self.start, error_range): - p1 = None - elif p2 is not None and is_equal_point(p2, self.start, error_range): - p2 = None - - def is_contained(angle, region, error): - if angle >= region[0] - error and angle <= region[1] + error: - return True - if angle < 0 and region[1] > 0: - angle = angle + 2 * pi - elif angle > 0 and region[0] < 0: - angle = angle - 2 * pi - return angle >= region[0] - error and angle <= region[1] + error - - aerror = error_range * self.radius - pts = [] - if p1 is not None and p1_t >= 0 and not is_equal_point(p1, self.start, error_range): - for region in self.angle_regions: - if is_contained(p1_angle, region, aerror): - pts.append(p1) - break - if p2 is not None and p2_t >= 0 and not is_equal_point(p2, self.start, error_range): - for region in self.angle_regions: - if is_contained(p2_angle, region, aerror): - pts.append(p2) - break - - return pts - - def intersections_with_arc(self, center, radius, angle_regions, error_range): - x1 = center[0] - self.center[0] - y1 = center[1] - self.center[1] - r1 = self.radius - r2 = radius - cd_sq = x1 * x1 + y1 * y1 - cd = sqrt(cd_sq) - rd = abs(r1 - r2) - - if (cd >= 0 and cd <= rd) or cd >= r1 + r2: - return [] - - A = (cd_sq + r1 * r1 - r2 * r2) / 2 - scale = sqrt(cd_sq * r1 * r1 - A * A) / cd_sq - xl = A * x1 / cd_sq - xr = y1 * scale - yl = A * y1 / cd_sq - yr = x1 * scale - - pt1_x = xl + xr - pt1_y = yl - yr - pt2_x = xl - xr - pt2_y = yl + yr - pt1_angle1 = atan2(pt1_y, pt1_x) - pt1_angle2 = atan2(pt1_y - y1, pt1_x - x1) - pt2_angle1 = atan2(pt2_y, pt2_x) - pt2_angle2 = atan2(pt2_y - y1, pt2_x - x1) - - aerror = error_range * self.radius - pts=[] - for region in self.angle_regions: - if pt1_angle1 >= region[0] and pt1_angle1 <= region[1]: - for region in angle_regions: - if pt1_angle2 >= region[0] - aerror and pt1_angle2 <= region[1] + aerror: - pts.append((pt1_x + self.center[0], pt1_y + self.center[1])) - break - break - for region in self.angle_regions: - if pt2_angle1 >= region[0] and pt2_angle1 <= region[1]: - for region in angle_regions: - if pt2_angle2 >= region[0] - aerror and pt2_angle2 <= region[1] + aerror: - pts.append((pt2_x + self.center[0], pt2_y + self.center[1])) - break - break - return pts - -class DxfPolylineStatement(DxfStatement): - def __init__(self, entity): - super(DxfPolylineStatement, self).__init__(entity) - self.start = (self.entity.points[0][0], self.entity.points[0][1]) - self.is_closed = self.entity.is_closed - if self.is_closed: - self.end = self.start - else: - self.end = (self.entity.points[-1][0], self.entity.points[-1][1]) - - def disassemble(self): - class Item: - pass - - def ptseq(): - for i in range(1, len(self.entity.points)): - yield i - if self.entity.is_closed: - yield 0 - - x0 = self.entity.points[0][0] - y0 = self.entity.points[0][1] - b = self.entity.bulge[0] - for idx in ptseq(): - pt = self.entity.points[idx] - x1 = pt[0] - y1 = pt[1] - if b == 0: - item = Item() - item.dxftype = 'LINE' - item.start = (x0, y0) - item.end = (x1, y1) - item.is_closed = False - yield DxfLineStatement.from_entity(item) - else: - ang = 4 * atan(b) - xm = x0 + x1 - ym = y0 + y1 - t = 1 / tan(ang / 2) - xc = (xm - t * (y1 - y0)) / 2 - yc = (ym + t * (x1 - x0)) / 2 - r = sqrt((x0 - xc)*(x0 - xc) + (y0 - yc)*(y0 - yc)) - rx0 = x0 - xc - ry0 = y0 - yc - rc = max(min(rx0 / r, 1.0), -1.0) - start_angle = acos(rc) if ry0 > 0 else 2 * pi - acos(rc) - start_angle *= 180 / pi - end_angle = start_angle + ang * 180 / pi - - item = Item() - item.dxftype = 'ARC' - item.start = (x0, y0) - item.end = (x1, y1) - item.start_angle = start_angle - item.end_angle = end_angle - item.radius = r - item.center = (xc, yc) - item.is_closed = end_angle - start_angle >= 360 - yield DxfArcStatement(item) - - x0 = x1 - y0 = y1 - b = self.entity.bulge[idx] - - def to_inch(self): - self.start = (inch(self.start[0]), inch(self.start[1])) - self.end = (inch(self.end[0]), inch(self.end[1])) - for idx in range(0, len(self.entity.points)): - self.entity.points[idx] = ( - inch(self.entity.points[idx][0]), inch(self.entity.points[idx][1])) - - def to_metric(self): - self.start = (metric(self.start[0]), metric(self.start[1])) - self.end = (metric(self.end[0]), metric(self.end[1])) - for idx in range(0, len(self.entity.points)): - self.entity.points[idx] = ( - metric(self.entity.points[idx][0]), metric(self.entity.points[idx][1])) - - def offset(self, offset_x, offset_y): - for idx in range(len(self.entity.points)): - self.entity.points[idx] = ( - self.entity.points[idx][0] + offset_x, self.entity.points[idx][1] + offset_y) - - def rotate(self, angle, center=(0, 0)): - for idx in range(len(self.entity.points)): - self.entity.points[idx] = rotate_point(self.entity.points[idx], angle, center) - -class DxfStatements(object): - def __init__(self, statements, units, dcode=10, draw_mode=None, fill_mode=None): - if draw_mode is None: - draw_mode = DxfFile.DM_LINE - if fill_mode is None: - fill_mode = DxfFile.FM_TURN_OVER - self._units = units - self.dcode = dcode - self.draw_mode = draw_mode - self.fill_mode = fill_mode - self.pitch = inch(1) if self._units == 'inch' else 1 - self.width = 0 - self.error_range = inch(ACCEPTABLE_ERROR) if self._units == 'inch' else ACCEPTABLE_ERROR - self.statements = list(filter( - lambda i: not (isinstance(i, DxfLineStatement) and \ - is_equal_point(i.start, i.end, self.error_range)), - statements - )) - self.close_paths, self.open_paths = generate_paths(self.statements, self.error_range) - self.sorted_close_paths = [] - self.polarity = True # True means dark, False means clear - - @property - def units(self): - return _units - - def _polarity_command(self, polarity=None): - if polarity is None: - polarity = self.polarity - return '%LPD*%' if polarity else '%LPC*%' - - def _prepare_sorted_close_paths(self): - if self.sorted_close_paths: - return - for i in range(0, len(self.close_paths)): - for j in range(i + 1, len(self.close_paths)): - containee, container = judge_containment( - self.close_paths[i], self.close_paths[j], self.error_range) - if containee is not None: - containee.containers.append(container) - self.sorted_close_paths = sorted(self.close_paths, key=lambda path: len(path.containers)) - - def to_gerber(self, settings=FileSettings()): - def gerbers(): - yield 'G75*' - yield self._polarity_command() - yield 'D{0}*'.format(self.dcode) - if self.draw_mode == DxfFile.DM_FILL: - yield 'G36*' - if self.fill_mode == DxfFile.FM_TURN_OVER: - self._prepare_sorted_close_paths() - polarity = self.polarity - level = 0 - for path in self.sorted_close_paths: - if len(path.containers) > level: - level = len(path.containers) - polarity = not polarity - yield 'G37*' - yield self._polarity_command(polarity) - yield 'G36*' - yield path.to_gerber(settings) - else: - for path in self.close_paths: - yield path.to_gerber(settings) - yield 'G37*' - else: - pitch = self.pitch if self.draw_mode == DxfFile.DM_MOUSE_BITES else 0 - for path in self.open_paths: - yield path.to_gerber(settings, pitch=pitch, width=self.width) - for path in self.close_paths: - yield path.to_gerber(settings, pitch=pitch, width=self.width) - - return '\n'.join(gerbers()) - - def to_excellon(self, settings=FileSettings()): - if self.draw_mode == DxfFile.DM_FILL: - return - def drills(): - pitch = self.pitch if self.draw_mode == DxfFile.DM_MOUSE_BITES else 0 - for path in self.open_paths: - yield path.to_excellon(settings, pitch=pitch, width=self.width) - for path in self.close_paths: - yield path.to_excellon(settings, pitch=pitch, width=self.width) - return '\n'.join(drills()) - - def to_inch(self): - if self._units == 'metric': - self._units = 'inch' - self.pitch = inch(self.pitch) - self.width = inch(self.width) - self.error_range = inch(self.error_range) - for path in self.open_paths: - path.to_inch() - for path in self.close_paths: - path.to_inch() - - def to_metric(self): - if self._units == 'inch': - self._units = 'metric' - self.pitch = metric(self.pitch) - self.width = metric(self.width) - self.error_range = metric(self.error_range) - for path in self.open_paths: - path.to_metric() - for path in self.close_paths: - path.to_metric() - - def offset(self, offset_x, offset_y): - for path in self.open_paths: - path.offset(offset_x, offset_y) - for path in self.close_paths: - path.offset(offset_x, offset_y) - - def rotate(self, angle, center=(0, 0)): - for path in self.open_paths: - path.rotate(angle, center) - for path in self.close_paths: - path.rotate(angle, center) - -class DxfFile(CamFile): - DM_LINE = 0 - DM_FILL = 1 - DM_MOUSE_BITES = 2 - - FM_SIMPLE = 0 - FM_TURN_OVER = 1 - - FT_RX274X = 0 - FT_EXCELLON = 1 - - @classmethod - def from_dxf(cls, dxf, settings=None, draw_mode=None, filename=None): - fsettings = settings if settings else \ - FileSettings(zero_suppression='leading') - - if dxf.header['$INSUNITS'] == 1: - fsettings.units = 'inch' - if not settings: - fsettings.format = (2, 5) - else: - fsettings.units = 'metric' - if not settings: - fsettings.format = (3, 4) - - statements = [] - for entity in dxf.entities: - if entity.dxftype == 'LWPOLYLINE': - statements.append(DxfPolylineStatement(entity)) - elif entity.dxftype == 'LINE': - statements.append(DxfLineStatement.from_entity(entity)) - elif entity.dxftype == 'CIRCLE': - statements.append(DxfArcStatement(entity)) - elif entity.dxftype == 'ARC': - statements.append(DxfArcStatement(entity)) - - return cls(statements, fsettings, draw_mode, filename) - - @classmethod - def rectangle(cls, width, height, left=0, bottom=0, units='metric', draw_mode=None, filename=None): - if units == 'metric': - settings = FileSettings(units=units, zero_suppression='leading', format=(3,4)) - else: - settings = FileSettings(units=units, zero_suppression='leading', format=(2,5)) - statements = [ - DxfLineStatement(None, (left, bottom), (left + width, bottom)), - DxfLineStatement(None, (left + width, bottom), (left + width, bottom + height)), - DxfLineStatement(None, (left + width, bottom + height), (left, bottom + height)), - DxfLineStatement(None, (left, bottom + height), (left, bottom)), - ] - return cls(statements, settings, draw_mode, filename) - - def __init__(self, statements, settings=None, draw_mode=None, filename=None): - if not settings: - settings = FileSettings(units='metric', format=(3,4), zero_suppression='leading') - if draw_mode == None: - draw_mode = self.DM_LINE - - super(DxfFile, self).__init__(settings=settings, filename=filename) - self._draw_mode = draw_mode - self._fill_mode = self.FM_TURN_OVER - - self.aperture = ADParamStmt.circle(dcode=10, diameter=0.0) - if settings.units == 'inch': - self.aperture.to_inch() - else: - self.aperture.to_metric() - self.statements = DxfStatements( - statements, self.units, dcode=self.aperture.d, draw_mode=self.draw_mode, fill_mode=self.filename) - - @property - def dcode(self): - return self.aperture.dcode - - @dcode.setter - def dcode(self, value): - self.aperture.d = value - self.statements.dcode = value - - @property - def width(self): - return self.aperture.modifiers[0][0] - - @width.setter - def width(self, value): - self.aperture.modifiers = ([float(value),],) - self.statements.width = value - - @property - def draw_mode(self): - return self._draw_mode - - @draw_mode.setter - def draw_mode(self, value): - self._draw_mode = value - self.statements.draw_mode = value - - @property - def fill_mode(self): - return self._fill_mode - - @fill_mode.setter - def fill_mode(self, value): - self._fill_mode = value - self.statements.fill_mode = value - - @property - def pitch(self): - return self.statements.pitch - - @pitch.setter - def pitch(self, value): - self.statements.pitch = value - - def write(self, filename=None, filetype=FT_RX274X): - self.settings.notation = 'absolute' - self.settings.zeros = 'trailing' - filename = filename if filename is not None else self.filename - with open(filename, 'w') as f: - if filetype == self.FT_RX274X: - write_gerber_header(f, self.settings) - f.write(self.aperture.to_gerber(self.settings) + '\n') - f.write(self.statements.to_gerber(self.settings) + '\n') - f.write('M02*\n') - else: - tools = [ExcellonTool(self.settings, number=1, diameter=self.width)] - write_excellon_header(f, self.settings, tools) - f.write('T01\n') - f.write(self.statements.to_excellon(self.settings) + '\n') - f.write('M30\n') - - - def to_inch(self): - if self.units == 'metric': - self.aperture.to_inch() - self.statements.to_inch() - self.pitch = inch(self.pitch) - self.units = 'inch' - - def to_metric(self): - if self.units == 'inch': - self.aperture.to_metric() - self.statements.to_metric() - self.pitch = metric(self.pitch) - self.units = 'metric' - - def offset(self, offset_x, offset_y): - self.statements.offset(offset_x, offset_y) - - def rotate(self, angle, center=(0, 0)): - self.statements.rotate(angle, center) - - def negate_polarity(self): - self.statements.polarity = not self.statements.polarity - -def loads(data, filename=None): - if sys.version_info.major == 2: - data = unicode(data) - stream = io.StringIO(data) - dxf = dxfgrabber.read(stream) - return DxfFile.from_dxf(dxf) diff --git a/gerbonara/gerber/panelize/dxf_path.py b/gerbonara/gerber/panelize/dxf_path.py deleted file mode 100644 index 201dcff..0000000 --- a/gerbonara/gerber/panelize/dxf_path.py +++ /dev/null @@ -1,412 +0,0 @@ -#!/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 diff --git a/gerbonara/gerber/panelize/gerber_statements.py b/gerbonara/gerber/panelize/gerber_statements.py deleted file mode 100644 index 208660e..0000000 --- a/gerbonara/gerber/panelize/gerber_statements.py +++ /dev/null @@ -1,49 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -# Copyright 2019 Hiroshi Murayama <opiopan@gmail.com> - -from ..gerber_statements import AMParamStmt, ADParamStmt -from ..utils import inch, metric -from .am_primitive import to_primitive_defs - -class ADParamStmtEx(ADParamStmt): - GEOMETRIES = { - 'C': [0,1], - 'R': [0,1,2], - 'O': [0,1,2], - 'P': [0,3], - } - - @classmethod - def from_stmt(cls, stmt): - modstr = ','.join([ - 'X'.join(['{0}'.format(x) for x in modifier]) - for modifier in stmt.modifiers]) - return cls(stmt.param, stmt.d, stmt.shape, modstr, stmt.units) - - def __init__(self, param, d, shape, modifiers, units): - super(ADParamStmtEx, self).__init__(param, d, shape, modifiers) - self.units = units - - def to_inch(self): - if self.units == 'inch': - return - self.units = 'inch' - if self.shape in self.GEOMETRIES: - indices = self.GEOMETRIES[self.shape] - self.modifiers = [tuple([ - inch(self.modifiers[0][i]) if i in indices else self.modifiers[0][i] \ - for i in range(len(self.modifiers[0])) - ])] - - def to_metric(self): - if self.units == 'metric': - return - self.units = 'metric' - if self.shape in self.GEOMETRIES: - indices = self.GEOMETRIES[self.shape] - self.modifiers = [tuple([ - metric(self.modifiers[0][i]) if i in indices else self.modifiers[0][i] \ - for i in range(len(self.modifiers[0])) - ])] diff --git a/gerbonara/gerber/panelize/utility.py b/gerbonara/gerber/panelize/utility.py deleted file mode 100644 index 0d57979..0000000 --- a/gerbonara/gerber/panelize/utility.py +++ /dev/null @@ -1,20 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -# Copyright 2019 Hiroshi Murayama <opiopan@gmail.com> - -from math import cos, sin, pi, sqrt - -def is_equal_value(a, b, error_range=0): - return (a - b) * (a - b) <= error_range * error_range - -def is_equal_point(a, b, error_range=0): - return is_equal_value(a[0], b[0], error_range) and \ - is_equal_value(a[1], b[1], error_range) - -def normalize_vec2d(vec): - length = sqrt(vec[0] * vec[0] + vec[1] * vec[1]) - return (vec[0] / length, vec[1] / length) - -def dot_vec2d(vec1, vec2): - return vec1[0] * vec2[0] + vec1[1] * vec2[1] diff --git a/gerbonara/gerber/render/excellon_backend.py b/gerbonara/gerber/render/excellon_backend.py deleted file mode 100644 index 765d68c..0000000 --- a/gerbonara/gerber/render/excellon_backend.py +++ /dev/null @@ -1,188 +0,0 @@ - -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.start = [HeaderBeginStmt()] - - # Current tool and position - self.handled_tools = set() - self.cur_tool = None - self.drill_mode = ExcellonContext.MODE_DRILL - self.drill_down = False - self._pos = (None, None) - - self.settings = settings - - self._start_header() - self._start_comments() - - def _start_header(self): - """Create the header from the settings""" - - self.header.append(UnitStmt.from_settings(self.settings)) - - if self.settings.notation == 'incremental': - raise NotImplementedError('Incremental mode is not implemented') - else: - self.body.append(AbsoluteModeStmt()) - - 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.start + self.comments + self.header + self.body_start + self.body + self._get_end() - - def set_bounds(self, bounds, *args, **kwargs): - pass - - def paint_background(self): - pass - - def _render_line(self, line, color): - raise ValueError('Invalid Excellon object') - def _render_arc(self, arc, color): - raise ValueError('Invalid Excellon object') - - def _render_region(self, region, color): - raise ValueError('Invalid Excellon object') - - def _render_level_polarity(self, region): - raise ValueError('Invalid Excellon object') - - def _render_circle(self, circle, color): - raise ValueError('Invalid Excellon object') - - def _render_rectangle(self, rectangle, color): - raise ValueError('Invalid Excellon object') - - def _render_obround(self, obround, color): - raise ValueError('Invalid Excellon object') - - def _render_polygon(self, polygon, color): - raise ValueError('Invalid Excellon object') - - def _simplify_point(self, point): - return (point[0] if point[0] != self._pos[0] else None, point[1] if point[1] != self._pos[1] else None) - - def _render_drill(self, drill, color): - - 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 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(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_mode = 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/gerbonara/gerber/render/render.py b/gerbonara/gerber/render/render.py deleted file mode 100644 index 580a7ea..0000000 --- a/gerbonara/gerber/render/render.py +++ /dev/null @@ -1,246 +0,0 @@ -#! /usr/bin/env python -# -*- coding: utf-8 -*- - -# copyright 2014 Hamilton Kibbe <ham@hamiltonkib.be> -# Modified from code by Paulo Henrique Silva <ph.silva@gmail.com> - -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at - -# http://www.apache.org/licenses/LICENSE-2.0 - -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -""" -Rendering -============ -**Gerber (RS-274X) and Excellon file rendering** - -Render Gerber and Excellon files to a variety of formats. The render module -currently supports SVG rendering using the `svgwrite` library. -""" - - -from ..primitives import * -from ..gerber_statements import (CommentStmt, UnknownStmt, EofStmt, ParamStmt, - CoordStmt, ApertureStmt, RegionModeStmt, - QuadrantModeStmt,) - - -class GerberContext(object): - """ Gerber rendering context base class - - Provides basic functionality and API for rendering gerber files. Medium- - specific renderers should subclass GerberContext and implement the drawing - functions. Colors are stored internally as 32-bit RGB and may need to be - converted to a native format in the rendering subclass. - - Attributes - ---------- - units : string - Measurement units. 'inch' or 'metric' - - color : tuple (<float>, <float>, <float>) - Color used for rendering as a tuple of normalized (red, green, blue) - values. - - drill_color : tuple (<float>, <float>, <float>) - Color used for rendering drill hits. Format is the same as for `color`. - - background_color : tuple (<float>, <float>, <float>) - Color of the background. Used when exposing areas in 'clear' level - polarity mode. Format is the same as for `color`. - - alpha : float - Rendering opacity. Between 0.0 (transparent) and 1.0 (opaque.) - """ - - def __init__(self, units='inch'): - self._units = units - self._color = (0.7215, 0.451, 0.200) - self._background_color = (0.0, 0.0, 0.0) - self._drill_color = (0.0, 0.0, 0.0) - self._alpha = 1.0 - self._invert = False - self.ctx = None - - @property - def units(self): - return self._units - - @units.setter - def units(self, units): - if units not in ('inch', 'metric'): - raise ValueError('Units may be "inch" or "metric"') - self._units = units - - @property - def color(self): - return self._color - - @color.setter - def color(self, color): - if len(color) != 3: - raise TypeError('Color must be a tuple of R, G, and B values') - for c in color: - if c < 0 or c > 1: - raise ValueError('Channel values must be between 0.0 and 1.0') - self._color = color - - @property - def drill_color(self): - return self._drill_color - - @drill_color.setter - def drill_color(self, color): - if len(color) != 3: - raise TypeError('Drill color must be a tuple of R, G, and B values') - for c in color: - if c < 0 or c > 1: - raise ValueError('Channel values must be between 0.0 and 1.0') - self._drill_color = color - - @property - def background_color(self): - return self._background_color - - @background_color.setter - def background_color(self, color): - if len(color) != 3: - raise TypeError('Background color must be a tuple of R, G, and B values') - for c in color: - if c < 0 or c > 1: - raise ValueError('Channel values must be between 0.0 and 1.0') - self._background_color = color - - @property - def alpha(self): - return self._alpha - - @alpha.setter - def alpha(self, alpha): - if alpha < 0 or alpha > 1: - raise ValueError('Alpha must be between 0.0 and 1.0') - self._alpha = alpha - - @property - def invert(self): - return self._invert - - @invert.setter - def invert(self, invert): - self._invert = invert - - def render(self, primitive): - if not primitive: - return - - self.pre_render_primitive(primitive) - - color = self.color - if isinstance(primitive, Line): - self._render_line(primitive, color) - elif isinstance(primitive, Arc): - self._render_arc(primitive, color) - elif isinstance(primitive, Region): - self._render_region(primitive, color) - elif isinstance(primitive, Circle): - self._render_circle(primitive, color) - elif isinstance(primitive, Rectangle): - self._render_rectangle(primitive, color) - elif isinstance(primitive, Obround): - self._render_obround(primitive, color) - elif isinstance(primitive, Polygon): - self._render_polygon(primitive, color) - elif isinstance(primitive, Drill): - self._render_drill(primitive, self.color) - elif isinstance(primitive, Slot): - self._render_slot(primitive, self.color) - elif isinstance(primitive, AMGroup): - self._render_amgroup(primitive, color) - elif isinstance(primitive, Outline): - self._render_region(primitive, color) - elif isinstance(primitive, TestRecord): - self._render_test_record(primitive, color) - - self.post_render_primitive(primitive) - - def set_bounds(self, bounds, *args, **kwargs): - """Called by the renderer to set the extents of the file to render. - - Parameters - ---------- - bounds: Tuple[Tuple[float, float], Tuple[float, float]] - ( (x_min, x_max), (y_min, y_max) - """ - pass - - def paint_background(self): - pass - - def new_render_layer(self): - pass - - def flatten(self): - pass - - def pre_render_primitive(self, primitive): - """ - Called before rendering a primitive. Use the callback to perform some action before rendering - a primitive, for example adding a comment. - """ - return - - def post_render_primitive(self, primitive): - """ - Called after rendering a primitive. Use the callback to perform some action after rendering - a primitive - """ - return - - - def _render_line(self, primitive, color): - pass - - def _render_arc(self, primitive, color): - pass - - def _render_region(self, primitive, color): - pass - - def _render_circle(self, primitive, color): - pass - - def _render_rectangle(self, primitive, color): - pass - - def _render_obround(self, primitive, color): - pass - - def _render_polygon(self, primitive, color): - pass - - def _render_drill(self, primitive, color): - pass - - def _render_slot(self, primitive, color): - pass - - def _render_amgroup(self, primitive, color): - pass - - def _render_test_record(self, primitive, color): - pass - - -class RenderSettings(object): - def __init__(self, color=(0.0, 0.0, 0.0), alpha=1.0, invert=False, - mirror=False): - self.color = color - self.alpha = alpha - self.invert = invert - self.mirror = mirror diff --git a/gerbonara/gerber/render/rs274x_backend.py b/gerbonara/gerber/render/rs274x_backend.py deleted file mode 100644 index bf9f164..0000000 --- a/gerbonara/gerber/render/rs274x_backend.py +++ /dev/null @@ -1,510 +0,0 @@ -"""Renders an in-memory Gerber file to statements which can be written to a string -""" -from copy import deepcopy - -try: - from cStringIO import StringIO -except(ImportError): - from io import StringIO - -from .render import GerberContext -from ..am_statements import * -from ..gerber_statements import * -from ..primitives import AMGroup, Arc, Circle, Line, Obround, Outline, Polygon, Rectangle - - -class AMGroupContext(object): - '''A special renderer to generate aperature macros from an AMGroup''' - - def __init__(self): - self.statements = [] - - def render(self, amgroup, name): - - if amgroup.stmt: - # We know the statement it was generated from, so use that to create the AMParamStmt - # It will give a much better result - - stmt = deepcopy(amgroup.stmt) - stmt.name = name - - return stmt - - else: - # Clone ourselves, then offset by the psotion so that - # our render doesn't have to consider offset. Just makes things simpler - nooffset_group = deepcopy(amgroup) - nooffset_group.position = (0, 0) - - # Now draw the shapes - for primitive in nooffset_group.primitives: - if isinstance(primitive, Outline): - self._render_outline(primitive) - elif isinstance(primitive, Circle): - self._render_circle(primitive) - elif isinstance(primitive, Rectangle): - self._render_rectangle(primitive) - elif isinstance(primitive, Line): - self._render_line(primitive) - elif isinstance(primitive, Polygon): - self._render_polygon(primitive) - else: - raise ValueError('amgroup') - - statement = AMParamStmt('AM', name, self._statements_to_string()) - return statement - - def _statements_to_string(self): - macro = '' - - for statement in self.statements: - macro += statement.to_gerber() - - return macro - - def _render_circle(self, circle): - self.statements.append(AMCirclePrimitive.from_primitive(circle)) - - def _render_rectangle(self, rectangle): - self.statements.append(AMCenterLinePrimitive.from_primitive(rectangle)) - - def _render_line(self, line): - self.statements.append(AMVectorLinePrimitive.from_primitive(line)) - - def _render_outline(self, outline): - self.statements.append(AMOutlinePrimitive.from_primitive(outline)) - - def _render_polygon(self, polygon): - self.statements.append(AMPolygonPrimitive.from_primitive(polygon)) - - def _render_thermal(self, thermal): - pass - - -class Rs274xContext(GerberContext): - - def __init__(self, settings): - GerberContext.__init__(self) - self.comments = [] - self.header = [] - self.body = [] - self.end = [EofStmt()] - - # Current values so we know if we have to execute - # moves, levey changes before anything else - self._level_polarity = None - self._pos = (None, None) - self._func = None - self._quadrant_mode = None - self._dcode = None - - # Primarily for testing and comarison to files, should we write - # flashes as a single statement or a move plus flash? Set to true - # to do in a single statement. Normally this can be false - self.condensed_flash = True - - # When closing a region, force a D02 staement to close a region. - # This is normally not necessary because regions are closed with a G37 - # staement, but this will add an extra statement for doubly close - # the region - self.explicit_region_move_end = False - - self._next_dcode = 10 - self._rects = {} - self._circles = {} - self._obrounds = {} - self._polygons = {} - self._macros = {} - - self._i_none = 0 - self._j_none = 0 - - self.settings = settings - - self._start_header(settings) - - def _start_header(self, settings): - self.header.append(FSParamStmt.from_settings(settings)) - self.header.append(MOParamStmt.from_units(settings.units)) - - def _simplify_point(self, point): - return (point[0] if point[0] != self._pos[0] else None, point[1] if point[1] != self._pos[1] else None) - - def _simplify_offset(self, point, offset): - - if point[0] != offset[0]: - xoffset = point[0] - offset[0] - else: - xoffset = self._i_none - - if point[1] != offset[1]: - yoffset = point[1] - offset[1] - else: - yoffset = self._j_none - - return (xoffset, yoffset) - - @property - def statements(self): - return self.comments + self.header + self.body + self.end - - def set_bounds(self, bounds, *args, **kwargs): - pass - - def paint_background(self): - pass - - def _select_aperture(self, aperture): - - # Select the right aperture if not already selected - if aperture: - if isinstance(aperture, Circle): - aper = self._get_circle(aperture.diameter, aperture.hole_diameter, aperture.hole_width, aperture.hole_height) - elif isinstance(aperture, Rectangle): - aper = self._get_rectangle(aperture.width, aperture.height) - elif isinstance(aperture, Obround): - aper = self._get_obround(aperture.width, aperture.height) - elif isinstance(aperture, AMGroup): - aper = self._get_amacro(aperture) - else: - raise NotImplementedError('Line with invalid aperture type') - - if aper.d != self._dcode: - self.body.append(ApertureStmt(aper.d)) - self._dcode = aper.d - - def pre_render_primitive(self, primitive): - - if hasattr(primitive, 'comment'): - self.body.append(CommentStmt(primitive.comment)) - - def _render_line(self, line, color, default_polarity='dark'): - - self._select_aperture(line.aperture) - - self._render_level_polarity(line, default_polarity) - - # Get the right function - if self._func != CoordStmt.FUNC_LINEAR: - func = CoordStmt.FUNC_LINEAR - else: - func = None - self._func = CoordStmt.FUNC_LINEAR - - if self._pos != line.start: - self.body.append(CoordStmt.move(func, self._simplify_point(line.start))) - self._pos = line.start - # We already set the function, so the next command doesn't require that - func = None - - point = self._simplify_point(line.end) - - # In some files, we see a lot of duplicated ponts, so omit those - if point[0] != None or point[1] != None: - self.body.append(CoordStmt.line(func, self._simplify_point(line.end))) - self._pos = line.end - elif func: - self.body.append(CoordStmt.mode(func)) - - def _render_arc(self, arc, color, default_polarity='dark'): - - # Optionally set the quadrant mode if it has changed: - if arc.quadrant_mode != self._quadrant_mode: - - if arc.quadrant_mode != 'multi-quadrant': - self.body.append(QuadrantModeStmt.single()) - else: - self.body.append(QuadrantModeStmt.multi()) - - self._quadrant_mode = arc.quadrant_mode - - # Select the right aperture if not already selected - self._select_aperture(arc.aperture) - - self._render_level_polarity(arc, default_polarity) - - # Find the right movement mode. Always set to be sure it is really right - dir = arc.direction - if dir == 'clockwise': - func = CoordStmt.FUNC_ARC_CW - self._func = CoordStmt.FUNC_ARC_CW - elif dir == 'counterclockwise': - func = CoordStmt.FUNC_ARC_CCW - self._func = CoordStmt.FUNC_ARC_CCW - else: - raise ValueError('Invalid circular interpolation mode') - - if self._pos != arc.start: - # TODO I'm not sure if this is right - self.body.append(CoordStmt.move(CoordStmt.FUNC_LINEAR, self._simplify_point(arc.start))) - self._pos = arc.start - - center = self._simplify_offset(arc.center, arc.start) - end = self._simplify_point(arc.end) - self.body.append(CoordStmt.arc(func, end, center)) - self._pos = arc.end - - def _render_region(self, region, color): - - self._render_level_polarity(region) - - self.body.append(RegionModeStmt.on()) - - for p in region.primitives: - - # Make programmatically generated primitives within a region with - # unset level polarity inherit the region's level polarity - if isinstance(p, Line): - self._render_line(p, color, default_polarity=region.level_polarity) - else: - self._render_arc(p, color, default_polarity=region.level_polarity) - - if self.explicit_region_move_end: - self.body.append(CoordStmt.move(None, None)) - - self.body.append(RegionModeStmt.off()) - - def _render_level_polarity(self, obj, default='dark'): - obj_polarity = obj.level_polarity if obj.level_polarity is not None else default - if obj_polarity != self._level_polarity: - self._level_polarity = obj_polarity - self.body.append(LPParamStmt('LP', obj_polarity)) - - def _render_flash(self, primitive, aperture): - - self._render_level_polarity(primitive) - - if aperture.d != self._dcode: - self.body.append(ApertureStmt(aperture.d)) - self._dcode = aperture.d - - if self.condensed_flash: - self.body.append(CoordStmt.flash(self._simplify_point(primitive.position))) - else: - self.body.append(CoordStmt.move(None, self._simplify_point(primitive.position))) - self.body.append(CoordStmt.flash(None)) - - self._pos = primitive.position - - def _get_circle(self, diameter, hole_diameter=None, hole_width=None, - hole_height=None, dcode = None): - '''Define a circlar aperture''' - - key = (diameter, hole_diameter, hole_width, hole_height) - aper = self._circles.get(key, None) - - if not aper: - if not dcode: - dcode = self._next_dcode - self._next_dcode += 1 - else: - self._next_dcode = max(dcode + 1, self._next_dcode) - - aper = ADParamStmt.circle(dcode, diameter, hole_diameter, hole_width, hole_height) - self._circles[(diameter, hole_diameter, hole_width, hole_height)] = aper - self.header.append(aper) - - return aper - - def _render_circle(self, circle, color): - - aper = self._get_circle(circle.diameter, circle.hole_diameter, circle.hole_width, circle.hole_height) - self._render_flash(circle, aper) - - def _get_rectangle(self, width, height, hole_diameter=None, hole_width=None, - hole_height=None, dcode = None): - '''Get a rectanglar aperture. If it isn't defined, create it''' - - key = (width, height, hole_diameter, hole_width, hole_height) - aper = self._rects.get(key, None) - - if not aper: - if not dcode: - dcode = self._next_dcode - self._next_dcode += 1 - else: - self._next_dcode = max(dcode + 1, self._next_dcode) - - aper = ADParamStmt.rect(dcode, width, height, hole_diameter, hole_width, hole_height) - self._rects[(width, height, hole_diameter, hole_width, hole_height)] = aper - self.header.append(aper) - - return aper - - def _render_rectangle(self, rectangle, color): - - aper = self._get_rectangle(rectangle.width, rectangle.height, - rectangle.hole_diameter, - rectangle.hole_width, rectangle.hole_height) - self._render_flash(rectangle, aper) - - def _get_obround(self, width, height, hole_diameter=None, hole_width=None, - hole_height=None, dcode = None): - - key = (width, height, hole_diameter, hole_width, hole_height) - aper = self._obrounds.get(key, None) - - if not aper: - if not dcode: - dcode = self._next_dcode - self._next_dcode += 1 - else: - self._next_dcode = max(dcode + 1, self._next_dcode) - - aper = ADParamStmt.obround(dcode, width, height, hole_diameter, hole_width, hole_height) - self._obrounds[key] = aper - self.header.append(aper) - - return aper - - def _render_obround(self, obround, color): - - aper = self._get_obround(obround.width, obround.height, - obround.hole_diameter, obround.hole_width, - obround.hole_height) - self._render_flash(obround, aper) - - def _render_polygon(self, polygon, color): - - aper = self._get_polygon(polygon.radius, polygon.sides, - polygon.rotation, polygon.hole_diameter, - polygon.hole_width, polygon.hole_height) - self._render_flash(polygon, aper) - - def _get_polygon(self, radius, num_vertices, rotation, hole_diameter=None, - hole_width=None, hole_height=None, dcode = None): - - key = (radius, num_vertices, rotation, hole_diameter, hole_width, hole_height) - aper = self._polygons.get(key, None) - - if not aper: - if not dcode: - dcode = self._next_dcode - self._next_dcode += 1 - else: - self._next_dcode = max(dcode + 1, self._next_dcode) - - aper = ADParamStmt.polygon(dcode, radius * 2, num_vertices, - rotation, hole_diameter, hole_width, - hole_height) - self._polygons[key] = aper - self.header.append(aper) - - return aper - - def _render_drill(self, drill, color): - raise ValueError('Drills are not valid in RS274X files') - - def _hash_amacro(self, amgroup): - '''Calculate a very quick hash code for deciding if we should even check AM groups for comparision''' - - # We always start with an X because this forms part of the name - # Basically, in some cases, the name might start with a C, R, etc. That can appear - # to conflict with normal aperture definitions. Technically, it shouldn't because normal - # aperture definitions should have a comma, but in some cases the commit is omitted - hash = 'X' - for primitive in amgroup.primitives: - - hash += primitive.__class__.__name__[0] - - bbox = primitive.bounding_box - hash += str((bbox[1][0] - bbox[0][0]) * 100000)[0:2] - hash += str((bbox[1][1] - bbox[0][1]) * 100000)[0:2] - - if hasattr(primitive, 'primitives'): - hash += str(len(primitive.primitives)) - - if isinstance(primitive, Rectangle): - hash += str(primitive.width * 1000000)[0:2] - hash += str(primitive.height * 1000000)[0:2] - elif isinstance(primitive, Circle): - hash += str(primitive.diameter * 1000000)[0:2] - - if len(hash) > 20: - # The hash might actually get quite complex, so stop before - # it gets too long - break - - return hash - - def _get_amacro(self, amgroup, dcode = None): - # Macros are a little special since we don't have a good way to compare them quickly - # but in most cases, this should work - - hash = self._hash_amacro(amgroup) - macro = None - macroinfo = self._macros.get(hash, None) - - if macroinfo: - - # We have a definition, but check that the groups actually are the same - for macro in macroinfo: - - # Macros should have positions, right? But if the macro is selected for non-flashes - # then it won't have a position. This is of course a bad gerber, but they do exist - if amgroup.position: - position = amgroup.position - else: - position = (0, 0) - - offset = (position[0] - macro[1].position[0], position[1] - macro[1].position[1]) - if amgroup.equivalent(macro[1], offset): - break - macro = None - - # Did we find one in the group0 - if not macro: - # This is a new macro, so define it - if not dcode: - dcode = self._next_dcode - self._next_dcode += 1 - else: - self._next_dcode = max(dcode + 1, self._next_dcode) - - # Create the statements - # TODO - amrenderer = AMGroupContext() - statement = amrenderer.render(amgroup, hash) - - self.header.append(statement) - - aperdef = ADParamStmt.macro(dcode, hash) - self.header.append(aperdef) - - # Store the dcode and the original so we can check if it really is the same - # If it didn't have a postition, set it to 0, 0 - if amgroup.position == None: - amgroup.position = (0, 0) - macro = (aperdef, amgroup) - - if macroinfo: - macroinfo.append(macro) - else: - self._macros[hash] = [macro] - - return macro[0] - - def _render_amgroup(self, amgroup, color): - - aper = self._get_amacro(amgroup) - self._render_flash(amgroup, aper) - - def _render_inverted_layer(self): - pass - - def new_render_layer(self): - # TODO Might need to implement this - pass - - def flatten(self): - # TODO Might need to implement this - pass - - def dump(self): - """Write the rendered file to a StringIO steam""" - statements = map(lambda stmt: stmt.to_gerber(self.settings), self.statements) - stream = StringIO() - for statement in statements: - stream.write(statement + '\n') - - return stream diff --git a/gerbonara/gerber/render/theme.py b/gerbonara/gerber/render/theme.py deleted file mode 100644 index 2f558a1..0000000 --- a/gerbonara/gerber/render/theme.py +++ /dev/null @@ -1,112 +0,0 @@ -#! /usr/bin/env python -# -*- coding: utf-8 -*- - -# Copyright 2013-2014 Paulo Henrique Silva <ph.silva@gmail.com> - -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at - -# http://www.apache.org/licenses/LICENSE-2.0 - -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - - -from .render import RenderSettings - -COLORS = { - 'black': (0.0, 0.0, 0.0), - 'white': (1.0, 1.0, 1.0), - 'red': (1.0, 0.0, 0.0), - 'green': (0.0, 1.0, 0.0), - 'yellow': (1.0, 1.0, 0), - 'blue': (0.0, 0.0, 1.0), - 'fr-4': (0.290, 0.345, 0.0), - 'green soldermask': (0.0, 0.412, 0.278), - 'blue soldermask': (0.059, 0.478, 0.651), - 'red soldermask': (0.968, 0.169, 0.165), - 'black soldermask': (0.298, 0.275, 0.282), - 'purple soldermask': (0.2, 0.0, 0.334), - 'enig copper': (0.694, 0.533, 0.514), - 'hasl copper': (0.871, 0.851, 0.839) -} - - -SPECTRUM = [ - (0.804, 0.216, 0), - (0.78, 0.776, 0.251), - (0.545, 0.451, 0.333), - (0.545, 0.137, 0.137), - (0.329, 0.545, 0.329), - (0.133, 0.545, 0.133), - (0, 0.525, 0.545), - (0.227, 0.373, 0.804), -] - - -class Theme(object): - - def __init__(self, name=None, **kwargs): - self.name = 'Default' if name is None else name - self.background = kwargs.get('background', RenderSettings(COLORS['fr-4'])) - self.topsilk = kwargs.get('topsilk', RenderSettings(COLORS['white'])) - self.bottomsilk = kwargs.get('bottomsilk', RenderSettings(COLORS['white'], mirror=True)) - self.topmask = kwargs.get('topmask', RenderSettings(COLORS['green soldermask'], alpha=0.85, invert=True)) - self.bottommask = kwargs.get('bottommask', RenderSettings(COLORS['green soldermask'], alpha=0.85, invert=True, mirror=True)) - self.top = kwargs.get('top', RenderSettings(COLORS['hasl copper'])) - self.bottom = kwargs.get('bottom', RenderSettings(COLORS['hasl copper'], mirror=True)) - self.drill = kwargs.get('drill', RenderSettings(COLORS['black'])) - self.ipc_netlist = kwargs.get('ipc_netlist', RenderSettings(COLORS['red'])) - self._internal = kwargs.get('internal', [RenderSettings(x) for x in SPECTRUM]) - self._internal_gen = None - - def __getitem__(self, key): - return getattr(self, key) - - @property - def internal(self): - if not self._internal_gen: - self._internal_gen = self._internal_gen_func() - return next(self._internal_gen) - - def _internal_gen_func(self): - for setting in self._internal: - yield setting - - def get(self, key, noneval=None): - val = getattr(self, key, None) - return val if val is not None else noneval - - -THEMES = { - 'default': Theme(), - 'OSH Park': Theme(name='OSH Park', - background=RenderSettings(COLORS['purple soldermask']), - top=RenderSettings(COLORS['enig copper']), - bottom=RenderSettings(COLORS['enig copper'], mirror=True), - topmask=RenderSettings(COLORS['purple soldermask'], alpha=0.85, invert=True), - bottommask=RenderSettings(COLORS['purple soldermask'], alpha=0.85, invert=True, mirror=True), - topsilk=RenderSettings(COLORS['white'], alpha=0.8), - bottomsilk=RenderSettings(COLORS['white'], alpha=0.8, mirror=True)), - - 'Blue': Theme(name='Blue', - topmask=RenderSettings(COLORS['blue soldermask'], alpha=0.8, invert=True), - bottommask=RenderSettings(COLORS['blue soldermask'], alpha=0.8, invert=True)), - - 'Transparent Copper': Theme(name='Transparent', - background=RenderSettings((0.9, 0.9, 0.9)), - top=RenderSettings(COLORS['red'], alpha=0.5), - bottom=RenderSettings(COLORS['blue'], alpha=0.5), - drill=RenderSettings((0.3, 0.3, 0.3))), - - 'Transparent Multilayer': Theme(name='Transparent Multilayer', - background=RenderSettings((0, 0, 0)), - top=RenderSettings(SPECTRUM[0], alpha=0.8), - bottom=RenderSettings(SPECTRUM[-1], alpha=0.8), - drill=RenderSettings((0.3, 0.3, 0.3)), - internal=[RenderSettings(x, alpha=0.5) for x in SPECTRUM[1:-1]]), -} |