Add PCB interface
this incorporates some of @chintal's layers.py changes PCB.from_directory() simplifies loading of multiple gerbers the PCB() class should be pretty helpful going forward... the context classes could use some cleaning up, although I'd like to wait until the freecad stuff gets merged, that way we can try to refactor the context base to support more use cases
This commit is contained in:
parent
af5541ac93
commit
6f876edd09
20 changed files with 2407 additions and 466 deletions
1
Makefile
1
Makefile
|
|
@ -31,4 +31,5 @@ doc-clean:
|
|||
.PHONY: examples
|
||||
examples:
|
||||
PYTHONPATH=. $(PYTHON) examples/cairo_example.py
|
||||
PYTHONPATH=. $(PYTHON) examples/pcb_example.py
|
||||
|
||||
|
|
|
|||
Binary file not shown.
|
Before Width: | Height: | Size: 102 KiB After Width: | Height: | Size: 102 KiB |
|
|
@ -36,7 +36,6 @@ 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'))
|
||||
|
||||
|
||||
# Create a new drawing context
|
||||
ctx = GerberCairoContext()
|
||||
|
||||
|
|
|
|||
1811
examples/gerbers/bottom_copper.GBL
Normal file
1811
examples/gerbers/bottom_copper.GBL
Normal file
File diff suppressed because it is too large
Load diff
66
examples/gerbers/bottom_mask.GBS
Normal file
66
examples/gerbers/bottom_mask.GBS
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
G75*
|
||||
%MOIN*%
|
||||
%OFA0B0*%
|
||||
%FSLAX24Y24*%
|
||||
%IPPOS*%
|
||||
%LPD*%
|
||||
%AMOC8*
|
||||
5,1,8,0,0,1.08239X$1,22.5*
|
||||
%
|
||||
%ADD10C,0.0634*%
|
||||
%ADD11C,0.1360*%
|
||||
%ADD12C,0.0680*%
|
||||
%ADD13C,0.1340*%
|
||||
%ADD14C,0.0476*%
|
||||
D10*
|
||||
X017200Y009464D03*
|
||||
X018200Y009964D03*
|
||||
X018200Y010964D03*
|
||||
X017200Y010464D03*
|
||||
X017200Y011464D03*
|
||||
X018200Y011964D03*
|
||||
D11*
|
||||
X020700Y012714D03*
|
||||
X020700Y008714D03*
|
||||
D12*
|
||||
X018350Y016514D02*
|
||||
X018350Y017114D01*
|
||||
X017350Y017114D02*
|
||||
X017350Y016514D01*
|
||||
X007350Y016664D02*
|
||||
X007350Y017264D01*
|
||||
X006350Y017264D02*
|
||||
X006350Y016664D01*
|
||||
X005350Y016664D02*
|
||||
X005350Y017264D01*
|
||||
X001800Y012564D02*
|
||||
X001200Y012564D01*
|
||||
X001200Y011564D02*
|
||||
X001800Y011564D01*
|
||||
X001800Y010564D02*
|
||||
X001200Y010564D01*
|
||||
X001200Y009564D02*
|
||||
X001800Y009564D01*
|
||||
X001800Y008564D02*
|
||||
X001200Y008564D01*
|
||||
D13*
|
||||
X002350Y005114D03*
|
||||
X002300Y016064D03*
|
||||
X020800Y016064D03*
|
||||
X020800Y005064D03*
|
||||
D14*
|
||||
X015650Y006264D03*
|
||||
X013500Y006864D03*
|
||||
X012100Y005314D03*
|
||||
X009250Y004064D03*
|
||||
X015200Y004514D03*
|
||||
X013550Y008764D03*
|
||||
X013350Y010114D03*
|
||||
X013300Y011464D03*
|
||||
X011650Y013164D03*
|
||||
X010000Y015114D03*
|
||||
X006500Y013714D03*
|
||||
X004150Y011564D03*
|
||||
X014250Y014964D03*
|
||||
X015850Y009914D03*
|
||||
M02*
|
||||
|
|
@ -1,354 +0,0 @@
|
|||
%
|
||||
M48
|
||||
M72
|
||||
T01C0.03200
|
||||
T02C0.03543
|
||||
T03C0.04000
|
||||
%
|
||||
T01
|
||||
X11212Y16343
|
||||
X80212Y16343
|
||||
X21212Y16343
|
||||
X99212Y22143
|
||||
X99212Y12143
|
||||
X40212Y16343
|
||||
T02
|
||||
X10812Y191043
|
||||
X70812Y111043
|
||||
X130812Y111043
|
||||
X80812Y141043
|
||||
X110812Y71043
|
||||
X160812Y51043
|
||||
X20812Y171043
|
||||
X30812Y91043
|
||||
X50812Y111043
|
||||
X50812Y121043
|
||||
X20812Y161043
|
||||
X90812Y111043
|
||||
X70812Y61043
|
||||
X40812Y171043
|
||||
X50812Y81043
|
||||
X160812Y61043
|
||||
X40812Y191043
|
||||
X30812Y31043
|
||||
X90812Y131043
|
||||
X10812Y31043
|
||||
X150812Y111043
|
||||
X170812Y51043
|
||||
X110812Y151043
|
||||
X10812Y51043
|
||||
X150812Y51043
|
||||
X140812Y121043
|
||||
X170812Y61043
|
||||
X30812Y61043
|
||||
X70812Y91043
|
||||
X70812Y101043
|
||||
X160812Y161043
|
||||
X40812Y81043
|
||||
X220812Y151043
|
||||
X180812Y71043
|
||||
X30812Y151043
|
||||
X50812Y161043
|
||||
X150812Y131043
|
||||
X40812Y61043
|
||||
X130812Y91043
|
||||
X90812Y61043
|
||||
X80812Y101043
|
||||
X30812Y191043
|
||||
X130812Y151043
|
||||
X60812Y31043
|
||||
X50812Y91043
|
||||
X40812Y111043
|
||||
X220812Y141043
|
||||
X30812Y81043
|
||||
X140812Y81043
|
||||
X60812Y61043
|
||||
X210812Y131043
|
||||
X160812Y71043
|
||||
X90812Y41043
|
||||
X120812Y151043
|
||||
X10812Y161043
|
||||
X80812Y151043
|
||||
X50812Y71043
|
||||
X160812Y151043
|
||||
X110812Y111043
|
||||
X30812Y121043
|
||||
X10812Y41043
|
||||
X20812Y41043
|
||||
X40812Y51043
|
||||
X10812Y151043
|
||||
X200812Y101043
|
||||
X70812Y41043
|
||||
X120812Y51043
|
||||
X40812Y41043
|
||||
X80812Y91043
|
||||
X170812Y161043
|
||||
X100812Y71043
|
||||
X40812Y31043
|
||||
X30812Y141043
|
||||
X180812Y131043
|
||||
X10812Y61043
|
||||
X120812Y141043
|
||||
X200812Y151043
|
||||
X90812Y121043
|
||||
X50812Y31043
|
||||
X170812Y121043
|
||||
X170812Y111043
|
||||
X60812Y121043
|
||||
X40812Y101043
|
||||
X120812Y121043
|
||||
X100812Y161043
|
||||
X10812Y81043
|
||||
X130812Y131043
|
||||
X60812Y81043
|
||||
X200812Y111043
|
||||
X140812Y51043
|
||||
X150812Y71043
|
||||
X160812Y111043
|
||||
X120812Y111043
|
||||
X130812Y101043
|
||||
X20812Y51043
|
||||
X20812Y201043
|
||||
X90812Y71043
|
||||
X190812Y61043
|
||||
X170812Y81043
|
||||
X70812Y71043
|
||||
X50812Y101043
|
||||
X150812Y81043
|
||||
X60812Y131043
|
||||
X190812Y121043
|
||||
X170812Y131043
|
||||
X130812Y121043
|
||||
X20812Y91043
|
||||
X70812Y151043
|
||||
X70812Y141043
|
||||
X180812Y111043
|
||||
X10812Y181043
|
||||
X40812Y131043
|
||||
X80812Y121043
|
||||
X120812Y61043
|
||||
X160812Y101043
|
||||
X90812Y31043
|
||||
X10812Y91043
|
||||
X80812Y71043
|
||||
X100812Y121043
|
||||
X100812Y51043
|
||||
X160812Y121043
|
||||
X40812Y71043
|
||||
X50812Y51043
|
||||
X180812Y81043
|
||||
X90812Y51043
|
||||
X60812Y71043
|
||||
X40812Y161043
|
||||
X190812Y141043
|
||||
X20812Y31043
|
||||
X100812Y151043
|
||||
X200812Y141043
|
||||
X180812Y151043
|
||||
X60812Y51043
|
||||
X120812Y131043
|
||||
X150812Y141043
|
||||
X180812Y51043
|
||||
X150812Y101043
|
||||
X170812Y101043
|
||||
X150812Y151043
|
||||
X30812Y111043
|
||||
X90812Y151043
|
||||
X80812Y131043
|
||||
X170812Y151043
|
||||
X80812Y51043
|
||||
X10812Y201043
|
||||
X60812Y151043
|
||||
X140812Y111043
|
||||
X100812Y91043
|
||||
X90812Y161043
|
||||
X130812Y81043
|
||||
X190812Y111043
|
||||
X140812Y101043
|
||||
X20812Y71043
|
||||
X150812Y121043
|
||||
X90812Y141043
|
||||
X60812Y111043
|
||||
X110812Y121043
|
||||
X30812Y71043
|
||||
X30812Y51043
|
||||
X210812Y141043
|
||||
X50812Y61043
|
||||
X140812Y131043
|
||||
X30812Y201043
|
||||
X190812Y101043
|
||||
X70812Y81043
|
||||
X20812Y121043
|
||||
X20812Y191043
|
||||
X80812Y161043
|
||||
X80812Y81043
|
||||
X20812Y151043
|
||||
X40812Y121043
|
||||
X80812Y31043
|
||||
X80812Y111043
|
||||
X190812Y151043
|
||||
X30812Y181043
|
||||
X60812Y91043
|
||||
X110812Y61043
|
||||
X180812Y61043
|
||||
X10812Y141043
|
||||
X50812Y131043
|
||||
X130812Y51043
|
||||
X50812Y151043
|
||||
X110812Y51043
|
||||
X70812Y131043
|
||||
X60812Y41043
|
||||
X200812Y161043
|
||||
X80812Y61043
|
||||
X140812Y161043
|
||||
X190812Y81043
|
||||
X20812Y141043
|
||||
X70812Y161043
|
||||
X140812Y151043
|
||||
X20812Y61043
|
||||
X20812Y81043
|
||||
X100812Y131043
|
||||
X200812Y131043
|
||||
X140812Y141043
|
||||
X40812Y151043
|
||||
X40812Y91043
|
||||
X60812Y101043
|
||||
X160812Y81043
|
||||
X130812Y71043
|
||||
X30812Y41043
|
||||
X10812Y71043
|
||||
X180812Y141043
|
||||
X170812Y141043
|
||||
X180812Y91043
|
||||
X180812Y101043
|
||||
X150812Y61043
|
||||
X120812Y161043
|
||||
X90812Y101043
|
||||
X200812Y121043
|
||||
X190812Y91043
|
||||
X160812Y141043
|
||||
X130812Y161043
|
||||
X20812Y101043
|
||||
X90812Y81043
|
||||
X190812Y161043
|
||||
X30812Y171043
|
||||
X40812Y181043
|
||||
X70812Y51043
|
||||
X110812Y101043
|
||||
X60812Y141043
|
||||
X120812Y101043
|
||||
X30812Y161043
|
||||
X100812Y141043
|
||||
X220812Y131043
|
||||
X50812Y141043
|
||||
X30812Y101043
|
||||
X60812Y161043
|
||||
X150812Y161043
|
||||
X20812Y131043
|
||||
X150812Y91043
|
||||
X100812Y61043
|
||||
X10812Y131043
|
||||
X30812Y131043
|
||||
X100812Y41043
|
||||
X140812Y61043
|
||||
X210812Y151043
|
||||
X70812Y121043
|
||||
X100812Y101043
|
||||
X180812Y121043
|
||||
X40812Y201043
|
||||
X190812Y71043
|
||||
X10812Y171043
|
||||
X110812Y141043
|
||||
X130812Y61043
|
||||
X110812Y81043
|
||||
X80812Y41043
|
||||
X50812Y41043
|
||||
X110812Y131043
|
||||
X190812Y131043
|
||||
X130812Y141043
|
||||
X140812Y91043
|
||||
X20812Y111043
|
||||
X140812Y71043
|
||||
X170812Y91043
|
||||
X120812Y91043
|
||||
X190812Y51043
|
||||
X120812Y81043
|
||||
X160812Y91043
|
||||
X100812Y81043
|
||||
X120812Y71043
|
||||
X10812Y121043
|
||||
X170812Y71043
|
||||
X110812Y91043
|
||||
X100812Y111043
|
||||
X110812Y161043
|
||||
X70812Y31043
|
||||
X90812Y91043
|
||||
X40812Y141043
|
||||
X20812Y181043
|
||||
X210812Y161043
|
||||
X180812Y161043
|
||||
X160812Y131043
|
||||
T03
|
||||
X86712Y189043
|
||||
X213012Y23043
|
||||
X126732Y201114
|
||||
X96712Y189043
|
||||
X86732Y201114
|
||||
X56732Y201114
|
||||
X142812Y23443
|
||||
X106712Y189043
|
||||
X112754Y11450
|
||||
X182720Y200950
|
||||
X106732Y201114
|
||||
X207259Y55639
|
||||
X207259Y81239
|
||||
X203131Y11150
|
||||
X76732Y201114
|
||||
X192720Y200950
|
||||
X66712Y189043
|
||||
X96732Y201114
|
||||
X193131Y11150
|
||||
X66732Y201114
|
||||
X203012Y23043
|
||||
X122754Y11450
|
||||
X76712Y189043
|
||||
X173131Y11150
|
||||
X192712Y188843
|
||||
X116712Y189043
|
||||
X116732Y201114
|
||||
X213131Y11150
|
||||
X162720Y200950
|
||||
X225059Y55639
|
||||
X183131Y11150
|
||||
X126712Y189043
|
||||
X183012Y23043
|
||||
X212712Y188843
|
||||
X163131Y11150
|
||||
X213563Y110846
|
||||
X122812Y23443
|
||||
X132812Y23443
|
||||
X182712Y188843
|
||||
X212720Y200950
|
||||
X202720Y200950
|
||||
X193012Y23043
|
||||
X213563Y120846
|
||||
X172720Y200950
|
||||
X225059Y81239
|
||||
X223563Y120846
|
||||
X56712Y189043
|
||||
X172712Y188843
|
||||
X213563Y100846
|
||||
X142720Y200950
|
||||
X163012Y23043
|
||||
X142754Y11450
|
||||
X223563Y110846
|
||||
X132754Y11450
|
||||
X142712Y188843
|
||||
X162712Y188843
|
||||
X152712Y188843
|
||||
X223563Y100846
|
||||
X202712Y188843
|
||||
X112812Y23443
|
||||
X173012Y23043
|
||||
X152720Y200950
|
||||
M30
|
||||
BIN
examples/pcb_bottom.png
Normal file
BIN
examples/pcb_bottom.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 38 KiB |
39
examples/pcb_example.py
Normal file
39
examples/pcb_example.py
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
#! /usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright 2015 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.
|
||||
|
||||
"""
|
||||
This example demonstrates the use of pcb-tools with cairo to render composite
|
||||
images using the PCB interface
|
||||
"""
|
||||
|
||||
import os
|
||||
from gerber import PCB
|
||||
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
|
||||
pcb = PCB.from_directory(GERBER_FOLDER)
|
||||
|
||||
pcb.theme = theme.THEMES['OSH Park']
|
||||
ctx.render_layers(pcb.top_layers, os.path.join(os.path.dirname(__file__), 'pcb_top.png'))
|
||||
ctx.render_layers(pcb.bottom_layers, os.path.join(os.path.dirname(__file__), 'pcb_bottom.png'))
|
||||
|
||||
BIN
examples/pcb_top.png
Normal file
BIN
examples/pcb_top.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 97 KiB |
|
|
@ -23,4 +23,5 @@ gerber-tools provides utilities for working with Gerber (RS-274X) and Excellon
|
|||
files in python.
|
||||
"""
|
||||
|
||||
from .common import read, loads
|
||||
from .common import read, loads
|
||||
from .pcb import PCB
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@
|
|||
|
||||
from . import rs274x
|
||||
from . import excellon
|
||||
from . import ipc356
|
||||
from .exceptions import ParseError
|
||||
from .utils import detect_file_format
|
||||
|
||||
|
|
@ -43,6 +44,8 @@ def read(filename):
|
|||
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')
|
||||
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@
|
|||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
|
||||
class ParseError(Exception):
|
||||
pass
|
||||
|
||||
|
|
|
|||
|
|
@ -27,8 +27,11 @@ _NNAME = re.compile(r'^NNAME\d+$')
|
|||
# Board Edge Coordinates
|
||||
_COORD = re.compile(r'X?(?P<x>[\d\s]*)?Y?(?P<y>[\d\s]*)?')
|
||||
|
||||
_SM_FIELD = {'0': 'none', '1': 'primary side', '2': 'secondary side', '3': 'both'}
|
||||
|
||||
_SM_FIELD = {
|
||||
'0': 'none',
|
||||
'1': 'primary side',
|
||||
'2': 'secondary side',
|
||||
'3': 'both'}
|
||||
|
||||
|
||||
def read(filename):
|
||||
|
|
@ -51,17 +54,17 @@ def read(filename):
|
|||
class IPC_D_356(CamFile):
|
||||
|
||||
@classmethod
|
||||
def from_file(self, filename):
|
||||
p = IPC_D_356_Parser()
|
||||
return p.parse(filename)
|
||||
def from_file(cls, filename):
|
||||
parser = IPC_D_356_Parser()
|
||||
return parser.parse(filename)
|
||||
|
||||
|
||||
def __init__(self, statements, settings, primitives=None):
|
||||
def __init__(self, statements, settings, primitives=None, filename=None):
|
||||
self.statements = statements
|
||||
self.units = settings.units
|
||||
self.angle_units = settings.angle_units
|
||||
self.primitives = [TestRecord((rec.x_coord, rec.y_coord), rec.net_name,
|
||||
rec.access) for rec in self.test_records]
|
||||
self.filename = filename
|
||||
|
||||
@property
|
||||
def settings(self):
|
||||
|
|
@ -95,8 +98,6 @@ class IPC_D_356(CamFile):
|
|||
adjacent_nets.add(record.net)
|
||||
nets.append(IPC356_Net(net, adjacent_nets))
|
||||
return nets
|
||||
|
||||
|
||||
|
||||
@property
|
||||
def components(self):
|
||||
|
|
@ -109,14 +110,12 @@ class IPC_D_356(CamFile):
|
|||
|
||||
@property
|
||||
def outlines(self):
|
||||
return [stmt for stmt in self.statements
|
||||
return [stmt for stmt in self.statements
|
||||
if isinstance(stmt, IPC356_Outline)]
|
||||
|
||||
|
||||
|
||||
@property
|
||||
def adjacency_records(self):
|
||||
return [record for record in self.statements
|
||||
return [record for record in self.statements
|
||||
if isinstance(record, IPC356_Adjacency)]
|
||||
|
||||
def render(self, ctx, layer='both', filename=None):
|
||||
|
|
@ -133,6 +132,7 @@ class IPC_D_356(CamFile):
|
|||
|
||||
class IPC_D_356_Parser(object):
|
||||
# TODO: Allow multi-line statements (e.g. Altium board edge)
|
||||
|
||||
def __init__(self):
|
||||
self.units = 'inch'
|
||||
self.angle_units = 'degrees'
|
||||
|
|
@ -158,8 +158,7 @@ class IPC_D_356_Parser(object):
|
|||
oldline = line
|
||||
self._parse_line(oldline)
|
||||
|
||||
return IPC_D_356(self.statements, self.settings)
|
||||
|
||||
return IPC_D_356(self.statements, self.settings, filename=filename)
|
||||
|
||||
def _parse_line(self, line):
|
||||
if not len(line):
|
||||
|
|
@ -201,18 +200,23 @@ class IPC_D_356_Parser(object):
|
|||
|
||||
elif line[0:3] == '378':
|
||||
# Conductor
|
||||
self.statements.append(IPC356_Conductor.from_line(line, self.settings))
|
||||
|
||||
self.statements.append(
|
||||
IPC356_Conductor.from_line(
|
||||
line, self.settings))
|
||||
|
||||
elif line[0:3] == '379':
|
||||
# Net Adjacency
|
||||
self.statements.append(IPC356_Adjacency.from_line(line))
|
||||
|
||||
|
||||
elif line[0:3] == '389':
|
||||
# Outline
|
||||
self.statements.append(IPC356_Outline.from_line(line, self.settings))
|
||||
self.statements.append(
|
||||
IPC356_Outline.from_line(
|
||||
line, self.settings))
|
||||
|
||||
|
||||
class IPC356_Comment(object):
|
||||
|
||||
@classmethod
|
||||
def from_line(cls, line):
|
||||
if line[0] != 'C':
|
||||
|
|
@ -228,6 +232,7 @@ class IPC356_Comment(object):
|
|||
|
||||
|
||||
class IPC356_Parameter(object):
|
||||
|
||||
@classmethod
|
||||
def from_line(cls, line):
|
||||
if line[0] != 'P':
|
||||
|
|
@ -246,13 +251,14 @@ class IPC356_Parameter(object):
|
|||
|
||||
|
||||
class IPC356_TestRecord(object):
|
||||
|
||||
@classmethod
|
||||
def from_line(cls, line, settings):
|
||||
offset = 0
|
||||
units = settings.units
|
||||
angle = settings.angle_units
|
||||
feature_types = {'1':'through-hole', '2': 'smt',
|
||||
'3':'tooling-feature', '4':'tooling-hole'}
|
||||
feature_types = {'1': 'through-hole', '2': 'smt',
|
||||
'3': 'tooling-feature', '4': 'tooling-hole'}
|
||||
access = ['both', 'top', 'layer2', 'layer3', 'layer4', 'layer5',
|
||||
'layer6', 'layer7', 'bottom']
|
||||
record = {}
|
||||
|
|
@ -290,21 +296,21 @@ class IPC356_TestRecord(object):
|
|||
if len(line) >= (43 + offset):
|
||||
end = len(line) - 1 if len(line) < (50 + offset) else (49 + offset)
|
||||
coord = int(line[42 + offset:end].strip())
|
||||
record['x_coord'] = (coord * 0.0001 if units == 'inch'
|
||||
record['x_coord'] = (coord * 0.0001 if units == 'inch'
|
||||
else coord * 0.001)
|
||||
|
||||
if len(line) >= (51 + offset):
|
||||
end = len(line) - 1 if len(line) < (58 + offset) else (57 + offset)
|
||||
coord = int(line[50 + offset:end].strip())
|
||||
record['y_coord'] = (coord * 0.0001 if units == 'inch'
|
||||
else coord * 0.001)
|
||||
record['y_coord'] = (coord * 0.0001 if units == 'inch'
|
||||
else coord * 0.001)
|
||||
|
||||
if len(line) >= (59 + offset):
|
||||
end = len(line) - 1 if len(line) < (63 + offset) else (62 + offset)
|
||||
dim = line[58 + offset:end].strip()
|
||||
if dim != '':
|
||||
record['rect_x'] = (int(dim) * 0.0001 if units == 'inch'
|
||||
else int(dim) * 0.001)
|
||||
else int(dim) * 0.001)
|
||||
|
||||
if len(line) >= (64 + offset):
|
||||
end = len(line) - 1 if len(line) < (68 + offset) else (67 + offset)
|
||||
|
|
@ -321,7 +327,7 @@ class IPC356_TestRecord(object):
|
|||
else math.degrees(rot))
|
||||
|
||||
if len(line) >= (74 + offset):
|
||||
end = 74 + offset
|
||||
end = 74 + offset
|
||||
sm_info = line[73 + offset:end].strip()
|
||||
record['soldermask_info'] = _SM_FIELD.get(sm_info)
|
||||
|
||||
|
|
@ -337,7 +343,8 @@ class IPC356_TestRecord(object):
|
|||
|
||||
def __repr__(self):
|
||||
return '<IPC-D-356 %s Test Record: %s>' % (self.net_name,
|
||||
self.feature_type)
|
||||
self.feature_type)
|
||||
|
||||
|
||||
class IPC356_Outline(object):
|
||||
|
||||
|
|
@ -365,24 +372,27 @@ class IPC356_Outline(object):
|
|||
|
||||
|
||||
class IPC356_Conductor(object):
|
||||
|
||||
@classmethod
|
||||
def from_line(cls, line, settings):
|
||||
if line[0:3] != '378':
|
||||
raise ValueError('Not a valid IPC-D-356 Conductor statement')
|
||||
|
||||
|
||||
scale = 0.0001 if settings.units == 'inch' else 0.001
|
||||
net_name = line[3:17].strip()
|
||||
layer = int(line[19:21])
|
||||
|
||||
|
||||
# Parse out aperture definiting
|
||||
raw_aperture = line[22:].split()[0]
|
||||
aperture_dict = _COORD.match(raw_aperture).groupdict()
|
||||
x = 0
|
||||
y = 0
|
||||
x = int(aperture_dict['x']) * scale if aperture_dict['x'] is not '' else None
|
||||
y = int(aperture_dict['y']) * scale if aperture_dict['y'] is not '' else None
|
||||
x = int(aperture_dict['x']) * \
|
||||
scale if aperture_dict['x'] is not '' else None
|
||||
y = int(aperture_dict['y']) * \
|
||||
scale if aperture_dict['y'] is not '' else None
|
||||
aperture = (x, y)
|
||||
|
||||
|
||||
# Parse out conductor shapes
|
||||
shapes = []
|
||||
coord_list = ' '.join(line[22:].split()[1:])
|
||||
|
|
@ -399,7 +409,7 @@ class IPC356_Conductor(object):
|
|||
shape.append((x * scale, y * scale))
|
||||
shapes.append(tuple(shape))
|
||||
return cls(net_name, layer, aperture, tuple(shapes))
|
||||
|
||||
|
||||
def __init__(self, net_name, layer, aperture, shapes):
|
||||
self.net_name = net_name
|
||||
self.layer = layer
|
||||
|
|
@ -417,18 +427,19 @@ class IPC356_Adjacency(object):
|
|||
if line[0:3] != '379':
|
||||
raise ValueError('Not a valid IPC-D-356 Conductor statement')
|
||||
nets = line[3:].strip().split()
|
||||
|
||||
|
||||
return cls(nets[0], nets[1:])
|
||||
|
||||
def __init__(self, net, adjacent_nets):
|
||||
self.net = net
|
||||
self.adjacent_nets = adjacent_nets
|
||||
|
||||
|
||||
def __repr__(self):
|
||||
return '<IPC-D-356 %s Adjacency Record>' % self.net
|
||||
|
||||
|
||||
class IPC356_EndOfFile(object):
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
|
|
@ -437,12 +448,14 @@ class IPC356_EndOfFile(object):
|
|||
|
||||
def __repr__(self):
|
||||
return '<IPC-D-356 EOF>'
|
||||
|
||||
|
||||
|
||||
class IPC356_Net(object):
|
||||
|
||||
def __init__(self, name, adjacent_nets):
|
||||
self.name = name
|
||||
self.adjacent_nets = set(adjacent_nets) if adjacent_nets is not None else set()
|
||||
|
||||
self.adjacent_nets = set(
|
||||
adjacent_nets) if adjacent_nets is not None else set()
|
||||
|
||||
def __repr__(self):
|
||||
return '<IPC-D-356 Net %s>' % self.name
|
||||
|
|
|
|||
236
gerber/layers.py
236
gerber/layers.py
|
|
@ -15,40 +15,212 @@
|
|||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
top_copper_ext = ['gtl', 'cmp', 'top', ]
|
||||
top_copper_name = ['art01', 'top', 'GTL', 'layer1', 'soldcom', 'comp', ]
|
||||
import os
|
||||
import re
|
||||
from collections import namedtuple
|
||||
|
||||
bottom_copper_ext = ['gbl', 'sld', 'bot', 'sol', ]
|
||||
bottom_coppper_name = ['art02', 'bottom', 'bot', 'GBL', 'layer2', 'soldsold', ]
|
||||
from .excellon import ExcellonFile
|
||||
from .ipc356 import IPC_D_356
|
||||
from .render.render import Renderable
|
||||
|
||||
internal_layer_ext = ['in', 'gt1', 'gt2', 'gt3', 'gt4', 'gt5', 'gt6', 'g1',
|
||||
'g2', 'g3', 'g4', 'g5', 'g6', ]
|
||||
internal_layer_name = ['art', 'internal']
|
||||
Hint = namedtuple('Hint', 'layer ext name')
|
||||
|
||||
power_plane_name = ['pgp', 'pwr', ]
|
||||
ground_plane_name = ['gp1', 'gp2', 'gp3', 'gp4', 'gt5', 'gp6', 'gnd',
|
||||
'ground', ]
|
||||
|
||||
top_silk_ext = ['gto', 'sst', 'plc', 'ts', 'skt', ]
|
||||
top_silk_name = ['sst01', 'topsilk', 'silk', 'slk', 'sst', ]
|
||||
|
||||
bottom_silk_ext = ['gbo', 'ssb', 'pls', 'bs', 'skb', ]
|
||||
bottom_silk_name = ['sst', 'bsilk', 'ssb', 'botsilk', ]
|
||||
|
||||
top_mask_ext = ['gts', 'stc', 'tmk', 'smt', 'tr', ]
|
||||
top_mask_name = ['sm01', 'cmask', 'tmask', 'mask1', 'maskcom', 'topmask',
|
||||
'mst', ]
|
||||
|
||||
bottom_mask_ext = ['gbs', 'sts', 'bmk', 'smb', 'br', ]
|
||||
bottom_mask_name = ['sm', 'bmask', 'mask2', 'masksold', 'botmask', 'msb', ]
|
||||
|
||||
top_paste_ext = ['gtp', 'tm']
|
||||
top_paste_name = ['sp01', 'toppaste', 'pst']
|
||||
|
||||
bottom_paste_ext = ['gbp', 'bm']
|
||||
bottom_paste_name = ['sp02', 'botpaste', 'psb']
|
||||
|
||||
board_outline_ext = ['gko']
|
||||
board_outline_name = ['BDR', 'border', 'out', ]
|
||||
hints = [
|
||||
Hint(layer='top',
|
||||
ext=['gtl', 'cmp', 'top', ],
|
||||
name=['art01', 'top', 'GTL', 'layer1', 'soldcom', 'comp', ]
|
||||
),
|
||||
Hint(layer='bottom',
|
||||
ext=['gbl', 'sld', 'bot', 'sol', 'bottom', ],
|
||||
name=['art02', 'bottom', 'bot', 'GBL', 'layer2', 'soldsold', ]
|
||||
),
|
||||
Hint(layer='internal',
|
||||
ext=['in', 'gt1', 'gt2', 'gt3', 'gt4', 'gt5', 'gt6', 'g1',
|
||||
'g2', 'g3', 'g4', 'g5', 'g6', ],
|
||||
name=['art', 'internal', 'pgp', 'pwr', 'gp1', 'gp2', 'gp3', 'gp4',
|
||||
'gt5', 'gp6', 'gnd', 'ground', ]
|
||||
),
|
||||
Hint(layer='topsilk',
|
||||
ext=['gto', 'sst', 'plc', 'ts', 'skt', 'topsilk', ],
|
||||
name=['sst01', 'topsilk', 'silk', 'slk', 'sst', ]
|
||||
),
|
||||
Hint(layer='bottomsilk',
|
||||
ext=['gbo', 'ssb', 'pls', 'bs', 'skb', 'bottomsilk', ],
|
||||
name=['bsilk', 'ssb', 'botsilk', ]
|
||||
),
|
||||
Hint(layer='topmask',
|
||||
ext=['gts', 'stc', 'tmk', 'smt', 'tr', 'topmask', ],
|
||||
name=['sm01', 'cmask', 'tmask', 'mask1', 'maskcom', 'topmask',
|
||||
'mst', ]
|
||||
),
|
||||
Hint(layer='bottommask',
|
||||
ext=['gbs', 'sts', 'bmk', 'smb', 'br', 'bottommask', ],
|
||||
name=['sm', 'bmask', 'mask2', 'masksold', 'botmask', 'msb', ]
|
||||
),
|
||||
Hint(layer='toppaste',
|
||||
ext=['gtp', 'tm', 'toppaste', ],
|
||||
name=['sp01', 'toppaste', 'pst']
|
||||
),
|
||||
Hint(layer='bottompaste',
|
||||
ext=['gbp', 'bm', 'bottompaste', ],
|
||||
name=['sp02', 'botpaste', 'psb']
|
||||
),
|
||||
Hint(layer='outline',
|
||||
ext=['gko', 'outline', ],
|
||||
name=['BDR', 'border', 'out', ]
|
||||
),
|
||||
Hint(layer='ipc_netlist',
|
||||
ext=['ipc'],
|
||||
name=[],
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
def guess_layer_class(filename):
|
||||
try:
|
||||
directory, name = os.path.split(filename)
|
||||
name, ext = os.path.splitext(name.lower())
|
||||
for hint in hints:
|
||||
patterns = [r'^(\w*[.-])*{}([.-]\w*)?$'.format(x) for x in hint.name]
|
||||
if ext[1:] in hint.ext or any(re.findall(p, name, re.IGNORECASE) for p in patterns):
|
||||
return hint.layer
|
||||
except:
|
||||
pass
|
||||
return 'unknown'
|
||||
|
||||
|
||||
def sort_layers(layers):
|
||||
layer_order = ['outline', 'toppaste', 'topsilk', 'topmask', 'top',
|
||||
'internal', 'bottom', 'bottommask', 'bottomsilk',
|
||||
'bottompaste', 'drill', ]
|
||||
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)
|
||||
return output
|
||||
|
||||
|
||||
class PCBLayer(Renderable):
|
||||
""" Base class for PCB Layers
|
||||
|
||||
Parameters
|
||||
----------
|
||||
source : CAMFile
|
||||
CAMFile representing the layer
|
||||
|
||||
|
||||
Attributes
|
||||
----------
|
||||
filename : string
|
||||
Source Filename
|
||||
|
||||
"""
|
||||
@classmethod
|
||||
def from_gerber(cls, camfile):
|
||||
filename = camfile.filename
|
||||
layer_class = guess_layer_class(filename)
|
||||
if isinstance(camfile, ExcellonFile) or (layer_class == 'drill'):
|
||||
return DrillLayer.from_gerber(camfile)
|
||||
elif layer_class == 'internal':
|
||||
return InternalLayer.from_gerber(camfile)
|
||||
if isinstance(camfile, IPC_D_356):
|
||||
layer_class = 'ipc_netlist'
|
||||
return cls(filename, layer_class, camfile)
|
||||
|
||||
def __init__(self, filename=None, layer_class=None, cam_source=None, **kwargs):
|
||||
super(PCBLayer, self).__init__(**kwargs)
|
||||
self.filename = filename
|
||||
self.layer_class = layer_class
|
||||
self.cam_source = cam_source
|
||||
self.surface = None
|
||||
self.primitives = cam_source.primitives if cam_source is not None else []
|
||||
|
||||
@property
|
||||
def bounds(self):
|
||||
if self.cam_source is not None:
|
||||
return self.cam_source.bounds
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
class DrillLayer(PCBLayer):
|
||||
@classmethod
|
||||
def from_gerber(cls, camfile):
|
||||
return cls(camfile.filename, camfile)
|
||||
|
||||
def __init__(self, filename=None, cam_source=None, layers=None, **kwargs):
|
||||
super(DrillLayer, self).__init__(filename, 'drill', cam_source, **kwargs)
|
||||
self.layers = layers if layers is not None else ['top', 'bottom']
|
||||
|
||||
|
||||
class InternalLayer(PCBLayer):
|
||||
@classmethod
|
||||
def from_gerber(cls, camfile):
|
||||
filename = camfile.filename
|
||||
try:
|
||||
order = int(re.search(r'\d+', filename).group())
|
||||
except:
|
||||
order = 0
|
||||
return cls(filename, camfile, order)
|
||||
|
||||
def __init__(self, filename=None, cam_source=None, order=0, **kwargs):
|
||||
super(InternalLayer, self).__init__(filename, 'internal', cam_source, **kwargs)
|
||||
self.order = order
|
||||
|
||||
def __eq__(self, other):
|
||||
if not hasattr(other, 'order'):
|
||||
raise TypeError()
|
||||
return (self.order == other.order)
|
||||
|
||||
def __ne__(self, other):
|
||||
if not hasattr(other, 'order'):
|
||||
raise TypeError()
|
||||
return (self.order != other.order)
|
||||
|
||||
def __gt__(self, other):
|
||||
if not hasattr(other, 'order'):
|
||||
raise TypeError()
|
||||
return (self.order > other.order)
|
||||
|
||||
def __lt__(self, other):
|
||||
if not hasattr(other, 'order'):
|
||||
raise TypeError()
|
||||
return (self.order < other.order)
|
||||
|
||||
def __ge__(self, other):
|
||||
if not hasattr(other, 'order'):
|
||||
raise TypeError()
|
||||
return (self.order >= other.order)
|
||||
|
||||
def __le__(self, other):
|
||||
if not hasattr(other, 'order'):
|
||||
raise TypeError()
|
||||
return (self.order <= other.order)
|
||||
|
||||
|
||||
class LayerSet(Renderable):
|
||||
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
|
||||
|
|
|
|||
107
gerber/pcb.py
Normal file
107
gerber/pcb.py
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
#! /usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# copyright 2015 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 .exceptions import ParseError
|
||||
from .layers import PCBLayer, LayerSet, sort_layers
|
||||
from .common import read as gerber_read
|
||||
from .utils import listdir
|
||||
from .render import theme
|
||||
|
||||
|
||||
class PCB(object):
|
||||
|
||||
@classmethod
|
||||
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)
|
||||
layers.append(layer)
|
||||
names.add(os.path.splitext(filename)[0])
|
||||
if verbose:
|
||||
print('Added {} layer <{}>'.format(layer.layer_class, filename))
|
||||
except ParseError:
|
||||
if verbose:
|
||||
print('Skipping file {}'.format(filename))
|
||||
# Try to guess board name
|
||||
if board_name is None:
|
||||
if len(names) == 1:
|
||||
board_name = names.pop()
|
||||
else:
|
||||
board_name = os.path.basename(directory)
|
||||
# Return PCB
|
||||
return cls(layers, board_name)
|
||||
|
||||
def __init__(self, layers, name=None):
|
||||
self.layers = sort_layers(layers)
|
||||
self.name = name
|
||||
self._theme = theme.THEMES['Default']
|
||||
self.theme = self._theme
|
||||
|
||||
def __len__(self):
|
||||
return len(self.layers)
|
||||
|
||||
@property
|
||||
def theme(self):
|
||||
return self._theme
|
||||
|
||||
@theme.setter
|
||||
def theme(self, theme):
|
||||
self._theme = theme
|
||||
for layer in self.layers:
|
||||
layer.settings = theme[layer.layer_class]
|
||||
|
||||
@property
|
||||
def top_layers(self):
|
||||
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
|
||||
|
||||
@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
|
||||
|
||||
@property
|
||||
def drill_layers(self):
|
||||
return [l for l in self.layers if l.layer_class == 'drill']
|
||||
|
||||
@property
|
||||
def layer_count(self):
|
||||
""" Number of *COPPER* layers
|
||||
"""
|
||||
return len([l for l in self.layers if l.layer_class in ('top', 'bottom', 'internal')])
|
||||
|
||||
@property
|
||||
def board_bounds(self):
|
||||
for layer in self.layers:
|
||||
if layer.layer_class == 'outline':
|
||||
return layer.bounds
|
||||
for layer in self.layers:
|
||||
if layer.layer_class == 'top':
|
||||
return layer.bounds
|
||||
|
||||
|
|
@ -17,7 +17,7 @@
|
|||
|
||||
|
||||
import cairocffi as cairo
|
||||
from operator import mul
|
||||
from operator import mul, div
|
||||
import math
|
||||
import tempfile
|
||||
|
||||
|
|
@ -39,16 +39,16 @@ class GerberCairoContext(GerberContext):
|
|||
self.bg = False
|
||||
self.mask = None
|
||||
self.mask_ctx = None
|
||||
self.origin_in_pixels = None
|
||||
self.size_in_pixels = None
|
||||
self.origin_in_inch = None
|
||||
self.size_in_inch = None
|
||||
|
||||
def set_bounds(self, bounds):
|
||||
def set_bounds(self, bounds, new_surface=False):
|
||||
origin_in_inch = (bounds[0][0], bounds[1][0])
|
||||
size_in_inch = (abs(bounds[0][1] - bounds[0][0]), abs(bounds[1][1] - bounds[1][0]))
|
||||
size_in_pixels = map(mul, size_in_inch, self.scale)
|
||||
self.origin_in_pixels = tuple(map(mul, origin_in_inch, self.scale)) if self.origin_in_pixels is None else self.origin_in_pixels
|
||||
self.size_in_pixels = size_in_pixels if self.size_in_pixels is None else self.size_in_pixels
|
||||
if self.surface is None:
|
||||
self.origin_in_inch = origin_in_inch if self.origin_in_inch is None else self.origin_in_inch
|
||||
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.ctx = cairo.Context(self.surface)
|
||||
|
|
@ -61,6 +61,36 @@ class GerberCairoContext(GerberContext):
|
|||
self.mask_ctx.scale(1, -1)
|
||||
self.mask_ctx.translate(-(origin_in_inch[0] * self.scale[0]), (-origin_in_inch[1]*self.scale[0]) - size_in_pixels[1])
|
||||
|
||||
def render_layers(self, layers, filename):
|
||||
""" Render a set of layers
|
||||
"""
|
||||
self.set_bounds(layers[0].bounds, True)
|
||||
self._paint_background(True)
|
||||
for layer in layers:
|
||||
self._render_layer(layer)
|
||||
self.dump(filename)
|
||||
|
||||
@property
|
||||
def origin_in_pixels(self):
|
||||
return tuple(map(mul, self.origin_in_inch, self.scale)) if self.origin_in_inch is not None else (0.0, 0.0)
|
||||
|
||||
@property
|
||||
def size_in_pixels(self):
|
||||
return tuple(map(mul, self.size_in_inch, self.scale)) if self.size_in_inch is not None else (0.0, 0.0)
|
||||
|
||||
def _render_layer(self, layer):
|
||||
self.color = layer.settings.color
|
||||
self.alpha = layer.settings.alpha
|
||||
self.invert = layer.settings.invert
|
||||
if layer.settings.mirror:
|
||||
raise Warning('mirrored layers aren\'t supported yet...')
|
||||
if self.invert:
|
||||
self._clear_mask()
|
||||
for p in layer.primitives:
|
||||
self.render(p)
|
||||
if self.invert:
|
||||
self._render_mask()
|
||||
|
||||
def _render_line(self, line, color):
|
||||
start = map(mul, line.start, self.scale)
|
||||
end = map(mul, line.end, self.scale)
|
||||
|
|
@ -178,12 +208,13 @@ class GerberCairoContext(GerberContext):
|
|||
self._render_circle(circle, color)
|
||||
|
||||
def _render_test_record(self, primitive, color):
|
||||
self.ctx.select_font_face('monospace', cairo.FONT_SLANT_NORMAL, cairo.FONT_WEIGHT_NORMAL)
|
||||
self.ctx.set_font_size(200)
|
||||
self._render_circle(Circle(primitive.position, 0.01), color)
|
||||
position = tuple(map(add, 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_rgb(*color)
|
||||
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.01) for coord in primitive.position])
|
||||
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)
|
||||
|
|
@ -196,14 +227,15 @@ class GerberCairoContext(GerberContext):
|
|||
def _render_mask(self):
|
||||
self.ctx.set_operator(cairo.OPERATOR_OVER)
|
||||
ptn = cairo.SurfacePattern(self.mask)
|
||||
ptn.set_matrix(cairo.Matrix(xx=1.0, yy=-1.0, x0=-self.origin_in_pixels[0], y0=self.size_in_pixels[1] + self.origin_in_pixels[1]))
|
||||
ptn.set_matrix(cairo.Matrix(xx=1.0, yy=-1.0, x0=-self.origin_in_pixels[0],
|
||||
y0=self.size_in_pixels[1] + self.origin_in_pixels[1]))
|
||||
self.ctx.set_source(ptn)
|
||||
self.ctx.paint()
|
||||
|
||||
def _paint_background(self):
|
||||
if not self.bg:
|
||||
def _paint_background(self, force=False):
|
||||
if (not self.bg) or force:
|
||||
self.bg = True
|
||||
self.ctx.set_source_rgba(*self.background_color)
|
||||
self.ctx.set_source_rgba(*self.background_color, alpha=1.0)
|
||||
self.ctx.paint()
|
||||
|
||||
def dump(self, filename):
|
||||
|
|
|
|||
|
|
@ -60,7 +60,6 @@ class GerberContext(object):
|
|||
def __init__(self, units='inch'):
|
||||
self._units = units
|
||||
self._color = (0.7215, 0.451, 0.200)
|
||||
self._drill_color = (0.25, 0.25, 0.25)
|
||||
self._background_color = (0.0, 0.0, 0.0)
|
||||
self._alpha = 1.0
|
||||
self._invert = False
|
||||
|
|
@ -150,7 +149,7 @@ class GerberContext(object):
|
|||
elif isinstance(primitive, Polygon):
|
||||
self._render_polygon(primitive, color)
|
||||
elif isinstance(primitive, Drill):
|
||||
self._render_drill(primitive, self.drill_color)
|
||||
self._render_drill(primitive, color)
|
||||
elif isinstance(primitive, TestRecord):
|
||||
self._render_test_record(primitive, color)
|
||||
else:
|
||||
|
|
@ -185,15 +184,7 @@ class GerberContext(object):
|
|||
|
||||
|
||||
class Renderable(object):
|
||||
def __init__(self, color=None, alpha=None, invert=False):
|
||||
self.color = color
|
||||
self.alpha = alpha
|
||||
self.invert = invert
|
||||
def __init__(self, settings=None):
|
||||
self.settings = settings
|
||||
self.primitives = []
|
||||
|
||||
def to_render(self):
|
||||
""" Override this in subclass. Should return a list of Primitives or Renderables
|
||||
"""
|
||||
raise NotImplementedError('to_render() must be implemented in subclass')
|
||||
|
||||
def apply_theme(self, theme):
|
||||
raise NotImplementedError('apply_theme() must be implemented in subclass')
|
||||
|
|
|
|||
|
|
@ -19,6 +19,9 @@
|
|||
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),
|
||||
'blue' : (0.0, 0.0, 1.0),
|
||||
'fr-4': (0.290, 0.345, 0.0),
|
||||
'green soldermask': (0.0, 0.612, 0.396),
|
||||
'blue soldermask': (0.059, 0.478, 0.651),
|
||||
|
|
@ -31,29 +34,38 @@ COLORS = {
|
|||
|
||||
|
||||
class RenderSettings(object):
|
||||
def __init__(self, color, alpha=1.0, invert=False):
|
||||
def __init__(self, color, alpha=1.0, invert=False, mirror=False):
|
||||
self.color = color
|
||||
self.alpha = alpha
|
||||
self.invert = False
|
||||
self.invert = invert
|
||||
self.mirror = mirror
|
||||
|
||||
|
||||
class Theme(object):
|
||||
def __init__(self, **kwargs):
|
||||
self.background = kwargs.get('background', RenderSettings(COLORS['black'], 0.0))
|
||||
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.topsilk = kwargs.get('topsilk', RenderSettings(COLORS['white']))
|
||||
self.bottomsilk = kwargs.get('bottomsilk', RenderSettings(COLORS['white']))
|
||||
self.topmask = kwargs.get('topmask', RenderSettings(COLORS['green soldermask'], 0.8, True))
|
||||
self.bottommask = kwargs.get('bottommask', RenderSettings(COLORS['green soldermask'], 0.8, 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))
|
||||
self.top = kwargs.get('top', RenderSettings(COLORS['hasl copper']))
|
||||
self.bottom = kwargs.get('top', RenderSettings(COLORS['hasl copper']))
|
||||
self.drill = kwargs.get('drill', self.background)
|
||||
self.drill = kwargs.get('drill', RenderSettings(COLORS['black']))
|
||||
self.ipc_netlist = kwargs.get('ipc_netlist', RenderSettings(COLORS['red']))
|
||||
|
||||
def __getitem__(self, key):
|
||||
return getattr(self, key)
|
||||
|
||||
THEMES = {
|
||||
'Default': Theme(),
|
||||
'Osh Park': Theme(top=COLORS['enig copper'],
|
||||
bottom=COLORS['enig copper'],
|
||||
topmask=COLORS['purple soldermask'],
|
||||
bottommask=COLORS['purple soldermask']),
|
||||
'OSH Park': Theme(name='OSH Park',
|
||||
top=RenderSettings(COLORS['enig copper']),
|
||||
bottom=RenderSettings(COLORS['enig copper']),
|
||||
topmask=RenderSettings(COLORS['purple soldermask'], alpha=0.8, invert=True),
|
||||
bottommask=RenderSettings(COLORS['purple soldermask'], alpha=0.8, invert=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)),
|
||||
}
|
||||
|
||||
|
|
|
|||
33
gerber/tests/test_layers.py
Normal file
33
gerber/tests/test_layers.py
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
#! /usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Author: Hamilton Kibbe <ham@hamiltonkib.be>
|
||||
|
||||
from .tests import *
|
||||
from ..layers import guess_layer_class, hints
|
||||
|
||||
|
||||
def test_guess_layer_class():
|
||||
""" Test layer type inferred correctly from filename
|
||||
"""
|
||||
|
||||
# Add any specific test cases here (filename, layer_class)
|
||||
test_vectors = [(None, 'unknown'), ('NCDRILL.TXT', 'unknown'),
|
||||
('example_board.gtl', 'top'),
|
||||
('exampmle_board.sst', 'topsilk'),
|
||||
('ipc-d-356.ipc', 'ipc_netlist'),]
|
||||
|
||||
for hint in hints:
|
||||
for ext in hint.ext:
|
||||
assert_equal(hint.layer, guess_layer_class('board.{}'.format(ext)))
|
||||
for name in hint.name:
|
||||
assert_equal(hint.layer, guess_layer_class('{}.pho'.format(name)))
|
||||
|
||||
for filename, layer_class in test_vectors:
|
||||
assert_equal(layer_class, guess_layer_class(filename))
|
||||
|
||||
|
||||
def test_sort_layers():
|
||||
""" Test layer ordering
|
||||
"""
|
||||
pass
|
||||
|
|
@ -26,6 +26,7 @@ files.
|
|||
# Author: Hamilton Kibbe <ham@hamiltonkib.be>
|
||||
# License:
|
||||
|
||||
import os
|
||||
from math import radians, sin, cos
|
||||
from operator import sub
|
||||
|
||||
|
|
@ -219,7 +220,10 @@ def detect_file_format(data):
|
|||
if 'M48' in line:
|
||||
return 'excellon'
|
||||
elif '%FS' in line:
|
||||
return'rs274x'
|
||||
return 'rs274x'
|
||||
elif ((len(line.split()) >= 2) and
|
||||
(line.split()[0] == 'P') and (line.split()[1] == 'JOB')):
|
||||
return 'ipc_d_356'
|
||||
return 'unknown'
|
||||
|
||||
|
||||
|
|
@ -288,3 +292,13 @@ def rotate_point(point, angle, center=(0.0, 0.0)):
|
|||
x = center[0] + (cos(angle) * xdelta) - (sin(angle) * ydelta)
|
||||
y = center[1] + (sin(angle) * xdelta) - (cos(angle) * ydelta)
|
||||
return (x, y)
|
||||
|
||||
|
||||
def listdir(directory, ignore_hidden=True, ignore_os=True):
|
||||
os_files = ('.DS_Store', 'Thumbs.db', 'ethumbs.db')
|
||||
files = os.listdir(directory)
|
||||
if ignore_hidden:
|
||||
files = [f for f in files if not f.startswith('.')]
|
||||
if ignore_os:
|
||||
files = [f for f in files if not f in os_files]
|
||||
return files
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue