Esempio n. 1
0
def gen_offset(opts, obj, offsetVal):
    """Generates an offset non-destructively."""
    doc = FreeCAD.ActiveDocument
    # First, we need to check if the object needs special treatment:
    treatment = 'standard'
    for input_part in opts['input_parts']:
        if obj.Name == input_part.fc_name:
            treatment = input_part.directive
            break

    if treatment == 'extrude' or treatment == 'lithography':
        treatment = 'standard'
    if treatment == 'standard':
        # Apparently the offset function is buggy for very small offsets...
        if offsetVal < 1e-5:
            offsetDupe = copy_move(obj)
        else:
            offset = doc.addObject("Part::Offset")
            offset.Source = obj
            offset.Value = offsetVal
            offset.Mode = 0
            offset.Join = 2
            doc.recompute()
            offsetDupe = copy_move(offset)
            doc.recompute()
            delete(offset)
    elif treatment == 'wire':
        offsetDupe = build_wire(partName, offset=offsetVal)
    elif treatment == 'wireShell':
        offsetDupe = build_wire_shell(partName, offset=offsetVal)
    elif treatment == 'SAG':
        offsetDupe = build_sag(partName, offset=offsetVal)
    doc.recompute()
    return offsetDupe
Esempio n. 2
0
def gen_offset(opts, obj, offsetVal):
    """Generates an offset non-destructively."""
    doc = FreeCAD.ActiveDocument
    # First, we need to identify if we are working with a special part:
    my_part_label = None
    for part_label in opts["built_part_names"]:  # Loop through built parts
        built_part_name = opts["built_part_names"][part_label]  # Part name
        if built_part_name == obj.Name:  # Is this the part we're working with now?
            my_part_label = part_label  # If so, set the label
            break
    if my_part_label is None:  # If we haven't found the part, it's not special
        treatment = "standard"
    else:  # If we have, figure out which directive we used to make it
        for input_part in opts["input_parts"]:
            if input_part.label == part_label:
                break
        treatment = input_part.directive
    # Extrude or lithography parts are treated normally:
    if treatment == "extrude" or treatment == "lithography":
        treatment = "standard"
    if treatment == "standard":
        # Apparently the offset function is buggy for very small offsets...
        if offsetVal < 1e-5:
            offsetDupe = copy_move(obj)
        else:
            offset = doc.addObject("Part::Offset")
            offset.Source = obj
            offset.Value = offsetVal
            offset.Mode = 0
            offset.Join = 2
            doc.recompute()
            offsetDupe = copy_move(offset)
            doc.recompute()
            delete(offset)
    elif treatment == "wire":
        offsetDupe = build_wire(input_part, offset=offsetVal)
    elif treatment == "wire_shell":
        offsetDupe = build_wire_shell(input_part, offset=offsetVal)
    elif treatment == "SAG":
        offsetDupe = build_sag(input_part, offset=offsetVal)
    doc.recompute()

    try:
        logging.debug(
            "%s (%s) -> %s (%s) [from %s]",
            obj.Name,
            obj.Label,
            offsetDupe.Name,
            offsetDupe.Label,
            input_part.label,
        )
    except:
        logging.debug(
            "%s (%s) -> %s (%s)", obj.Name, obj.Label, offsetDupe.Name, offsetDupe.Label
        )

    return offsetDupe
Esempio n. 3
0
def gen_G(info, opts, layerNum, objID):
    """Generate the gate deposition for a given layerNum and objID."""

    layerobj = info.lithoDict['layers'][layerNum]['objIDs'][objID]
    logging.debug('>>> layer %d obj %d (part:%s B:%s C:%s sketch:%s)', layerNum, objID,
                  layerobj['partName'], layerobj['B'].Name, layerobj['C'].Name,
                  layerobj['sketch'].Name)

    if 'G' not in layerobj:
        if () not in layerobj['HDict']:
            layerobj['HDict'][()] = H_offset(info, opts, layerNum, objID)

        if DBG_OUT:
            FreeCAD.ActiveDocument.saveAs('tmp_after_H_offset.fcstd')
        # TODO: reuse new function
        # This block fixes multifuses for wireshells with too big offsets,
        # by forcing all participating object shells into a new solid.
        # It still needs to be coerced into handling disjoint "solids".
        # ~ solid_hlist = []
        # ~ import Part
        # ~ for obj in layerobj['HDict'][()]:
            # ~ obj.Shape.Solids
            # ~ try:

                # ~ __s__ = obj.Shape.Faces
                # ~ __s__ = Part.Solid(Part.Shell(__s__))
                # ~ __o__ = FreeCAD.ActiveDocument.addObject("Part::Feature", obj.Name + "_solid")
                # ~ __o__.Label = obj.Label + "_solid"
                # ~ __o__.Shape = __s__

            # ~ except Part.OCCError:
                #Draft.downgrade(obj,delete=True)  # doesn't work without GUI
                # ~ for solid in obj.Shape.Solids:
                    # ~ for shell in solid.Shells:
                        # ~ pass

            # ~ solid_hlist.append(__o__)
            # ~ info.trash.append(obj)
            # ~ info.trash.append(__o__)
            # ~ info.trash.append(__s__)

        # ~ layerobj['HDict'][()] = solid_hlist
        # ~ logging.debug('new HDict: %s', [o.Name + ' (' + o.Label + ')' for o in layerobj['HDict'][()]])

        H = genUnion(layerobj['HDict'][()],
                     consumeInputs=False)
        info.trash.append(H)
        if info.fillShells:
            G = copy_move(H)
        else:
            U = gen_U(info, layerNum, objID)
            G = subtract(H, U)
            delete(U)
        layerobj['G'] = G

    G = layerobj['G']
    partName = layerobj['partName']
    G.Label = partName
    logging.debug('<<< G from H: %s (%s)', G.Name, G.Label)
    return G
Esempio n. 4
0
def gen_offset(opts, obj, offsetVal):
    """Generates an offset non-destructively."""
    doc = FreeCAD.ActiveDocument
    # First, we need to check if the object needs special treatment:
    treatment = 'standard'
    try:
        partname = next(label for (label, built_name) in
                        opts['built_part_names'].iteritems() if built_name == obj.Name)
        input_part = next(input_part for input_part in
                          opts['input_parts'] if input_part.label == partname)
        treatment = input_part.directive
    except:
        pass

    if treatment == 'extrude' or treatment == 'lithography':
        treatment = 'standard'

    if treatment == 'standard':
        # Apparently the offset function is buggy for very small offsets...
        if offsetVal < 1e-5:
            offsetDupe = copy_move(obj)
        else:
            offset = doc.addObject("Part::Offset")
            offset.Source = obj
            offset.Value = offsetVal
            offset.Mode = 0
            offset.Join = 2
            doc.recompute()
            offsetDupe = copy_move(offset)
            doc.recompute()
            delete(offset)
    elif treatment == 'wire':
        offsetDupe = build_wire(input_part, offset=offsetVal)
    elif treatment == 'wire_shell':
        offsetDupe = build_wire_shell(input_part, offset=offsetVal)
    elif treatment == 'SAG':
        offsetDupe = build_sag(input_part, offset=offsetVal)
    doc.recompute()

    try:
        logging.debug("%s (%s) -> %s (%s) [from %s]", obj.Name, obj.Label,
                      offsetDupe.Name, offsetDupe.Label, input_part.label)
    except:
        logging.debug("%s (%s) -> %s (%s)", obj.Name, obj.Label,
                      offsetDupe.Name, offsetDupe.Label)

    return offsetDupe
Esempio n. 5
0
def makeSAG(sketch, zBot, zMid, zTop, tIn, tOut, offset=0.):
    doc = FreeCAD.ActiveDocument
    # First, compute the geometric quantities we will need:
    a = zTop - zMid  # height of the top part
    b = tOut + tIn  # width of one of the trianglular pieces of the top
    alpha = np.abs(np.arctan(a / np.float(b)))  # lower angle of the top part
    c = a + 2 * offset  # height of the top part including the offset
    # horizontal width of the trianglular part of the top after offset
    d = c / np.tan(alpha)
    # horizontal shift in the triangular part of the top after an offset
    f = offset / np.sin(alpha)

    sketchList = splitSketch(sketch)
    returnParts = []
    for tempSketch in sketchList:
        # TODO: right now, if we try to taper the top of the SAG wire to a point, this
        # breaks, since the offset of topSketch is empty. We should detect and handle this.
        # For now, just make sure that the wire has a small flat top.
        botSketch = draftOffset(tempSketch, offset)  # the base of the wire
        midSketch = draftOffset(tempSketch, f + d - tIn)  # the base of the cap
        topSketch = draftOffset(tempSketch, -tIn + f)  # the top of the cap
        delete(tempSketch)  # remove the copied sketch part
        # Make the bottom wire:
        rectPartTemp = extrude(botSketch, zMid - zBot)
        rectPart = copy_move(rectPartTemp, moveVec=(0., 0., zBot - offset))
        delete(rectPartTemp)
        # make the cap of the wire:
        topSketchTemp = copy_move(topSketch, moveVec=(
            0., 0., zTop - zMid + 2 * offset))
        capPartTemp = doc.addObject('Part::Loft', sketch.Name + '_cap')
        capPartTemp.Sections = [midSketch, topSketchTemp]
        capPartTemp.Solid = True
        doc.recompute()
        capPart = copy_move(capPartTemp, moveVec=(0., 0., zMid - offset))
        delete(capPartTemp)
        delete(topSketchTemp)
        delete(topSketch)
        delete(midSketch)
        delete(botSketch)
        returnParts += [capPart, rectPart]
        returnPart = genUnion(returnParts, consumeInputs=True
                                           if not DBG_OUT else False)
    return returnPart
Esempio n. 6
0
def buildWire(sketch, zBottom, width, faceOverride=None, offset=0.0):
    """Given a line segment, build a nanowire of given cross-sectional width
    with a bottom location at zBottom. Offset produces an offset with a specified
    offset.
    """
    doc = FreeCAD.ActiveDocument
    if faceOverride is None:
        face = makeHexFace(sketch, zBottom - offset, width + 2 * offset)
    else:
        face = faceOverride
    sketchForSweep = extendSketch(sketch, offset)
    mySweepTemp = doc.addObject("Part::Sweep", sketch.Name + "_wire")
    mySweepTemp.Sections = [face]
    mySweepTemp.Spine = sketchForSweep
    mySweepTemp.Solid = True
    doc.recompute()
    mySweep = copy_move(mySweepTemp)
    deepRemove(mySweepTemp)
    return mySweep
Esempio n. 7
0
def gen_G(info, opts, layerNum, objID):
    """Generate the gate deposition for a given layerNum and objID."""
    layer = info.lithoDict['layers'][layerNum]
    if 'G' not in layer['objIDs'][objID]:
        if () not in layer['objIDs'][objID]['HDict']:
            layer['objIDs'][objID]['HDict'][()] = H_offset(
                info, opts, layerNum, objID)

        # ~ import sys
        # ~ sys.stderr.write('>>> ' + str(layer['objIDs'][objID]['HDict']) + '\n')
        # ~ # TODO: reuse new function
        # ~ # This block fixes multifuses for wireshells with too big offsets,
        # ~ # by forcing all participating object shells into a new solid.
        # ~ solid_hlist = []
        # ~ import Part
        # ~ for obj in layer['objIDs'][objID]['HDict'][()]:
        # ~ __s__ = obj.Shape.Faces
        # ~ __s__ = Part.Solid(Part.Shell(__s__))
        # ~ __o__ = FreeCAD.ActiveDocument.addObject("Part::Feature", obj.Name + "_solid")
        # ~ __o__.Label = obj.Label + "_solid"
        # ~ __o__.Shape = __s__
        # ~ solid_hlist.append(__o__)
        # ~ info.trash.append(obj)
        # ~ info.trash.append(__o__)
        # ~ info.trash.append(__s__)

        # ~ layer['objIDs'][objID]['HDict'][()] = solid_hlist
        # ~ sys.stderr.write('>>> ' + str(layer['objIDs'][objID]['HDict']) + '\n')

        H = genUnion(layer['objIDs'][objID]['HDict'][()], consumeInputs=False)
        info.trash.append(H)
        if info.fillShells:
            G = copy_move(H)
        else:
            U = gen_U(info, layerNum, objID)
            G = subtract(H, U)
            delete(U)
        layer['objIDs'][objID]['G'] = G
    G = layer['objIDs'][objID]['G']
    partName = layer['objIDs'][objID]['partName']
    G.Label = partName
    return G
Esempio n. 8
0
def build(opts):
    """Build the 3D geometry in FreeCAD.

    Parameters
    ----------
    opts : dict
        Options dict in the QMT Geometry3D.__init__ input format.
        Options dict in the QMT Geometry3D.__init__ input format.

    Returns
    -------
    Geo3DData object.

    """
    doc = FreeCAD.ActiveDocument
    geo = Geo3DData(opts.get("lunit", None))

    # Schedule for deletion all objects not explicitly selected by the user
    input_parts_names = []
    for part in opts["input_parts"]:
        if part.fc_name is None:
            obj_list = doc.getObjectsByLabel(part.label)
            if len(obj_list) != 1:
                msg = f"Part labeled {part.label} returned object list {obj_list}"
                raise KeyError(msg)
            fc_name = obj_list[0].Name
            part.fc_name = fc_name
        else:
            fc_name = part.fc_name
        input_parts_names += [fc_name]

    blacklist = []
    for obj in doc.Objects:
        if (obj.Name not in input_parts_names) and (obj.TypeId !=
                                                    "Spreadsheet::Sheet"):
            blacklist.append(obj)

    # Update the model parameters
    if "params" in opts:
        # Extend params dictionary to original parts schema
        fcdict = {
            key: (value, "freeCAD")
            for (key, value) in opts["params"].items()
        }
        set_params(doc, fcdict)

    doc.recompute(
    )  # recompute here to update any sketches that change due to parameters

    if "built_part_names" not in opts:
        opts["built_part_names"] = {}
    if "serial_stp_parts" not in opts:
        opts["serial_stp_parts"] = {}

    # Build the parts
    info_holder = DummyInfo()  # temporary workaround to support old litho code
    built_parts = []
    for input_part in opts["input_parts"]:

        if isinstance(input_part, part_3d.ExtrudePart):
            part = build_extrude(input_part)
        elif isinstance(input_part, part_3d.SAGPart):
            part = build_sag(input_part)
        elif isinstance(input_part, part_3d.WirePart):
            part = build_wire(input_part)
        elif isinstance(input_part, part_3d.WireShellPart):
            part = build_wire_shell(input_part)
        elif isinstance(input_part, part_3d.LithographyPart):
            part = build_lithography(input_part, opts, info_holder)
        elif isinstance(input_part, part_3d.Geo3DPart):
            part = build_pass(input_part)
        else:
            raise ValueError(
                f"{input_part} is not a recognized Geo3DPart type")

        assert part is not None
        doc.recompute()
        built_parts.append(part)
        # needed for litho steps
        opts["built_part_names"][input_part.label] = part.Name

    # Cleanup
    if not DBG_OUT:
        collect_garbage(info_holder)
        for obj in blacklist:
            delete(obj)
        doc.recompute()

    # Subtraction (removes the need for subtractlists)
    for i, (input_part,
            part) in enumerate(zip(opts["input_parts"], built_parts)):
        if input_part.virtual:
            continue
        for other_input_part, other_part in zip(opts["input_parts"][0:i],
                                                built_parts[0:i]):
            if other_input_part.virtual:
                continue
            if checkOverlap([part, other_part]):
                cut = subtract(
                    part,
                    copy_move(other_part),
                    consumeInputs=True if not DBG_OUT else False,
                )
                simple_copy = doc.addObject("Part::Feature", "simple_copy")
                # no solid, just its shape (can be disjoint)
                simple_copy.Shape = cut.Shape
                delete(cut)
                part = simple_copy
                built_parts[i] = simple_copy

    # Update names and store the built parts
    built_parts_dict = {}  # dict for cross sections
    for input_part, built_part in zip(opts["input_parts"], built_parts):
        built_part.Label = input_part.label  # here it's collision free
        output_part = deepcopy(input_part)
        output_part.serial_stp = store_serial([built_part], exportCAD, "stp")
        output_part.serial_stl = store_serial([built_part], exportMeshed,
                                              "stl")
        output_part.built_fc_name = built_part.Name
        geo.add_part(output_part.label, output_part)
        # dict for cross sections
        built_parts_dict[input_part.label] = built_part

    # Build cross sections:
    for xsec_name in opts["xsec_dict"]:
        axis = opts["xsec_dict"][xsec_name]["axis"]
        distance = opts["xsec_dict"][xsec_name]["distance"]
        polygons = buildCrossSection(xsec_name, axis, distance,
                                     built_parts_dict)
        geo.add_xsec(xsec_name, polygons, axis=axis, distance=distance)

    # Store the FreeCAD document
    geo.set_data(doc)

    return geo
Esempio n. 9
0
def makeSAG(sketch, zBot, zMid, zTop, tIn, tOut, offset=0.0):
    doc = FreeCAD.ActiveDocument
    assert zBot <= zMid
    assert zMid <= zTop

    # First, compute the geometric quantities we will need:
    a = zTop - zMid  # height of the top part
    b = tOut + tIn  # width of one of the triangular pieces of the top
    # if there is no slope to the roof, it's a different geometry which we don't handle:
    assert not np.isclose(
        b, 0
    ), "Either overshoot or inner displacement values need to be non-zero for SAG \
                                 (otherwise use extrude)"

    # This also means there would be no slope to the roof:
    assert not np.isclose(
        a, 0
    ), "Top and middle z values need to be different for SAG (otherwise use extrude)."
    alpha = np.arctan(a / b)  # lower angle of the top part
    c = a + 2 * offset  # height of the top part including the offset
    # horizontal width of the trianglular part of the top after offset
    d = c / np.tan(alpha)
    # horizontal shift in the triangular part of the top after an offset
    f = offset * (1 - np.cos(alpha)) / np.sin(alpha)

    sketchList = splitSketch(sketch)
    returnParts = []
    for tempSketch in sketchList:
        botSketch = draftOffset(tempSketch, offset)  # the base of the wire
        midSketch = draftOffset(tempSketch, f + d - tIn)  # the base of the cap
        top_offset = f - tIn
        topSketch = draftOffset(tempSketch, top_offset)  # the top of the cap
        # If topSketch has been shrunk exactly to a line or a point, relax the offset to 5E-5. Any closer and FreeCAD seems to suffer from numerical errors
        if topSketch.Shape.Area == 0:
            top_offset -= 5e-5
            delete(topSketch)
            topSketch = draftOffset(tempSketch, top_offset)

        delete(tempSketch)  # remove the copied sketch part
        # Make the bottom wire:
        if zMid - zBot != 0:
            rectPartTemp = extrude(botSketch, zMid - zBot)
            rectPart = copy_move(rectPartTemp,
                                 moveVec=(0.0, 0.0, zBot - offset))
            delete(rectPartTemp)
        else:
            rectPart = None

        # make the cap of the wire:
        topSketchTemp = copy_move(topSketch,
                                  moveVec=(0.0, 0.0, zTop - zMid + 2 * offset))
        capPartTemp = doc.addObject("Part::Loft", f"{sketch.Name}_cap")
        capPartTemp.Sections = [midSketch, topSketchTemp]
        capPartTemp.Solid = True
        doc.recompute()
        capPart = copy_move(capPartTemp, moveVec=(0.0, 0.0, zMid - offset))
        delete(capPartTemp)
        delete(topSketchTemp)

        delete(topSketch)
        delete(midSketch)
        delete(botSketch)
        returnPart = (genUnion([capPart, rectPart],
                               consumeInputs=True if not DBG_OUT else False)
                      if rectPart is not None else capPart)
        returnParts.append(returnPart)
    return returnParts
Esempio n. 10
0
def buildAlShell(sketch,
                 zBottom,
                 width,
                 verts,
                 thickness,
                 depoZone=None,
                 etchZone=None,
                 offset=0.0):
    """Builds a shell on a nanowire parameterized by sketch, zBottom, and width.

    Here, verts describes the vertices that are covered, and thickness describes
    the thickness of the shell. depoZone, if given, is extruded and intersected
    with the shell (for an etch). Note that offset here *is not* a real offset -
    for simplicity we keep this a thin shell that lies cleanly on top of the
    bigger wire offset. There's no need to include the bottom portion since that's
    already taken up by the wire.

    Parameters
    ----------
    sketch :

    zBottom :

    width :

    verts :

    thickness :

    depoZone :
        (Default value = None)
    etchZone :
        (Default value = None)
    offset :
        (Default value = 0.0)

    Returns
    -------


    """
    lineSegments = findSegments(sketch)[0]
    x0, y0, z0 = lineSegments[0]
    x1, y1, z1 = lineSegments[1]
    dx = x1 - x0
    dy = y1 - y0
    rAxis = np.array([-dy, dx, 0])
    # axis perpendicular to the wire in the xy plane
    rAxis /= np.sqrt(np.sum(rAxis**2))
    zAxis = np.array([0, 0, 1.0])
    doc = FreeCAD.ActiveDocument
    shellList = []
    for vert in verts:
        # Make the original wire (including an offset if applicable)
        originalWire = buildWire(sketch, zBottom, width, offset=offset)
        # Now make the shifted wire:
        angle = vert * np.pi / 3.0
        dirVec = rAxis * np.cos(angle) + zAxis * np.sin(angle)
        shiftVec = (thickness) * dirVec
        transVec = FreeCAD.Vector(tuple(shiftVec))
        face = makeHexFace(sketch, zBottom - offset,
                           width + 2 * offset)  # make the bigger face
        shiftedFace = Draft.move(face, transVec, copy=False)
        extendedSketch = extendSketch(sketch, offset)
        # The shell offset is handled manually since we are using faceOverride to
        # input a shifted starting face:
        shiftedWire = buildWire(extendedSketch,
                                zBottom,
                                width,
                                faceOverride=shiftedFace)
        delete(extendedSketch)
        shellCut = doc.addObject("Part::Cut", f"{sketch.Name}_cut_{vert}")
        shellCut.Base = shiftedWire
        shellCut.Tool = originalWire
        doc.recompute()
        shell = Draft.move(shellCut, FreeCAD.Vector(0.0, 0.0, 0.0), copy=True)
        doc.recompute()
        delete(shellCut)
        delete(originalWire)
        delete(shiftedWire)
        shellList.append(shell)
    if len(shellList) > 1:
        coatingUnion = doc.addObject("Part::MultiFuse",
                                     f"{sketch.Name}_coating")
        coatingUnion.Shapes = shellList
        doc.recompute()
        coatingUnionClone = copy_move(coatingUnion)
        doc.removeObject(coatingUnion.Name)
        for shell in shellList:
            doc.removeObject(shell.Name)
    elif len(shellList) == 1:
        coatingUnionClone = shellList[0]
    else:
        raise NameError(
            "Trying to build an empty Al shell. If no shell is desired, omit the AlVerts key from "
            "the json.")
    if (depoZone is None) and (etchZone is None):
        return coatingUnionClone

    elif depoZone is not None:
        coatingBB = getBB(coatingUnionClone)
        zMin = coatingBB[4]
        zMax = coatingBB[5]
        depoVol = extrudeBetween(depoZone, zMin, zMax)
        etchedCoatingUnionClone = intersect(
            [depoVol, coatingUnionClone],
            consumeInputs=True if not DBG_OUT else False)
        return etchedCoatingUnionClone
    else:  # etchZone instead
        coatingBB = getBB(coatingUnionClone)
        zMin = coatingBB[4]
        zMax = coatingBB[5]
        etchVol = extrudeBetween(etchZone, zMin, zMax)
        etchedCoatingUnionClone = subtract(
            coatingUnionClone,
            etchVol,
            consumeInputs=True if not DBG_OUT else False)
        return etchedCoatingUnionClone
Esempio n. 11
0
def build(opts):
    '''Build the 3D geometry in FreeCAD.

    :param dict opts:   Options dict in the QMT Geometry3D.__init__ input format.
    :return geo:        Geo3DData object with the built objects.
    '''
    doc = FreeCAD.ActiveDocument
    geo = Geo3DData()

    # Schedule for deletion all objects not explicitly selected by the user
    input_parts_names = [part.fc_name for part in opts['input_parts']]
    blacklist = []
    for obj in doc.Objects:
        if (obj.Name not in input_parts_names) and (obj.TypeId !=
                                                    'Spreadsheet::Sheet'):
            blacklist.append(obj)

    # Update the model parameters
    if 'params' in opts:
        # Extend params dictionary to original parts schema
        fcdict = {
            key: (value, 'freeCAD')
            for (key, value) in opts['params'].items()
        }
        set_params(doc, fcdict)

    if 'built_part_names' not in opts:
        opts['built_part_names'] = {}
    if 'serial_stp_parts' not in opts:
        opts['serial_stp_parts'] = {}

    # Build the parts
    info_holder = DummyInfo()  # temporary workaround to support old litho code
    built_parts = []
    for input_part in opts['input_parts']:

        if input_part.directive == 'extrude':
            part = build_extrude(input_part)
        elif input_part.directive == 'SAG':
            part = build_sag(input_part)
        elif input_part.directive == 'wire':
            part = build_wire(input_part)
        elif input_part.directive == 'wire_shell':
            part = build_wire_shell(input_part)
        elif input_part.directive == 'lithography':
            part = build_lithography(input_part, opts, info_holder)
        elif input_part.directive == '3d_shape':
            part = build_pass(input_part)
        else:
            raise ValueError('Directive ' + input_part.directive +
                             ' is not a recognized directive type.')

        assert part is not None
        doc.recompute()
        built_parts.append(part)
        opts['built_part_names'][
            input_part.label] = part.Name  # needed for litho steps

    # Cleanup
    collect_garbage(info_holder)
    for obj in blacklist:
        delete(obj)
    doc.recompute()

    # Subtraction (removes the need for subtractlists)
    for i, part in enumerate(built_parts):
        for other_part in built_parts[0:i]:
            if checkOverlap([part, other_part]):
                cut = subtract(part, copy_move(other_part), consumeInputs=True)
                simple_copy = doc.addObject('Part::Feature', "simple_copy")
                simple_copy.Shape = cut.Shape  # no solid, just its shape (can be disjoint)
                delete(cut)
                part = simple_copy
                built_parts[i] = simple_copy

    # Update names and store the built parts
    for input_part, built_part in zip(opts['input_parts'], built_parts):
        built_part.Label = input_part.label  # here it's collision free
        input_part.serial_stp = store_serial([built_part], exportCAD, 'stp')
        input_part.built_fc_name = built_part.Name
        geo.add_part(input_part.label, input_part)

    # Store the FreeCAD document
    geo.set_data('fcdoc', doc)

    return geo