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
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
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
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
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
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
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
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
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
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
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