kopia lustrzana https://gitlab.com/gerbolyze/gerbonara
kicad: Add schematic file format support
rodzic
08c4091e57
commit
58142cb0c7
|
@ -226,6 +226,7 @@ class TextEffect:
|
|||
hide: Flag() = False
|
||||
justify: OmitDefault(Justify) = field(default_factory=Justify)
|
||||
|
||||
|
||||
@sexp_type('tstamp')
|
||||
class Timestamp:
|
||||
value: str = field(default_factory=uuid.uuid4)
|
||||
|
@ -242,6 +243,7 @@ class Timestamp:
|
|||
def bump(self):
|
||||
self.value = uuid.uuid4()
|
||||
|
||||
|
||||
@sexp_type('uuid')
|
||||
class UUID:
|
||||
value: str = field(default_factory=uuid.uuid4)
|
||||
|
@ -258,6 +260,7 @@ class UUID:
|
|||
def bump(self):
|
||||
self.value = uuid.uuid4()
|
||||
|
||||
|
||||
@sexp_type('tedit')
|
||||
class EditTime:
|
||||
value: str = field(default_factory=time.time)
|
||||
|
@ -274,6 +277,15 @@ class EditTime:
|
|||
def bump(self):
|
||||
self.value = time.time()
|
||||
|
||||
|
||||
@sexp_type('paper')
|
||||
class PageSettings:
|
||||
page_format: str = 'A4'
|
||||
width: float = None
|
||||
height: float = None
|
||||
portrait: Flag() = False
|
||||
|
||||
|
||||
@sexp_type('property')
|
||||
class Property:
|
||||
key: str = ''
|
||||
|
|
|
@ -306,6 +306,18 @@ class DimensionStyle:
|
|||
keep_text_aligned: Flag() = False
|
||||
|
||||
|
||||
@sexp_type('image')
|
||||
class Image:
|
||||
at: AtPos = field(default_factory=AtPos)
|
||||
scale: Named(float) = None
|
||||
layer: Named(str) = None
|
||||
uuid: UUID = field(default_factory=UUID)
|
||||
data: str = ''
|
||||
|
||||
def offset(self, x=0, y=0):
|
||||
self.at = self.at.with_offset(x, y)
|
||||
|
||||
|
||||
@sexp_type('dimension')
|
||||
class Dimension:
|
||||
locked: Flag() = False
|
||||
|
|
|
@ -59,14 +59,6 @@ class GeneralSection:
|
|||
thickness: Named(float) = 1.60
|
||||
|
||||
|
||||
@sexp_type('paper')
|
||||
class PageSettings:
|
||||
page_format: str = 'A4'
|
||||
width: float = None
|
||||
height: float = None
|
||||
portrait: Flag() = False
|
||||
|
||||
|
||||
@sexp_type('layers')
|
||||
class LayerSettings:
|
||||
index: int = 0
|
||||
|
@ -158,18 +150,6 @@ class Net:
|
|||
name: str = ''
|
||||
|
||||
|
||||
@sexp_type('image')
|
||||
class Image:
|
||||
at: AtPos = field(default_factory=AtPos)
|
||||
scale: Named(float) = None
|
||||
layer: Named(str) = None
|
||||
uuid: UUID = field(default_factory=UUID)
|
||||
data: str = ''
|
||||
|
||||
def offset(self, x=0, y=0):
|
||||
self.at = self.at.with_offset(x, y)
|
||||
|
||||
|
||||
@sexp_type('segment')
|
||||
class TrackSegment:
|
||||
start: Rename(XYCoord) = field(default_factory=XYCoord)
|
||||
|
@ -322,7 +302,7 @@ class Board:
|
|||
polygons: List(gr.Polygon) = field(default_factory=list)
|
||||
curves: List(gr.Curve) = field(default_factory=list)
|
||||
dimensions: List(gr.Dimension) = field(default_factory=list)
|
||||
images: List(Image) = field(default_factory=list)
|
||||
images: List(gr.Image) = field(default_factory=list)
|
||||
# Tracks
|
||||
track_segments: List(TrackSegment) = field(default_factory=list)
|
||||
track_arcs: List(TrackArc) = field(default_factory=list)
|
||||
|
@ -368,7 +348,7 @@ class Board:
|
|||
self.curves.remove(obj)
|
||||
case gr.Dimension():
|
||||
self.dimensions.remove(obj)
|
||||
case Image():
|
||||
case gr.Image():
|
||||
self.images.remove(obj)
|
||||
case TrackSegment():
|
||||
self.track_segments.remove(obj)
|
||||
|
@ -405,7 +385,7 @@ class Board:
|
|||
self.curves.append(obj)
|
||||
case gr.Dimension():
|
||||
self.dimensions.append(obj)
|
||||
case Image():
|
||||
case gr.Image():
|
||||
self.images.append(obj)
|
||||
case TrackSegment():
|
||||
self.track_segments.append(obj)
|
||||
|
|
|
@ -0,0 +1,293 @@
|
|||
"""
|
||||
Library for handling KiCad's schematic files (`*.kicad_sch`).
|
||||
"""
|
||||
|
||||
import math
|
||||
from pathlib import Path
|
||||
from dataclasses import field, KW_ONLY
|
||||
from itertools import chain
|
||||
import re
|
||||
import fnmatch
|
||||
import os.path
|
||||
|
||||
from .sexp import *
|
||||
from .base_types import *
|
||||
from .primitives import *
|
||||
from .symbols import Symbol
|
||||
from . import graphical_primitives as gr
|
||||
|
||||
from .. import primitives as cad_pr
|
||||
|
||||
from ... import graphic_primitives as gp
|
||||
from ... import graphic_objects as go
|
||||
from ... import apertures as ap
|
||||
from ...layers import LayerStack
|
||||
from ...newstroke import Newstroke
|
||||
from ...utils import MM, rotate_point
|
||||
|
||||
|
||||
@sexp_type('path')
|
||||
class SheetPath:
|
||||
path: str = '/'
|
||||
page: Named(str) = '1'
|
||||
|
||||
|
||||
@sexp_type('junction')
|
||||
class Junction:
|
||||
at: AtPos = field(default_factory=AtPos)
|
||||
diameter: Named(float) = 0
|
||||
color: Color = field(default_factory=lambda: Color(0, 0, 0, 0))
|
||||
uuid: UUID = field(default_factory=UUID)
|
||||
|
||||
|
||||
@sexp_type('no_connect')
|
||||
class NoConnect:
|
||||
at: AtPos = field(default_factory=AtPos)
|
||||
uuid: UUID = field(default_factory=UUID)
|
||||
|
||||
|
||||
@sexp_type('bus_entry')
|
||||
class BusEntry:
|
||||
at: AtPos = field(default_factory=AtPos)
|
||||
size: Rename(XYCoord) = field(default_factory=lambda: XYCoord(2.54, 2.54))
|
||||
stroke: Stroke = field(default_factory=Stroke)
|
||||
uuid: UUID = field(default_factory=UUID)
|
||||
|
||||
|
||||
@sexp_type('wire')
|
||||
class Wire:
|
||||
points: PointList = field(default_factory=PointList)
|
||||
stroke: Stroke = field(default_factory=Stroke)
|
||||
uuid: UUID = field(default_factory=UUID)
|
||||
|
||||
|
||||
@sexp_type('bus')
|
||||
class Bus:
|
||||
points: PointList = field(default_factory=PointList)
|
||||
stroke: Stroke = field(default_factory=Stroke)
|
||||
uuid: UUID = field(default_factory=UUID)
|
||||
|
||||
|
||||
@sexp_type('polyline')
|
||||
class Polyline:
|
||||
points: PointList = field(default_factory=PointList)
|
||||
stroke: Stroke = field(default_factory=Stroke)
|
||||
uuid: UUID = field(default_factory=UUID)
|
||||
|
||||
|
||||
@sexp_type('text')
|
||||
class Text:
|
||||
text: str = ''
|
||||
exclude_from_sim: Named(YesNoAtom()) = True
|
||||
at: AtPos = field(default_factory=AtPos)
|
||||
effects: TextEffect = field(default_factory=TextEffect)
|
||||
uuid: UUID = field(default_factory=UUID)
|
||||
|
||||
|
||||
@sexp_type('label')
|
||||
class LocalLabel:
|
||||
text: str = ''
|
||||
at: AtPos = field(default_factory=AtPos)
|
||||
fields_autoplaced: Wrap(Flag()) = False
|
||||
effects: TextEffect = field(default_factory=TextEffect)
|
||||
uuid: UUID = field(default_factory=UUID)
|
||||
|
||||
|
||||
@sexp_type('global_label')
|
||||
class GlobalLabel:
|
||||
text: str = ''
|
||||
shape: Named(AtomChoice(Atom.input, Atom.output, Atom.bidirectional, Atom.tri_state, Atom.passive)) = Atom.input
|
||||
at: AtPos = field(default_factory=AtPos)
|
||||
fields_autoplaced: Wrap(Flag()) = False
|
||||
effects: TextEffect = field(default_factory=TextEffect)
|
||||
uuid: UUID = field(default_factory=UUID)
|
||||
properties: List(Property) = field(default_factory=list)
|
||||
|
||||
|
||||
@sexp_type('hierarchical_label')
|
||||
class HierarchicalLabel:
|
||||
text: str = ''
|
||||
shape: Named(AtomChoice(Atom.input, Atom.output, Atom.bidirectional, Atom.tri_state, Atom.passive)) = Atom.input
|
||||
at: AtPos = field(default_factory=AtPos)
|
||||
fields_autoplaced: Wrap(Flag()) = False
|
||||
effects: TextEffect = field(default_factory=TextEffect)
|
||||
uuid: UUID = field(default_factory=UUID)
|
||||
|
||||
|
||||
@sexp_type('pin')
|
||||
class Pin:
|
||||
name: str = '1'
|
||||
uuid: UUID = field(default_factory=UUID)
|
||||
|
||||
|
||||
# Suddenly, we're doing syntax like this is yaml or something.
|
||||
@sexp_type('path')
|
||||
class SymbolCrosslinkSheet:
|
||||
path: str = ''
|
||||
reference: Named(str) = ''
|
||||
unit: Named(int) = 1
|
||||
|
||||
|
||||
@sexp_type('project')
|
||||
class SymbolCrosslinkProject:
|
||||
project_name: str = ''
|
||||
instances: List(SymbolCrosslinkSheet) = field(default_factory=list)
|
||||
|
||||
|
||||
@sexp_type('mirror')
|
||||
class MirrorFlags:
|
||||
x: Flag() = False
|
||||
y: Flag() = False
|
||||
|
||||
|
||||
@sexp_type('property')
|
||||
class DrawnProperty:
|
||||
key: str = None
|
||||
value: str = None
|
||||
at: AtPos = field(default_factory=AtPos)
|
||||
hide: Flag() = False
|
||||
effects: TextEffect = field(default_factory=TextEffect)
|
||||
|
||||
|
||||
@sexp_type('symbol')
|
||||
class SymbolInstance:
|
||||
name: str = None
|
||||
lib_name: Named(str) = ''
|
||||
lib_id: Named(str) = ''
|
||||
at: AtPos = field(default_factory=AtPos)
|
||||
mirror: OmitDefault(MirrorFlags) = field(default_factory=MirrorFlags)
|
||||
unit: Named(int) = 1
|
||||
in_bom: Named(YesNoAtom()) = True
|
||||
on_board: Named(YesNoAtom()) = True
|
||||
dnp: Named(YesNoAtom()) = True
|
||||
fields_autoplaced: Wrap(Flag()) = True
|
||||
uuid: UUID = field(default_factory=UUID)
|
||||
properties: List(DrawnProperty) = field(default_factory=list)
|
||||
# AFAICT this property is completely redundant.
|
||||
pins: List(Pin) = field(default_factory=list)
|
||||
# AFAICT this property, too, is completely redundant. It ultimately just lists paths and references of at most
|
||||
# three other uses of the same symbol in this schematic.
|
||||
instances: Named(List(SymbolCrosslinkProject)) = field(default_factory=list)
|
||||
|
||||
|
||||
@sexp_type('path')
|
||||
class SubsheetCrosslinkSheet:
|
||||
path: str = ''
|
||||
page: Named(str) = ''
|
||||
|
||||
|
||||
@sexp_type('project')
|
||||
class SubsheetCrosslinkProject:
|
||||
project_name: str = ''
|
||||
instances: List(SymbolCrosslinkSheet) = field(default_factory=list)
|
||||
|
||||
|
||||
@sexp_type('pin')
|
||||
class SubsheetPin:
|
||||
name: str = '1'
|
||||
shape: AtomChoice(Atom.input, Atom.output, Atom.bidirectional, Atom.tri_state, Atom.passive) = Atom.input
|
||||
at: AtPos = field(default_factory=AtPos)
|
||||
effects: TextEffect = field(default_factory=TextEffect)
|
||||
uuid: UUID = field(default_factory=UUID)
|
||||
|
||||
|
||||
@sexp_type('sheet')
|
||||
class Subsheet:
|
||||
at: AtPos = field(default_factory=AtPos)
|
||||
size: Rename(XYCoord) = field(default_factory=lambda: XYCoord(2.54, 2.54))
|
||||
fields_autoplaced: Wrap(Flag()) = True
|
||||
stroke: Stroke = field(default_factory=Stroke)
|
||||
fill: gr.FillMode = field(default_factory=gr.FillMode)
|
||||
uuid: UUID = field(default_factory=UUID)
|
||||
_properties: List(DrawnProperty) = field(default_factory=list)
|
||||
pins: List(SubsheetPin) = field(default_factory=list)
|
||||
# AFAICT this is completely redundant, just like the one in SymbolInstance
|
||||
instances: Named(List(SubsheetCrosslinkProject)) = field(default_factory=list)
|
||||
_: KW_ONLY
|
||||
sheet_name: object = field(default_factory=lambda: DrawnProperty('Sheetname', ''))
|
||||
file_name: object = field(default_factory=lambda: DrawnProperty('Sheetfile', ''))
|
||||
parent: object = None
|
||||
|
||||
def __after_parse__(self, parent):
|
||||
self.sheet_name, self.file_name = self._properties
|
||||
self.parent = parent
|
||||
|
||||
def __before_sexp__(self):
|
||||
self._properties = [self.sheet_name, self.file_name]
|
||||
|
||||
def open(self, search_dir=None, safe=True):
|
||||
if search_dir is None:
|
||||
if not self.parent.original_filename:
|
||||
raise FileNotFoundError('No search path given and path of parent schematic unknown')
|
||||
else:
|
||||
search_dir = Path(self.parent.original_filename).parent
|
||||
else:
|
||||
search_dir = Path(search_dir)
|
||||
|
||||
resolved = search_dir / self.file_name.value
|
||||
if safe and os.path.commonprefix((search_dir.parts, resolved.parts)) != search_dir.parts:
|
||||
raise ValueError('Subsheet path traversal to parent directory attempted in Subsheet.open(..., safe=True)')
|
||||
|
||||
return Schematic.open(resolved)
|
||||
|
||||
|
||||
SUPPORTED_FILE_FORMAT_VERSIONS = [20220914]
|
||||
@sexp_type('kicad_sch')
|
||||
class Schematic:
|
||||
_version: Named(int, name='version') = 20211014
|
||||
generator: Named(Atom) = Atom.gerbonara
|
||||
uuid: UUID = field(default_factory=UUID)
|
||||
page_settings: PageSettings = field(default_factory=PageSettings)
|
||||
path: SheetPath = field(default_factory=SheetPath)
|
||||
lib_symbols: Named(Array(Symbol)) = field(default_factory=list)
|
||||
junctions: List(Junction) = field(default_factory=list)
|
||||
no_connects: List(NoConnect) = field(default_factory=list)
|
||||
bus_entries: List(BusEntry) = field(default_factory=list)
|
||||
wires: List(Wire) = field(default_factory=list)
|
||||
buses: List(Bus) = field(default_factory=list)
|
||||
images: List(gr.Image) = field(default_factory=list)
|
||||
polylines: List(Polyline) = field(default_factory=list)
|
||||
texts: List(Text) = field(default_factory=list)
|
||||
local_labels: List(LocalLabel) = field(default_factory=list)
|
||||
global_labels: List(GlobalLabel) = field(default_factory=list)
|
||||
hierarchical_labels: List(HierarchicalLabel) = field(default_factory=list)
|
||||
symbols: List(SymbolInstance) = field(default_factory=list)
|
||||
subsheets: List(Subsheet) = field(default_factory=list)
|
||||
sheet_instances: Named(List(SubsheetCrosslinkSheet)) = field(default_factory=list)
|
||||
_ : SEXP_END = None
|
||||
original_filename: str = None
|
||||
|
||||
@property
|
||||
def version(self):
|
||||
return self._version
|
||||
|
||||
@version.setter
|
||||
def version(self, value):
|
||||
if value not in SUPPORTED_FILE_FORMAT_VERSIONS:
|
||||
raise FormatError(f'File format version {value} is not supported. Supported versions are {", ".join(map(str, SUPPORTED_FILE_FORMAT_VERSIONS))}.')
|
||||
|
||||
def write(self, filename=None):
|
||||
with open(filename or self.original_filename, 'w') as f:
|
||||
f.write(self.serialize())
|
||||
|
||||
def serialize(self):
|
||||
return build_sexp(sexp(type(self), self)[0])
|
||||
|
||||
@classmethod
|
||||
def open(kls, pcb_file, *args, **kwargs):
|
||||
return kls.load(Path(pcb_file).read_text(), *args, **kwargs, original_filename=pcb_file)
|
||||
|
||||
@classmethod
|
||||
def load(kls, data, *args, **kwargs):
|
||||
return kls.parse(data, *args, **kwargs)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
import sys
|
||||
from ...layers import LayerStack
|
||||
sch = Schematic.open(sys.argv[1])
|
||||
print('Loaded schematic with', len(sch.wires), 'wires and', len(sch.symbols), 'symbols.')
|
||||
for subsh in sch.subsheets:
|
||||
subsh = subsh.open()
|
||||
print('Loaded sub-sheet with', len(subsh.wires), 'wires and', len(subsh.symbols), 'symbols.')
|
||||
|
|
@ -414,7 +414,7 @@ SUPPORTED_FILE_FORMAT_VERSIONS = [20211014, 20220914]
|
|||
@sexp_type('kicad_symbol_lib')
|
||||
class Library:
|
||||
_version: Named(int, name='version') = 20211014
|
||||
generator: Named(Atom) = Atom.kicad_library_utils
|
||||
generator: Named(Atom) = Atom.gerbonara
|
||||
symbols: List(Symbol) = field(default_factory=list)
|
||||
_ : SEXP_END = None
|
||||
original_filename: str = None
|
||||
|
|
Ładowanie…
Reference in New Issue