Cleanup, rendering fixes.
fixed rendering of tented vias fixed rendering of semi-transparent layers fixed file type detection issues added some examplesrefactor
|
@ -34,8 +34,6 @@ nosetests.xml
|
|||
.mr.developer.cfg
|
||||
.project
|
||||
.pydevproject
|
||||
.idea/workspace.xml
|
||||
.idea/misc.xml
|
||||
.idea
|
||||
|
||||
# Komodo Files
|
||||
|
@ -43,4 +41,4 @@ nosetests.xml
|
|||
|
||||
# OS Files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
Thumbs.db
|
||||
|
|
4
Makefile
|
@ -20,6 +20,10 @@ test-coverage:
|
|||
rm -rf coverage .coverage
|
||||
$(NOSETESTS) -s -v --with-coverage --cover-package=gerber
|
||||
|
||||
.PHONY: install
|
||||
install:
|
||||
PYTHONPATH=. $(PYTHON) setup.py install
|
||||
|
||||
.PHONY: doc-html
|
||||
doc-html:
|
||||
(cd $(DOC_ROOT); make html)
|
||||
|
|
|
@ -6,7 +6,7 @@ pcb-tools
|
|||
|
||||
Tools to handle Gerber and Excellon files in Python.
|
||||
|
||||
Useage Example:
|
||||
Usage Example:
|
||||
---------------
|
||||
import gerber
|
||||
from gerber.render import GerberCairoContext
|
||||
|
@ -27,6 +27,7 @@ Rendering Examples:
|
|||
-------------------
|
||||
###Top Composite rendering
|
||||
![Composite Top Image](examples/cairo_example.png)
|
||||
![Composite Bottom Image](examples/cairo_bottom.png)
|
||||
|
||||
Source code for this example can be found [here](examples/cairo_example.py).
|
||||
|
||||
|
|
Po Szerokość: | Wysokość: | Rozmiar: 42 KiB |
Przed Szerokość: | Wysokość: | Rozmiar: 102 KiB Po Szerokość: | Wysokość: | Rozmiar: 98 KiB |
|
@ -24,46 +24,54 @@ a .png file.
|
|||
"""
|
||||
|
||||
import os
|
||||
from gerber import read
|
||||
from gerber.render import GerberCairoContext, theme
|
||||
from gerber import load_layer
|
||||
from gerber.render import GerberCairoContext, RenderSettings, theme
|
||||
|
||||
GERBER_FOLDER = os.path.abspath(os.path.join(os.path.dirname(__file__), 'gerbers'))
|
||||
|
||||
|
||||
# Open the gerber files
|
||||
copper = read(os.path.join(GERBER_FOLDER, 'copper.GTL'))
|
||||
mask = read(os.path.join(GERBER_FOLDER, 'soldermask.GTS'))
|
||||
silk = read(os.path.join(GERBER_FOLDER, 'silkscreen.GTO'))
|
||||
drill = read(os.path.join(GERBER_FOLDER, 'ncdrill.DRD'))
|
||||
copper = load_layer(os.path.join(GERBER_FOLDER, 'copper.GTL'))
|
||||
mask = load_layer(os.path.join(GERBER_FOLDER, 'soldermask.GTS'))
|
||||
silk = load_layer(os.path.join(GERBER_FOLDER, 'silkscreen.GTO'))
|
||||
drill = load_layer(os.path.join(GERBER_FOLDER, 'ncdrill.DRD'))
|
||||
|
||||
# Create a new drawing context
|
||||
ctx = GerberCairoContext()
|
||||
|
||||
# Set opacity and color for copper layer
|
||||
ctx.alpha = 1.0
|
||||
ctx.color = theme.COLORS['hasl copper']
|
||||
|
||||
# Draw the copper layer
|
||||
copper.render(ctx)
|
||||
|
||||
# Set opacity and color for soldermask layer
|
||||
ctx.alpha = 0.75
|
||||
ctx.color = theme.COLORS['green soldermask']
|
||||
# Draw the copper layer. render_layer() uses the default color scheme for the
|
||||
# layer, based on the layer type. Copper layers are rendered as
|
||||
ctx.render_layer(copper)
|
||||
|
||||
# Draw the soldermask layer
|
||||
mask.render(ctx, invert=True)
|
||||
ctx.render_layer(mask)
|
||||
|
||||
# Set opacity and color for silkscreen layer
|
||||
ctx.alpha = 1.0
|
||||
ctx.color = theme.COLORS['white']
|
||||
|
||||
# Draw the silkscreen layer
|
||||
silk.render(ctx)
|
||||
# The default style can be overridden by passing a RenderSettings instance to
|
||||
# render_layer().
|
||||
# First, create a settings object:
|
||||
our_settings = RenderSettings(color=theme.COLORS['white'], alpha=0.85)
|
||||
|
||||
# Set opacity for drill layer
|
||||
ctx.alpha = 1.0
|
||||
ctx.color = theme.COLORS['black']
|
||||
drill.render(ctx)
|
||||
# Draw the silkscreen layer, and specify the rendering settings to use
|
||||
ctx.render_layer(silk, settings=our_settings)
|
||||
|
||||
# Draw the drill layer
|
||||
ctx.render_layer(drill)
|
||||
|
||||
# Write output to png file
|
||||
ctx.dump(os.path.join(os.path.dirname(__file__), 'cairo_example.png'))
|
||||
|
||||
# Load the bottom layers
|
||||
copper = load_layer(os.path.join(GERBER_FOLDER, 'bottom_copper.GBL'))
|
||||
mask = load_layer(os.path.join(GERBER_FOLDER, 'bottom_mask.GBS'))
|
||||
|
||||
# Clear the drawing
|
||||
ctx.clear()
|
||||
|
||||
# Render bottom layers
|
||||
ctx.render_layer(copper)
|
||||
ctx.render_layer(mask)
|
||||
ctx.render_layer(drill)
|
||||
|
||||
# Write png file
|
||||
ctx.dump(os.path.join(os.path.dirname(__file__), 'cairo_bottom.png'))
|
||||
|
|
Przed Szerokość: | Wysokość: | Rozmiar: 40 KiB Po Szerokość: | Wysokość: | Rozmiar: 36 KiB |
|
@ -1,7 +1,7 @@
|
|||
#! /usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright 2015 Hamilton Kibbe <ham@hamiltonkib.be>
|
||||
# Copyright 2016 Hamilton Kibbe <ham@hamiltonkib.be>
|
||||
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
|
@ -27,14 +27,25 @@ from gerber.render import GerberCairoContext, theme
|
|||
|
||||
GERBER_FOLDER = os.path.abspath(os.path.join(os.path.dirname(__file__), 'gerbers'))
|
||||
|
||||
|
||||
# Create a new drawing context
|
||||
ctx = GerberCairoContext()
|
||||
|
||||
# Create a new PCB
|
||||
# Create a new PCB instance
|
||||
pcb = PCB.from_directory(GERBER_FOLDER)
|
||||
|
||||
# Render PCB
|
||||
ctx.render_layers(pcb.top_layers, os.path.join(os.path.dirname(__file__), 'pcb_top.png',), theme.THEMES['OSH Park'])
|
||||
ctx.render_layers(pcb.bottom_layers, os.path.join(os.path.dirname(__file__), 'pcb_bottom.png'), theme.THEMES['OSH Park'])
|
||||
# Render PCB top view
|
||||
ctx.render_layers(pcb.top_layers,
|
||||
os.path.join(os.path.dirname(__file__), 'pcb_top.png',),
|
||||
theme.THEMES['OSH Park'])
|
||||
|
||||
# Render PCB bottom view
|
||||
ctx.render_layers(pcb.bottom_layers,
|
||||
os.path.join(os.path.dirname(__file__), 'pcb_bottom.png'),
|
||||
theme.THEMES['OSH Park'])
|
||||
|
||||
# Render copper layers only
|
||||
ctx.render_layers(pcb.copper_layers + pcb.drill_layers,
|
||||
os.path.join(os.path.dirname(__file__),
|
||||
'pcb_transparent_copper.png'),
|
||||
theme.THEMES['Transparent Copper'])
|
||||
|
||||
|
|
Przed Szerokość: | Wysokość: | Rozmiar: 96 KiB Po Szerokość: | Wysokość: | Rozmiar: 90 KiB |
Po Szerokość: | Wysokość: | Rozmiar: 83 KiB |
|
@ -24,4 +24,5 @@ files in python.
|
|||
"""
|
||||
|
||||
from .common import read, loads
|
||||
from .layers import load_layer, load_layer_data
|
||||
from .pcb import PCB
|
||||
|
|
|
@ -251,7 +251,7 @@ class CamFile(object):
|
|||
def to_metric(self):
|
||||
pass
|
||||
|
||||
def render(self, ctx, invert=False, filename=None):
|
||||
def render(self, ctx=None, invert=False, filename=None):
|
||||
""" Generate image of layer.
|
||||
|
||||
Parameters
|
||||
|
@ -262,13 +262,16 @@ class CamFile(object):
|
|||
filename : string <optional>
|
||||
If provided, save the rendered image to `filename`
|
||||
"""
|
||||
if ctx is None:
|
||||
from .render import GerberCairoContext
|
||||
ctx = GerberCairoContext()
|
||||
ctx.set_bounds(self.bounds)
|
||||
ctx._paint_background()
|
||||
ctx.invert = invert
|
||||
ctx._new_render_layer()
|
||||
for p in self.primitives:
|
||||
ctx.render(p)
|
||||
ctx._flatten()
|
||||
ctx._paint()
|
||||
|
||||
if filename is not None:
|
||||
ctx.dump(filename)
|
||||
|
|
|
@ -33,42 +33,41 @@ def read(filename):
|
|||
Returns
|
||||
-------
|
||||
file : CncFile subclass
|
||||
CncFile object representing the file, either GerberFile or
|
||||
ExcellonFile. Returns None if file is not an Excellon or Gerber file.
|
||||
CncFile object representing the file, either GerberFile, ExcellonFile,
|
||||
or IPCNetlist. Returns None if file is not of the proper type.
|
||||
"""
|
||||
with open(filename, 'rU') as f:
|
||||
data = f.read()
|
||||
fmt = detect_file_format(data)
|
||||
if fmt == 'rs274x':
|
||||
return rs274x.read(filename)
|
||||
elif fmt == 'excellon':
|
||||
return excellon.read(filename)
|
||||
elif fmt == 'ipc_d_356':
|
||||
return ipc356.read(filename)
|
||||
else:
|
||||
raise ParseError('Unable to detect file format')
|
||||
return loads(data, filename)
|
||||
|
||||
|
||||
def loads(data):
|
||||
def loads(data, filename=None):
|
||||
""" Read gerber or excellon file contents from a string and return a
|
||||
representative object.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
data : string
|
||||
gerber or excellon file contents as a string.
|
||||
Source file contents as a string.
|
||||
|
||||
filename : string, optional
|
||||
String containing the filename of the data source.
|
||||
|
||||
Returns
|
||||
-------
|
||||
file : CncFile subclass
|
||||
CncFile object representing the file, either GerberFile or
|
||||
ExcellonFile. Returns None if file is not an Excellon or Gerber file.
|
||||
CncFile object representing the data, either GerberFile, ExcellonFile,
|
||||
or IPCNetlist. Returns None if data is not of the proper type.
|
||||
"""
|
||||
|
||||
fmt = detect_file_format(data)
|
||||
if fmt == 'rs274x':
|
||||
return rs274x.loads(data)
|
||||
return rs274x.loads(data, filename)
|
||||
elif fmt == 'excellon':
|
||||
return excellon.loads(data)
|
||||
return excellon.loads(data, filename)
|
||||
elif fmt == 'ipc_d_356':
|
||||
return ipc356.loads(data, filename)
|
||||
else:
|
||||
raise TypeError('Unable to detect file format')
|
||||
raise ParseError('Unable to detect file format')
|
||||
|
||||
|
||||
|
|
|
@ -28,7 +28,7 @@ import operator
|
|||
|
||||
try:
|
||||
from cStringIO import StringIO
|
||||
except(ImportError):
|
||||
except ImportError:
|
||||
from io import StringIO
|
||||
|
||||
from .excellon_statements import *
|
||||
|
@ -57,13 +57,16 @@ def read(filename):
|
|||
return ExcellonParser(settings).parse(filename)
|
||||
|
||||
|
||||
def loads(data):
|
||||
def loads(data, filename=None):
|
||||
""" Read data from string and return an ExcellonFile
|
||||
Parameters
|
||||
----------
|
||||
data : string
|
||||
string containing Excellon file contents
|
||||
|
||||
filename : string, optional
|
||||
string containing the filename of the data source
|
||||
|
||||
Returns
|
||||
-------
|
||||
file : :class:`gerber.excellon.ExcellonFile`
|
||||
|
@ -72,7 +75,7 @@ def loads(data):
|
|||
"""
|
||||
# File object should use settings from source file by default.
|
||||
settings = FileSettings(**detect_excellon_format(data))
|
||||
return ExcellonParser(settings).parse_raw(data)
|
||||
return ExcellonParser(settings).parse_raw(data, filename)
|
||||
|
||||
|
||||
class DrillHit(object):
|
||||
|
|
|
@ -35,7 +35,7 @@ _SM_FIELD = {
|
|||
|
||||
|
||||
def read(filename):
|
||||
""" Read data from filename and return an IPC_D_356
|
||||
""" Read data from filename and return an IPCNetlist
|
||||
Parameters
|
||||
----------
|
||||
filename : string
|
||||
|
@ -43,19 +43,38 @@ def read(filename):
|
|||
|
||||
Returns
|
||||
-------
|
||||
file : :class:`gerber.ipc356.IPC_D_356`
|
||||
An IPC_D_356 object created from the specified file.
|
||||
file : :class:`gerber.ipc356.IPCNetlist`
|
||||
An IPCNetlist object created from the specified file.
|
||||
|
||||
"""
|
||||
# File object should use settings from source file by default.
|
||||
return IPC_D_356.from_file(filename)
|
||||
return IPCNetlist.from_file(filename)
|
||||
|
||||
|
||||
class IPC_D_356(CamFile):
|
||||
def loads(data, filename=None):
|
||||
""" Generate an IPCNetlist object from IPC-D-356 data in memory
|
||||
|
||||
Parameters
|
||||
----------
|
||||
data : string
|
||||
string containing netlist file contents
|
||||
|
||||
filename : string, optional
|
||||
string containing the filename of the data source
|
||||
|
||||
Returns
|
||||
-------
|
||||
file : :class:`gerber.ipc356.IPCNetlist`
|
||||
An IPCNetlist created from the specified file.
|
||||
"""
|
||||
return IPCNetlistParser().parse_raw(data, filename)
|
||||
|
||||
|
||||
class IPCNetlist(CamFile):
|
||||
|
||||
@classmethod
|
||||
def from_file(cls, filename):
|
||||
parser = IPC_D_356_Parser()
|
||||
parser = IPCNetlistParser()
|
||||
return parser.parse(filename)
|
||||
|
||||
def __init__(self, statements, settings, primitives=None, filename=None):
|
||||
|
@ -130,7 +149,7 @@ class IPC_D_356(CamFile):
|
|||
ctx.dump(filename)
|
||||
|
||||
|
||||
class IPC_D_356_Parser(object):
|
||||
class IPCNetlistParser(object):
|
||||
# TODO: Allow multi-line statements (e.g. Altium board edge)
|
||||
|
||||
def __init__(self):
|
||||
|
@ -145,9 +164,13 @@ class IPC_D_356_Parser(object):
|
|||
|
||||
def parse(self, filename):
|
||||
with open(filename, 'rU') as f:
|
||||
oldline = ''
|
||||
for line in f:
|
||||
# Check for existing multiline data...
|
||||
data = f.read()
|
||||
return self.parse_raw(data, filename)
|
||||
|
||||
def parse_raw(self, data, filename=None):
|
||||
oldline = ''
|
||||
for line in data.splitlines():
|
||||
# Check for existing multiline data...
|
||||
if oldline != '':
|
||||
if len(line) and line[0] == '0':
|
||||
oldline = oldline.rstrip('\r\n') + line[3:].rstrip()
|
||||
|
@ -158,7 +181,7 @@ class IPC_D_356_Parser(object):
|
|||
oldline = line
|
||||
self._parse_line(oldline)
|
||||
|
||||
return IPC_D_356(self.statements, self.settings, filename=filename)
|
||||
return IPCNetlist(self.statements, self.settings, filename=filename)
|
||||
|
||||
def _parse_line(self, line):
|
||||
if not len(line):
|
||||
|
|
|
@ -19,8 +19,9 @@ import os
|
|||
import re
|
||||
from collections import namedtuple
|
||||
|
||||
from . import common
|
||||
from .excellon import ExcellonFile
|
||||
from .ipc356 import IPC_D_356
|
||||
from .ipc356 import IPCNetlist
|
||||
|
||||
|
||||
Hint = namedtuple('Hint', 'layer ext name')
|
||||
|
@ -73,9 +74,21 @@ hints = [
|
|||
ext=['ipc'],
|
||||
name=[],
|
||||
),
|
||||
Hint(layer='drawing',
|
||||
ext=['fab'],
|
||||
name=['assembly drawing', 'assembly', 'fabrication', 'fab drawing']
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
def load_layer(filename):
|
||||
return PCBLayer.from_cam(common.read(filename))
|
||||
|
||||
|
||||
def load_layer_data(data, filename=None):
|
||||
return PCBLayer.from_cam(common.loads(data, filename))
|
||||
|
||||
|
||||
def guess_layer_class(filename):
|
||||
try:
|
||||
directory, name = os.path.split(filename)
|
||||
|
@ -89,24 +102,30 @@ def guess_layer_class(filename):
|
|||
return 'unknown'
|
||||
|
||||
|
||||
def sort_layers(layers):
|
||||
def sort_layers(layers, from_top=True):
|
||||
layer_order = ['outline', 'toppaste', 'topsilk', 'topmask', 'top',
|
||||
'internal', 'bottom', 'bottommask', 'bottomsilk',
|
||||
'bottompaste', 'drill', ]
|
||||
'bottompaste']
|
||||
append_after = ['drill', 'drawing']
|
||||
|
||||
output = []
|
||||
drill_layers = [layer for layer in layers if layer.layer_class == 'drill']
|
||||
internal_layers = list(sorted([layer for layer in layers
|
||||
if layer.layer_class == 'internal']))
|
||||
|
||||
for layer_class in layer_order:
|
||||
if layer_class == 'internal':
|
||||
output += internal_layers
|
||||
elif layer_class == 'drill':
|
||||
output += drill_layers
|
||||
else:
|
||||
for layer in layers:
|
||||
if layer.layer_class == layer_class:
|
||||
output.append(layer)
|
||||
if not from_top:
|
||||
output = list(reversed(output))
|
||||
|
||||
for layer_class in append_after:
|
||||
for layer in layers:
|
||||
if layer.layer_class == layer_class:
|
||||
output.append(layer)
|
||||
return output
|
||||
|
||||
|
||||
|
@ -126,14 +145,14 @@ class PCBLayer(object):
|
|||
|
||||
"""
|
||||
@classmethod
|
||||
def from_gerber(cls, camfile):
|
||||
def from_cam(cls, camfile):
|
||||
filename = camfile.filename
|
||||
layer_class = guess_layer_class(filename)
|
||||
if isinstance(camfile, ExcellonFile) or (layer_class == 'drill'):
|
||||
return DrillLayer.from_gerber(camfile)
|
||||
return DrillLayer.from_cam(camfile)
|
||||
elif layer_class == 'internal':
|
||||
return InternalLayer.from_gerber(camfile)
|
||||
if isinstance(camfile, IPC_D_356):
|
||||
return InternalLayer.from_cam(camfile)
|
||||
if isinstance(camfile, IPCNetlist):
|
||||
layer_class = 'ipc_netlist'
|
||||
return cls(filename, layer_class, camfile)
|
||||
|
||||
|
@ -155,9 +174,10 @@ class PCBLayer(object):
|
|||
def __repr__(self):
|
||||
return '<PCBLayer: {}>'.format(self.layer_class)
|
||||
|
||||
|
||||
class DrillLayer(PCBLayer):
|
||||
@classmethod
|
||||
def from_gerber(cls, camfile):
|
||||
def from_cam(cls, camfile):
|
||||
return cls(camfile.filename, camfile)
|
||||
|
||||
def __init__(self, filename=None, cam_source=None, layers=None, **kwargs):
|
||||
|
@ -168,11 +188,11 @@ class DrillLayer(PCBLayer):
|
|||
class InternalLayer(PCBLayer):
|
||||
|
||||
@classmethod
|
||||
def from_gerber(cls, camfile):
|
||||
def from_cam(cls, camfile):
|
||||
filename = camfile.filename
|
||||
try:
|
||||
order = int(re.search(r'\d+', filename).group())
|
||||
except:
|
||||
except AttributeError:
|
||||
order = 0
|
||||
return cls(filename, camfile, order)
|
||||
|
||||
|
@ -209,23 +229,3 @@ class InternalLayer(PCBLayer):
|
|||
if not hasattr(other, 'order'):
|
||||
raise TypeError()
|
||||
return (self.order <= other.order)
|
||||
|
||||
|
||||
class LayerSet(object):
|
||||
|
||||
def __init__(self, name, layers, **kwargs):
|
||||
super(LayerSet, self).__init__(**kwargs)
|
||||
self.name = name
|
||||
self.layers = list(layers)
|
||||
|
||||
def __len__(self):
|
||||
return len(self.layers)
|
||||
|
||||
def __getitem__(self, item):
|
||||
return self.layers[item]
|
||||
|
||||
def to_render(self):
|
||||
return self.layers
|
||||
|
||||
def apply_theme(self, theme):
|
||||
pass
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
|
||||
import os
|
||||
from .exceptions import ParseError
|
||||
from .layers import PCBLayer, LayerSet, sort_layers
|
||||
from .layers import PCBLayer, sort_layers
|
||||
from .common import read as gerber_read
|
||||
from .utils import listdir
|
||||
|
||||
|
@ -29,22 +29,26 @@ class PCB(object):
|
|||
def from_directory(cls, directory, board_name=None, verbose=False):
|
||||
layers = []
|
||||
names = set()
|
||||
|
||||
# Validate
|
||||
directory = os.path.abspath(directory)
|
||||
if not os.path.isdir(directory):
|
||||
raise TypeError('{} is not a directory.'.format(directory))
|
||||
|
||||
# Load gerber files
|
||||
for filename in listdir(directory, True, True):
|
||||
try:
|
||||
camfile = gerber_read(os.path.join(directory, filename))
|
||||
layer = PCBLayer.from_gerber(camfile)
|
||||
layer = PCBLayer.from_cam(camfile)
|
||||
layers.append(layer)
|
||||
names.add(os.path.splitext(filename)[0])
|
||||
if verbose:
|
||||
print('Added {} layer <{}>'.format(layer.layer_class, filename))
|
||||
print('[PCB]: Added {} layer <{}>'.format(layer.layer_class,
|
||||
filename))
|
||||
except ParseError:
|
||||
if verbose:
|
||||
print('Skipping file {}'.format(filename))
|
||||
print('[PCB]: Skipping file {}'.format(filename))
|
||||
|
||||
# Try to guess board name
|
||||
if board_name is None:
|
||||
if len(names) == 1:
|
||||
|
@ -66,14 +70,16 @@ class PCB(object):
|
|||
board_layers = [l for l in reversed(self.layers) if l.layer_class in
|
||||
('topsilk', 'topmask', 'top')]
|
||||
drill_layers = [l for l in self.drill_layers if 'top' in l.layers]
|
||||
return board_layers + drill_layers
|
||||
# Drill layer goes under soldermask for proper rendering of tented vias
|
||||
return [board_layers[0]] + drill_layers + board_layers[1:]
|
||||
|
||||
@property
|
||||
def bottom_layers(self):
|
||||
board_layers = [l for l in self.layers if l.layer_class in
|
||||
('bottomsilk', 'bottommask', 'bottom')]
|
||||
drill_layers = [l for l in self.drill_layers if 'bottom' in l.layers]
|
||||
return board_layers + drill_layers
|
||||
# Drill layer goes under soldermask for proper rendering of tented vias
|
||||
return [board_layers[0]] + drill_layers + board_layers[1:]
|
||||
|
||||
@property
|
||||
def drill_layers(self):
|
||||
|
@ -81,8 +87,9 @@ class PCB(object):
|
|||
|
||||
@property
|
||||
def copper_layers(self):
|
||||
return [layer for layer in self.layers if layer.layer_class in
|
||||
('top', 'bottom', 'internal')]
|
||||
return list(reversed([layer for layer in self.layers if
|
||||
layer.layer_class in
|
||||
('top', 'bottom', 'internal')]))
|
||||
|
||||
@property
|
||||
def layer_count(self):
|
||||
|
|
|
@ -166,7 +166,6 @@ class Primitive(object):
|
|||
in zip(self.position,
|
||||
(x_offset, y_offset))])
|
||||
|
||||
|
||||
def _changed(self):
|
||||
""" Clear memoized properties.
|
||||
|
||||
|
@ -568,11 +567,11 @@ class Rectangle(Primitive):
|
|||
|
||||
@property
|
||||
def axis_aligned_width(self):
|
||||
return (self._cos_theta * self.width + self._sin_theta * self.height)
|
||||
return (self._cos_theta * self.width) + (self._sin_theta * self.height)
|
||||
|
||||
@property
|
||||
def axis_aligned_height(self):
|
||||
return (self._cos_theta * self.height + self._sin_theta * self.width)
|
||||
return (self._cos_theta * self.height) + (self._sin_theta * self.width)
|
||||
|
||||
|
||||
class Diamond(Primitive):
|
||||
|
@ -640,25 +639,24 @@ class Diamond(Primitive):
|
|||
|
||||
@property
|
||||
def axis_aligned_width(self):
|
||||
return (self._cos_theta * self.width + self._sin_theta * self.height)
|
||||
return (self._cos_theta * self.width) + (self._sin_theta * self.height)
|
||||
|
||||
@property
|
||||
def axis_aligned_height(self):
|
||||
return (self._cos_theta * self.height + self._sin_theta * self.width)
|
||||
return (self._cos_theta * self.height) + (self._sin_theta * self.width)
|
||||
|
||||
|
||||
class ChamferRectangle(Primitive):
|
||||
"""
|
||||
"""
|
||||
|
||||
def __init__(self, position, width, height, chamfer, corners, **kwargs):
|
||||
def __init__(self, position, width, height, chamfer, corners=None, **kwargs):
|
||||
super(ChamferRectangle, self).__init__(**kwargs)
|
||||
validate_coordinates(position)
|
||||
self._position = position
|
||||
self._width = width
|
||||
self._height = height
|
||||
self._chamfer = chamfer
|
||||
self._corners = corners
|
||||
self._corners = corners if corners is not None else [True] * 4
|
||||
self._to_convert = ['position', 'width', 'height', 'chamfer']
|
||||
|
||||
@property
|
||||
|
@ -718,7 +716,37 @@ class ChamferRectangle(Primitive):
|
|||
|
||||
@property
|
||||
def vertices(self):
|
||||
# TODO
|
||||
if self._vertices is None:
|
||||
vertices = []
|
||||
delta_w = self.width / 2.
|
||||
delta_h = self.height / 2.
|
||||
# order is UR, UL, LL, LR
|
||||
rect_corners = [
|
||||
((self.position[0] + delta_w), (self.position[1] + delta_h)),
|
||||
((self.position[0] - delta_w), (self.position[1] + delta_h)),
|
||||
((self.position[0] - delta_w), (self.position[1] - delta_h)),
|
||||
((self.position[0] + delta_w), (self.position[1] - delta_h))
|
||||
]
|
||||
for idx, corner, chamfered in enumerate((rect_corners, self.corners)):
|
||||
x, y = corner
|
||||
if chamfered:
|
||||
if idx == 0:
|
||||
vertices.append((x - self.chamfer, y))
|
||||
vertices.append((x, y - self.chamfer))
|
||||
elif idx == 1:
|
||||
vertices.append((x + self.chamfer, y))
|
||||
vertices.append((x, y - self.chamfer))
|
||||
elif idx == 2:
|
||||
vertices.append((x + self.chamfer, y))
|
||||
vertices.append((x, y + self.chamfer))
|
||||
elif idx == 3:
|
||||
vertices.append((x - self.chamfer, y))
|
||||
vertices.append((x, y + self.chamfer))
|
||||
else:
|
||||
vertices.append(corner)
|
||||
self._vertices = [((x * self._cos_theta - y * self._sin_theta),
|
||||
(x * self._sin_theta + y * self._cos_theta))
|
||||
for x, y in vertices]
|
||||
return self._vertices
|
||||
|
||||
@property
|
||||
|
@ -1142,3 +1170,4 @@ class TestRecord(Primitive):
|
|||
self.position = position
|
||||
self.net_name = net_name
|
||||
self.layer = layer
|
||||
self._to_convert = ['position']
|
||||
|
|
|
@ -25,3 +25,4 @@ SVG is the only supported format.
|
|||
|
||||
|
||||
from .cairo_backend import GerberCairoContext
|
||||
from .render import RenderSettings
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
|
||||
|
||||
import cairocffi as cairo
|
||||
import os
|
||||
import tempfile
|
||||
import copy
|
||||
|
||||
|
@ -36,16 +37,16 @@ class GerberCairoContext(GerberContext):
|
|||
super(GerberCairoContext, self).__init__()
|
||||
self.scale = (scale, scale)
|
||||
self.surface = None
|
||||
self.surface_buffer = None
|
||||
self.ctx = None
|
||||
self.active_layer = None
|
||||
self.active_matrix = None
|
||||
self.output_ctx = None
|
||||
self.bg = False
|
||||
self.mask = None
|
||||
self.mask_ctx = None
|
||||
self.has_bg = False
|
||||
self.origin_in_inch = None
|
||||
self.size_in_inch = None
|
||||
self._xform_matrix = None
|
||||
self._render_count = 0
|
||||
|
||||
@property
|
||||
def origin_in_pixels(self):
|
||||
|
@ -66,10 +67,8 @@ class GerberCairoContext(GerberContext):
|
|||
self.size_in_inch = size_in_inch if self.size_in_inch is None else self.size_in_inch
|
||||
if (self.surface is None) or new_surface:
|
||||
self.surface_buffer = tempfile.NamedTemporaryFile()
|
||||
self.surface = cairo.SVGSurface(
|
||||
self.surface_buffer, size_in_pixels[0], size_in_pixels[1])
|
||||
self.surface = cairo.SVGSurface(self.surface_buffer, size_in_pixels[0], size_in_pixels[1])
|
||||
self.output_ctx = cairo.Context(self.surface)
|
||||
self.output_ctx.set_fill_rule(cairo.FILL_RULE_EVEN_ODD)
|
||||
self.output_ctx.scale(1, -1)
|
||||
self.output_ctx.translate(-(origin_in_inch[0] * self.scale[0]),
|
||||
(-origin_in_inch[1] * self.scale[0]) - size_in_pixels[1])
|
||||
|
@ -77,20 +76,44 @@ class GerberCairoContext(GerberContext):
|
|||
x0=-self.origin_in_pixels[0],
|
||||
y0=self.size_in_pixels[1] + self.origin_in_pixels[1])
|
||||
|
||||
def render_layers(self, layers, filename, theme=THEMES['default']):
|
||||
def render_layer(self, layer, filename=None, settings=None, bgsettings=None,
|
||||
verbose=False):
|
||||
if settings is None:
|
||||
settings = THEMES['default'].get(layer.layer_class, RenderSettings())
|
||||
if bgsettings is None:
|
||||
bgsettings = THEMES['default'].get('background', RenderSettings())
|
||||
|
||||
if self._render_count == 0:
|
||||
if verbose:
|
||||
print('[Render]: Rendering Background.')
|
||||
self.clear()
|
||||
self.set_bounds(layer.bounds)
|
||||
self._paint_background(bgsettings)
|
||||
if verbose:
|
||||
print('[Render]: Rendering {} Layer.'.format(layer.layer_class))
|
||||
self._render_count += 1
|
||||
self._render_layer(layer, settings)
|
||||
if filename is not None:
|
||||
self.dump(filename, verbose)
|
||||
|
||||
def render_layers(self, layers, filename, theme=THEMES['default'],
|
||||
verbose=False):
|
||||
""" Render a set of layers
|
||||
"""
|
||||
self.set_bounds(layers[0].bounds, True)
|
||||
self._paint_background(True)
|
||||
|
||||
self.clear()
|
||||
bgsettings = theme['background']
|
||||
for layer in layers:
|
||||
self._render_layer(layer, theme)
|
||||
self.dump(filename)
|
||||
settings = theme.get(layer.layer_class, RenderSettings())
|
||||
self.render_layer(layer, settings=settings, bgsettings=bgsettings,
|
||||
verbose=verbose)
|
||||
self.dump(filename, verbose)
|
||||
|
||||
def dump(self, filename):
|
||||
def dump(self, filename, verbose=False):
|
||||
""" Save image as `filename`
|
||||
"""
|
||||
is_svg = filename.lower().endswith(".svg")
|
||||
is_svg = os.path.splitext(filename.lower())[1] == '.svg'
|
||||
if verbose:
|
||||
print('[Render]: Writing image to {}'.format(filename))
|
||||
if is_svg:
|
||||
self.surface.finish()
|
||||
self.surface_buffer.flush()
|
||||
|
@ -115,30 +138,33 @@ class GerberCairoContext(GerberContext):
|
|||
self.surface_buffer.flush()
|
||||
return self.surface_buffer.read()
|
||||
|
||||
def _render_layer(self, layer, theme=THEMES['default']):
|
||||
settings = theme.get(layer.layer_class, RenderSettings())
|
||||
self.color = settings.color
|
||||
self.alpha = settings.alpha
|
||||
self.invert = settings.invert
|
||||
def clear(self):
|
||||
self.surface = None
|
||||
self.output_ctx = None
|
||||
self.has_bg = False
|
||||
self.origin_in_inch = None
|
||||
self.size_in_inch = None
|
||||
self._xform_matrix = None
|
||||
self._render_count = 0
|
||||
if hasattr(self.surface_buffer, 'close'):
|
||||
self.surface_buffer.close()
|
||||
self.surface_buffer = None
|
||||
|
||||
def _render_layer(self, layer, settings):
|
||||
self.invert = settings.invert
|
||||
# Get a new clean layer to render on
|
||||
self._new_render_layer(mirror=settings.mirror)
|
||||
for prim in layer.primitives:
|
||||
self.render(prim)
|
||||
# Add layer to image
|
||||
self._flatten()
|
||||
self._paint(settings.color, settings.alpha)
|
||||
|
||||
def _render_line(self, line, color):
|
||||
start = [pos * scale for pos, scale in zip(line.start, self.scale)]
|
||||
end = [pos * scale for pos, scale in zip(line.end, self.scale)]
|
||||
if not self.invert:
|
||||
self.ctx.set_source_rgba(*color, alpha=self.alpha)
|
||||
self.ctx.set_operator(cairo.OPERATOR_OVER
|
||||
if line.level_polarity == 'dark'
|
||||
else cairo.OPERATOR_CLEAR)
|
||||
else:
|
||||
self.ctx.set_source_rgba(0.0, 0.0, 0.0, 1.0)
|
||||
self.ctx.set_operator(cairo.OPERATOR_CLEAR)
|
||||
self.ctx.set_operator(cairo.OPERATOR_SOURCE
|
||||
if line.level_polarity == 'dark' and
|
||||
(not self.invert) else cairo.OPERATOR_CLEAR)
|
||||
if isinstance(line.aperture, Circle):
|
||||
width = line.aperture.diameter
|
||||
self.ctx.set_line_width(width * self.scale[0])
|
||||
|
@ -162,14 +188,9 @@ class GerberCairoContext(GerberContext):
|
|||
angle1 = arc.start_angle
|
||||
angle2 = arc.end_angle
|
||||
width = arc.aperture.diameter if arc.aperture.diameter != 0 else 0.001
|
||||
if not self.invert:
|
||||
self.ctx.set_source_rgba(*color, alpha=self.alpha)
|
||||
self.ctx.set_operator(cairo.OPERATOR_OVER
|
||||
if arc.level_polarity == 'dark'
|
||||
else cairo.OPERATOR_CLEAR)
|
||||
else:
|
||||
self.ctx.set_source_rgba(0.0, 0.0, 0.0, 1.0)
|
||||
self.ctx.set_operator(cairo.OPERATOR_CLEAR)
|
||||
self.ctx.set_operator(cairo.OPERATOR_SOURCE
|
||||
if arc.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) # You actually have to do this...
|
||||
|
@ -181,14 +202,9 @@ class GerberCairoContext(GerberContext):
|
|||
self.ctx.move_to(*end) # ...lame
|
||||
|
||||
def _render_region(self, region, color):
|
||||
if not self.invert:
|
||||
self.ctx.set_source_rgba(*color, alpha=self.alpha)
|
||||
self.ctx.set_operator(cairo.OPERATOR_OVER
|
||||
if region.level_polarity == 'dark'
|
||||
else cairo.OPERATOR_CLEAR)
|
||||
else:
|
||||
self.ctx.set_source_rgba(0.0, 0.0, 0.0, 1.0)
|
||||
self.ctx.set_operator(cairo.OPERATOR_CLEAR)
|
||||
self.ctx.set_operator(cairo.OPERATOR_SOURCE
|
||||
if region.level_polarity == 'dark' and
|
||||
(not self.invert) else cairo.OPERATOR_CLEAR)
|
||||
self.ctx.set_line_width(0)
|
||||
self.ctx.set_line_cap(cairo.LINE_CAP_ROUND)
|
||||
self.ctx.move_to(*self.scale_point(region.primitives[0].start))
|
||||
|
@ -210,29 +226,22 @@ class GerberCairoContext(GerberContext):
|
|||
|
||||
def _render_circle(self, circle, color):
|
||||
center = self.scale_point(circle.position)
|
||||
if not self.invert:
|
||||
self.ctx.set_source_rgba(*color, alpha=self.alpha)
|
||||
self.ctx.set_operator(
|
||||
cairo.OPERATOR_OVER if circle.level_polarity == 'dark' else cairo.OPERATOR_CLEAR)
|
||||
else:
|
||||
self.ctx.set_source_rgba(0.0, 0.0, 0.0, 1.0)
|
||||
self.ctx.set_operator(cairo.OPERATOR_CLEAR)
|
||||
self.ctx.set_operator(cairo.OPERATOR_SOURCE
|
||||
if circle.level_polarity == 'dark' and
|
||||
(not self.invert) else cairo.OPERATOR_CLEAR)
|
||||
self.ctx.set_line_width(0)
|
||||
self.ctx.arc(*center, radius=circle.radius *
|
||||
self.scale[0], angle1=0, angle2=2 * math.pi)
|
||||
self.ctx.arc(*center, radius=(circle.radius * self.scale[0]), angle1=0,
|
||||
angle2=(2 * math.pi))
|
||||
self.ctx.fill()
|
||||
|
||||
def _render_rectangle(self, rectangle, color):
|
||||
lower_left = self.scale_point(rectangle.lower_left)
|
||||
width, height = tuple([abs(coord) for coord in self.scale_point((rectangle.width, rectangle.height))])
|
||||
|
||||
if not self.invert:
|
||||
self.ctx.set_source_rgba(*color, alpha=self.alpha)
|
||||
self.ctx.set_operator(
|
||||
cairo.OPERATOR_OVER if rectangle.level_polarity == 'dark' else cairo.OPERATOR_CLEAR)
|
||||
else:
|
||||
self.ctx.set_source_rgba(0.0, 0.0, 0.0, 1.0)
|
||||
self.ctx.set_operator(cairo.OPERATOR_CLEAR)
|
||||
width, height = tuple([abs(coord) for coord in
|
||||
self.scale_point((rectangle.width,
|
||||
rectangle.height))])
|
||||
self.ctx.set_operator(cairo.OPERATOR_SOURCE
|
||||
if rectangle.level_polarity == 'dark' and
|
||||
(not self.invert) else cairo.OPERATOR_CLEAR)
|
||||
self.ctx.set_line_width(0)
|
||||
self.ctx.rectangle(*lower_left, width=width, height=height)
|
||||
self.ctx.fill()
|
||||
|
@ -247,34 +256,31 @@ class GerberCairoContext(GerberContext):
|
|||
self._render_circle(circle, color)
|
||||
|
||||
def _render_test_record(self, primitive, color):
|
||||
position = [pos + origin for pos, origin in zip(primitive.position, self.origin_in_inch)]
|
||||
self.ctx.set_operator(cairo.OPERATOR_OVER)
|
||||
position = [pos + origin for pos, origin in
|
||||
zip(primitive.position, self.origin_in_inch)]
|
||||
self.ctx.select_font_face(
|
||||
'monospace', cairo.FONT_SLANT_NORMAL, cairo.FONT_WEIGHT_BOLD)
|
||||
self.ctx.set_font_size(13)
|
||||
self._render_circle(Circle(position, 0.015), color)
|
||||
self.ctx.set_source_rgba(*color, alpha=self.alpha)
|
||||
self.ctx.set_operator(
|
||||
cairo.OPERATOR_OVER if primitive.level_polarity == 'dark' else cairo.OPERATOR_CLEAR)
|
||||
self.ctx.move_to(*[self.scale[0] * (coord + 0.015)
|
||||
for coord in position])
|
||||
self.ctx.set_operator(cairo.OPERATOR_SOURCE
|
||||
if primitive.level_polarity == 'dark' and
|
||||
(not self.invert) else cairo.OPERATOR_CLEAR)
|
||||
self.ctx.move_to(*[self.scale[0] * (coord + 0.015) for coord in position])
|
||||
self.ctx.scale(1, -1)
|
||||
self.ctx.show_text(primitive.net_name)
|
||||
self.ctx.scale(1, -1)
|
||||
|
||||
def _new_render_layer(self, color=None, mirror=False):
|
||||
size_in_pixels = self.scale_point(self.size_in_inch)
|
||||
matrix = copy.copy(self._xform_matrix)
|
||||
layer = cairo.SVGSurface(None, size_in_pixels[0], size_in_pixels[1])
|
||||
ctx = cairo.Context(layer)
|
||||
ctx.set_fill_rule(cairo.FILL_RULE_EVEN_ODD)
|
||||
ctx.scale(1, -1)
|
||||
ctx.translate(-(self.origin_in_inch[0] * self.scale[0]),
|
||||
(-self.origin_in_inch[1] * self.scale[0]) - size_in_pixels[1])
|
||||
(-self.origin_in_inch[1] * self.scale[0]) - size_in_pixels[1])
|
||||
if self.invert:
|
||||
ctx.set_operator(cairo.OPERATOR_OVER)
|
||||
ctx.set_source_rgba(*self.color, alpha=self.alpha)
|
||||
ctx.paint()
|
||||
matrix = copy.copy(self._xform_matrix)
|
||||
if mirror:
|
||||
matrix.xx = -1.0
|
||||
matrix.x0 = self.origin_in_pixels[0] + self.size_in_pixels[0]
|
||||
|
@ -282,21 +288,23 @@ class GerberCairoContext(GerberContext):
|
|||
self.active_layer = layer
|
||||
self.active_matrix = matrix
|
||||
|
||||
def _flatten(self):
|
||||
self.output_ctx.set_operator(cairo.OPERATOR_OVER)
|
||||
def _paint(self, color=None, alpha=None):
|
||||
color = color if color is not None else self.color
|
||||
alpha = alpha if alpha is not None else self.alpha
|
||||
ptn = cairo.SurfacePattern(self.active_layer)
|
||||
ptn.set_matrix(self.active_matrix)
|
||||
self.output_ctx.set_source(ptn)
|
||||
self.output_ctx.paint()
|
||||
self.output_ctx.set_source_rgba(*color, alpha=alpha)
|
||||
self.output_ctx.mask(ptn)
|
||||
self.ctx = None
|
||||
self.active_layer = None
|
||||
self.active_matrix = None
|
||||
|
||||
def _paint_background(self, force=False):
|
||||
if (not self.bg) or force:
|
||||
self.bg = True
|
||||
self.output_ctx.set_operator(cairo.OPERATOR_OVER)
|
||||
self.output_ctx.set_source_rgba(*self.background_color, alpha=1.0)
|
||||
def _paint_background(self, settings=None):
|
||||
color = settings.color if settings is not None else self.background_color
|
||||
alpha = settings.alpha if settings is not None else 1.0
|
||||
if not self.has_bg:
|
||||
self.has_bg = True
|
||||
self.output_ctx.set_source_rgba(*color, alpha=alpha)
|
||||
self.output_ctx.paint()
|
||||
|
||||
def scale_point(self, point):
|
||||
|
|
|
@ -45,7 +45,8 @@ class GerberContext(object):
|
|||
Measurement units. 'inch' or 'metric'
|
||||
|
||||
color : tuple (<float>, <float>, <float>)
|
||||
Color used for rendering as a tuple of normalized (red, green, blue) values.
|
||||
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`.
|
||||
|
@ -62,8 +63,9 @@ class GerberContext(object):
|
|||
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.invert = False
|
||||
self.ctx = None
|
||||
|
||||
@property
|
||||
|
@ -125,14 +127,6 @@ class GerberContext(object):
|
|||
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):
|
||||
color = self.color
|
||||
if isinstance(primitive, Line):
|
||||
|
@ -156,7 +150,6 @@ class GerberContext(object):
|
|||
else:
|
||||
return
|
||||
|
||||
|
||||
def _render_line(self, primitive, color):
|
||||
pass
|
||||
|
||||
|
@ -186,8 +179,8 @@ class GerberContext(object):
|
|||
|
||||
|
||||
class RenderSettings(object):
|
||||
|
||||
def __init__(self, color=(0.0, 0.0, 0.0), alpha=1.0, invert=False, mirror=False):
|
||||
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
|
||||
|
|
|
@ -25,12 +25,12 @@ COLORS = {
|
|||
'green': (0.0, 1.0, 0.0),
|
||||
'blue': (0.0, 0.0, 1.0),
|
||||
'fr-4': (0.290, 0.345, 0.0),
|
||||
'green soldermask': (0.0, 0.612, 0.396),
|
||||
'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.686, 0.525, 0.510),
|
||||
'enig copper': (0.694, 0.533, 0.514),
|
||||
'hasl copper': (0.871, 0.851, 0.839)
|
||||
}
|
||||
|
||||
|
@ -39,11 +39,11 @@ 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['black'], alpha=0.0))
|
||||
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.8, invert=True))
|
||||
self.bottommask = kwargs.get('bottommask', RenderSettings(COLORS['green soldermask'], alpha=0.8, invert=True, 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']))
|
||||
|
@ -60,12 +60,21 @@ class Theme(object):
|
|||
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.8, invert=True),
|
||||
bottommask=RenderSettings(COLORS['purple soldermask'], alpha=0.8, invert=True, 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))),
|
||||
}
|
||||
|
|
|
@ -48,7 +48,7 @@ def read(filename):
|
|||
return GerberParser().parse(filename)
|
||||
|
||||
|
||||
def loads(data):
|
||||
def loads(data, filename=None):
|
||||
""" Generate a GerberFile object from rs274x data in memory
|
||||
|
||||
Parameters
|
||||
|
@ -56,12 +56,15 @@ def loads(data):
|
|||
data : string
|
||||
string containing gerber file contents
|
||||
|
||||
filename : string, optional
|
||||
string containing the filename of the data source
|
||||
|
||||
Returns
|
||||
-------
|
||||
file : :class:`gerber.rs274x.GerberFile`
|
||||
A GerberFile created from the specified file.
|
||||
"""
|
||||
return GerberParser().parse_raw(data)
|
||||
return GerberParser().parse_raw(data, filename)
|
||||
|
||||
|
||||
class GerberFile(CamFile):
|
||||
|
|
|
@ -14,7 +14,7 @@ IPC_D_356_FILE = os.path.join(os.path.dirname(__file__),
|
|||
|
||||
def test_read():
|
||||
ipcfile = read(IPC_D_356_FILE)
|
||||
assert(isinstance(ipcfile, IPC_D_356))
|
||||
assert(isinstance(ipcfile, IPCNetlist))
|
||||
|
||||
|
||||
def test_parser():
|
||||
|
|
|
@ -1,11 +1,33 @@
|
|||
#! /usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Author: Hamilton Kibbe <ham@hamiltonkib.be>
|
||||
# copyright 2016 Hamilton Kibbe <ham@hamiltonkib.be>
|
||||
#
|
||||
# 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.
|
||||
|
||||
|
||||
import os
|
||||
|
||||
from .tests import *
|
||||
from ..layers import guess_layer_class, hints
|
||||
from ..layers import *
|
||||
from ..common import read
|
||||
|
||||
NCDRILL_FILE = os.path.join(os.path.dirname(__file__),
|
||||
'resources/ncdrill.DRD')
|
||||
NETLIST_FILE = os.path.join(os.path.dirname(__file__),
|
||||
'resources/ipc-d-356.ipc')
|
||||
COPPER_FILE = os.path.join(os.path.dirname(__file__),
|
||||
'resources/top_copper.GTL')
|
||||
|
||||
def test_guess_layer_class():
|
||||
""" Test layer type inferred correctly from filename
|
||||
|
@ -30,4 +52,51 @@ def test_guess_layer_class():
|
|||
def test_sort_layers():
|
||||
""" Test layer ordering
|
||||
"""
|
||||
pass
|
||||
layers = [
|
||||
PCBLayer(layer_class='drawing'),
|
||||
PCBLayer(layer_class='drill'),
|
||||
PCBLayer(layer_class='bottompaste'),
|
||||
PCBLayer(layer_class='bottomsilk'),
|
||||
PCBLayer(layer_class='bottommask'),
|
||||
PCBLayer(layer_class='bottom'),
|
||||
PCBLayer(layer_class='internal'),
|
||||
PCBLayer(layer_class='top'),
|
||||
PCBLayer(layer_class='topmask'),
|
||||
PCBLayer(layer_class='topsilk'),
|
||||
PCBLayer(layer_class='toppaste'),
|
||||
PCBLayer(layer_class='outline'),
|
||||
]
|
||||
|
||||
layer_order = ['outline', 'toppaste', 'topsilk', 'topmask', 'top',
|
||||
'internal', 'bottom', 'bottommask', 'bottomsilk',
|
||||
'bottompaste', 'drill', 'drawing']
|
||||
bottom_order = list(reversed(layer_order[:10])) + layer_order[10:]
|
||||
assert_equal([l.layer_class for l in sort_layers(layers)], layer_order)
|
||||
assert_equal([l.layer_class for l in sort_layers(layers, from_top=False)],
|
||||
bottom_order)
|
||||
|
||||
|
||||
def test_PCBLayer_from_file():
|
||||
layer = PCBLayer.from_cam(read(COPPER_FILE))
|
||||
assert_true(isinstance(layer, PCBLayer))
|
||||
layer = PCBLayer.from_cam(read(NCDRILL_FILE))
|
||||
assert_true(isinstance(layer, DrillLayer))
|
||||
layer = PCBLayer.from_cam(read(NETLIST_FILE))
|
||||
assert_true(isinstance(layer, PCBLayer))
|
||||
assert_equal(layer.layer_class, 'ipc_netlist')
|
||||
|
||||
|
||||
def test_PCBLayer_bounds():
|
||||
source = read(COPPER_FILE)
|
||||
layer = PCBLayer.from_cam(source)
|
||||
assert_equal(source.bounds, layer.bounds)
|
||||
|
||||
|
||||
def test_DrillLayer_from_cam():
|
||||
no_exceptions = True
|
||||
try:
|
||||
layer = DrillLayer.from_cam(read(NCDRILL_FILE))
|
||||
assert_true(isinstance(layer, DrillLayer))
|
||||
except:
|
||||
no_exceptions = False
|
||||
assert_true(no_exceptions)
|
||||
|
|
|
@ -291,9 +291,9 @@ def rotate_point(point, angle, center=(0.0, 0.0)):
|
|||
`point` rotated about `center` by `angle` degrees.
|
||||
"""
|
||||
angle = radians(angle)
|
||||
xdelta, ydelta = tuple(map(sub, point, center))
|
||||
x = center[0] + (cos(angle) * xdelta) - (sin(angle) * ydelta)
|
||||
y = center[1] + (sin(angle) * xdelta) - (cos(angle) * ydelta)
|
||||
x_delta, y_delta = tuple(map(sub, point, center))
|
||||
x = center[0] + (cos(angle) * x_delta) - (sin(angle) * y_delta)
|
||||
y = center[1] + (sin(angle) * x_delta) - (cos(angle) * y_delta)
|
||||
return (x, y)
|
||||
|
||||
|
||||
|
|