diff --git a/commands/commandCreateBin/entry.py b/commands/commandCreateBin/entry.py index 3568d35..973965a 100644 --- a/commands/commandCreateBin/entry.py +++ b/commands/commandCreateBin/entry.py @@ -86,6 +86,7 @@ BIN_SCREW_HOLES_INPUT_ID = 'bin_screw_holes' BIN_MAGNET_CUTOUTS_INPUT_ID = 'bin_magnet_cutouts' BIN_MAGNET_CUTOUTS_TABS_INPUT_ID = 'bin_magnet_cutouts_tabs' +BIN_MAGNET_REFINED_HOLE_INPUT_ID = 'bin_magnet_refined_hole' BIN_SCREW_DIAMETER_INPUT = 'screw_diameter' BIN_MAGNET_DIAMETER_INPUT = 'magnet_diameter' BIN_MAGNET_HEIGHT_INPUT = 'magnet_height' @@ -188,6 +189,7 @@ def initDefaultUiState(): commandUIState.initValue(BIN_SCREW_DIAMETER_INPUT, const.DIMENSION_SCREW_HOLE_DIAMETER, adsk.core.ValueCommandInput.classType()) commandUIState.initValue(BIN_MAGNET_CUTOUTS_INPUT_ID, False, adsk.core.BoolValueCommandInput.classType()) commandUIState.initValue(BIN_MAGNET_CUTOUTS_TABS_INPUT_ID, False, adsk.core.BoolValueCommandInput.classType()) + commandUIState.initValue(BIN_MAGNET_REFINED_HOLE_INPUT_ID, False, adsk.core.BoolValueCommandInput.classType()) commandUIState.initValue(BIN_MAGNET_DIAMETER_INPUT, const.DIMENSION_MAGNET_CUTOUT_DIAMETER, adsk.core.ValueCommandInput.classType()) commandUIState.initValue(BIN_MAGNET_HEIGHT_INPUT, const.DIMENSION_MAGNET_CUTOUT_DEPTH, adsk.core.ValueCommandInput.classType()) @@ -643,6 +645,8 @@ def command_created(args: adsk.core.CommandCreatedEventArgs): commandUIState.registerCommandInput(generateMagnetSocketCheckboxInput) generateMagnetsTabCheckboxInput = baseFeaturesGroup.children.addBoolValueInput(BIN_MAGNET_CUTOUTS_TABS_INPUT_ID, 'Add tabs to magnet sockets', True, '', commandUIState.getState(BIN_MAGNET_CUTOUTS_TABS_INPUT_ID)) commandUIState.registerCommandInput(generateMagnetsTabCheckboxInput) + refinedMagnetHoleCheckboxInput = baseFeaturesGroup.children.addBoolValueInput(BIN_MAGNET_REFINED_HOLE_INPUT_ID, 'Side-insertion magnet slots (no glue)', True, '', commandUIState.getState(BIN_MAGNET_REFINED_HOLE_INPUT_ID)) + commandUIState.registerCommandInput(refinedMagnetHoleCheckboxInput) magnetSizeInput = baseFeaturesGroup.children.addValueInput(BIN_MAGNET_DIAMETER_INPUT, 'Magnet cutout diameter', defaultLengthUnits, adsk.core.ValueInput.createByReal(commandUIState.getState(BIN_MAGNET_DIAMETER_INPUT))) magnetSizeInput.minimumValue = 0.1 magnetSizeInput.isMinimumInclusive = True @@ -806,6 +810,7 @@ def onChangeValidate(): commandUIState.getInput(BIN_SCREW_HOLES_INPUT_ID).isEnabled = generateBase commandUIState.getInput(BIN_MAGNET_CUTOUTS_INPUT_ID).isEnabled = generateBase commandUIState.getInput(BIN_MAGNET_CUTOUTS_TABS_INPUT_ID).isEnabled = generateBase + commandUIState.getInput(BIN_MAGNET_REFINED_HOLE_INPUT_ID).isEnabled = generateBase commandUIState.getInput(BIN_MAGNET_DIAMETER_INPUT).isEnabled = generateBase commandUIState.getInput(BIN_MAGNET_HEIGHT_INPUT).isEnabled = generateBase commandUIState.getInput(BIN_SCREW_DIAMETER_INPUT).isEnabled = generateBase @@ -866,6 +871,7 @@ def generateBin(args: adsk.core.CommandEventArgs): bin_magnet_cutouts: adsk.core.BoolValueCommandInput = inputs.itemById(BIN_MAGNET_CUTOUTS_INPUT_ID) bin_screw_hole_diameter: adsk.core.ValueCommandInput = inputs.itemById(BIN_SCREW_DIAMETER_INPUT) bin_magnet_cutouts_tabs: adsk.core.BoolValueCommandInput = inputs.itemById(BIN_MAGNET_CUTOUTS_TABS_INPUT_ID) + bin_magnet_refined_hole: adsk.core.BoolValueCommandInput = inputs.itemById(BIN_MAGNET_REFINED_HOLE_INPUT_ID) bin_magnet_cutout_diameter: adsk.core.ValueCommandInput = inputs.itemById(BIN_MAGNET_DIAMETER_INPUT) bin_magnet_cutout_depth: adsk.core.ValueCommandInput = inputs.itemById(BIN_MAGNET_HEIGHT_INPUT) with_lip: adsk.core.BoolValueCommandInput = inputs.itemById(BIN_WITH_LIP_INPUT_ID) @@ -920,6 +926,7 @@ def generateBin(args: adsk.core.CommandEventArgs): baseGeneratorInput.hasScrewHoles = bin_screw_holes.value and not isShelled baseGeneratorInput.hasMagnetCutouts = bin_magnet_cutouts.value and not isShelled baseGeneratorInput.hasMagnetCutoutsTabs = bin_magnet_cutouts_tabs.value and not isShelled + baseGeneratorInput.hasRefinedMagnetHole = bin_magnet_refined_hole.value and not isShelled baseGeneratorInput.screwHolesDiameter = bin_screw_hole_diameter.value baseGeneratorInput.magnetCutoutsDiameter = bin_magnet_cutout_diameter.value baseGeneratorInput.magnetCutoutsDepth = bin_magnet_cutout_depth.value diff --git a/lib/gridfinityUtils/baseGenerator.py b/lib/gridfinityUtils/baseGenerator.py index 802649c..014fc11 100644 --- a/lib/gridfinityUtils/baseGenerator.py +++ b/lib/gridfinityUtils/baseGenerator.py @@ -203,18 +203,62 @@ def createSingleGridfinityBaseBody( # magnet cutouts if input.hasMagnetCutouts: - magnetSocketBody = shapeUtils.simpleCylinder( - baseBottomPlane, - 0, - -input.magnetCutoutsDepth, - input.magnetCutoutsDiameter / 2, - cutoutCenterPoint, - targetComponent, - ) - cutoutBodies.add(magnetSocketBody) + if input.hasRefinedMagnetHole: + # Refined magnet hole (grizzie17's design): + # - Press-fit pocket slightly smaller than magnet, with side insertion slot + # - Thin floor below pocket prevents magnet falling out + # - Poke-through hole below for magnet removal + pocketRadius = input.magnetCutoutsDiameter / 2 - const.DIMENSION_REFINED_HOLE_SQUEEZE + pocketDepth = const.DIMENSION_REFINED_HOLE_DEPTH + floorThickness = const.DIMENSION_REFINED_HOLE_FLOOR + + # Slot exit point at the base edge, centered on the hole Y position + slotEdgePoint = adsk.core.Point3D.create( + input.originPoint.x, + cutoutCenterPoint.y, + 0, + ) + # Pocket is offset up from the bottom face by the floor thickness + magnetSocketBody = shapeUtils.refinedMagnetHoleBody( + baseBottomPlane, + -floorThickness, + pocketRadius, + pocketDepth, + slotEdgePoint, + cutoutCenterPoint, + targetComponent, + ) + cutoutBodies.add(magnetSocketBody) + + # Poke-through slot for magnet removal with a toothpick. + # Goes from the base bottom face all the way through to the top + # of the pocket, on the opposite side from the insertion slot. + pokeRadius = const.DIMENSION_REFINED_POKE_THROUGH_DIAMETER / 2 + pokeDepth = floorThickness + pocketDepth + pokeBody = shapeUtils.refinedPokeThrough( + baseBottomPlane, + 0, + pokeDepth, + pokeRadius, + slotEdgePoint, + cutoutCenterPoint, + targetComponent, + ) + pokeBody.name = "Refined poke-through slot" + cutoutBodies.add(pokeBody) + else: + magnetSocketBody = shapeUtils.simpleCylinder( + baseBottomPlane, + 0, + -input.magnetCutoutsDepth, + input.magnetCutoutsDiameter / 2, + cutoutCenterPoint, + targetComponent, + ) + cutoutBodies.add(magnetSocketBody) - # magnet tab cutouts - if input.hasMagnetCutoutsTabs: + # magnet tab cutouts (only for standard holes, not refined) + if input.hasMagnetCutoutsTabs and not input.hasRefinedMagnetHole: magnetTabCutoutSketch = createTabAtCircleEdgeSketch( baseBottomPlane, input.magnetCutoutsDiameter / 2, @@ -232,8 +276,8 @@ def createSingleGridfinityBaseBody( targetComponent, ) cutoutBodies.add(magnetTabCutoutExtrude.bodies.item(0)) - - if input.hasScrewHoles and (const.BIN_BASE_HEIGHT - input.magnetCutoutsDepth) > const.BIN_MAGNET_HOLE_GROOVE_DEPTH: + + if not input.hasRefinedMagnetHole and input.hasScrewHoles and (const.BIN_BASE_HEIGHT - input.magnetCutoutsDepth) > const.BIN_MAGNET_HOLE_GROOVE_DEPTH: grooveBody = shapeUtils.simpleCylinder( baseBottomPlane, -input.magnetCutoutsDepth, diff --git a/lib/gridfinityUtils/baseGeneratorInput.py b/lib/gridfinityUtils/baseGeneratorInput.py index 1bec31c..365fadc 100644 --- a/lib/gridfinityUtils/baseGeneratorInput.py +++ b/lib/gridfinityUtils/baseGeneratorInput.py @@ -7,6 +7,7 @@ def __init__(self): self.hasMagnetCutouts = False self.hasScrewHoles = False self.hasBottomChamfer = True + self.hasRefinedMagnetHole = False self.screwHolesDiameter = DIMENSION_SCREW_HOLE_DIAMETER self.magnetCutoutsDiameter = DIMENSION_MAGNET_CUTOUT_DIAMETER self.magnetCutoutsDepth = DIMENSION_MAGNET_CUTOUT_DEPTH diff --git a/lib/gridfinityUtils/const.py b/lib/gridfinityUtils/const.py index fefb27e..4179f85 100644 --- a/lib/gridfinityUtils/const.py +++ b/lib/gridfinityUtils/const.py @@ -40,5 +40,15 @@ DIMENSION_MAGNET_CUTOUT_DEPTH = 0.24 DIMENSION_PRINT_HELPER_GROOVE_DEPTH = 0.03 +# Refined magnet hole constants (based on grizzie17's Gridfinity Refined design) +# Pocket is slightly smaller than magnet for press-fit (6.5mm - 2*0.32mm = 5.86mm) +DIMENSION_REFINED_HOLE_SQUEEZE = 0.032 +# Magnet height minus tolerance (2mm - 0.1mm = 1.9mm) +DIMENSION_REFINED_HOLE_DEPTH = 0.19 +# Thin floor below the pocket to prevent magnet from falling out (h_slit * 2 = 0.4mm) +DIMENSION_REFINED_HOLE_FLOOR = 0.04 +# Poke-through hole diameter for magnet removal with a toothpick +DIMENSION_REFINED_POKE_THROUGH_DIAMETER = 0.25 + DEFAULT_FILTER_TOLERANCE = 0.00001 \ No newline at end of file diff --git a/lib/gridfinityUtils/shapeUtils.py b/lib/gridfinityUtils/shapeUtils.py index 4be4571..77c613e 100644 --- a/lib/gridfinityUtils/shapeUtils.py +++ b/lib/gridfinityUtils/shapeUtils.py @@ -1,5 +1,5 @@ import adsk.core, adsk.fusion, traceback -import os +import os, math from . import extrudeUtils, sketchUtils @@ -62,6 +62,168 @@ def simpleCylinder( cylinderExtrude.name = "Simple cylinder extrude" return cylinderExtrude.bodies.item(0) +def refinedMagnetHoleBody( + plane: adsk.core.Base, + planeOffset: float, + pocketRadius: float, + pocketDepth: float, + slotEdgePoint: adsk.core.Point3D, + centerPoint: adsk.core.Point3D, + targetComponent: adsk.fusion.Component, +): + """Create a refined magnet hole cutout body (grizzie17's Gridfinity Refined design). + + The pocket is a keyhole shape drawn as a single closed profile: a semicircular + pocket connected to a rectangular slot extending to the base edge. + + Arguments: + plane: the base bottom face + planeOffset: offset from the bottom face to the TOP of the pocket + (negative = into the base, typically -(floor thickness)) + pocketRadius: radius of the circular magnet pocket + pocketDepth: depth of the pocket (magnet height minus tolerance) + slotEdgePoint: the point on the base edge where the slot exits, + in model space (center of the slot width at the edge) + centerPoint: center of the magnet pocket in model space + """ + constructionPlaneInput = targetComponent.constructionPlanes.createInput() + constructionPlaneInput.setByOffset(plane, adsk.core.ValueInput.createByReal(planeOffset)) + constructionPlane = targetComponent.constructionPlanes.add(constructionPlaneInput) + + sketch: adsk.fusion.Sketch = targetComponent.sketches.add(constructionPlane) + sketch.name = "Refined magnet hole sketch" + + ctr = sketch.modelToSketchSpace(centerPoint) + ctr.z = 0 + edge = sketch.modelToSketchSpace(slotEdgePoint) + edge.z = 0 + + # Compute slot direction (center → edge) and perpendicular in sketch space + dx = edge.x - ctr.x + dy = edge.y - ctr.y + length = math.sqrt(dx * dx + dy * dy) + dirX = dx / length + dirY = dy / length + perpX = -dirY + perpY = dirX + r = pocketRadius + + # Keyhole as a single closed profile: + # - Semicircular arc on the far side (away from slot) + # - Two parallel lines forming the slot sides + # - A closing line at the base edge + arcStart = adsk.core.Point3D.create(ctr.x + perpX * r, ctr.y + perpY * r, 0) + arcMid = adsk.core.Point3D.create(ctr.x - dirX * r, ctr.y - dirY * r, 0) + arcEnd = adsk.core.Point3D.create(ctr.x - perpX * r, ctr.y - perpY * r, 0) + + edgeTop = adsk.core.Point3D.create(edge.x + perpX * r, edge.y + perpY * r, 0) + edgeBottom = adsk.core.Point3D.create(edge.x - perpX * r, edge.y - perpY * r, 0) + + arcs = sketch.sketchCurves.sketchArcs + lines = sketch.sketchCurves.sketchLines + + arcs.addByThreePoints(arcStart, arcMid, arcEnd) + lines.addByTwoPoints(arcEnd, edgeBottom) + lines.addByTwoPoints(edgeBottom, edgeTop) + lines.addByTwoPoints(edgeTop, arcStart) + + pocketExtrude = extrudeUtils.simpleDistanceExtrude( + sketch.profiles.item(0), + adsk.fusion.FeatureOperations.NewBodyFeatureOperation, + -pocketDepth, + adsk.fusion.ExtentDirections.PositiveExtentDirection, + [], + targetComponent, + ) + pocketExtrude.name = "Refined magnet pocket extrude" + return pocketExtrude.bodies.item(0) + + +def refinedPokeThrough( + plane: adsk.core.Base, + planeOffset: float, + depth: float, + pokeRadius: float, + slotEdgePoint: adsk.core.Point3D, + centerPoint: adsk.core.Point3D, + targetComponent: adsk.fusion.Component, +): + """Create a poke-through slot for magnet removal with a toothpick. + + Draws a stadium/oblong shape (two semicircles connected by lines) positioned + on the OPPOSITE side of the pocket from the insertion slot (toward the base + interior). This lets you push the magnet out from behind with a toothpick. + + Positions match the OpenSCAD reference: near end at ~3.4mm from center, + far end at ~6.93mm from center, in the direction away from the base edge. + """ + constructionPlaneInput = targetComponent.constructionPlanes.createInput() + constructionPlaneInput.setByOffset(plane, adsk.core.ValueInput.createByReal(planeOffset)) + constructionPlane = targetComponent.constructionPlanes.add(constructionPlaneInput) + + sketch: adsk.fusion.Sketch = targetComponent.sketches.add(constructionPlane) + sketch.name = "Refined poke-through sketch" + + ctr = sketch.modelToSketchSpace(centerPoint) + ctr.z = 0 + edge = sketch.modelToSketchSpace(slotEdgePoint) + edge.z = 0 + + # Direction from center AWAY from the edge (toward base interior) + # This is the opposite of the slot direction + dx = ctr.x - edge.x + dy = ctr.y - edge.y + length = math.sqrt(dx * dx + dy * dy) + dirX = dx / length + dirY = dy / length + perpX = -dirY + perpY = dirX + + # Position the poke-through slot along the interior direction, + # matching OpenSCAD: near end at 3.4mm, far end at 6.93mm from center + # As fractions of the center-to-edge distance (8mm): + nearFrac = 0.425 + farFrac = 0.866 + nearCenter = adsk.core.Point3D.create( + ctr.x + dirX * length * nearFrac, ctr.y + dirY * length * nearFrac, 0, + ) + farCenter = adsk.core.Point3D.create( + ctr.x + dirX * length * farFrac, ctr.y + dirY * length * farFrac, 0, + ) + + pr = pokeRadius + + # Stadium shape: two semicircles connected by two lines + # Near semicircle (closer to pocket center) + nearArcStart = adsk.core.Point3D.create(nearCenter.x + perpX * pr, nearCenter.y + perpY * pr, 0) + nearArcMid = adsk.core.Point3D.create(nearCenter.x - dirX * pr, nearCenter.y - dirY * pr, 0) + nearArcEnd = adsk.core.Point3D.create(nearCenter.x - perpX * pr, nearCenter.y - perpY * pr, 0) + + # Far semicircle (toward base interior) + farArcStart = adsk.core.Point3D.create(farCenter.x - perpX * pr, farCenter.y - perpY * pr, 0) + farArcMid = adsk.core.Point3D.create(farCenter.x + dirX * pr, farCenter.y + dirY * pr, 0) + farArcEnd = adsk.core.Point3D.create(farCenter.x + perpX * pr, farCenter.y + perpY * pr, 0) + + arcs = sketch.sketchCurves.sketchArcs + lines = sketch.sketchCurves.sketchLines + + arcs.addByThreePoints(nearArcStart, nearArcMid, nearArcEnd) + lines.addByTwoPoints(nearArcEnd, farArcStart) + arcs.addByThreePoints(farArcStart, farArcMid, farArcEnd) + lines.addByTwoPoints(farArcEnd, nearArcStart) + + pokeExtrude = extrudeUtils.simpleDistanceExtrude( + sketch.profiles.item(0), + adsk.fusion.FeatureOperations.NewBodyFeatureOperation, + -depth, + adsk.fusion.ExtentDirections.PositiveExtentDirection, + [], + targetComponent, + ) + pokeExtrude.name = "Refined poke-through extrude" + return pokeExtrude.bodies.item(0) + + def simpleBox( plane: adsk.core.Base, planeOffset: float,