Merge pull request #53 from curtacircuitos/pcb_interface
Add PCB interface
This commit is contained in:
commit
7a53251463
20 changed files with 2435 additions and 500 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
|
||||
|
|
|
|||
240
gerber/layers.py
240
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', ]
|
||||
|
||||
internal_layer_ext = ['in', 'gt1', 'gt2', 'gt3', 'gt4', 'gt5', 'gt6', 'g1',
|
||||
'g2', 'g3', 'g4', 'g5', 'g6', ]
|
||||
internal_layer_name = ['art', 'internal']
|
||||
|
||||
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', ]
|
||||
from .excellon import ExcellonFile
|
||||
from .ipc356 import IPC_D_356
|
||||
|
||||
|
||||
Hint = namedtuple('Hint', 'layer ext name')
|
||||
|
||||
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(object):
|
||||
""" 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(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
|
||||
|
|
|
|||
94
gerber/pcb.py
Normal file
94
gerber/pcb.py
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
#! /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
|
||||
|
||||
|
||||
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
|
||||
|
||||
def __len__(self):
|
||||
return len(self.layers)
|
||||
|
||||
@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
|
||||
|
||||
|
|
@ -21,7 +21,8 @@ from operator import mul
|
|||
import math
|
||||
import tempfile
|
||||
|
||||
from .render import GerberContext
|
||||
from .render import GerberContext, RenderSettings
|
||||
from .theme import THEMES
|
||||
from ..primitives import *
|
||||
|
||||
try:
|
||||
|
|
@ -39,16 +40,25 @@ 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
|
||||
self._xform_matrix = None
|
||||
|
||||
def set_bounds(self, bounds):
|
||||
@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 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:
|
||||
size_in_pixels = tuple(map(mul, size_in_inch, self.scale))
|
||||
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)
|
||||
|
|
@ -60,6 +70,58 @@ class GerberCairoContext(GerberContext):
|
|||
self.mask_ctx.set_fill_rule(cairo.FILL_RULE_EVEN_ODD)
|
||||
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])
|
||||
self._xform_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])
|
||||
|
||||
def render_layers(self, layers, filename, theme=THEMES['default']):
|
||||
""" Render a set of layers
|
||||
"""
|
||||
self.set_bounds(layers[0].bounds, True)
|
||||
self._paint_background(True)
|
||||
for layer in layers:
|
||||
self._render_layer(layer, theme)
|
||||
self.dump(filename)
|
||||
|
||||
def dump(self, filename):
|
||||
""" Save image as `filename`
|
||||
"""
|
||||
is_svg = filename.lower().endswith(".svg")
|
||||
if is_svg:
|
||||
self.surface.finish()
|
||||
self.surface_buffer.flush()
|
||||
with open(filename, "w") as f:
|
||||
self.surface_buffer.seek(0)
|
||||
f.write(self.surface_buffer.read())
|
||||
f.flush()
|
||||
else:
|
||||
self.surface.write_to_png(filename)
|
||||
|
||||
def dump_str(self):
|
||||
""" Return a string containing the rendered image.
|
||||
"""
|
||||
fobj = StringIO()
|
||||
self.surface.write_to_png(fobj)
|
||||
return fobj.getvalue()
|
||||
|
||||
def dump_svg_str(self):
|
||||
""" Return a string containg the rendered SVG.
|
||||
"""
|
||||
self.surface.finish()
|
||||
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
|
||||
if settings.mirror:
|
||||
raise Warning('mirrored layers aren\'t supported yet...')
|
||||
if self.invert:
|
||||
self._clear_mask()
|
||||
for prim in layer.primitives:
|
||||
self.render(prim)
|
||||
if self.invert:
|
||||
self._render_mask()
|
||||
|
||||
def _render_line(self, line, color):
|
||||
start = map(mul, line.start, self.scale)
|
||||
|
|
@ -178,12 +240,14 @@ 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)
|
||||
self.ctx.set_source_rgb(*color)
|
||||
position = tuple(map(add, primitive.position, self.origin_in_inch))
|
||||
self.ctx.set_operator(cairo.OPERATOR_OVER)
|
||||
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.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,38 +260,12 @@ 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(self._xform_matrix)
|
||||
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):
|
||||
is_svg = filename.lower().endswith(".svg")
|
||||
if is_svg:
|
||||
self.surface.finish()
|
||||
self.surface_buffer.flush()
|
||||
with open(filename, "w") as f:
|
||||
self.surface_buffer.seek(0)
|
||||
f.write(self.surface_buffer.read())
|
||||
f.flush()
|
||||
else:
|
||||
self.surface.write_to_png(filename)
|
||||
|
||||
def dump_str(self):
|
||||
""" Return a string containing the rendered image.
|
||||
"""
|
||||
fobj = StringIO()
|
||||
self.surface.write_to_png(fobj)
|
||||
return fobj.getvalue()
|
||||
|
||||
def dump_svg_str(self):
|
||||
""" Return a string containg the rendered SVG.
|
||||
"""
|
||||
self.surface.finish()
|
||||
self.surface_buffer.flush()
|
||||
return self.surface_buffer.read()
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
@ -184,16 +183,10 @@ class GerberContext(object):
|
|||
pass
|
||||
|
||||
|
||||
class Renderable(object):
|
||||
def __init__(self, color=None, alpha=None, invert=False):
|
||||
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
|
||||
|
||||
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')
|
||||
|
|
|
|||
|
|
@ -16,9 +16,14 @@
|
|||
# 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),
|
||||
'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),
|
||||
|
|
@ -30,30 +35,36 @@ COLORS = {
|
|||
}
|
||||
|
||||
|
||||
class RenderSettings(object):
|
||||
def __init__(self, color, alpha=1.0, invert=False):
|
||||
self.color = color
|
||||
self.alpha = alpha
|
||||
self.invert = False
|
||||
|
||||
|
||||
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)
|
||||
|
||||
def get(self, key, noneval=None):
|
||||
val = getattr(self, key)
|
||||
return val if val is not None else noneval
|
||||
|
||||
|
||||
THEMES = {
|
||||
'Default': Theme(),
|
||||
'Osh Park': Theme(top=COLORS['enig copper'],
|
||||
bottom=COLORS['enig copper'],
|
||||
topmask=COLORS['purple soldermask'],
|
||||
bottommask=COLORS['purple soldermask']),
|
||||
'default': Theme(),
|
||||
'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