initial commit
This commit is contained in:
commit
9febca7da6
14 changed files with 1551 additions and 0 deletions
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
.vscode
|
||||
.python-version
|
||||
*.pyc
|
||||
__pycache__
|
||||
pcb_tools_extension.egg-info
|
||||
74
README.md
Normal file
74
README.md
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
PCB tools extension
|
||||
===
|
||||
PCB tools extension is a Python library to panelize gerber files.
|
||||
This library is designed based on [PCB tools](https://github.com/curtacircuitos/pcb-tools) which provides cool functionality to handle PCB such as generationg PCB image from gerber files.
|
||||
|
||||
PCB tools extension adds following function to PCB tools.
|
||||
|
||||
- Rotate PCB data (imprementation is not completed)
|
||||
- Save loding PCB data
|
||||
- Merge multiple PCB data
|
||||
- Translate DXF file to gerber data
|
||||
|
||||
Only RS-274x format and Excellon drill format data can be handled by current version of this library.
|
||||
|
||||
## How to panelize
|
||||
Following code is a example to panelize two top metal layer files.
|
||||
|
||||
``` python
|
||||
import gerberex
|
||||
|
||||
ctx = gerberex.GerberComposition()
|
||||
|
||||
metal1 = gerberex.read('board1.gtl')
|
||||
ctx.merge(metal1)
|
||||
|
||||
metal2 = gerberex.read('board2.gtl')
|
||||
metal2.to_metric()
|
||||
metal2.offset(30, 0)
|
||||
ctx.merge(metal2)
|
||||
|
||||
ctx.dump('panelized-board.gtl')
|
||||
```
|
||||
|
||||
In case of Excellon drill data, you have to use ```DrillCompositon``` instead of ```GerberComposition```.
|
||||
|
||||
```python
|
||||
import gerberex
|
||||
|
||||
ctx = gerberex.DrillComposition()
|
||||
|
||||
drill1 = gerberex.read('board1.txt')
|
||||
ctx.merge(drill1)
|
||||
|
||||
drill2 = gerberex.read('board2.txt')
|
||||
drill2.to_metric()
|
||||
drill2.offset(30, 0)
|
||||
ctx.merge(drill2)
|
||||
|
||||
ctx.dump('panelized-board.txt')
|
||||
```
|
||||
|
||||
## DXF file translation
|
||||
You can also load a dxf file and handle that as same as RX-274x gerber file.<br>
|
||||
This function is useful to generate outline data of pnanelized PCB boad.
|
||||
|
||||
```python
|
||||
import gerberex
|
||||
|
||||
ctx = gerberex.GerberComposition()
|
||||
dxf = gerberex.read('outline.dxf')
|
||||
ctx.merge(dxf)
|
||||
```
|
||||
## Panelized board image Example
|
||||
This image is generated by original [PCB tools](https://github.com/curtacircuitos/pcb-tools) fucntion.
|
||||
|
||||
<p align="center">
|
||||
<img alt="description" src="https://raw.githubusercontent.com/wiki/opiopan/pcb-tools-extension/images/panelized.jpg" width=750>
|
||||
</p>
|
||||
|
||||
|
||||
## Installation
|
||||
```shell
|
||||
$ git clone https://github.com/opiopan/pcb-tools-extension.git
|
||||
$ pip install pcb-tools-extension
|
||||
8
gerberex/__init__.py
Normal file
8
gerberex/__init__.py
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright 2019 Hiroshi Murayama <opiopan@gmail.com>
|
||||
|
||||
from gerberex.common import (read, loads)
|
||||
from gerberex.composition import (GerberComposition, DrillComposition)
|
||||
from gerberex.dxf import DxfFile
|
||||
183
gerberex/am_expression.py
Normal file
183
gerberex/am_expression.py
Normal file
|
|
@ -0,0 +1,183 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright 2019 Hiroshi Murayama <opiopan@gmail.com>
|
||||
|
||||
from gerber.utils import *
|
||||
from gerber.am_eval import OpCode
|
||||
from gerber.am_statements import *
|
||||
|
||||
class AMExpression(object):
|
||||
CONSTANT = 1
|
||||
VARIABLE = 2
|
||||
OPERATOR = 3
|
||||
|
||||
def __init__(self, kind):
|
||||
self.kind = kind
|
||||
|
||||
@property
|
||||
def value(self):
|
||||
return self
|
||||
|
||||
def optimize(self):
|
||||
pass
|
||||
|
||||
def to_inch(self):
|
||||
return AMOperatorExpression(AMOperatorExpression.DIV, self,
|
||||
AMConstantExpression(MILLIMETERS_PER_INCH))
|
||||
|
||||
def to_metric(self):
|
||||
return AMOperatorExpression(AMOperatorExpression.MUL, self,
|
||||
AMConstantExpression(MILLIMETERS_PER_INCH))
|
||||
|
||||
def to_gerber(self, settings=None):
|
||||
pass
|
||||
|
||||
def to_instructions(self):
|
||||
pass
|
||||
|
||||
class AMConstantExpression(AMExpression):
|
||||
def __init__(self, value):
|
||||
super(AMConstantExpression, self).__init__(AMExpression.CONSTANT)
|
||||
self._value = value
|
||||
|
||||
@property
|
||||
def value(self):
|
||||
return self._value
|
||||
|
||||
def optimize(self):
|
||||
return self
|
||||
|
||||
def to_gerber(self, settings=None):
|
||||
return str(self._value)
|
||||
|
||||
def to_instructions(self):
|
||||
return [(OpCode.PUSH, self._value)]
|
||||
|
||||
class AMVariableExpression(AMExpression):
|
||||
def __init__(self, number):
|
||||
super(AMVariableExpression, self).__init__(AMExpression.VARIABLE)
|
||||
self.number = number
|
||||
|
||||
def optimize(self):
|
||||
return self
|
||||
|
||||
def to_gerber(self, settings=None):
|
||||
return '$%d' % self.number
|
||||
|
||||
def to_instructions(self):
|
||||
return (OpCode.LOAD, self.number)
|
||||
|
||||
class AMOperatorExpression(AMExpression):
|
||||
ADD = '+'
|
||||
SUB = '-'
|
||||
MUL = 'X'
|
||||
DIV = '/'
|
||||
|
||||
def __init__(self, op, lvalue, rvalue):
|
||||
super(AMOperatorExpression, self).__init__(AMExpression.OPERATOR)
|
||||
self.op = op
|
||||
self.lvalue = lvalue
|
||||
self.rvalue = rvalue
|
||||
|
||||
def optimize(self):
|
||||
self.lvalue = self.lvalue.optimize()
|
||||
self.rvalue = self.rvalue.optimize()
|
||||
|
||||
if isinstance(self.lvalue, AMConstantExpression) and isinstance(self.rvalue, AMConstantExpression):
|
||||
lvalue = float(self.lvalue.value)
|
||||
rvalue = float(self.rvalue.value)
|
||||
value = lvalue + rvalue if self.op == self.ADD else \
|
||||
lvalue - rvalue if self.op == self.SUB else \
|
||||
lvalue * rvalue if self.op == self.MUL else \
|
||||
lvalue / rvalue if self.op == self.DIV else None
|
||||
return AMConstantExpression(value)
|
||||
elif self.op == self.ADD:
|
||||
if self.rvalue.value == 0:
|
||||
return self.lvalue
|
||||
elif self.lvalue.value == 0:
|
||||
return self.rvalue
|
||||
elif self.op == self.SUB:
|
||||
if self.rvalue.value == 0:
|
||||
return self.lvalue
|
||||
elif self.lvalue.value == 0 and isinstance(self.rvalue, AMConstantExpression):
|
||||
return AMConstantExpression(-self.rvalue.value)
|
||||
elif self.op == self.MUL:
|
||||
if self.rvalue.value == 1:
|
||||
return self.lvalue
|
||||
elif self.lvalue.value == 1:
|
||||
return self.rvalue
|
||||
elif self.lvalue == 0 or self.rvalue == 0:
|
||||
return AMConstantExpression(0)
|
||||
elif self.op == self.DIV:
|
||||
if self.rvalue.value == 1:
|
||||
return self.lvalue
|
||||
elif self.lvalue.value == 0:
|
||||
return AMConstantExpression(0)
|
||||
|
||||
return self
|
||||
|
||||
def to_gerber(self, settings=None):
|
||||
return '(%s)%s(%s)' % (self.lvalue.to_gerber(settings), self.op, self.rvalue.to_gerber(settings))
|
||||
|
||||
def to_instructions(self):
|
||||
for i in self.lvalue.to_instructions():
|
||||
yield i
|
||||
for i in self.rvalue.to_instructions():
|
||||
yield i
|
||||
op = OpCode.ADD if self.op == self.ADD else\
|
||||
OpCode.SUB if self.op == self.SUB else\
|
||||
OpCode.MUL if self.op == self.MUL else\
|
||||
OpCode.DIV
|
||||
yield (op, None)
|
||||
|
||||
def eval_macro(instructions):
|
||||
stack = []
|
||||
|
||||
def pop():
|
||||
return stack.pop()
|
||||
|
||||
def push(op):
|
||||
stack.append(op)
|
||||
|
||||
def top():
|
||||
return stack[-1]
|
||||
|
||||
def empty():
|
||||
return len(stack) == 0
|
||||
|
||||
for opcode, argument in instructions:
|
||||
if opcode == OpCode.PUSH:
|
||||
push(AMConstantExpression(argument))
|
||||
|
||||
elif opcode == OpCode.LOAD:
|
||||
push(AMVariableExpression(argument))
|
||||
|
||||
elif opcode == OpCode.STORE:
|
||||
yield (-argument, [pop()])
|
||||
|
||||
elif opcode == OpCode.ADD:
|
||||
op1 = pop()
|
||||
op2 = pop()
|
||||
push(AMOperatorExpression(AMOperatorExpression.ADD, op2, op1))
|
||||
|
||||
elif opcode == OpCode.SUB:
|
||||
op1 = pop()
|
||||
op2 = pop()
|
||||
push(AMOperatorExpression(AMOperatorExpression.SUB, op2, op1))
|
||||
|
||||
elif opcode == OpCode.MUL:
|
||||
op1 = pop()
|
||||
op2 = pop()
|
||||
push(AMOperatorExpression(AMOperatorExpression.MUL, op2, op1))
|
||||
|
||||
elif opcode == OpCode.DIV:
|
||||
op1 = pop()
|
||||
op2 = pop()
|
||||
push(AMOperatorExpression(AMOperatorExpression.DIV, op2, op1))
|
||||
|
||||
elif opcode == OpCode.PRIM:
|
||||
yield (argument, stack)
|
||||
stack = []
|
||||
|
||||
|
||||
437
gerberex/am_primitive.py
Normal file
437
gerberex/am_primitive.py
Normal file
|
|
@ -0,0 +1,437 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright 2019 Hiroshi Murayama <opiopan@gmail.com>
|
||||
|
||||
from gerber.utils import *
|
||||
from gerber.am_statements import *
|
||||
from gerber.am_eval import OpCode
|
||||
|
||||
from gerberex.am_expression import eval_macro
|
||||
|
||||
class AMPrimitiveDef(AMPrimitive):
|
||||
def __init__(self, code, exposure=None, rotation=0):
|
||||
super(AMPrimitiveDef, self).__init__(code, exposure)
|
||||
self.rotation = rotation
|
||||
|
||||
def to_inch(self):
|
||||
pass
|
||||
|
||||
def to_metric(self):
|
||||
pass
|
||||
|
||||
def to_gerber(self, settings=None):
|
||||
pass
|
||||
|
||||
def to_instructions(self):
|
||||
pass
|
||||
|
||||
class AMCommentPrimitiveDef(AMPrimitiveDef):
|
||||
@classmethod
|
||||
def from_modifiers(cls, code, modifiers):
|
||||
return cls(code, modifiers[0])
|
||||
|
||||
def __init__(self, code, comment):
|
||||
super(AMCommentPrimitiveDef, self).__init__(code)
|
||||
self.comment = comment
|
||||
|
||||
def to_gerber(self, settings=None):
|
||||
return '%d %s*' % (self.code, self.comment.to_gerber())
|
||||
|
||||
def to_instructions(self):
|
||||
return [(OpCode.PUSH, self.comment), (OpCode.PRIM, self.code)]
|
||||
|
||||
class AMCirclePrimitiveDef(AMPrimitiveDef):
|
||||
@classmethod
|
||||
def from_modifiers(cls, code, modifiers):
|
||||
exposure = 'on' if modifiers[0] == 1 else 'off',
|
||||
diameter = modifiers[1],
|
||||
center_x = modifiers[2],
|
||||
center_y = modifiers[3],
|
||||
rotation = modifiers[4]
|
||||
return cls(code, expressions, center_x, center_y, rotation)
|
||||
|
||||
def __init__(self, code, exposure, diameter, center_x, center_y, rotation):
|
||||
super(AMCirclePrimitiveDef, self).__init__(code, exposure, rotation)
|
||||
self.diameter = diameter
|
||||
self.center_x = center_x
|
||||
self.center_y = center_y
|
||||
|
||||
def to_inch(self):
|
||||
self.diameter = self.diameter.to_inch().optimize()
|
||||
self.center_x = self.center_x.to_inch().optimize()
|
||||
self.center_y = self.center_y.to_inch().optimize()
|
||||
|
||||
def to_metric(self):
|
||||
self.diameter = self.diameter.to_metric().optimize()
|
||||
self.center_x = self.center_x.to_metric().optimize()
|
||||
self.center_y = self.center_y.to_metric().optimize()
|
||||
|
||||
def to_gerber(self, settings=None):
|
||||
data = dict(code = self.code,
|
||||
exposure = 1 if self.exposure == 'on' else 0,
|
||||
diameter = self.diameter.to_gerber(settings),
|
||||
x = self.center_x.to_gerber(settings),
|
||||
y = self.center_y.to_gerber(settings),
|
||||
rotation = self.rotation.to_gerber(settings))
|
||||
return '{code},{exposure},{diameter},{x},{y},{rotation}*'.format(**data)
|
||||
|
||||
def to_instructions(self):
|
||||
yield (OpCode.PUSH, 1 if self.exposure == 'on' else 0)
|
||||
for modifier in [self.diameter, self.center_x, self.center_y, self.rotation]:
|
||||
for i in modifier.to_instructions():
|
||||
yield i
|
||||
yield (OpCode.PRIM, self.code)
|
||||
|
||||
class AMVectorLinePrimitiveDef(AMPrimitiveDef):
|
||||
@classmethod
|
||||
def from_modifiers(cls, code, modifiers):
|
||||
code = code
|
||||
exposure = 'on' if modifiers[0] == 1 else 'off'
|
||||
width = modifiers[1]
|
||||
start_x = modifiers[2]
|
||||
start_y = modifiers[3]
|
||||
end_x = modifiers[4]
|
||||
end_y = modifiers[5]
|
||||
rotation = modifiers[6]
|
||||
return cls(code, exposure, width, start_x, start_y, end_x, end_y, rotation)
|
||||
|
||||
def __init__(self, code, exposure, width, start_x, start_y, end_x, end_y, rotation):
|
||||
super(AMVectorLinePrimitiveDef, self).__init__(code, exposure, rotation)
|
||||
self.width = width
|
||||
self.start_x = start_x
|
||||
self.start_y = start_y
|
||||
self.end_x = end_x
|
||||
self.end_y = end_y
|
||||
|
||||
def to_inch(self):
|
||||
self.width = self.width.to_inch().optimize()
|
||||
self.start_x = self.start_x.to_inch().optimize()
|
||||
self.start_y = self.start_y.to_inch().optimize()
|
||||
self.end_x = self.end_x.to_inch().optimize()
|
||||
self.end_y = self.end_x.to_inch().optimize()
|
||||
|
||||
def to_metric(self):
|
||||
self.width = self.width.to_metric().optimize()
|
||||
self.start_x = self.start_x.to_metric().optimize()
|
||||
self.start_y = self.start_y.to_metric().optimize()
|
||||
self.end_x = self.end_x.to_metric().optimize()
|
||||
self.end_y = self.end_x.to_metric().optimize()
|
||||
|
||||
def to_gerber(self, settings=None):
|
||||
data = dict(code = self.code,
|
||||
exposure = 1 if self.exposure == 'on' else 0,
|
||||
width = self.width.to_gerber(settings),
|
||||
start_x = self.start_x.to_gerber(settings),
|
||||
start_y = self.start_y.to_gerber(settings),
|
||||
end_x = self.end_x.to_gerber(settings),
|
||||
end_y = self.end_y.to_gerber(settings),
|
||||
rotation = self.rotation.to_gerber(settings))
|
||||
return '{code},{exposure},{width},{start_x},{start_y},{end_x},{end_y},{rotation}*'.format(**data)
|
||||
|
||||
def to_instructions(self):
|
||||
yield (OpCode.PUSH, 1 if self.exposure == 'on' else 0)
|
||||
modifiers = [self.width, self.start_x, self.start_y, self.end_x, self.end_y, self.rotation]
|
||||
for modifier in modifiers:
|
||||
for i in modifier.to_instructions():
|
||||
yield i
|
||||
yield (OpCode.PRIM, self.code)
|
||||
|
||||
class AMCenterLinePrimitiveDef(AMPrimitiveDef):
|
||||
@classmethod
|
||||
def from_modifiers(cls, code, modifiers):
|
||||
code = code
|
||||
exposure = 'on' if modifiers[0] == 1 else 'off'
|
||||
width = modifiers[1]
|
||||
height = modifiers[2]
|
||||
x = modifiers[3]
|
||||
y = modifiers[4]
|
||||
rotation = modifiers[5]
|
||||
return cls(code, exposure, width, height, x, y, rotation)
|
||||
|
||||
def __init__(self, code, exposure, width, height, x, y, rotation):
|
||||
super(AMCenterLinePrimitiveDef, self).__init__(code, exposure, rotation)
|
||||
self.width = width
|
||||
self.height = height
|
||||
self.x = x
|
||||
self.y = y
|
||||
|
||||
def to_inch(self):
|
||||
self.width = self.width.to_inch().optimize()
|
||||
self.height = self.height.to_inch().optimize()
|
||||
self.x = self.x.to_inch().optimize()
|
||||
self.y = self.y.to_inch().optimize()
|
||||
|
||||
def to_metric(self):
|
||||
self.width = self.width.to_metric().optimize()
|
||||
self.height = self.height.to_metric().optimize()
|
||||
self.x = self.x.to_metric().optimize()
|
||||
self.y = self.y.to_metric().optimize()
|
||||
|
||||
def to_gerber(self, settings=None):
|
||||
data = dict(code = self.code,
|
||||
exposure = 1 if self.exposure == 'on' else 0,
|
||||
width = self.width.to_gerber(settings),
|
||||
height = self.height.to_gerber(settings),
|
||||
x = self.x.to_gerber(settings),
|
||||
y = self.y.to_gerber(settings),
|
||||
rotation = self.rotation.to_gerber(settings))
|
||||
return '{code},{exposure},{width},{height},{x},{y},{rotation}*'.format(**data)
|
||||
|
||||
def to_instructions(self):
|
||||
yield (OpCode.PUSH, 1 if self.exposure == 'on' else 0)
|
||||
modifiers = [self.width, self.height, self.x, self.y, self.rotation]
|
||||
for modifier in modifiers:
|
||||
for i in modifier.to_instructions():
|
||||
yield i
|
||||
yield (OpCode.PRIM, self.code)
|
||||
|
||||
class AMOutlinePrimitiveDef(AMPrimitiveDef):
|
||||
@classmethod
|
||||
def from_modifiers(cls, code, modifiers):
|
||||
num_points = modifiers[1] + 1
|
||||
code = code
|
||||
exposure = 'on' if modifiers[0] == 1 else 'off'
|
||||
addrs = modifiers[2:num_points * 2]
|
||||
rotation = modifiers[3 + num_points * 2]
|
||||
return cls(code, exposure, addrs, rotation)
|
||||
|
||||
def __init__(self, code, exposure, addrs, rotation):
|
||||
super(AMOutlinePrimitiveDef, self).__init__(code, exposure, rotation)
|
||||
self.addrs = addrs
|
||||
|
||||
def to_inch(self):
|
||||
self.addrs = [i.to_inch() for i in self.addrs]
|
||||
|
||||
def to_metric(self):
|
||||
self.addrs = [i.to_metric() for i in self.addrs]
|
||||
|
||||
def to_gerber(self, settings=None):
|
||||
def strs():
|
||||
yield '%d,%d,%d' % (self.code,
|
||||
1 if self.exposure == 'on' else 0,
|
||||
len(self.addrs) / 2 - 1)
|
||||
for i in self.addrs:
|
||||
yield i.to_gerber(settings)
|
||||
yield self.rotation.to_gerber(settings)
|
||||
|
||||
return '%s*' % ','.join(strs())
|
||||
|
||||
def to_instructions(self):
|
||||
yield (OpCode.PUSH, 1 if self.exposure == 'on' else 0)
|
||||
yield (OpCode.PUSH, int(len(self.addrs) / 2 - 1))
|
||||
for modifier in self.addrs:
|
||||
for i in modifier.to_instructions():
|
||||
yield i
|
||||
for i in self.rotation.to_instructions():
|
||||
yield i
|
||||
yield (OpCode.PRIM, self.code)
|
||||
|
||||
class AMPolygonPrimitiveDef(AMPrimitiveDef):
|
||||
@classmethod
|
||||
def from_modifiers(cls, code, modifiers):
|
||||
code = code
|
||||
exposure = 'on' if modifiers[0] == 1 else 'off'
|
||||
vertices = modifiers[1]
|
||||
x = modifiers[2]
|
||||
y = modifiers[3]
|
||||
diameter = modifiers[4]
|
||||
rotation = modifiers[5]
|
||||
return cls(code, exposure, vertices, x, y, diameter, rotation)
|
||||
|
||||
def __init__(self, code, exposure, vertices, x, y, diameter, rotation):
|
||||
super(AMPolygonPrimitiveDef, self).__init__(code, exposure, rotation)
|
||||
self.vertices = vertices
|
||||
self.x = x
|
||||
self.y = y
|
||||
self.diameter = diameter
|
||||
|
||||
def to_inch(self):
|
||||
self.x = self.x.to_inch().optimize()
|
||||
self.y = self.y.to_inch().optimize()
|
||||
self.diameter = self.diameter.to_inch().optimize()
|
||||
|
||||
def to_metric(self):
|
||||
self.x = self.x.to_metric().optimize()
|
||||
self.y = self.y.to_metric().optimize()
|
||||
self.diameter = self.diameter.to_inch().optimize()
|
||||
|
||||
def to_gerber(self, settings=None):
|
||||
data = dict(code = self.code,
|
||||
exposure = 1 if self.exposure == 'on' else 0,
|
||||
vertices = self.vertices.to_gerber(settings),
|
||||
x = self.x.to_gerber(settings),
|
||||
y = self.y.to_gerber(settings),
|
||||
diameter = self.diameter.to_gerber(settings),
|
||||
rotation = self.rotation.to_gerber(settings))
|
||||
return '{code},{exposure},{vertices},{x},{y},{diameter},{rotation}*'.format(**data)
|
||||
|
||||
def to_instructions(self):
|
||||
yield (OpCode.PUSH, 1 if self.exposure == 'on' else 0)
|
||||
modifiers = [self.vertices, self.x, self.y, self.diameter, self.rotation]
|
||||
for modifier in modifiers:
|
||||
for i in modifier.to_instructions():
|
||||
yield i
|
||||
yield (OpCode.PRIM, self.code)
|
||||
|
||||
class AMMoirePrimitiveDef(AMPrimitiveDef):
|
||||
@classmethod
|
||||
def from_modifiers(cls, code, modifiers):
|
||||
code = code
|
||||
exposure = 'on'
|
||||
x = modifiers[0]
|
||||
y = modifiers[1]
|
||||
diameter = modifiers[2]
|
||||
ring_thickness = modifiers[3]
|
||||
gap = modifiers[4]
|
||||
max_rings = modifiers[5]
|
||||
crosshair_thickness = modifiers[6]
|
||||
crosshair_length = modifiers[7]
|
||||
rotation = modifiers[8]
|
||||
return cls(code, exposure, x, y, diameter, ring_thickness, gap,
|
||||
max_rings, crosshair_thickness, crosshair_length, rotation)
|
||||
|
||||
def __init__(self, code, exposure, x, y, diameter, ring_thickness, gap, max_rings, crosshair_thickness, crosshair_length, rotation):
|
||||
super(AMMoirePrimitiveDef, self).__init__(code, exposure, rotation)
|
||||
self.x = x
|
||||
self.y = y
|
||||
self.diameter = diameter
|
||||
self.ring_thickness = ring_thickness
|
||||
self.gap = gap
|
||||
self.max_rings = max_rings
|
||||
self.crosshair_thickness = crosshair_thickness
|
||||
self.crosshair_length = crosshair_length
|
||||
|
||||
def to_inch(self):
|
||||
self.x = self.x.to_inch().optimize()
|
||||
self.y = self.y.to_inch().optimize()
|
||||
self.diameter = self.diameter.to_inch().optimize()
|
||||
self.ring_thickness = self.ring_thickness.to_inch().optimize()
|
||||
self.gap = self.gap.to_inch().optimize()
|
||||
self.crosshair_thickness = self.crosshair_thickness.to_inch().optimize()
|
||||
self.crosshair_length = self.crosshair_length.to_inch().optimize()
|
||||
|
||||
def to_metric(self):
|
||||
self.x = self.x.to_metric().optimize()
|
||||
self.y = self.y.to_metric().optimize()
|
||||
self.diameter = self.diameter.to_metric().optimize()
|
||||
self.ring_thickness = self.ring_thickness.to_metric().optimize()
|
||||
self.gap = self.gap.to_metric().optimize()
|
||||
self.crosshair_thickness = self.crosshair_thickness.to_metric().optimize()
|
||||
self.crosshair_length = self.crosshair_length.to_metric().optimize()
|
||||
|
||||
def to_gerber(self, settings=None):
|
||||
data = dict(code = self.code,
|
||||
x = self.x.to_gerber(settings),
|
||||
y = self.y.to_gerber(settings),
|
||||
diameter = self.diameter.to_gerber(settings),
|
||||
ring_thickness = self.ring_thickness.to_gerber(settings),
|
||||
gap = self.gap.to_gerber(settings),
|
||||
max_rings = self.max_rings.to_gerber(settings),
|
||||
crosshair_thickness = self.crosshair_thickness.to_gerber(settings),
|
||||
crosshair_length = self.crosshair_length.to_gerber(settings),
|
||||
rotation = self.rotation.to_gerber(settings))
|
||||
return '{code},{x},{y},{diameter},{ring_thickness},{gap},{max_rings},'\
|
||||
'{crosshair_thickness},{crosshair_length},{rotation}*'.format(**data)
|
||||
|
||||
def to_instructions(self):
|
||||
modifiers = [self.x, self.y, self.diameter,
|
||||
self.ring_thickness, self.gap, self.max_rings,
|
||||
self.crosshair_thickness, self.crosshair_length,
|
||||
self.rotation]
|
||||
for modifier in modifiers:
|
||||
for i in modifier.to_instructions():
|
||||
yield i
|
||||
yield (OpCode.PRIM, self.code)
|
||||
|
||||
class AMThermalPrimitiveDef(AMPrimitiveDef):
|
||||
@classmethod
|
||||
def from_modifiers(cls, code, modifiers):
|
||||
code = code
|
||||
exposure = 'on'
|
||||
x = modifiers[0]
|
||||
y = modifiers[1]
|
||||
outer_diameter = modifiers[2]
|
||||
inner_diameter = modifiers[3]
|
||||
gap = modifiers[4]
|
||||
rotation = modifiers[5]
|
||||
return cls(code, exposure, x, y, outer_diameter, inner_diameter, gap, rotation)
|
||||
|
||||
def __init__(self, code, exposure, x, y, outer_diameter, inner_diameter, gap, rotation):
|
||||
super(AMThermalPrimitiveDef, self).__init__(code, exposure, rotation)
|
||||
self.x = x
|
||||
self.y = y
|
||||
self.outer_diameter = outer_diameter
|
||||
self.inner_diameter = inner_diameter
|
||||
self.gap = gap
|
||||
|
||||
def to_inch(self):
|
||||
self.x = self.x.to_inch().optimize()
|
||||
self.y = self.y.to_inch().optimize()
|
||||
self.outer_diameter = self.outer_diameter.to_inch().optimize()
|
||||
self.inner_diameter = self.inner_diameter.to_inch().optimize()
|
||||
self.gap = self.gap.to_inch().optimize()
|
||||
|
||||
def to_metric(self):
|
||||
self.x = self.x.to_metric().optimize()
|
||||
self.y = self.y.to_metric().optimize()
|
||||
self.outer_diameter = self.outer_diameter.to_metric().optimize()
|
||||
self.inner_diameter = self.inner_diameter.to_metric().optimize()
|
||||
self.gap = self.gap.to_metric().optimize()
|
||||
|
||||
def to_gerber(self, settings=None):
|
||||
data = dict(code = self.code,
|
||||
x = self.x.to_gerber(settings),
|
||||
y = self.y.to_gerber(settings),
|
||||
outer_diameter = self.outer_diameter.to_gerber(settings),
|
||||
inner_diameter = self.inner_diameter.to_gerber(settings),
|
||||
gap = self.gap.to_gerber(settings),
|
||||
rotation = self.rotation.to_gerber(settings))
|
||||
return '{code},{x},{y},{outer_diameter},{inner_diameter},'\
|
||||
'{gap},{rotation}*'.format(**data)
|
||||
|
||||
def to_instructions(self):
|
||||
modifiers = [self.x, self.y, self.outer_diameter,
|
||||
self.inner_diameter, self.gap, self.rotation]
|
||||
for modifier in modifiers:
|
||||
for i in modifier.to_instructions():
|
||||
yield i
|
||||
yield (OpCode.PRIM, self.code)
|
||||
|
||||
class AMVariableDef(object):
|
||||
def __init__(self, number, value):
|
||||
self.number = number
|
||||
self.value = value
|
||||
|
||||
def to_inch(self):
|
||||
return self
|
||||
|
||||
def to_metric(self):
|
||||
return self
|
||||
|
||||
def to_gerber(self, settings=None):
|
||||
return '$%d=%s*' % (self.number, self.value.to_gerber(settings))
|
||||
|
||||
def to_instructions(self):
|
||||
for i in self.value.to_instructions():
|
||||
yield i
|
||||
yield (OpCode.STORE, self.number)
|
||||
|
||||
def to_primitive_defs(instructions):
|
||||
classes = {
|
||||
0: AMCommentPrimitiveDef,
|
||||
1: AMCirclePrimitiveDef,
|
||||
2: AMVectorLinePrimitiveDef,
|
||||
20: AMVectorLinePrimitiveDef,
|
||||
21: AMCenterLinePrimitiveDef,
|
||||
4: AMOutlinePrimitiveDef,
|
||||
5: AMPolygonPrimitiveDef,
|
||||
6: AMMoirePrimitiveDef,
|
||||
7: AMThermalPrimitiveDef,
|
||||
}
|
||||
for code, modifiers in eval_macro(instructions):
|
||||
if code < 0:
|
||||
yield AMVariableDef(-code, modifiers[0])
|
||||
else:
|
||||
primitive = classes[code]
|
||||
yield primitive.from_modifiers(code, modifiers)
|
||||
35
gerberex/common.py
Normal file
35
gerberex/common.py
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright 2019 Hiroshi Murayama <opiopan@gmail.com>
|
||||
|
||||
import os
|
||||
from gerber.common import loads as loads_org
|
||||
from gerber.exceptions import ParseError
|
||||
from gerber.utils import detect_file_format
|
||||
import gerber.rs274x
|
||||
import gerber.ipc356
|
||||
import gerberex.rs274x
|
||||
import gerberex.excellon
|
||||
import gerberex.dxf
|
||||
|
||||
def read(filename, format=None):
|
||||
with open(filename, 'rU') as f:
|
||||
data = f.read()
|
||||
return loads(data, filename, format=format)
|
||||
|
||||
|
||||
def loads(data, filename=None, format=None):
|
||||
if os.path.splitext(filename if filename else '')[1].lower() == '.dxf':
|
||||
return gerberex.dxf.loads(data, filename)
|
||||
|
||||
fmt = detect_file_format(data)
|
||||
if fmt == 'rs274x':
|
||||
file = gerber.rs274x.loads(data, filename=filename)
|
||||
return gerberex.rs274x.GerberFile.from_gerber_file(file)
|
||||
elif fmt == 'excellon':
|
||||
return gerberex.excellon.loads(data, filename=filename, format=format)
|
||||
elif fmt == 'ipc_d_356':
|
||||
return ipc356.loads(data, filename=filename)
|
||||
else:
|
||||
raise ParseError('Unable to detect file format')
|
||||
201
gerberex/composition.py
Normal file
201
gerberex/composition.py
Normal file
|
|
@ -0,0 +1,201 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright 2019 Hiroshi Murayama <opiopan@gmail.com>
|
||||
import os
|
||||
from functools import reduce
|
||||
from gerber.cam import FileSettings
|
||||
from gerber.gerber_statements import EofStmt
|
||||
from gerber.excellon_statements import *
|
||||
import gerberex.rs274x
|
||||
import gerberex.excellon
|
||||
import gerberex.dxf
|
||||
|
||||
class Composition(object):
|
||||
def __init__(self, settings = None, comments = None):
|
||||
self.settings = settings
|
||||
self.comments = comments if comments != None else []
|
||||
|
||||
class GerberComposition(Composition):
|
||||
APERTURE_ID_BIAS = 10
|
||||
|
||||
def __init__(self, settings=None, comments=None):
|
||||
super(GerberComposition, self).__init__(settings, comments)
|
||||
self.param_statements = []
|
||||
self.aperture_macros = {}
|
||||
self.apertures = []
|
||||
self.drawings = []
|
||||
|
||||
def merge(self, file):
|
||||
if isinstance(file, gerberex.rs274x.GerberFile):
|
||||
self._merge_gerber(file)
|
||||
elif isinstance(file, gerberex.dxf.DxfFile):
|
||||
self._merge_dxf(file)
|
||||
else:
|
||||
raise Exception('unsupported file type')
|
||||
|
||||
def dump(self, path):
|
||||
def statements():
|
||||
for s in self.param_statements:
|
||||
yield s
|
||||
for k in self.aperture_macros:
|
||||
yield self.aperture_macros[k]
|
||||
for s in self.apertures:
|
||||
yield s
|
||||
for s in self.drawings:
|
||||
yield s
|
||||
yield EofStmt()
|
||||
with open(path, 'w') as f:
|
||||
for statement in statements():
|
||||
f.write(statement.to_gerber(self.settings) + '\n')
|
||||
|
||||
def _merge_gerber(self, file):
|
||||
param_statements = []
|
||||
aperture_macro_map = {}
|
||||
aperture_map = {}
|
||||
|
||||
if self.settings:
|
||||
if self.settings.units == 'metric':
|
||||
file.to_metric()
|
||||
else:
|
||||
file.to_inch()
|
||||
|
||||
for statement in file.statements:
|
||||
if statement.type == 'COMMENT':
|
||||
self.comments.append(statement.comment)
|
||||
elif statement.type == 'PARAM':
|
||||
if statement.param == 'AM':
|
||||
name = statement.name
|
||||
newname = self._register_aperture_macro(statement)
|
||||
aperture_macro_map[name] = newname
|
||||
elif statement.param == 'AD':
|
||||
if not statement.shape in ['C', 'R', 'O']:
|
||||
statement.shape = aperture_macro_map[statement.shape]
|
||||
dnum = statement.d
|
||||
newdnum = self._register_aperture(statement)
|
||||
aperture_map[dnum] = newdnum
|
||||
elif statement.param == 'LP':
|
||||
self.drawings.append(statement)
|
||||
else:
|
||||
param_statements.append(statement)
|
||||
elif statement.type in ['EOF', "DEPRECATED"]:
|
||||
pass
|
||||
else:
|
||||
if statement.type == 'APERTURE':
|
||||
statement.d = aperture_map[statement.d]
|
||||
self.drawings.append(statement)
|
||||
|
||||
if not self.settings:
|
||||
self.settings = file.settings
|
||||
self.param_statements = param_statements
|
||||
|
||||
def _merge_dxf(self, file):
|
||||
if self.settings:
|
||||
if self.settings.units == 'metric':
|
||||
file.to_metric()
|
||||
else:
|
||||
file.to_inch()
|
||||
|
||||
file.dcode = self._register_aperture(file.aperture)
|
||||
self.drawings.append(file.statements)
|
||||
|
||||
if not self.settings:
|
||||
self.settings = file.settings
|
||||
self.param_statements = file.header
|
||||
|
||||
|
||||
def _register_aperture_macro(self, statement):
|
||||
name = statement.name
|
||||
newname = name
|
||||
offset = 0
|
||||
while newname in self.aperture_macros:
|
||||
offset += 1
|
||||
newname = '%s_%d' % (name, offset)
|
||||
statement.name = newname
|
||||
self.aperture_macros[newname] = statement
|
||||
return newname
|
||||
|
||||
def _register_aperture(self, statement):
|
||||
statement.d = len(self.apertures) + self.APERTURE_ID_BIAS
|
||||
self.apertures.append(statement)
|
||||
return statement.d
|
||||
|
||||
class DrillComposition(Composition):
|
||||
def __init__(self, settings=None, comments=None):
|
||||
super(DrillComposition, self).__init__(settings, comments)
|
||||
self.header1_statements = []
|
||||
self.header2_statements = []
|
||||
self.tools = []
|
||||
self.hits = []
|
||||
|
||||
def merge(self, file):
|
||||
if isinstance(file, gerberex.excellon.ExcellonFileEx):
|
||||
self._merge_excellon(file)
|
||||
else:
|
||||
raise Exception('unsupported file type')
|
||||
|
||||
def dump(self, path):
|
||||
def statements():
|
||||
for s in self.header1_statements:
|
||||
yield s.to_excellon(self.settings)
|
||||
for t in self.tools:
|
||||
yield t.to_excellon(self.settings)
|
||||
for s in self.header2_statements:
|
||||
yield s.to_excellon(self.settings)
|
||||
for t in self.tools:
|
||||
yield ToolSelectionStmt(t.number).to_excellon(self.settings)
|
||||
for h in self.hits:
|
||||
if h.tool.number == t.number:
|
||||
yield CoordinateStmt(*h.position).to_excellon(self.settings)
|
||||
yield EndOfProgramStmt().to_excellon()
|
||||
|
||||
with open(path, 'w') as f:
|
||||
for statement in statements():
|
||||
f.write(statement + '\n')
|
||||
|
||||
def _merge_excellon(self, file):
|
||||
tool_map = {}
|
||||
|
||||
if not self.settings:
|
||||
self.settings = file.settings
|
||||
else:
|
||||
if self.settings.units == 'metric':
|
||||
file.to_metric()
|
||||
else:
|
||||
file.to_inch()
|
||||
|
||||
if not self.header1_statements:
|
||||
in_header1 = True
|
||||
for statement in file.statements:
|
||||
if not isinstance(statement, ToolSelectionStmt):
|
||||
if isinstance(statement, ExcellonTool):
|
||||
in_header1 = False
|
||||
else:
|
||||
if in_header1:
|
||||
self.header1_statements.append(statement)
|
||||
else:
|
||||
self.header2_statements.append(statement)
|
||||
else:
|
||||
break
|
||||
|
||||
for tool in iter(file.tools.values()):
|
||||
num = tool.number
|
||||
tool_map[num] = self._register_tool(tool)
|
||||
|
||||
for hit in file.hits:
|
||||
hit.tool = tool_map[hit.tool.number]
|
||||
self.hits.append(hit)
|
||||
|
||||
def _register_tool(self, tool):
|
||||
for existing in self.tools:
|
||||
if existing.equivalent(tool):
|
||||
return existing
|
||||
new_tool = ExcellonTool.from_tool(tool)
|
||||
new_tool.settings = self.settings
|
||||
def toolnums():
|
||||
for tool in self.tools:
|
||||
yield tool.number
|
||||
max_num = reduce(lambda x, y: x if x > y else y, toolnums(), 0)
|
||||
new_tool.number = max_num + 1
|
||||
self.tools.append(new_tool)
|
||||
return new_tool
|
||||
357
gerberex/dxf.py
Normal file
357
gerberex/dxf.py
Normal file
|
|
@ -0,0 +1,357 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright 2019 Hiroshi Murayama <opiopan@gmail.com>
|
||||
|
||||
import io
|
||||
from math import pi, cos, sin, tan, atan, atan2, acos, asin, sqrt
|
||||
from gerber.cam import CamFile, FileSettings
|
||||
from gerber.utils import inch, metric, write_gerber_value
|
||||
from gerber.gerber_statements import ADParamStmt
|
||||
import dxfgrabber
|
||||
|
||||
class DxfStatement(object):
|
||||
def __init__(self, entity):
|
||||
self.entity = entity
|
||||
|
||||
def to_gerber(self, settings=None):
|
||||
pass
|
||||
|
||||
def to_inch(self):
|
||||
pass
|
||||
|
||||
def to_metric(self):
|
||||
pass
|
||||
|
||||
class DxfLineStatement(DxfStatement):
|
||||
def __init__(self, entity):
|
||||
super(DxfLineStatement, self).__init__(entity)
|
||||
|
||||
def to_gerber(self, settings=FileSettings):
|
||||
x0 = self.entity.start[0]
|
||||
y0 = self.entity.start[1]
|
||||
x1 = self.entity.end[0]
|
||||
y1 = self.entity.end[1]
|
||||
return 'G01*\nX{0}Y{1}D02*\nX{2}Y{3}D01*'.format(
|
||||
write_gerber_value(x0, settings.format,
|
||||
settings.zero_suppression),
|
||||
write_gerber_value(y0, settings.format,
|
||||
settings.zero_suppression),
|
||||
write_gerber_value(x1, settings.format,
|
||||
settings.zero_suppression),
|
||||
write_gerber_value(y1, settings.format,
|
||||
settings.zero_suppression)
|
||||
)
|
||||
|
||||
def to_inch(self):
|
||||
self.entity.start[idx] = (
|
||||
inch(self.entity.start[idx][0]), inch(self.entity.start[idx][1]))
|
||||
self.entity.end[idx] = (
|
||||
inch(self.entity.end[idx][0]), inch(self.entity.end[idx][1]))
|
||||
|
||||
def to_metric(self):
|
||||
self.entity.start[idx] = (
|
||||
metric(self.entity.start[idx][0]), inch(self.entity.start[idx][1]))
|
||||
self.entity.end[idx] = (
|
||||
metric(self.entity.end[idx][0]), inch(self.entity.end[idx][1]))
|
||||
|
||||
class DxfCircleStatement(DxfStatement):
|
||||
def __init__(self, entity):
|
||||
super(DxfCircleStatement, self).__init__(entity)
|
||||
|
||||
def to_gerber(self, settings=FileSettings):
|
||||
r = self.entity.radius
|
||||
x0 = self.entity.center[0]
|
||||
y0 = self.entity.center[1]
|
||||
return 'G01*\nX{0}Y{1}D02*\n' \
|
||||
'G75*\nG03*\nX{2}Y{3}I{4}J{5}D01*'.format(
|
||||
write_gerber_value(x0 + r, settings.format,
|
||||
settings.zero_suppression),
|
||||
write_gerber_value(y0, settings.format,
|
||||
settings.zero_suppression),
|
||||
|
||||
write_gerber_value(x0 + r, settings.format,
|
||||
settings.zero_suppression),
|
||||
write_gerber_value(y0, settings.format,
|
||||
settings.zero_suppression),
|
||||
write_gerber_value(-r, settings.format,
|
||||
settings.zero_suppression),
|
||||
write_gerber_value(0, settings.format,
|
||||
settings.zero_suppression)
|
||||
)
|
||||
|
||||
def to_inch(self):
|
||||
self.entity.radius = inch(self.entity.radius)
|
||||
self.entity.center[idx] = (
|
||||
inch(self.entity.center[idx][0]), inch(self.entity.center[idx][1]))
|
||||
|
||||
def to_metric(self):
|
||||
self.entity.radius = metric(self.entity.radius)
|
||||
self.entity.center[idx] = (
|
||||
metric(self.entity.center[idx][0]), metric(self.entity.center[idx][1]))
|
||||
|
||||
class DxfArcStatement(DxfStatement):
|
||||
def __init__(self, entity):
|
||||
super(DxfArcStatement, self).__init__(entity)
|
||||
|
||||
def to_gerber(self, settings=FileSettings):
|
||||
deg0 = self.entity.start_angle
|
||||
deg1 = self.entity.end_angle
|
||||
r = self.entity.radius
|
||||
x0 = self.entity.center[0]
|
||||
y0 = self.entity.center[1]
|
||||
begin_x = x0 + r * cos(deg0 / 180. * pi)
|
||||
begin_y = y0 + r * sin(deg0 / 180. * pi)
|
||||
end_x = x0 + r * cos(deg1 / 180. * pi)
|
||||
end_y = y0 + r * sin(deg1 / 180. * pi)
|
||||
|
||||
return 'G01*\nX{0}Y{1}D02*\n' \
|
||||
'G75*\nG{2}*\nX{3}Y{4}I{5}J{6}D01*'.format(
|
||||
write_gerber_value(begin_x, settings.format,
|
||||
settings.zero_suppression),
|
||||
write_gerber_value(begin_y, settings.format,
|
||||
settings.zero_suppression),
|
||||
'03' if deg0 > deg1 else '02',
|
||||
write_gerber_value(end_x, settings.format,
|
||||
settings.zero_suppression),
|
||||
write_gerber_value(end_y, settings.format,
|
||||
settings.zero_suppression),
|
||||
write_gerber_value(x0 - begin_x, settings.format,
|
||||
settings.zero_suppression),
|
||||
write_gerber_value(y0 - begin_y, settings.format,
|
||||
settings.zero_suppression)
|
||||
)
|
||||
|
||||
def to_inch(self):
|
||||
self.entity.start_angle = inch(self.entity.start_angle)
|
||||
self.entity.end_angle = inch(self.entity.end_angle)
|
||||
self.entity.radius = inch(self.entity.radius)
|
||||
self.entity.center[idx] = (
|
||||
inch(self.entity.center[idx][0]), inch(self.entity.center[idx][1]))
|
||||
|
||||
def to_metric(self):
|
||||
self.entity.start_angle = metric(self.entity.start_angle)
|
||||
self.entity.end_angle = metric(self.entity.end_angle)
|
||||
self.entity.radius = metric(self.entity.radius)
|
||||
self.entity.center[idx] = (
|
||||
metric(self.entity.center[idx][0]), metric(self.entity.center[idx][1]))
|
||||
|
||||
class DxfPolylineStatement(DxfStatement):
|
||||
def __init__(self, entity):
|
||||
super(DxfPolylineStatement, self).__init__(entity)
|
||||
|
||||
def to_gerber(self, settings=FileSettings()):
|
||||
x0 = self.entity.points[0][0]
|
||||
y0 = self.entity.points[0][1]
|
||||
b = self.entity.bulge[0]
|
||||
gerber = 'G01*\nX{0}Y{1}D02*\nG75*'.format(
|
||||
write_gerber_value(x0, settings.format,
|
||||
settings.zero_suppression),
|
||||
write_gerber_value(y0, settings.format,
|
||||
settings.zero_suppression),
|
||||
)
|
||||
|
||||
def ptseq():
|
||||
for i in range(1, len(self.entity.points)):
|
||||
yield i
|
||||
if self.entity.is_closed:
|
||||
yield 0
|
||||
|
||||
for idx in ptseq():
|
||||
pt = self.entity.points[idx]
|
||||
x1 = pt[0]
|
||||
y1 = pt[1]
|
||||
if b == 0:
|
||||
gerber += '\nG01*\nX{0}Y{1}D01*'.format(
|
||||
write_gerber_value(x1, settings.format,
|
||||
settings.zero_suppression),
|
||||
write_gerber_value(y1, settings.format,
|
||||
settings.zero_suppression),
|
||||
)
|
||||
else:
|
||||
ang = 4 * atan(b)
|
||||
xm = x0 + x1
|
||||
ym = y0 + y1
|
||||
t = 1 / tan(ang / 2)
|
||||
xc = (xm - t * (y1 - y0)) / 2
|
||||
yc = (ym + t * (x1 - x0)) / 2
|
||||
r = sqrt((x0 - xc)*(x0 - xc) + (y0 - yc)*(y0 - yc))
|
||||
|
||||
gerber += '\nG{0}*\nX{1}Y{2}I{3}J{4}D01*'.format(
|
||||
'03' if ang > 0 else '02',
|
||||
write_gerber_value(x1, settings.format,
|
||||
settings.zero_suppression),
|
||||
write_gerber_value(y1, settings.format,
|
||||
settings.zero_suppression),
|
||||
write_gerber_value(xc - x0, settings.format,
|
||||
settings.zero_suppression),
|
||||
write_gerber_value(yc - y0, settings.format,
|
||||
settings.zero_suppression)
|
||||
)
|
||||
|
||||
x0 = x1
|
||||
y0 = y1
|
||||
b = self.entity.bulge[idx]
|
||||
|
||||
return gerber
|
||||
|
||||
def to_inch(self):
|
||||
for idx in range(0, len(self.entity.points)):
|
||||
self.entity.points[idx] = (
|
||||
inch(self.entity.points[idx][0]), inch(self.entity.points[idx][1]))
|
||||
self.entity.bulge[idx] = inch(self.entity.bulge[idx])
|
||||
|
||||
def to_metric(self):
|
||||
for idx in range(0, len(self.entity.points)):
|
||||
self.entity.points[idx] = (
|
||||
metric(self.entity.points[idx][0]), metric(self.entity.points[idx][1]))
|
||||
self.entity.bulge[idx] = metric(self.entity.bulge[idx])
|
||||
|
||||
class DxfStatements(object):
|
||||
def __init__(self, entities, units, dcode=10, draw_mode=None):
|
||||
if draw_mode == None:
|
||||
draw_mode = DxfFile.DM_LINE
|
||||
self._units = units
|
||||
self.dcode = dcode
|
||||
self.draw_mode = draw_mode
|
||||
self.statements = []
|
||||
for entity in entities:
|
||||
if entity.dxftype == 'LWPOLYLINE':
|
||||
self.statements.append(DxfPolylineStatement(entity))
|
||||
elif entity.dxftype == 'LINE':
|
||||
self.statements.append(DxfLineStatement(entity))
|
||||
elif entity.dxftype == 'CIRCLE':
|
||||
self.statements.append(DxfCircleStatement(entity))
|
||||
elif entity.dxftype == 'ARC':
|
||||
self.statements.append(DxfArcStatement(entity))
|
||||
|
||||
@property
|
||||
def units(self):
|
||||
return _units
|
||||
|
||||
def to_gerber(self, settings=FileSettings()):
|
||||
def gerbers():
|
||||
yield 'D{0}*'.format(self.dcode)
|
||||
if self.draw_mode == DxfFile.DM_FILL:
|
||||
yield 'G36*'
|
||||
for statement in self.statements:
|
||||
if isinstance(statement, DxfCircleStatement) or \
|
||||
(isinstance(statement, DxfPolylineStatement) and statement.entity.is_closed):
|
||||
yield statement.to_gerber(settings)
|
||||
yield 'G37*'
|
||||
else:
|
||||
for statement in self.statements:
|
||||
yield statement.to_gerber(settings)
|
||||
|
||||
return '\n'.join(gerbers())
|
||||
|
||||
def to_inch(self):
|
||||
if self._units == 'metric':
|
||||
self._units = 'inch'
|
||||
for statement in self.statements:
|
||||
statement.to_inch()
|
||||
|
||||
def to_metric(self):
|
||||
if self._units == 'inch':
|
||||
self._units = 'metric'
|
||||
for statement in self.statements:
|
||||
statement.to_metric()
|
||||
|
||||
class DxfHeaderStatement(object):
|
||||
def to_gerber(self, settings):
|
||||
return 'G75*\n'\
|
||||
'%MO{0}*%\n'\
|
||||
'%OFA0B0*%\n'\
|
||||
'%FS{1}AX{2}{3}Y{4}{5}*%\n'\
|
||||
'%IPPOS*%\n'\
|
||||
'%LPD*%'.format(
|
||||
'IN' if settings.units == 'inch' else 'MM',
|
||||
'L' if settings.zero_suppression == 'leading' else 'T',
|
||||
settings.format[0], settings.format[1],
|
||||
settings.format[0], settings.format[1]
|
||||
)
|
||||
|
||||
def to_inch(self):
|
||||
pass
|
||||
|
||||
def to_metric(self):
|
||||
pass
|
||||
|
||||
class DxfFile(CamFile):
|
||||
DM_LINE = 0
|
||||
DM_FILL = 1
|
||||
|
||||
def __init__(self, dxf, settings=FileSettings(), draw_mode=None, filename=None):
|
||||
if draw_mode == None:
|
||||
draw_mode = self.DM_LINE
|
||||
if dxf.header['$INSUNITS'] == 1:
|
||||
settings.units = 'inch'
|
||||
settings.format = (2, 5)
|
||||
else:
|
||||
settings.units = 'metric'
|
||||
settings.format = (3, 4)
|
||||
|
||||
super(DxfFile, self).__init__(settings=settings, filename=filename)
|
||||
self._draw_mode = draw_mode
|
||||
self.header = DxfHeaderStatement()
|
||||
self.aperture = ADParamStmt.circle(dcode=10, diameter=0.0)
|
||||
self.statements = DxfStatements(dxf.entities, self.units, dcode=self.aperture.d, draw_mode=self.draw_mode)
|
||||
|
||||
@property
|
||||
def dcode(self):
|
||||
return self.aperture.dcode
|
||||
|
||||
@dcode.setter
|
||||
def dcode(self, value):
|
||||
self.aperture.d = value
|
||||
self.statements.dcode = value
|
||||
|
||||
@property
|
||||
def width(self):
|
||||
return self.aperture.modifiers[0][0]
|
||||
|
||||
@width.setter
|
||||
def width(self, value):
|
||||
self.aperture.modifiers = ([float(value),],)
|
||||
|
||||
@property
|
||||
def draw_mode(self):
|
||||
return self._draw_mode
|
||||
|
||||
@draw_mode.setter
|
||||
def draw_mode(self, value):
|
||||
self._draw_mode = value
|
||||
self.statements.draw_mode = value
|
||||
|
||||
def write(self, filename=None):
|
||||
if self.settings.notation != 'absolute':
|
||||
raise Exception('DXF file\'s notation must be absolute ')
|
||||
|
||||
filename = filename if filename is not None else self.filename
|
||||
with open(filename, 'w') as f:
|
||||
f.write(self.header.to_gerber(self.settings) + '\n')
|
||||
f.write(self.aperture.to_gerber(self.settings) + '\n')
|
||||
f.write(self.statements.to_gerber(self.settings) + '\n')
|
||||
f.write('M02*\n')
|
||||
|
||||
def to_inch(self):
|
||||
if self.units == 'metric':
|
||||
self.header.to_inch()
|
||||
self.aperture.to_inch()
|
||||
self.statements.to_inch()
|
||||
self.units = 'inch'
|
||||
|
||||
def to_metric(self):
|
||||
if self.units == 'inch':
|
||||
self.header.to_metric()
|
||||
self.aperture.to_metric()
|
||||
self.statements.to_metric()
|
||||
self.units = 'metric'
|
||||
|
||||
def offset(self, ofset_x, offset_y):
|
||||
raise Exception('Not supported')
|
||||
|
||||
def loads(data, filename=None):
|
||||
stream = io.StringIO(data)
|
||||
dxf = dxfgrabber.read(stream)
|
||||
return DxfFile(dxf)
|
||||
42
gerberex/excellon.py
Normal file
42
gerberex/excellon.py
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright 2019 Hiroshi Murayama <opiopan@gmail.com>
|
||||
|
||||
from gerber.excellon import (ExcellonParser, detect_excellon_format, ExcellonFile)
|
||||
from gerber.excellon_statements import UnitStmt
|
||||
from gerber.cam import FileSettings
|
||||
|
||||
def loads(data, filename=None, settings=None, tools=None, format=None):
|
||||
if not settings:
|
||||
settings = FileSettings(**detect_excellon_format(data))
|
||||
if format:
|
||||
settings.format = format
|
||||
file = ExcellonParser(settings, tools).parse_raw(data, filename)
|
||||
return ExcellonFileEx.from_file(file)
|
||||
|
||||
class ExcellonFileEx(ExcellonFile):
|
||||
@classmethod
|
||||
def from_file(cls, file):
|
||||
statements = [
|
||||
UnitStmtEx.from_statement(s) if isinstance(s, UnitStmt) else s \
|
||||
for s in file.statements
|
||||
]
|
||||
return cls(statements, file.tools, file.hits, file.settings, file.filename)
|
||||
|
||||
def __init__(self, statements, tools, hits, settings, filename=None):
|
||||
super(ExcellonFileEx, self).__init__(statements, tools, hits, settings, filename)
|
||||
|
||||
class UnitStmtEx(UnitStmt):
|
||||
@classmethod
|
||||
def from_statement(cls, stmt):
|
||||
return cls(units=stmt.units, zeros=stmt.zeros, format=stmt.format, id=stmt.id)
|
||||
|
||||
def __init__(self, units='inch', zeros='leading', format=None, **kwargs):
|
||||
super(UnitStmtEx, self).__init__(units, zeros, format, **kwargs)
|
||||
|
||||
def to_excellon(self, settings=None):
|
||||
stmt = '%s,%s,%s.%s' % ('INCH' if self.units == 'inch' else 'METRIC',
|
||||
'LZ' if self.zeros == 'leading' else 'TZ',
|
||||
'0' * self.format[0], '0' * self.format[1])
|
||||
return stmt
|
||||
25
gerberex/rs274x.py
Normal file
25
gerberex/rs274x.py
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright 2019 Hiroshi Murayama <opiopan@gmail.com>
|
||||
|
||||
import gerber.rs274x
|
||||
from gerberex.statements import (AMParamStmt, AMParamStmtEx)
|
||||
|
||||
class GerberFile(gerber.rs274x.GerberFile):
|
||||
@classmethod
|
||||
def from_gerber_file(cls, gerber_file):
|
||||
if not isinstance(gerber_file, gerber.rs274x.GerberFile):
|
||||
raise Exception('only gerber.rs274x.GerberFile object is specified')
|
||||
|
||||
def swap_statement(statement):
|
||||
if isinstance(statement, AMParamStmt) and not isinstance(statement, AMParamStmtEx):
|
||||
return AMParamStmtEx.from_stmt(statement)
|
||||
else:
|
||||
return statement
|
||||
statements = [swap_statement(statement) for statement in gerber_file.statements]
|
||||
return cls(statements, gerber_file.settings, gerber_file.primitives,\
|
||||
gerber_file.apertures, gerber_file.filename)
|
||||
|
||||
def __init__(self, statements, settings, primitives, apertures, filename=None):
|
||||
super(GerberFile, self).__init__(statements, settings, primitives, apertures, filename)
|
||||
34
gerberex/statements.py
Normal file
34
gerberex/statements.py
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright 2019 Hiroshi Murayama <opiopan@gmail.com>
|
||||
|
||||
from gerber.gerber_statements import AMParamStmt
|
||||
from gerberex.am_primitive import to_primitive_defs
|
||||
|
||||
class AMParamStmtEx(AMParamStmt):
|
||||
@classmethod
|
||||
def from_stmt(cls, stmt):
|
||||
return cls(stmt.param, stmt.name, stmt.macro)
|
||||
|
||||
def __init__(self, param, name, macro):
|
||||
super(AMParamStmtEx, self).__init__(param, name, macro)
|
||||
self.primitive_defs = to_primitive_defs(self.instructions)
|
||||
|
||||
def to_inch(self):
|
||||
if self.units == 'metric':
|
||||
self.units = 'inch'
|
||||
for p in self.primitive_defs:
|
||||
p.to_inch()
|
||||
|
||||
def to_metric(self):
|
||||
if self.units == 'inch':
|
||||
self.units = 'metric'
|
||||
for p in self.primitive_defs:
|
||||
p.to_metric()
|
||||
|
||||
def to_gerber(self, settings = None):
|
||||
def plist():
|
||||
for p in self.primitive_defs:
|
||||
yield p.to_gerber(settings)
|
||||
return "%%AM%s*\n%s%%" % (self.name, '\n'.join(plist()))
|
||||
49
setup.py
Normal file
49
setup.py
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright 2013-2014 Paulo Henrique Silva <ph.silva@gmail.com>
|
||||
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
|
||||
import os
|
||||
|
||||
METADATA = {
|
||||
'name': 'pcb-tools-extension',
|
||||
'version': 0.1,
|
||||
'author': 'Hiroshi Murayama <opiopan@gmail.com>',
|
||||
'author_email': "opiopan@gmail.com",
|
||||
'description': ("Extension for pcb-tools package to panelize gerber files"),
|
||||
'license': "Apache",
|
||||
'keywords': "pcb gerber tools extension",
|
||||
'url': "http://github.com/opiopan/pcb-tools-extension",
|
||||
'packages': ['gerberex'],
|
||||
'classifiers':[
|
||||
"Development Status :: 3 - Alpha",
|
||||
"Topic :: Utilities",
|
||||
"License :: OSI Approved :: Apple Public Source License",
|
||||
],
|
||||
}
|
||||
|
||||
SETUPTOOLS_METADATA = {
|
||||
'install_requires': ['pcb-tools', 'dxfgrabber'],
|
||||
}
|
||||
|
||||
|
||||
def install():
|
||||
""" Install using setuptools, fallback to distutils
|
||||
"""
|
||||
try:
|
||||
from setuptools import setup
|
||||
METADATA.update(SETUPTOOLS_METADATA)
|
||||
setup(**METADATA)
|
||||
except ImportError:
|
||||
from sys import stderr
|
||||
stderr.write('Could not import setuptools, using distutils')
|
||||
stderr.write('NOTE: You will need to install dependencies manualy')
|
||||
from distutils.core import setup
|
||||
setup(**METADATA)
|
||||
|
||||
if __name__ == '__main__':
|
||||
install()
|
||||
38
test/panelimage.py
Normal file
38
test/panelimage.py
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
#!/usr/bin/env python
|
||||
from gerber import load_layer
|
||||
from gerber.render import RenderSettings, theme
|
||||
from gerber.render.cairo_backend import GerberCairoContext
|
||||
|
||||
print('loading ', end='', flush=True)
|
||||
copper = load_layer('panelized.GTL')
|
||||
print('.', end='', flush=True)
|
||||
mask = load_layer('panelized.GTS')
|
||||
print('.', end='', flush=True)
|
||||
silk = load_layer('panelized.GTO')
|
||||
print('.', end='', flush=True)
|
||||
drill = load_layer('panelized.TXT')
|
||||
print('.', end='', flush=True)
|
||||
outline = load_layer('panelized-fill.GML')
|
||||
print('.', end='', flush=True)
|
||||
print('. end', flush=True)
|
||||
|
||||
print('panelizing ', end='', flush=True)
|
||||
ctx = GerberCairoContext(scale=30)
|
||||
print('.', end='', flush=True)
|
||||
ctx.render_layer(copper)
|
||||
print('.', end='', flush=True)
|
||||
ctx.render_layer(mask)
|
||||
print('.', end='', flush=True)
|
||||
|
||||
our_settings = RenderSettings(color=theme.COLORS['white'], alpha=0.85)
|
||||
ctx.render_layer(silk, settings=our_settings)
|
||||
print('.', end='', flush=True)
|
||||
|
||||
ctx.render_layer(outline)
|
||||
print('.', end='', flush=True)
|
||||
ctx.render_layer(drill)
|
||||
print('.', end='', flush=True)
|
||||
print('. end', flush=True)
|
||||
|
||||
print('dumping top...')
|
||||
ctx.dump('panelized.png')
|
||||
63
test/test.py
Normal file
63
test/test.py
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
import gerberex
|
||||
from gerberex.dxf import DxfFile
|
||||
import gerber
|
||||
from gerber.render.cairo_backend import GerberCairoContext
|
||||
|
||||
def merge():
|
||||
ctx = gerberex.GerberComposition()
|
||||
a = gerberex.read('test.GTL')
|
||||
a.to_metric()
|
||||
ctx.merge(a)
|
||||
|
||||
b = gerberex.read('test.GTL')
|
||||
b.to_metric()
|
||||
b.offset(0, 25)
|
||||
ctx.merge(b)
|
||||
|
||||
c = gerberex.read('test2.GTL')
|
||||
c.to_metric()
|
||||
c.offset(0, 60)
|
||||
ctx.merge(c)
|
||||
|
||||
c = gerberex.read('test.GML')
|
||||
c.to_metric()
|
||||
ctx.merge(c)
|
||||
|
||||
ctx.dump('test-merged.GTL')
|
||||
|
||||
def merge2():
|
||||
ctx = gerberex.DrillComposition()
|
||||
a = gerberex.read('test.TXT')
|
||||
a.to_metric()
|
||||
ctx.merge(a)
|
||||
|
||||
b = gerberex.read('test.TXT')
|
||||
b.to_metric()
|
||||
b.offset(0, 25)
|
||||
ctx.merge(b)
|
||||
|
||||
c = gerberex.read('test2.TXT')
|
||||
c.to_metric()
|
||||
c.offset(0, 60)
|
||||
ctx.merge(c)
|
||||
|
||||
ctx.dump('test-merged.TXT')
|
||||
|
||||
|
||||
#merge2()
|
||||
|
||||
file = gerberex.read('outline.dxf')
|
||||
file.to_metric()
|
||||
w = file.width
|
||||
file.draw_mode = DxfFile.DM_FILL
|
||||
file.write('outline.GML')
|
||||
|
||||
copper = gerber.load_layer('test-merged.GTL')
|
||||
ctx = GerberCairoContext(scale=10)
|
||||
ctx.render_layer(copper)
|
||||
outline = gerber.load_layer('test.GML')
|
||||
outline.cam_source.to_metric()
|
||||
ctx.render_layer(outline)
|
||||
drill = gerber.load_layer('test-merged.TXT')
|
||||
ctx.render_layer(drill)
|
||||
ctx.dump('test.png')
|
||||
Loading…
Add table
Add a link
Reference in a new issue