diff --git a/GridFinityDividerBoxMaker/GridFinityDividerBoxMaker.manifest b/GridFinityDividerBoxMaker/GridFinityDividerBoxMaker.manifest new file mode 100644 index 0000000..28ab33c --- /dev/null +++ b/GridFinityDividerBoxMaker/GridFinityDividerBoxMaker.manifest @@ -0,0 +1,10 @@ +{ + "autodeskProduct": "Fusion360", + "type": "script", + "author": "", + "description": { + "": "" + }, + "supportedOS": "windows|mac", + "editEnabled": true +} \ No newline at end of file diff --git a/GridFinityDividerBoxMaker/GridFinityDividerBoxMaker.py b/GridFinityDividerBoxMaker/GridFinityDividerBoxMaker.py index 5b55ef5..63cf495 100755 --- a/GridFinityDividerBoxMaker/GridFinityDividerBoxMaker.py +++ b/GridFinityDividerBoxMaker/GridFinityDividerBoxMaker.py @@ -5,28 +5,50 @@ from typing import Tuple import adsk.core, adsk.fusion, adsk.cam, traceback +# Foot is 6mm high +# +defaultBoxName = "Box" +defaultSlotsWide = 2 +defaultSlotsDeep = 2 + +# 52" Husky case +# +defaultSlotsHigh = 1.45 + +# 62" Husky case +# +# defaultSlotsHigh = 1.25 + +defaultDividerCount = 0 +defaultBaseOnly = False +defaultIncludeScoop = True +defaultIncludeLedge = True +defaultIncludeMagnets = False + +# global set of event handlers to keep them referenced for the duration of the command +handlers = [] +app = adsk.core.Application.get() +if app: + ui = app.userInterface + +newComp = None + # Why? SCALE = 0.1 -# User parameters -slotsWide = 2 -slotsDeep = 3 -slotsHigh = 1.5 -dividerCount = 5 - -magnetDiameter = 6.5 * SCALE -magnetThiccness = 2.5 * SCALE +magnetDiameter = 6.5 * SCALE * 0.75 +magnetThickness = 2.5 * SCALE * 0.75 # Sizes! -# FIXME: make these strings, e.g. "42 mm" (maybe, some contexts that's not right for) baseCornerRadius = 4 * SCALE baseLip = .8 * SCALE slotDimension = 42 * SCALE nestingDepth = 5 * SCALE nestingRimWidth = 2.4 * SCALE nestingClearance = .25 * SCALE -wallThiccness = 1.2 * SCALE +wallThickness = 1.2 * SCALE holeOffset = 8 * SCALE +ledgeOffset = 0.2 * SCALE # Derived sizes nestingVerticalClearance = nestingClearance * 1.416 # Empirically determined from original sketch @@ -59,10 +81,10 @@ def createReal(r) -> adsk.core.ValueInput: def close(a, b): return abs(a - b) < 1e-5 * SCALE -# Geometry def createBaseRectSketch(component: adsk.fusion.Component) -> adsk.fusion.Profile: base_sketch = component.sketches.add(component.xZConstructionPlane) + base_sketch.name = "Base Sketch" p0 = create2DPoint(0, 0) p1 = create2DPoint(slotDimension, slotDimension) base_rect = base_sketch.sketchCurves.sketchLines.addTwoPointRectangle(p0, p1) @@ -72,7 +94,7 @@ def createBaseRectSketch(component: adsk.fusion.Component) -> adsk.fusion.Profil def createMagnetHolesSketch(component: adsk.fusion.Component) -> adsk.core.ObjectCollection: sketch: adsk.fusion.Sketch = component.sketches.add(component.xZConstructionPlane) - + sketch.name = "Magnet Holes Sketch" sketch.sketchCurves.sketchCircles.addByCenterRadius(createPoint(holeOffset, holeOffset, 0), magnetDiameter / 2.) sketch.sketchCurves.sketchCircles.addByCenterRadius(createPoint(slotDimension - holeOffset, holeOffset, 0), magnetDiameter / 2.) sketch.sketchCurves.sketchCircles.addByCenterRadius(createPoint(slotDimension - holeOffset, slotDimension - holeOffset, 0), magnetDiameter / 2.) @@ -84,9 +106,10 @@ def createMagnetHolesSketch(component: adsk.fusion.Component) -> adsk.core.Objec return circles -def createCurvedRect(component: adsk.fusion.Component, width: float, depth: float, radius: float, z: float) -> Tuple[adsk.core.ObjectCollection, adsk.fusion.Profile]: +def createCurvedRect(component: adsk.fusion.Component, name, width: float, depth: float, radius: float, z: float) -> Tuple[adsk.core.ObjectCollection, adsk.fusion.Profile]: path = adsk.core.ObjectCollection.create() sketch: adsk.fusion.Sketch = component.sketches.add(component.xZConstructionPlane) + sketch.name = name lines = sketch.sketchCurves.sketchLines p = lambda x, y: createPoint(x, y , z) @@ -137,8 +160,9 @@ def createCurvedRect(component: adsk.fusion.Component, width: float, depth: floa # FIXME: not actually a path return path, sketch.profiles.item(0) + def createBaseSweepSketch(component: adsk.fusion.Component) -> adsk.core.ObjectCollection: - b, _ = createCurvedRect(component, slotDimension, slotDimension, baseCornerRadius, 0) + b, _ = createCurvedRect(component, "Base Sweep Sketch", slotDimension, slotDimension, baseCornerRadius, 0) return b # Note that this attempts to combins the new bodies with the original to give a single body - this only works if they touch @@ -170,9 +194,10 @@ def rectPattern(body: adsk.fusion.BRepBody, wide: int, deep: int, dim: float) -> return pattern -def createRimSketch(component: adsk.fusion.Component) -> adsk.fusion.Profile: +def createRimSketch(component: adsk.fusion.Component, slotsHigh) -> adsk.fusion.Profile: cornerVerticalOffset = nestingClearance * .416 # Empirically determined from existing sketch sketch = component.sketches.add(component.xYConstructionPlane) + sketch.name = "Rim Sketch" lines = sketch.sketchCurves.sketchLines h = -slotDimension / 2 p = lambda x, y: createPoint(x, y , h) @@ -190,46 +215,48 @@ def createRimSketch(component: adsk.fusion.Component) -> adsk.fusion.Profile: return sketch.profiles.item(0) -def createIndentSketch(component: adsk.fusion.Component) -> adsk.fusion.Profile: +def createIndentSketch(component: adsk.fusion.Component, slotsHigh) -> adsk.fusion.Profile: sketch = component.sketches.add(component.xYConstructionPlane) + sketch.name = "Indent Sketch" lines = sketch.sketchCurves.sketchLines h = -slotDimension / 2 p = lambda x, y: createPoint(x, y , h) - # Note that this is wallThiccness below p4 in createRimSketch + # Note that this is wallThickness below p4 in createRimSketch x = nestingRimWidth + baseLip - nestingVerticalClearance - p0 = p(x, slotsHigh * slotDimension - wallThiccness) - p1 = p(x - wallThiccness, slotsHigh * slotDimension - 2 * wallThiccness) + p0 = p(x, slotsHigh * slotDimension - wallThickness) + p1 = p(x - wallThickness, slotsHigh * slotDimension - 2 * wallThickness) lines.addByTwoPoints(p0, p1) - p2 = p(x - wallThiccness, nestingDepth + wallThiccness + 1 * SCALE) + p2 = p(x - wallThickness, nestingDepth + wallThickness + 1 * SCALE) # FIXME: fillet this edge lines.addByTwoPoints(p1, p2) - p3 = p(x, nestingDepth + wallThiccness + 1 * SCALE) + p3 = p(x, nestingDepth + wallThickness + 1 * SCALE) lines.addByTwoPoints(p2, p3) lines.addByTwoPoints(p3, p0) return sketch.profiles.item(0) -def createLedgeSketch(component: adsk.fusion.Component) -> adsk.fusion.Profile: +def createLedgeSketch(component: adsk.fusion.Component, slotsHigh) -> adsk.fusion.Profile: angle = 54 sketch: adsk.fusion.Sketch = component.sketches.add(component.yZConstructionPlane) + sketch.name = "Ledge Sketch" lines = sketch.sketchCurves.sketchLines - h = wallThiccness * 2 + h = wallThickness p = lambda x, y: createPoint(x, y , h) - y = slotDimension * slotsHigh - p0 = p(wallThiccness, y) - p1 = p(wallThiccness + 16 * SCALE, y) + y = slotDimension * slotsHigh - ledgeOffset + p0 = p(wallThickness, y) + p1 = p(wallThickness + 16 * SCALE, y) lines.addByTwoPoints(p0, p1) d = math.sin(angle * math.pi / 180) * 16 * SCALE - p2 = p(wallThiccness, y - d) + p2 = p(wallThickness, y - d) lines.addByTwoPoints(p1, p2) lines.addByTwoPoints(p2, p0) return sketch.profiles.item(0) - -def createDividerSketch(component: adsk.fusion.Component, pos: float) -> adsk.fusion.Profile: + +def createDividerSketch(component: adsk.fusion.Component, pos: float, slotsHigh, slotsDeep) -> adsk.fusion.Profile: sketch: adsk.fusion.Sketch = component.sketches.add(component.yZConstructionPlane) lines = sketch.sketchCurves.sketchLines p = lambda x, y: createPoint(x, y, pos) @@ -237,189 +264,419 @@ def createDividerSketch(component: adsk.fusion.Component, pos: float) -> adsk.fu p0 = p(0, nestingDepth) p1 = p(slotsDeep * slotDimension, nestingDepth) lines.addByTwoPoints(p0, p1) - p2 = p(slotsDeep * slotDimension, slotsHigh * slotDimension) + p2 = p(slotsDeep * slotDimension, slotsHigh * slotDimension - ledgeOffset) lines.addByTwoPoints(p1, p2) - p3 = p(0, slotsHigh * slotDimension) + p3 = p(0, slotsHigh * slotDimension - ledgeOffset) lines.addByTwoPoints(p2, p3) lines.addByTwoPoints(p3, p0) - return sketch.profiles.item(0) - - + return sketch.profiles.item(0) + +class BoxCommandExecuteHandler(adsk.core.CommandEventHandler): + def __init__(self): + super().__init__() + def notify(self, args): + try: + unitsMgr = app.activeProduct.unitsManager + command = args.firingEvent.sender + inputs = command.commandInputs + + box = Box() + for input in inputs: + if input.id == 'boxName': + box.boxName = input.value + elif input.id == 'slotsWide': + box.slotsWide = input.value + elif input.id == 'slotsDeep': + box.slotsDeep = input.value + elif input.id == 'slotsHigh': + box.slotsHigh = input.value + # ui.messageBox(str(box.slotsHigh)) + elif input.id == 'dividerCount': + box.dividerCount = input.value + elif input.id == 'includeScoop': + box.includeScoop = input.value + elif input.id == 'baseOnly': + box.baseOnly = input.value + elif input.id == 'includeLedge': + box.includeLedge = input.value + elif input.id == 'includeMagnets': + box.includeMagnets = input.value + + box.buildBox(); + + args.isValidResult = True + + except: + if ui: + ui.messageBox('Failed:\n{}'.format(traceback.format_exc())) + +class BoxCommandDestroyHandler(adsk.core.CommandEventHandler): + def __init__(self): + super().__init__() + def notify(self, args): + try: + # when the command is done, terminate the script + # this will release all globals which will remove all event handlers + adsk.terminate() + except: + if ui: + ui.messageBox('Failed:\n{}'.format(traceback.format_exc())) + +class BoxCommandCreatedHandler(adsk.core.CommandCreatedEventHandler): + def __init__(self): + super().__init__() + def notify(self, args): + try: + cmd = args.command + cmd.isRepeatable = False + onExecute = BoxCommandExecuteHandler() + cmd.execute.add(onExecute) + onExecutePreview = BoxCommandExecuteHandler() + cmd.executePreview.add(onExecutePreview) + onDestroy = BoxCommandDestroyHandler() + cmd.destroy.add(onDestroy) + # keep the handler referenced beyond this function + handlers.append(onExecute) + handlers.append(onExecutePreview) + handlers.append(onDestroy) + + #define the inputs + inputs = cmd.commandInputs + inputs.addStringValueInput('boxName', 'Box Name', defaultBoxName) + + initSlotsWide = adsk.core.ValueInput.createByReal(defaultSlotsWide) + inputs.addIntegerSpinnerCommandInput('slotsWide', 'Slots Wide', 1, 20, 1, defaultSlotsWide) + + initSlotsDeep = adsk.core.ValueInput.createByReal(defaultSlotsDeep) + inputs.addIntegerSpinnerCommandInput('slotsDeep', 'Slots Deep', 1, 20, 1, defaultSlotsDeep) + + # I'd love to make this just MMs + initSlotsHigh = adsk.core.ValueInput.createByReal(defaultSlotsHigh) + inputs.addFloatSpinnerCommandInput('slotsHigh', 'Slots High', '', 0.25, 10.0, 0.01, defaultSlotsHigh) + + initDividerCount = adsk.core.ValueInput.createByReal(defaultDividerCount) + inputs.addIntegerSpinnerCommandInput('dividerCount', 'Divider Count', 0, 10, 1, defaultDividerCount) + + initIncludeScoop = adsk.core.ValueInput.createByReal(defaultIncludeScoop) + inputs.addBoolValueInput('includeScoop', 'Include Scoop?', True, '', defaultIncludeScoop) + + initBaseOnly = adsk.core.ValueInput.createByReal(defaultBaseOnly) + inputs.addBoolValueInput('baseOnly', 'Base Only?', True, '', defaultBaseOnly) + + initIncludeLedge = adsk.core.ValueInput.createByReal(defaultIncludeLedge) + inputs.addBoolValueInput('includeLedge', 'Include Ledge?', True, '', defaultIncludeLedge) + + initIncludeMagnets = adsk.core.ValueInput.createByReal(defaultIncludeMagnets) + inputs.addBoolValueInput('includeMagnets', 'Include Magnets?', True, '', defaultIncludeMagnets) + + except: + if ui: + ui.messageBox('Failed:\n{}'.format(traceback.format_exc())) + +class Box: + def __init__(self): + self._boxName = defaultBoxName + self._slotsWide = defaultSlotsWide + self._slotsDeep = defaultSlotsDeep + self._slotsHigh = defaultSlotsHigh + self._dividerCount = defaultDividerCount + self._includeScoop = defaultIncludeScoop + self._baseOnly = defaultBaseOnly + self._includeLedge = defaultIncludeLedge + self._includeMagnets = defaultIncludeMagnets + + #properties + @property + def boxName(self): + return self._boxName + @boxName.setter + def boxName(self, value): + self._boxName = value + + @property + def slotsWide(self): + return self._slotsWide + @slotsWide.setter + def slotsWide(self, value): + self._slotsWide = value + + @property + def slotsDeep(self): + return self._slotsDeep + @slotsDeep.setter + def slotsDeep(self, value): + self._slotsDeep = value + + @property + def slotsHigh(self): + return self._slotsHigh + @slotsHigh.setter + def slotsHigh(self, value): + self._slotsHigh = value + + @property + def dividerCount(self): + return self._dividerCount + @dividerCount.setter + def dividerCount(self, value): + self._dividerCount = value + + @property + def includeScoop(self): + return self._includeScoop + @includeScoop.setter + def includeScoop(self, value): + self._includeScoop = value + + @property + def baseOnly(self): + return self._baseOnly + @baseOnly.setter + def baseOnly(self, value): + self._baseOnly = value + + @property + def includeLedge(self): + return self._includeLedge + @includeLedge.setter + def includeLedge(self, value): + self._includeLedge = value + + @property + def includeMagnets(self): + return self._includeMagnets + @includeMagnets.setter + def includeMagnets(self, value): + self._includeMagnets = value + + def buildBox(self): + try: + # Get the active design. + app = adsk.core.Application.get() + product = app.activeProduct + design = adsk.fusion.Design.cast(product) + ui = app.userInterface + + global component + + # Units + design.fusionUnitsManager.distanceDisplayUnits = adsk.fusion.DistanceUnits.MillimeterDistanceUnits + + # Main component + component = createComponent(design, self.boxName) + if component is None: + ui.messageBox('New component failed to create', 'New Component Failed') + return + + # Sketch base + base_rect_profile = createBaseRectSketch(component) + # Extrude + distance = createDistance(nestingDepth) + base = component.features.extrudeFeatures.addSimple(base_rect_profile, distance, NEW_BODY) + base_body = base.bodies.item(0) + base_body.name = "Base" + + if self.includeMagnets: + # Magnet hole sketch + magnet_holes_profile = createMagnetHolesSketch(component) + + # Extrude for magnets + distance = createDistance(magnetThickness) + component.features.extrudeFeatures.addSimple(magnet_holes_profile, distance, CUT) + + # Profile for the edge + edge_sketch = component.sketches.add(component.xYConstructionPlane) + edge_sketch.name = "Edge Profile Sketch" + lines = edge_sketch.sketchCurves.sketchLines + h = -slotDimension / 2 + p0 = createPoint(-1, 0, h) + p1a = createPoint(-1, nestingDepth, h) + lines.addByTwoPoints(p0, p1a) + p1 = createPoint(0, nestingDepth, h) + lines.addByTwoPoints(p1a, p1) + p2 = createPoint(nestingRimWidth + baseLip, 0, h) + lines.addByTwoPoints(p0, p2) + p3 = createPoint(nestingRimWidth, baseLip, h) + lines.addByTwoPoints(p2, p3) + p4 = createPoint(nestingRimWidth, nestingDepth - nestingRimWidth, h) + lines.addByTwoPoints(p1, p4) + lines.addByTwoPoints(p4, p3) + + # FIXME: this really offends me! + edge_profile = edge_sketch.profiles.item(0) + + # Path to sweep profile along (derived from baserect) + sweep_path_objects = createBaseSweepSketch(component) + sweep_path = component.features.createPath(sweep_path_objects) + + # Do the sweeps + sweeps = component.features.sweepFeatures + sweep_input = sweeps.createInput(edge_profile, sweep_path, CUT) + sweep = sweeps.add(sweep_input) + + # Copy for the whole base + if self.slotsWide > 1 or self.slotsDeep > 1: + rectPattern(base_body, self.slotsWide, self.slotsDeep, slotDimension) + + # Now the box + box_path_objects, box_profile = createCurvedRect(component, "Box Profile", self.slotsWide * slotDimension, self.slotsDeep * slotDimension, baseCornerRadius, nestingDepth) + if self.baseOnly: + distance = createDistance(1*SCALE) + base = component.features.extrudeFeatures.addSimple(box_profile, distance, JOIN) + return + + distance = createDistance(self.slotsHigh * slotDimension - nestingDepth) + base = component.features.extrudeFeatures.addSimple(box_profile, distance, JOIN) + + # Rim + rim_profile = createRimSketch(component, self.slotsHigh) + box_path = component.features.createPath(box_path_objects) + sweep_input = sweeps.createInput(rim_profile, box_path, JOIN) + sweep = sweeps.add(sweep_input) + + # Put a fillet on the top edge + e = base_body.edges + top_edge = e.item(0) + for n in range(e.count): + edge = e.item(n) + bb = edge.boundingBox + mx = bb.maxPoint + mn = bb.minPoint + if mn.y > top_edge.boundingBox.minPoint.y: + #print(f'[{mn.x}, {mn.y}, {mn.z}] -> [{mx.x}, {mx.y}, {mx.z}]') + top_edge = edge + fillet_edges = adsk.core.ObjectCollection.create() + fillet_edges.add(top_edge) + + fillets = component.features.filletFeatures + fillet_input = fillets.createInput() + fillet_radius = createDistance(.6 * SCALE) + fillet_input.addConstantRadiusEdgeSet(fillet_edges, fillet_radius, True) + fillet_input.isG2 = False + fillet_input.isRollingBallCorner = True + top_fillet = fillets.add(fillet_input) + + # Make the hole in the box + # Find the profile to extrude + f = base_body.faces + extrude_face = None + count = 0 + for n in range(f.count): + bb = f.item(n).boundingBox + mx = bb.maxPoint + mn = bb.minPoint + if close(mn.y, self.slotsHigh * slotDimension) and close(mx.y, self.slotsHigh * slotDimension): + #print(f'[{mn.x}, {mn.y}, {mn.z}] -> [{mx.x}, {mx.y}, {mx.z}]') + count += 1 + extrude_face = f.item(n) + assert count == 1 + + # And extrude it + # FIXME: the + 1 * SCALE is ad hoc (but copied from the original) + distance = createDistance(-(self.slotsHigh * slotDimension - (nestingDepth + wallThickness + 1 * SCALE))) + component.features.extrudeFeatures.addSimple(extrude_face, distance, CUT) + + # Indent the lower part of the box to leave a rim around the top + indent_profile = createIndentSketch(component, self.slotsHigh) + sweep_input = sweeps.createInput(indent_profile, box_path, CUT) + sweep = sweeps.add(sweep_input) + + if self.includeLedge and self.slotsHigh >= 0.43 : + # Add the ledge + ledge_profile = createLedgeSketch(component, self.slotsHigh) + distance = createDistance(self.slotsWide * slotDimension - wallThickness * 2) + component.features.extrudeFeatures.addSimple(ledge_profile, distance, JOIN) + + if self.includeScoop: + # Add the curved scoop + edges = base_body.edges + ty = nestingDepth + wallThickness + 1 * SCALE + #print(ty) + fillet_edge = None + count = 0 + for n in range(edges.count): + edge = edges.item(n) + bb = edge.boundingBox + mx = bb.maxPoint + mn = bb.minPoint + # FIXME: where does -8.2354 come from? + if close(mn.y, ty) and close(mx.y, ty) and close(mn.x, baseCornerRadius) and close(mx.x, self.slotsWide * slotDimension - baseCornerRadius) and close(mn.z, -(self.slotsDeep * slotDimension - .1646)): + #print(f'[{mn.x}, {mn.y}, {mn.z}] -> [{mx.x}, {mx.y}, {mx.z}]') + fillet_edge = edge + count += 1 + assert count == 1 + + fillet_input = fillets.createInput() + fillet_radius = createDistance(slotDimension * self.slotsHigh / 2) + edges = adsk.core.ObjectCollection.create() + edges.add(fillet_edge) + fillet_input.addConstantRadiusEdgeSet(edges, fillet_radius, False) + fillet_input.isG2 = False + fillet_input.isRollingBallCorner = True + fillets.add(fillet_input) + + # # Now we're a box. :-) + base_body.name = self.boxName + + # Finally, dividers + # FIXME: these end up not *quite* the same size + l = self.slotsWide * slotDimension - 2 * wallThickness + for n in range(self.dividerCount): + y = (n + 1) * l / (self.dividerCount + 1) + wallThickness / 2 + divider_profile = createDividerSketch(component, y, slotsHigh=self.slotsHigh, slotsDeep=self.slotsDeep) + distance = createDistance(wallThickness) + divider = component.features.extrudeFeatures.addSimple(divider_profile, distance, JOIN) + + edges = adsk.core.ObjectCollection.create() + #print(y) + for n in range(divider.faces.count): + f = divider.faces.item(n) + bb = f.boundingBox + mx = bb.maxPoint + mn = bb.minPoint + if (close(mn.x, y) and close(mx.x, y)) or (close(mn.x, y + wallThickness) and close(mx.x, y + wallThickness)): + #print(f'[{mn.x}, {mn.y}, {mn.z}] -> [{mx.x}, {mx.y}, {mx.z}]') + for e in range(f.edges.count): + edges.add(f.edges.item(e)) + fillet_input = fillets.createInput() + fillet_radius = createDistance(.6 * SCALE) + fillet_input.addConstantRadiusEdgeSet(edges, fillet_radius, True) + fillet_input.isG2 = False + fillet_input.isRollingBallCorner = True + fillets.add(fillet_input) + except: + if ui: + ui.messageBox('Failed to compute the box. This is most likely because the input values define an invalid box.') + + def run(context): - # Get the active design. - app = adsk.core.Application.get() - product = app.activeProduct - design = adsk.fusion.Design.cast(product) - - # Units - design.fusionUnitsManager.distanceDisplayUnits = adsk.fusion.DistanceUnits.MillimeterDistanceUnits - - # Main component - component = createComponent(design, "Divider Box") - - # Sketch base - base_rect_profile = createBaseRectSketch(component) - # Extrude - distance = createDistance(nestingDepth) - base = component.features.extrudeFeatures.addSimple(base_rect_profile, distance, NEW_BODY) - base_body = base.bodies.item(0) - base_body.name = "Base" - - # Magnet holes - magnet_holes_profile = createMagnetHolesSketch(component) - # Extrude - distance = createDistance(magnetThiccness) - component.features.extrudeFeatures.addSimple(magnet_holes_profile, distance, CUT) - - # Profile for the edge - edge_sketch = component.sketches.add(component.xYConstructionPlane) - lines = edge_sketch.sketchCurves.sketchLines - h = -slotDimension / 2 - p0 = createPoint(-1, 0, h) - p1a = createPoint(-1, nestingDepth, h) - lines.addByTwoPoints(p0, p1a) - p1 = createPoint(0, nestingDepth, h) - lines.addByTwoPoints(p1a, p1) - p2 = createPoint(nestingRimWidth + baseLip, 0, h) - lines.addByTwoPoints(p0, p2) - p3 = createPoint(nestingRimWidth, baseLip, h) - lines.addByTwoPoints(p2, p3) - p4 = createPoint(nestingRimWidth, nestingDepth - nestingRimWidth, h) - lines.addByTwoPoints(p1, p4) - lines.addByTwoPoints(p4, p3) - # FIXME: this really offends me! - edge_profile = edge_sketch.profiles.item(0) - - # Path to sweep profile along (derived from baserect) - sweep_path_objects = createBaseSweepSketch(component) - sweep_path = component.features.createPath(sweep_path_objects) - - # Do the sweep - sweeps = component.features.sweepFeatures - sweep_input = sweeps.createInput(edge_profile, sweep_path, CUT) - sweep = sweeps.add(sweep_input) - - # Copy for the whole base - rectPattern(base_body, slotsWide, slotsDeep, slotDimension) - - # Now the box - box_path_objects, box_profile = createCurvedRect(component, slotsWide * slotDimension, slotsDeep * slotDimension, baseCornerRadius, nestingDepth) - distance = createDistance(slotsHigh * slotDimension - nestingDepth) - base = component.features.extrudeFeatures.addSimple(box_profile, distance, JOIN) - - # Rim - rim_profile = createRimSketch(component) - box_path = component.features.createPath(box_path_objects) - sweep_input = sweeps.createInput(rim_profile, box_path, JOIN) - sweep = sweeps.add(sweep_input) - - # Put a fillet on the top edge - e = base_body.edges - top_edge = e.item(0) - for n in range(e.count): - edge = e.item(n) - bb = edge.boundingBox - mx = bb.maxPoint - mn = bb.minPoint - if mn.y > top_edge.boundingBox.minPoint.y: - #print(f'[{mn.x}, {mn.y}, {mn.z}] -> [{mx.x}, {mx.y}, {mx.z}]') - top_edge = edge - fillet_edges = adsk.core.ObjectCollection.create() - fillet_edges.add(top_edge) - - fillets = component.features.filletFeatures - fillet_input = fillets.createInput() - fillet_radius = createDistance(.6 * SCALE) - fillet_input.addConstantRadiusEdgeSet(fillet_edges, fillet_radius, True) - fillet_input.isG2 = False - fillet_input.isRollingBallCorner = True - top_fillet = fillets.add(fillet_input) - - # Make the hole in the box - # Find the profile to extrude - f = base_body.faces - extrude_face = None - count = 0 - for n in range(f.count): - bb = f.item(n).boundingBox - mx = bb.maxPoint - mn = bb.minPoint - if close(mn.y, slotsHigh * slotDimension) and close(mx.y, slotsHigh * slotDimension): - #print(f'[{mn.x}, {mn.y}, {mn.z}] -> [{mx.x}, {mx.y}, {mx.z}]') - count += 1 - extrude_face = f.item(n) - assert count == 1 - - # And extrude it - # FIXME: the + 1 * SCALE is ad hoc (but copied from the original) - distance = createDistance(-(slotsHigh * slotDimension - (nestingDepth + wallThiccness + 1 * SCALE))) - component.features.extrudeFeatures.addSimple(extrude_face, distance, CUT) - - # Indent the lower part of the box to leave a rim around the top - indent_profile = createIndentSketch(component) - sweep_input = sweeps.createInput(indent_profile, box_path, CUT) - sweep = sweeps.add(sweep_input) - - # Add the ledge - ledge_profile = createLedgeSketch(component) - distance = createDistance(slotsWide * slotDimension - wallThiccness * 4) - component.features.extrudeFeatures.addSimple(ledge_profile, distance, JOIN) - - # Add the curved scoop - edges = base_body.edges - ty = nestingDepth + wallThiccness + 1 * SCALE - #print(ty) - fillet_edge = None - count = 0 - for n in range(edges.count): - edge = edges.item(n) - bb = edge.boundingBox - mx = bb.maxPoint - mn = bb.minPoint - # FIXME: where does -8.2354 come from? - if close(mn.y, ty) and close(mx.y, ty) and close(mn.x, baseCornerRadius) and close(mx.x, slotsWide * slotDimension - baseCornerRadius) and close(mn.z, -(slotsDeep * slotDimension - .1646)): - #print(f'[{mn.x}, {mn.y}, {mn.z}] -> [{mx.x}, {mx.y}, {mx.z}]') - fillet_edge = edge - count += 1 - assert count == 1 - - fillet_input = fillets.createInput() - fillet_radius = createDistance(slotDimension * slotsHigh / 2) - edges = adsk.core.ObjectCollection.create() - edges.add(fillet_edge) - fillet_input.addConstantRadiusEdgeSet(edges, fillet_radius, False) - fillet_input.isG2 = False - fillet_input.isRollingBallCorner = True - fillets.add(fillet_input) - - - # Now we're a box. :-) - base_body.name = "Box" - - # Finally, dividers - # FIXME: these end up not *quite* the same size - l = slotsWide * slotDimension - 2 * wallThiccness - for n in range(dividerCount): - y = (n + 1) * l / (dividerCount + 1) + wallThiccness / 2 - divider_profile = createDividerSketch(component, y) - distance = createDistance(wallThiccness) - divider = component.features.extrudeFeatures.addSimple(divider_profile, distance, JOIN) - - edges = adsk.core.ObjectCollection.create() - #print(y) - for n in range(divider.faces.count): - f = divider.faces.item(n) - bb = f.boundingBox - mx = bb.maxPoint - mn = bb.minPoint - if (close(mn.x, y) and close(mx.x, y)) or (close(mn.x, y + wallThiccness) and close(mx.x, y + wallThiccness)): - #print(f'[{mn.x}, {mn.y}, {mn.z}] -> [{mx.x}, {mx.y}, {mx.z}]') - for e in range(f.edges.count): - edges.add(f.edges.item(e)) - fillet_input = fillets.createInput() - fillet_radius = createDistance(.6 * SCALE) - fillet_input.addConstantRadiusEdgeSet(edges, fillet_radius, True) - fillet_input.isG2 = False - fillet_input.isRollingBallCorner = True - fillets.add(fillet_input) + try: + product = app.activeProduct + design = adsk.fusion.Design.cast(product) + if not design: + ui.messageBox('It is not supported in current workspace, please change to MODEL workspace and try again.') + return + commandDefinitions = ui.commandDefinitions + + #check the command exists or not + # + cmdDef = commandDefinitions.itemById('GridfinityDividerBox') + if not cmdDef: + cmdDef = commandDefinitions.addButtonDefinition('GridfinityDividerBox', + 'Create a Gridfinity Box', + 'Create a Gridfinity Box.') + + onCommandCreated = BoxCommandCreatedHandler() + cmdDef.commandCreated.add(onCommandCreated) + # keep the handler referenced beyond this function + handlers.append(onCommandCreated) + inputs = adsk.core.NamedValues.create() + + cmdDef.execute(inputs) + + # prevent this module from being terminate when the script returns, because we are waiting for event handlers to fire + adsk.autoTerminate(False) + except: + if ui: + ui.messageBox('Failed:\n{}'.format(traceback.format_exc())) diff --git a/README.md b/README.md index 0a77848..f4ad406 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ # gfdb GridFinity Divider Boxes in Fusion 360 +NOTE: This is no longer maintained, but has been copied to a new repo for my f360 utils, https://github.com/jamiesmith/fusion360scripts + ## Bugs - There are a few fillets missing. diff --git a/RemoteHolsterMaker/RemoteHolsterMaker.manifest b/RemoteHolsterMaker/RemoteHolsterMaker.manifest new file mode 100644 index 0000000..28ab33c --- /dev/null +++ b/RemoteHolsterMaker/RemoteHolsterMaker.manifest @@ -0,0 +1,10 @@ +{ + "autodeskProduct": "Fusion360", + "type": "script", + "author": "", + "description": { + "": "" + }, + "supportedOS": "windows|mac", + "editEnabled": true +} \ No newline at end of file diff --git a/RemoteHolsterMaker/RemoteHolsterMaker.py b/RemoteHolsterMaker/RemoteHolsterMaker.py new file mode 100755 index 0000000..5676dd2 --- /dev/null +++ b/RemoteHolsterMaker/RemoteHolsterMaker.py @@ -0,0 +1,625 @@ +# Author- Jamie Smith +# Description- Make a remote holster +# +import math +from typing import Tuple +# import sys +# sys.path.append("/Users/jamie/Library/Application Support/Autodesk/Autodesk Fusion 360/API/Python/defs") +import adsk.core, adsk.fusion, adsk.cam, traceback + +# Some Constants +# +# Don't know why this is here +SCALE = 0.1 + +# Default Values +# +defaultHolsterName = "Holster" +defaultRemoteWidth = 80.0 +defaultRemoteLength = 44.0 +defaultRemoteThickness = 15.0 +defaultFrontSlotWidth = 10.0 +defaultFrontHeight = 22.0 +defaultBackCornerRound = 4.0 +defaultFillet = 0.5 +defaultFrontSlotRound = 3.0 +defaultSideThickness = 3.0 +defaultBackThickness = 3.0 +defaultBottomThickness = 3.0 +defaultTolerance = 0.5 + +# Actual values +holsterName = defaultHolsterName +remoteWidth = defaultRemoteWidth * SCALE +remoteLength = defaultRemoteLength * SCALE +remoteThickness = defaultRemoteThickness * SCALE +frontSlotWidth = defaultFrontSlotWidth * SCALE +frontHeight = defaultFrontHeight * SCALE +backCornerRound = defaultBackCornerRound * SCALE +fillet = defaultFillet * SCALE +frontSlotRound = defaultFrontSlotRound * SCALE +sideThickness = defaultSideThickness * SCALE +backThickness = defaultBackThickness * SCALE +bottomThickness = defaultBottomThickness * SCALE +tolerance = defaultTolerance * SCALE + +# global set of event handlers to keep them referenced for the duration of the command +handlers = [] +app = adsk.core.Application.get() +if app: + ui = app.userInterface + +newComp = None + +# Consts +CUT = adsk.fusion.FeatureOperations.CutFeatureOperation +JOIN = adsk.fusion.FeatureOperations.JoinFeatureOperation +NEW_BODY = adsk.fusion.FeatureOperations.NewBodyFeatureOperation + +def createComponent(design: adsk.fusion.Design, name: str) -> adsk.fusion.Component: + rootComp = design.rootComponent + allOccs = rootComp.occurrences + newOcc = allOccs.addNewComponent(adsk.core.Matrix3D.create()) + comp = newOcc.component + comp.name = name + return comp + +def createPoint(x: float, y: float, z: float) -> adsk.core.Point3D: + return adsk.core.Point3D.create(x, y, z) + +def create2DPoint(x, y): + return createPoint(x, y, 0) + +def createDistance(d) -> adsk.core.ValueInput: + return adsk.core.ValueInput.createByReal(d) + +def createReal(r) -> adsk.core.ValueInput: + return adsk.core.ValueInput.createByReal(r) + +def close(a, b): + return abs(a - b) < 1e-5 * SCALE + +# Some basic utility commands +# +def createBaseRectSketch(component: adsk.fusion.Component, holster) -> adsk.fusion.Profile: + base_sketch = component.sketches.add(component.xYConstructionPlane) + base_sketch.name = "Base Sketch" + p0 = create2DPoint(0, 0) + p1 = create2DPoint((holster.remoteWidth + 2 * holster.sideThickness) * SCALE, (holster.remoteThickness + holster.sideThickness + holster.backThickness) * SCALE) + base_rect = base_sketch.sketchCurves.sketchLines.addTwoPointRectangle(p0, p1) + # FIXME: there must be a better way! + base_rect_profile = base_sketch.profiles.item(0) + return base_rect_profile + +def createCurvedRect(component: adsk.fusion.Component, name, width: float, depth: float, radius: float, z: float) -> Tuple[adsk.core.ObjectCollection, adsk.fusion.Profile]: + path = adsk.core.ObjectCollection.create() + sketch: adsk.fusion.Sketch = component.sketches.add(component.xZConstructionPlane) + sketch.name = name + lines = sketch.sketchCurves.sketchLines + p = lambda x, y: createPoint(x, y , z) + + p0 = p(radius, 0) + p1 = p(width - radius, 0) + l0 = lines.addByTwoPoints(p0, p1) + p2 = p(width, radius) + p3 = p(width, depth - radius) + l1 = lines.addByTwoPoints(p2, p3) + p4 = p(width - radius, depth) + p5 = p(radius, depth) + l2 = lines.addByTwoPoints(p4, p5) + p6 = p(0, depth - radius) + p7 = p(0, radius) + l3 = lines.addByTwoPoints(p6, p7) + + arcs = sketch.sketchCurves.sketchArcs + + p7c = p(radius, radius) + a0 = arcs.addByCenterStartSweep(p7c, p7, math.pi / 2) + # FIXME: you'd've thought all these merges made a single thing. You'd be wrong. + # The arcs end up joined to the lines they were started on but the merges do nothing. + #a0.endSketchPoint.merge(l0.startSketchPoint) + #path.add(a0) + p1c = p(width - radius, radius) + a1 = arcs.addByCenterStartSweep(p1c, p1, math.pi / 2) + #a1.endSketchPoint.merge(l1.startSketchPoint) + #path.add(a1) + p3c = p(width - radius, depth - radius) + a2 = arcs.addByCenterStartSweep(p3c, p3, math.pi / 2) + #a2.endSketchPoint.merge(l2.startSketchPoint) + #path.add(a2) + p5c = p(radius, depth - radius) + a3 = arcs.addByCenterStartSweep(p5c, p5, math.pi / 2) + #a3.endSketchPoint.merge(l3.startSketchPoint) + #path.add(a3) + + # These have to be in exactly the right order + path.add(l3) + path.add(a3) + path.add(l2) + path.add(a2) + path.add(l1) + path.add(a1) + path.add(l0) + path.add(a0) + + # FIXME: not actually a path + return path, sketch.profiles.item(0) + +# def createBaseSweepSketch(component: adsk.fusion.Component) -> adsk.core.ObjectCollection: +# b, _ = createCurvedRect(component, "Base Sweep Sketch", slotDimension, slotDimension, baseCornerRadius, 0) +# return b + +class HolsterCommandExecuteHandler(adsk.core.CommandEventHandler): + def __init__(self): + super().__init__() + def notify(self, args): + try: + unitsMgr = app.activeProduct.unitsManager + command = args.firingEvent.sender + inputs = command.commandInputs + + holster = Holster() + + for input in inputs: + if input.id == 'holsterName': + holster.holsterName = input.value + elif input.id == 'remoteWidth': + holster.remoteWidth = input.value + elif input.id == 'remoteLength': + holster.remoteLength = input.value + elif input.id == 'remoteThickness': + holster.remoteThickness = input.value + elif input.id == 'frontSlotWidth': + holster.frontSlotWidth = input.value + elif input.id == 'frontHeight': + holster.frontHeight = input.value + elif input.id == 'backCornerRound': + holster.backCornerRound = input.value + elif input.id == 'fillet': + holster.fillet = input.value + elif input.id == 'frontSlotRound': + holster.frontSlotRound = input.value + elif input.id == 'sideThickness': + holster.sideThickness = input.value + elif input.id == 'backThickness': + holster.backThickness = input.value + elif input.id == 'bottomThickness': + holster.bottomThickness = input.value + elif input.id == 'tolerance': + holster.tolerance = input.value + elif input.id == 'temp': + holster.temp = input.value + + holster.buildHolster(); + + args.isValidResult = True + + except: + if ui: + ui.messageBox('Failed:\n{}'.format(traceback.format_exc())) + +class HolsterCommandDestroyHandler(adsk.core.CommandEventHandler): + def __init__(self): + super().__init__() + def notify(self, args): + try: + # when the command is done, terminate the script + # this will release all globals which will remove all event handlers + adsk.terminate() + except: + if ui: + ui.messageBox('Failed:\n{}'.format(traceback.format_exc())) + +class HolsterCommandCreatedHandler(adsk.core.CommandCreatedEventHandler): + def __init__(self): + super().__init__() + def notify(self, args): + try: + cmd = args.command + cmd.isRepeatable = False + onExecute = HolsterCommandExecuteHandler() + cmd.execute.add(onExecute) + onExecutePreview = HolsterCommandExecuteHandler() + cmd.executePreview.add(onExecutePreview) + onDestroy = HolsterCommandDestroyHandler() + cmd.destroy.add(onDestroy) + # keep the handler referenced beyond this function + handlers.append(onExecute) + handlers.append(onExecutePreview) + handlers.append(onDestroy) + + #define the inputs + inputs = cmd.commandInputs + + finestIncrement = 1.0 + + # JRS: These values / ranges are not set + # + inputs.addStringValueInput('holsterName', 'Holster Name', defaultHolsterName) + + initRemoteWidth = adsk.core.ValueInput.createByReal(defaultRemoteWidth) + inputs.addFloatSpinnerCommandInput('remoteWidth', 'Remote Width', '', 0.25, 100.0, finestIncrement, defaultRemoteWidth) + + initremoteLength = adsk.core.ValueInput.createByReal(defaultRemoteLength) + inputs.addFloatSpinnerCommandInput('remoteLength', 'Remote Height', '', 0.25, 100.0, finestIncrement, defaultRemoteLength) + + initRemoteThickness = adsk.core.ValueInput.createByReal(defaultRemoteThickness) + inputs.addFloatSpinnerCommandInput('remoteThickness', 'Remote Thickness', '', 0.25, 100.0, finestIncrement, defaultRemoteThickness) + + initFrontHeight = adsk.core.ValueInput.createByReal(defaultFrontHeight) + inputs.addFloatSpinnerCommandInput('frontHeight', 'Front Height', '', 0.25, 100.0, finestIncrement, defaultFrontHeight) + + initFrontSlotWidth = adsk.core.ValueInput.createByReal(defaultFrontSlotWidth) + inputs.addFloatSpinnerCommandInput('frontSlotWidth', 'Front Slot Width', '', 0.25, 100.0, finestIncrement, defaultFrontSlotWidth) + + initBackCornerRound = adsk.core.ValueInput.createByReal(defaultBackCornerRound) + inputs.addFloatSpinnerCommandInput('backCornerRound', 'Back Corner Round', '', 0.25, 100.0, finestIncrement, defaultBackCornerRound) + + initFillet = adsk.core.ValueInput.createByReal(defaultFillet) + inputs.addFloatSpinnerCommandInput('fillet', 'Overall Fillet', '', 0.25, 100.0, 0.01, defaultFillet) + + initFrontSlotRound = adsk.core.ValueInput.createByReal(defaultFrontSlotRound) + inputs.addFloatSpinnerCommandInput('frontSlotRound', 'Front Slot Round', '', 0.25, 100.0, finestIncrement, defaultFrontSlotRound) + + initSideThickness = adsk.core.ValueInput.createByReal(defaultSideThickness) + inputs.addFloatSpinnerCommandInput('sideThickness', 'Side Thickness', '', 0.25, 100.0, finestIncrement, defaultSideThickness) + + initBackThickness = adsk.core.ValueInput.createByReal(defaultBackThickness) + inputs.addFloatSpinnerCommandInput('backThickness', 'Back Thickness', '', 0.25, 100.0, finestIncrement, defaultBackThickness) + + initBottomThickness = adsk.core.ValueInput.createByReal(defaultBottomThickness) + inputs.addFloatSpinnerCommandInput('bottomThickness', 'Bottom Thickness', '', 0.25, 100.0, finestIncrement, defaultBottomThickness) + + initTolerance = adsk.core.ValueInput.createByReal(defaultTolerance) + inputs.addFloatSpinnerCommandInput('tolerance', 'Tolerance', '', 0.25, 100.0, finestIncrement, defaultTolerance) + + initTemp = adsk.core.ValueInput.createByReal(0) + inputs.addIntegerSpinnerCommandInput('temp', 'Temp', 0, 50, 1, 0) + + + except: + if ui: + ui.messageBox('Failed:\n{}'.format(traceback.format_exc())) + +class Holster: + def __init__(self): + self._holsterName = defaultHolsterName + self._remoteWidth = defaultRemoteWidth + self._remoteLength = defaultRemoteLength + self._remoteThickness = defaultRemoteThickness + self._frontSlotWidth = defaultFrontSlotWidth + self._frontHeight = defaultFrontHeight + self._backCornerRound = defaultBackCornerRound + self._fillet = defaultFillet + self._frontSlotRound = defaultFrontSlotRound + self._sideThickness = defaultSideThickness + self._backThickness = defaultBackThickness + self._bottomThickness = defaultBottomThickness + self._tolerance = defaultTolerance + self._temp = 0 + + # properties + # + @property + def holsterName(self): + return self._holsterName + @holsterName.setter + def holsterName(self, value): + self._holsterName = value + + @property + def remoteWidth(self): + return self._remoteWidth + @remoteWidth.setter + def remoteWidth(self, value): + self._remoteWidth = value + + @property + def remoteLength(self): + return self._remoteLength + @remoteLength.setter + def remoteLength(self, value): + self._remoteLength = value + + @property + def remoteThickness(self): + return self._remoteThickness + @remoteThickness.setter + def remoteThickness(self, value): + self._remoteThickness = value + + @property + def frontSlotWidth(self): + return self._frontSlotWidth + @frontSlotWidth.setter + def frontSlotWidth(self, value): + self._frontSlotWidth = value + + @property + def frontHeight(self): + return self._frontHeight + @frontHeight.setter + def frontHeight(self, value): + self._frontHeight = value + + @property + def backCornerRound(self): + return self._backCornerRound + @backCornerRound.setter + def backCornerRound(self, value): + self._backCornerRound = value + + @property + def fillet(self): + return self._fillet + @fillet.setter + def fillet(self, value): + self._fillet = value + + @property + def frontSlotRound(self): + return self._frontSlotRound + @frontSlotRound.setter + def frontSlotRound(self, value): + self._frontSlotRound = value + + @property + def sideThickness(self): + return self._sideThickness + @sideThickness.setter + def sideThickness(self, value): + self._sideThickness = value + + @property + def backThickness(self): + return self._backThickness + @backThickness.setter + def backThickness(self, value): + self._backThickness = value + + @property + def bottomThickness(self): + return self._bottomThickness + @bottomThickness.setter + def bottomThickness(self, value): + self._bottomThickness = value + + @property + def tolerance(self): + return self._tolerance + @tolerance.setter + def tolerance(self, value): + self._tolerance = value + + @property + def temp(self): + return self._temp + @temp.setter + def temp(self, value): + self._temp = value + + def buildHolster(self): + try: + # Get the active design. + app = adsk.core.Application.get() + product = app.activeProduct + design = adsk.fusion.Design.cast(product) + ui = app.userInterface + + global component + + # Units + design.fusionUnitsManager.distanceDisplayUnits = adsk.fusion.DistanceUnits.MillimeterDistanceUnits + + # Main component + component = createComponent(design, self.holsterName) + + if component is None: + ui.messageBox('New component failed to create', 'New Component Failed') + return + + # This is the flow of how I built the holster by hand + # + # 1) create the base rectangle on the XY plane, (remoteWidth + 2*sideThickness) x (remoteThickness + sideThickness + backThickness) + # 2) extrude that rectangle to the remoteLength + bottomThickness + # 3) add rectangle of side thickness (OK, not exactly- could have a different back thickness) + # 4) extrude that offset to remoteLength (leaving BottomThickness) + # Sketch base + base_rect_profile = createBaseRectSketch(component, self) + distance = createDistance((self.remoteLength + self.bottomThickness) * SCALE) + # ui.messageBox("Dimensions %s x %s x %s" % (str(self.remoteLength), str(self.remoteWidth), str(self.remoteThickness))) + + # Extrude to full height + # + holster = component.features.extrudeFeatures.addSimple(base_rect_profile, distance, NEW_BODY) + holster_body = holster.bodies.item(0) + holster_body.name = "Holster" + + # Cut out the pocket + # + pocket_profile = createPocketSketch(component, self) + pocket_depth = createDistance(self.remoteLength * SCALE * -1) + component.features.extrudeFeatures.addSimple(pocket_profile, pocket_depth, CUT) + + # Push down the front + # + front_profile = createFrontSketch(component, self) + front_depth = createDistance((self.remoteLength - self.frontHeight) * SCALE * -1) + component.features.extrudeFeatures.addSimple(front_profile, front_depth, CUT) + + # Create Slot + # + slot_profile = createSlotSketch(component, self) + slot_depth = createDistance((self.remoteThickness + self.sideThickness) * SCALE) + component.features.extrudeFeatures.addSimple(slot_profile, slot_depth, CUT) + + # Round the back corners + # + edges = holster_body.edges + fillet_edges = adsk.core.ObjectCollection.create() + + for n in range(edges.count): + edge = edges.item(n) + if math.isclose(edge.length, self.backThickness * SCALE): + # Need to figure out where it is + bb = edge.boundingBox + maxZ = bb.maxPoint.z + minZ = bb.minPoint.z + if math.isclose(maxZ, minZ) and math.isclose(maxZ, (self.remoteLength + self.bottomThickness) * SCALE): + fillet_edges.add(edge) + + fillets = component.features.filletFeatures + fillet_input = fillets.createInput() + fillet_radius = createDistance(self.backCornerRound * SCALE) + fillet_input.addConstantRadiusEdgeSet(fillet_edges, fillet_radius, True) + fillet_input.isG2 = False + fillet_input.isRollingBallCorner = True + top_fillet = fillets.add(fillet_input) + + fillet_edges.clear() + for n in range(edges.count): + edge = edges.item(n) + if math.isclose(edge.length, self.sideThickness * SCALE): + # Need to figure out where it is + bb = edge.boundingBox + maxZ = bb.maxPoint.z + minZ = bb.minPoint.z + minY = bb.minPoint.y + + if math.isclose(maxZ, minZ) and math.isclose(maxZ, (self.frontHeight + self.bottomThickness) * SCALE) and math.isclose(minY, 0): + fillet_edges.add(edge) + + fillets = component.features.filletFeatures + fillet_input = fillets.createInput() + fillet_radius = createDistance(self.frontSlotRound * SCALE) + fillet_input.addConstantRadiusEdgeSet(fillet_edges, fillet_radius, True) + fillet_input.isG2 = False + fillet_input.isRollingBallCorner = True + top_fillet = fillets.add(fillet_input) + + self.includeScrewHoles = True + + if self.includeScrewHoles: + # Magnet hole sketch + screwHolesProfile = createScrewHolesSketch(component, self, 2) + + # Extrude for magnets + distance = createDistance(self.backThickness * SCALE) + component.features.extrudeFeatures.addSimple(screwHolesProfile, distance, CUT) + + screwHolesProfile = createScrewHolesSketch(component, self, 4) + distance = createDistance(self.backThickness / 3 * SCALE) + component.features.extrudeFeatures.addSimple(screwHolesProfile, distance, CUT) + + # Soften everything + # + fillet_edges.clear() + for n in range(edges.count): + edge = edges.item(n) + fillet_edges.add(edge) + + fillets = component.features.filletFeatures + fillet_input = fillets.createInput() + fillet_radius = createDistance(self.fillet * SCALE) + fillet_input.addConstantRadiusEdgeSet(fillet_edges, fillet_radius, True) + fillet_input.isG2 = False + fillet_input.isRollingBallCorner = True + top_fillet = fillets.add(fillet_input) + + + except: + if ui: + ui.messageBox('Failed to compute the holster. This is most likely because the input values define an invalid holster.') + ui.messageBox('Failed:\n{}'.format(traceback.format_exc())) + + +def createScrewHolesSketch(component: adsk.fusion.Component, holster, diameter) -> adsk.core.ObjectCollection: + sketch: adsk.fusion.Sketch = component.sketches.add(component.xZConstructionPlane) + sketch.name = "Screw Holes Sketch" + + holesCenter = (holster.sideThickness + holster.remoteWidth / 2) + holesBack = (holster.sideThickness + holster.remoteThickness) + holesSpace = (holster.bottomThickness + holster.remoteLength) / 4 * -1 + + sketch.sketchCurves.sketchCircles.addByCenterRadius(createPoint(holesCenter * SCALE, holesSpace * SCALE, holesBack * SCALE), diameter * SCALE) + sketch.sketchCurves.sketchCircles.addByCenterRadius(createPoint(holesCenter * SCALE, 3 * holesSpace * SCALE, holesBack * SCALE), diameter * SCALE) + + circles = adsk.core.ObjectCollection.create() + for n in range(2): + circles.add(sketch.profiles.item(n)) + + return circles + +def createPocketSketch(component: adsk.fusion.Component, holster) -> adsk.core.ObjectCollection: + sketch: adsk.fusion.Sketch = component.sketches.add(component.xYConstructionPlane) + sketch.name = "Pocket Sketch" + + p1 = createPoint((holster.sideThickness) * SCALE, (holster.sideThickness) * SCALE, (holster.remoteLength + holster.bottomThickness) * SCALE) + p2 = createPoint((holster.sideThickness + holster.remoteWidth) * SCALE, (holster.sideThickness + holster.remoteThickness) * SCALE, (holster.remoteLength + holster.bottomThickness) * SCALE) + + sketch.sketchCurves.sketchLines.addTwoPointRectangle(p1, p2) + + rect = adsk.core.ObjectCollection.create() + rect.add(sketch.profiles.item(0)) + + return rect + +def createFrontSketch(component: adsk.fusion.Component, holster) -> adsk.core.ObjectCollection: + sketch: adsk.fusion.Sketch = component.sketches.add(component.xYConstructionPlane) + sketch.name = "Front Sketch" + + p1 = createPoint(0, 0, (holster.remoteLength + holster.bottomThickness) * SCALE) + p2 = createPoint((2 * holster.sideThickness + holster.remoteWidth) * SCALE, (holster.sideThickness + holster.remoteThickness) * SCALE, (holster.remoteLength + holster.bottomThickness) * SCALE) + + sketch.sketchCurves.sketchLines.addTwoPointRectangle(p1, p2) + + rect = adsk.core.ObjectCollection.create() + rect.add(sketch.profiles.item(0)) + + return rect + +def createSlotSketch(component: adsk.fusion.Component, holster) -> adsk.core.ObjectCollection: + sketch: adsk.fusion.Sketch = component.sketches.add(component.xZConstructionPlane) + sketch.name = "Slot Sketch" + + slotLeft = (2 * holster.sideThickness + holster.remoteWidth - holster.frontSlotWidth) / 2 + p1 = createPoint(slotLeft * SCALE, 0, 0) + p2 = createPoint((slotLeft + holster.frontSlotWidth) * SCALE, -1 * (holster.frontHeight + holster.bottomThickness) * SCALE, 0) + + sketch.sketchCurves.sketchLines.addTwoPointRectangle(p1, p2) + + rect = adsk.core.ObjectCollection.create() + rect.add(sketch.profiles.item(0)) + + return rect + +def run(context): + try: + product = app.activeProduct + design = adsk.fusion.Design.cast(product) + if not design: + ui.messageBox('It is not supported in current workspace, please change to MODEL workspace and try again.') + return + commandDefinitions = ui.commandDefinitions + + #check the command exists or not + # + cmdDef = commandDefinitions.itemById('RemoteHolster') + if not cmdDef: + cmdDef = commandDefinitions.addButtonDefinition('RemoteHolster', + 'Create a Remote Holster', + 'Create a Remote Holster.') + + onCommandCreated = HolsterCommandCreatedHandler() + cmdDef.commandCreated.add(onCommandCreated) + # keep the handler referenced beyond this function + handlers.append(onCommandCreated) + inputs = adsk.core.NamedValues.create() + + cmdDef.execute(inputs) + + # prevent this module from being terminate when the script returns, because we are waiting for event handlers to fire + adsk.autoTerminate(False) + except: + if ui: + ui.messageBox('Failed:\n{}'.format(traceback.format_exc())) diff --git a/copy-to-scripts-dir.sh b/copy-to-scripts-dir.sh new file mode 100755 index 0000000..ae4d7ef --- /dev/null +++ b/copy-to-scripts-dir.sh @@ -0,0 +1,16 @@ +#!/bin/sh + +# Note that this might not be right for everyone +FUSION_360_API_DIR="/Users/$USER/Library/Application Support/Autodesk/Autodesk Fusion 360/API" +FUSION_360_SCRIPTS_DIR="${FUSION_360_API_DIR}/Scripts" + +if [ -d "${FUSION_360_API_DIR}" ] +then + mkdir -p "${FUSION_360_SCRIPTS_DIR}" + cd /Users/$USER/Projects/GitRepo/gfdb + cp -r GridFinityDividerBoxMaker "${FUSION_360_SCRIPTS_DIR}" + cp -r RemoteHolsterMaker "${FUSION_360_SCRIPTS_DIR}/" +else + echo "Fusion 360 API directory (${FUSION_360_API_DIR}) not found, aborting" + exit 9 +fi