Argmaster / pygerber

Python implementation of Gerber X3/X2 standard with 2D rendering engine.
https://argmaster.github.io/pygerber/stable
MIT License
54 stars 10 forks source link

Enhancement: IPC2581 Support #345

Open sjgallagher2 opened 1 day ago

sjgallagher2 commented 1 day ago

This may be out of scope for this project, but I think pygerber has enough infrastructure to add support for IPC2581. That standard is supported by an increasing number of board houses and CAD packages, and parsing the files is fairly straightforward.

A CAD packages produces a .cvg file, which is an XML file following the IPC2581 standard. Unfortunately, the standard is not freely available (unless you become a member, which is free). Even still, being a plain text XML file it's easy to understand the structure:

Most interesting is the Ecad node. It is divided into CadHeader and CadData. The CadHeader has layers in Spec nodes which have optional MATERIAL properties, e.g. Solder Resist. The CadData node contains layer information (each layer has a name, function, side, and polarity), stackup information under a Stackup node, and actual geometry (both traces and component geometry) under a Step node.

Here's a sort of tree view of the nodes:

# IPC2581
# . Content
# . . FunctionMode
# . . StepRef
# . . LayerRef
# . . BomRef
# . . DictionaryStandard
# . . . EntryStandard
# . . . . RectCenter
# . . . . Circle
# . . DictionaryColor
# . . . EntryColor
# . . . . Color
# . LogisticHeader
# . . Role
# . . Enterprise
# . . Person
# . Bom
# . . BomHeader
# . . . StepRef
# . . BomItem
# . . . RefDes
# . . . Characteristics
# . Ecad
# . . CadHeader
# . . . Spec
# . . . . General
# . . . . . Property
# . . CadData
# . . . Layer
# . . . Stackup
# . . . . StackupGroup
# . . . . . StackupLayer
# . . . . . . SpecRef
# . . . Step
# . . . . Datum
# . . . . Profile
# . . . . . Polygon
# . . . . . . PolyBegin
# . . . . . . PolyStepCurve
# . . . . . . PolyStepSegment
# . . . . . Cutout
# . . . . . . PolyBegin
# . . . . . . PolyStepCurve
# . . . . . . PolyStepSegment
# . . . . PadStack
# . . . . . LayerPad
# . . . . . . Xform
# . . . . . . Location
# . . . . . . StandardPrimitiveRef
# . . . . . . PinRef
# . . . . . LayerHole
# . . . . . . Span
# . . . . Package
# . . . . . Outline
# . . . . . . Polygon
# . . . . . . . PolyBegin
# . . . . . . . PolyStepCurve
# . . . . . . . PolyStepSegment
# . . . . . . LineDesc
# . . . . . Pin
# . . . . . . Location
# . . . . . . StandardPrimitiveRef
# . . . . Component
# . . . . . Xform
# . . . . . Location
# . . . . LayerFeature
# . . . . . Set
# . . . . . . ColorRef
# . . . . . . Hole
# . . . . . . Features
# . . . . . . . Contour
# . . . . . . . . Polygon
# . . . . . . . . . PolyBegin
# . . . . . . . . . PolyStepSegment
# . . . . . . . UserSpecial
# . . . . . . . . Arc
# . . . . . . . . . LineDesc
# . . . . . . . . Line
# . . . . . . . . . LineDesc

This is not comprehensive, it's built up based on a random example file I had.

Examples

Parsing Layers and Nets

def get_layer_list(root: ET.Element):
    """
    Return a list of layers in dictionary format for this document

    Parameters
    ----------
    root : ET.Element

    Returns
    -------
    List of layer dictionaries with elements:
        name            Layer name
        layerFunction   Layer function as a string, one of {'DRILL', 'DOCUMENT', 'PASTEMASK', 'LEGEND', 'SOLDERMASK', 'SIGNAL', 'DIELCORE'}
        side            PCB side, one of {'TOP', 'BOTTOM', 'INTERNAL'}
        polarity        Layer drawing polarity, one of {'POSITIVE','NEGATIVE'}
        thickness       Layer thickness as float
        sequence        Layer sequence position as int
        z               Layer z position as float (bottom is z=0)

    """
    rpf = '{http://webstds.ipc.org/2581}'  # Root prefix
    # Get layer names and functions
    layers = root.findall(f'{rpf}Ecad/{rpf}CadData/{rpf}Layer')
    layers = [l.attrib for l in layers]

    # Get layer thicknesses
    for layer in layers:
        name = layer['name']
        stackuplayer = root.find(
            f'{rpf}Ecad/{rpf}CadData/{rpf}Stackup/{rpf}StackupGroup/{rpf}StackupLayer[@layerOrGroupRef="{name}"]')
        layer['thickness'] = float(stackuplayer.attrib['thickness'])
        layer['sequence'] = int(stackuplayer.attrib['sequence'])

    thicknesses = [layer['thickness'] for layer in layers]
    zpos = np.cumsum(-np.array(thicknesses)) + np.sum(thicknesses)
    for i, layer in enumerate(layers):
        layer['z'] = np.around(zpos[i], decimals=6)

    return layers

def get_layer_net_list(root: ET.Element, layername: str):
    """
    Return list of layer nets (as strings) given root and layer name

    Parameters
    ----------
    root : ET.Element
    layername : str

    Returns
    -------
    None.

    """
    rpf = '{http://webstds.ipc.org/2581}'  # Root prefix
    LayerFeatureRoot = root.find(f'{rpf}Ecad/{rpf}CadData/{rpf}Step/{rpf}LayerFeature[@layerRef="{layername}"]')
    LayerSets = LayerFeatureRoot.findall(f'{rpf}Set')
    netnames = []
    for lset in LayerSets:
        if 'net' in lset.attrib.keys():
            #if lset.attrib['net'] != 'No Net':
            netnames.append(lset.attrib['net'])
    return netnames

Parsing Line and Arc Elements

def parse_Line_element(line_elem: ET.Element, z: float = 0.):
    """
    Given the XML element for a Line, parse into an OCCT Geom_TrimmedCurve

    :param line_elem:
    :param z: z-coordinate for layer
    """
    prec = 10  # number of decimal places to keep, in case of precision issues
    startX = np.around(float(line_elem.attrib['startX']), prec)
    startY = np.around(float(line_elem.attrib['startY']), prec)
    endX = np.around(float(line_elem.attrib['endX']), prec)
    endY = np.around(float(line_elem.attrib['endY']), prec)
    start_pt = (startX, startY, z)
    end_pt = (endX, endY, z)

    LineDesc_elem = line_elem.find(f'{rpf}LineDesc')
    tracewidth = np.around(float(LineDesc_elem.attrib['lineWidth']), 3)
    endstyle = LineDesc_elem.attrib['lineEnd']

def parse_Arc_element(arc_elem: ET.Element, z: float = 0.):
    """
    Given the XML element for an Arc, parse into an OCCT Geom_TrimmedCurve

    :param arc_elem:
    :param z:
    """
    prec = 10
    startX = np.around(float(arc_elem.attrib['startX']), prec)
    startY = np.around(float(arc_elem.attrib['startY']), prec)
    endX = np.around(float(arc_elem.attrib['endX']), prec)
    endY = np.around(float(arc_elem.attrib['endY']), prec)
    centerX = np.around(float(arc_elem.attrib['centerX']), prec)
    centerY = np.around(float(arc_elem.attrib['centerY']), prec)
    is_cw = bool(arc_elem.attrib['clockwise'] == 'true')

    start_pt = (startX, startY, z)
    end_pt = (endX, endY, z)
    center_pt = (centerX, centerY, z)
    radius = distance(center_pt,start_pt)

    LineDesc_elem = arc_elem.find(f'{rpf}LineDesc')
    tracewidth = np.around(float(LineDesc_elem.attrib['lineWidth']), 3)
    endstyle = LineDesc_elem.attrib['lineEnd']

IPC2581 has information on geometry, stackup, BOM, components, vias, and pads. This means generating 3D geometry is possible without any additional information, and everything is in a single file which is convenient. I wrote myself a simple proof-of-concept program that uses Open CASCADE Technology (OCCT) as the geometry kernel, so you can generate .IGES and .STEP files. The only tools available for generating CAD data from Gerbers or IPC2581 .cvg files seem to be paid tools (e.g. Zofsz, NETEX-G) so including basic export functionality for STEP or IGES could be a good addition. But that's just one reason why IPC2581 support would be beneficial.

Argmaster commented 1 day ago

Hi, thanks for the suggestion.

In general PyGerber was designed with support for other PCB-realted file formats in mind and hence addiotion of IPC-2851 related modules is welcome. I have reached out to IPC-2581 consortium via contact form, we will see how they respond. I am not so keen on reverse enginieering the format as I am not interested in facing legal action for violation of intelectual property. Additionally I would like the implementation to be high quality rather than limited by what we could have guessed on our own.

Assuming positive response on their side, we could look into implementation of IPC-2851. I must admit that currently I don't have enough processing capacity to tackle it myself, so help would be welcome.

Since implementation will likely require additional dependencies I would prefer to have this feature as dedicated extras set to avoid limiting poratability of PyGerber and keep minimal set of dependencies required to use core functionalities of PyGerber (similarily to how language server and SVG rendering is done). Implementation details are yet to be discussed, but I would like to mention tools like CadQuery and lxml/BeautifulSoup4 which could be helpful in implementing IPC-2851.

I hope that Ucamco wont feel ofended by addition IPC-2851 but if they happen to be, we will probably resort to creating separate library for that.