From 6057a227021531e08eda54ba774cf2096e877a5d Mon Sep 17 00:00:00 2001 From: me Date: Tue, 30 Dec 2025 23:45:07 -0500 Subject: [PATCH 1/8] feat: Add new tab generation methods with root and tip thickness controls. --- commands/commandCreateBin/entry.py | 85 +++++++- lib/gridfinityUtils/binBodyGenerator.py | 24 ++- lib/gridfinityUtils/binBodyGeneratorInput.py | 7 +- lib/gridfinityUtils/binBodyTabGenerator.py | 201 +++++++++++++----- .../binBodyTabGeneratorInput.py | 27 +++ lib/gridfinityUtils/const.py | 12 ++ 6 files changed, 291 insertions(+), 65 deletions(-) diff --git a/commands/commandCreateBin/entry.py b/commands/commandCreateBin/entry.py index 30c9479..4cd4446 100644 --- a/commands/commandCreateBin/entry.py +++ b/commands/commandCreateBin/entry.py @@ -96,6 +96,10 @@ BIN_TAB_WIDTH_INPUT_ID = 'bin_tab_width' BIN_TAB_POSITION_INPUT_ID = 'bin_tab_position' BIN_TAB_ANGLE_INPUT_ID = 'bin_tab_angle' +BIN_TAB_METHOD_INPUT_ID = 'bin_tab_method' +BIN_TAB_ROOT_THICKNESS_INPUT_ID = 'bin_tab_root_thickness' +BIN_TAB_TIP_THICKNESS_INPUT_ID = 'bin_tab_tip_thickness' +BIN_TAB_UNIFORM_THICKNESS_INPUT_ID = 'bin_tab_uniform_thickness' BIN_WITH_LIP_INPUT_ID = 'with_lip' BIN_WITH_LIP_NOTCHES_INPUT_ID = 'with_lip_notches' BIN_COMPARTMENT_REAL_DIMENSIONS_TABLE = "compartment_real_dimensions" @@ -161,7 +165,7 @@ def initDefaultUiState(): commandUIState.initValue(BIN_XY_CLEARANCE_INPUT_ID, const.BIN_XY_CLEARANCE, adsk.core.ValueCommandInput.classType()) commandUIState.initValue(BIN_WIDTH_INPUT_ID, 2, adsk.core.IntegerSpinnerCommandInput.classType()) commandUIState.initValue(BIN_LENGTH_INPUT_ID, 3, adsk.core.IntegerSpinnerCommandInput.classType()) - commandUIState.initValue(BIN_HEIGHT_INPUT_ID, 5, adsk.core.ValueCommandInput.classType()) + commandUIState.initValue(BIN_HEIGHT_INPUT_ID, 5, adsk.core.IntegerSpinnerCommandInput.classType()) commandUIState.initValue(BIN_GENERATE_BODY_INPUT_ID, True, adsk.core.BoolValueCommandInput.classType()) commandUIState.initValue(BIN_TYPE_DROPDOWN_ID, BIN_TYPE_HOLLOW, adsk.core.DropDownCommandInput.classType()) @@ -181,6 +185,10 @@ def initDefaultUiState(): commandUIState.initValue(BIN_TAB_WIDTH_INPUT_ID, const.BIN_TAB_WIDTH, adsk.core.ValueCommandInput.classType()) commandUIState.initValue(BIN_TAB_POSITION_INPUT_ID, 0, adsk.core.ValueCommandInput.classType()) commandUIState.initValue(BIN_TAB_ANGLE_INPUT_ID, '45 deg', adsk.core.ValueCommandInput.classType()) + commandUIState.initValue(BIN_TAB_METHOD_INPUT_ID, const.BIN_TAB_METHOD_ANGLE, adsk.core.DropDownCommandInput.classType()) + commandUIState.initValue(BIN_TAB_ROOT_THICKNESS_INPUT_ID, const.BIN_TAB_DEFAULT_ROOT_THICKNESS, adsk.core.ValueCommandInput.classType()) + commandUIState.initValue(BIN_TAB_UNIFORM_THICKNESS_INPUT_ID, const.BIN_TAB_DEFAULT_IS_UNIFORM, adsk.core.BoolValueCommandInput.classType()) + commandUIState.initValue(BIN_TAB_TIP_THICKNESS_INPUT_ID, const.BIN_TAB_DEFAULT_TIP_THICKNESS, adsk.core.ValueCommandInput.classType()) commandUIState.initValue(BIN_GENERATE_BASE_INPUT_ID, True, adsk.core.BoolValueCommandInput.classType()) commandUIState.initValue(BIN_SCREW_HOLES_INPUT_ID, False, adsk.core.BoolValueCommandInput.classType()) @@ -369,8 +377,11 @@ def render_compartments_table(inputs: adsk.core.CommandInputs): compartmentsGroup: adsk.core.GroupCommandInput = commandUIState.getInput(BIN_COMPARTMENTS_GROUP_ID) binCompartmentsTable = compartmentsGroup.children.addTableCommandInput(BIN_COMPARTMENTS_TABLE_ID, "Compartments", 5, "1:1:1:1:1") addButton = compartmentsGroup.commandInputs.addBoolValueInput(BIN_COMPARTMENTS_TABLE_ADD_ID, "Add", False, "", False) + commandUIState.registerCommandInput(addButton) removeButton = compartmentsGroup.commandInputs.addBoolValueInput(BIN_COMPARTMENTS_TABLE_REMOVE_ID, "Remove", False, "", False) + commandUIState.registerCommandInput(removeButton) populateUniform = compartmentsGroup.commandInputs.addBoolValueInput(BIN_COMPARTMENTS_TABLE_UNIFORM_ID, "Reset to uniform", False, "", False) + commandUIState.registerCommandInput(populateUniform) binCompartmentsTable.addToolbarCommandInput(addButton) binCompartmentsTable.addToolbarCommandInput(removeButton) binCompartmentsTable.addToolbarCommandInput(populateUniform) @@ -548,8 +559,7 @@ def command_created(args: adsk.core.CommandCreatedEventArgs): commandUIState.registerCommandInput(binWidthInput) binLengthInput = binDimensionsGroup.children.addIntegerSpinnerCommandInput(BIN_LENGTH_INPUT_ID, 'Bin length, Y (u)', 1, 100, 1, commandUIState.getState(BIN_LENGTH_INPUT_ID)) commandUIState.registerCommandInput(binLengthInput) - binHeightInput = binDimensionsGroup.children.addValueInput(BIN_HEIGHT_INPUT_ID, 'Bin height, Z (u)', '', adsk.core.ValueInput.createByReal(commandUIState.getState(BIN_HEIGHT_INPUT_ID))) - binHeightInput.minimumValue = 1 + binHeightInput = binDimensionsGroup.children.addIntegerSpinnerCommandInput(BIN_HEIGHT_INPUT_ID, 'Bin height, Z (u)', 1, 100, 1, int(commandUIState.getState(BIN_HEIGHT_INPUT_ID))) binHeightInput.isMinimumInclusive = True commandUIState.registerCommandInput(binHeightInput) @@ -623,6 +633,26 @@ def command_created(args: adsk.core.CommandCreatedEventArgs): tabObverhangAngleInput.maximumValue = math.radians(65) tabObverhangAngleInput.isMaximumInclusive = True commandUIState.registerCommandInput(tabObverhangAngleInput) + + tabMethodDropdown = binTabFeaturesGroup.children.addDropDownCommandInput(BIN_TAB_METHOD_INPUT_ID, 'Tab construction method', adsk.core.DropDownStyles.LabeledIconDropDownStyle) + tabMethodDropdownDefaultValue = commandUIState.getState(BIN_TAB_METHOD_INPUT_ID) + tabMethodDropdown.listItems.add(const.BIN_TAB_METHOD_ANGLE, tabMethodDropdownDefaultValue == const.BIN_TAB_METHOD_ANGLE) + tabMethodDropdown.listItems.add(const.BIN_TAB_METHOD_DIMENSIONS, tabMethodDropdownDefaultValue == const.BIN_TAB_METHOD_DIMENSIONS) + commandUIState.registerCommandInput(tabMethodDropdown) + + rootThicknessInput = binTabFeaturesGroup.children.addValueInput(BIN_TAB_ROOT_THICKNESS_INPUT_ID, 'Root thickness (mm)', defaultLengthUnits, adsk.core.ValueInput.createByReal(commandUIState.getState(BIN_TAB_ROOT_THICKNESS_INPUT_ID))) + rootThicknessInput.minimumValue = 0.1 + rootThicknessInput.isMinimumInclusive = True + commandUIState.registerCommandInput(rootThicknessInput) + + uniformThicknessInput = binTabFeaturesGroup.children.addBoolValueInput(BIN_TAB_UNIFORM_THICKNESS_INPUT_ID, 'Uniform thickness', True, '', commandUIState.getState(BIN_TAB_UNIFORM_THICKNESS_INPUT_ID)) + commandUIState.registerCommandInput(uniformThicknessInput) + + tipThicknessInput = binTabFeaturesGroup.children.addValueInput(BIN_TAB_TIP_THICKNESS_INPUT_ID, 'Tip thickness (mm)', defaultLengthUnits, adsk.core.ValueInput.createByReal(commandUIState.getState(BIN_TAB_TIP_THICKNESS_INPUT_ID))) + tipThicknessInput.minimumValue = 0.1 + tipThicknessInput.isMinimumInclusive = True + commandUIState.registerCommandInput(tipThicknessInput) + for input in binTabFeaturesGroup.children: if not input.id == BIN_HAS_TAB_INPUT_ID: input.isEnabled = commandUIState.getState(BIN_HAS_TAB_INPUT_ID) @@ -809,7 +839,7 @@ def onChangeValidate(): commandUIState.getInput(BIN_MAGNET_CUTOUTS_TABS_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 + commandUIState.getInput(BIN_SCREW_DIAMETER_INPUT).isEnabled = generateBase and commandUIState.getState(BIN_SCREW_HOLES_INPUT_ID) generateBody: bool = commandUIState.getState(BIN_GENERATE_BODY_INPUT_ID) binType: str = commandUIState.getState(BIN_TYPE_DROPDOWN_ID) @@ -827,13 +857,37 @@ def onChangeValidate(): generateScoop: bool = commandUIState.getState(BIN_HAS_SCOOP_INPUT_ID) commandUIState.getInput(BIN_SCOOP_MAX_RADIUS_INPUT_ID).isEnabled = generateScoop + + gridType: str = commandUIState.getState(BIN_COMPARTMENTS_GRID_TYPE_ID) + isCustomGrid = gridType == BIN_COMPARTMENTS_GRID_TYPE_CUSTOM + commandUIState.getInput(BIN_COMPARTMENTS_TABLE_ID).isVisible = isCustomGrid + commandUIState.getInput(BIN_COMPARTMENTS_TABLE_ADD_ID).isVisible = isCustomGrid + commandUIState.getInput(BIN_COMPARTMENTS_TABLE_REMOVE_ID).isVisible = isCustomGrid + commandUIState.getInput(BIN_COMPARTMENTS_TABLE_UNIFORM_ID).isVisible = isCustomGrid generateTab: bool = commandUIState.getState(BIN_HAS_TAB_INPUT_ID) + tabMethod: str = commandUIState.getState(BIN_TAB_METHOD_INPUT_ID) + isDimensionsMethod = tabMethod == const.BIN_TAB_METHOD_DIMENSIONS + isUniformThickness = commandUIState.getState(BIN_TAB_UNIFORM_THICKNESS_INPUT_ID) + commandUIState.getInput(BIN_TAB_LENGTH_INPUT_ID).isEnabled = generateTab commandUIState.getInput(BIN_TAB_WIDTH_INPUT_ID).isEnabled = generateTab - commandUIState.getInput(BIN_TAB_ANGLE_INPUT_ID).isEnabled = generateTab commandUIState.getInput(BIN_TAB_POSITION_INPUT_ID).isEnabled = generateTab + commandUIState.getInput(BIN_TAB_ANGLE_INPUT_ID).isVisible = not isDimensionsMethod + commandUIState.getInput(BIN_TAB_ANGLE_INPUT_ID).isEnabled = generateTab and not isDimensionsMethod + + commandUIState.getInput(BIN_TAB_METHOD_INPUT_ID).isEnabled = generateTab + + commandUIState.getInput(BIN_TAB_ROOT_THICKNESS_INPUT_ID).isVisible = isDimensionsMethod + commandUIState.getInput(BIN_TAB_ROOT_THICKNESS_INPUT_ID).isEnabled = generateTab and isDimensionsMethod + + commandUIState.getInput(BIN_TAB_UNIFORM_THICKNESS_INPUT_ID).isVisible = isDimensionsMethod + commandUIState.getInput(BIN_TAB_UNIFORM_THICKNESS_INPUT_ID).isEnabled = generateTab and isDimensionsMethod + + commandUIState.getInput(BIN_TAB_TIP_THICKNESS_INPUT_ID).isVisible = isDimensionsMethod + commandUIState.getInput(BIN_TAB_TIP_THICKNESS_INPUT_ID).isEnabled = generateTab and isDimensionsMethod and not isUniformThickness + compartmentsGridType: str = commandUIState.getState(BIN_COMPARTMENTS_GRID_TYPE_ID) commandUIState.getInput(BIN_COMPARTMENTS_TABLE_ID).isVisible = compartmentsGridType == BIN_COMPARTMENTS_GRID_TYPE_CUSTOM @@ -883,6 +937,7 @@ def generateBin(args: adsk.core.CommandEventArgs): binCompartmentsTable: adsk.core.TableCommandInput = inputs.itemById(BIN_COMPARTMENTS_TABLE_ID) compartmentsX: adsk.core.IntegerSpinnerCommandInput = inputs.itemById(BIN_COMPARTMENTS_GRID_BASE_WIDTH_ID) compartmentsY: adsk.core.IntegerSpinnerCommandInput = inputs.itemById(BIN_COMPARTMENTS_GRID_BASE_LENGTH_ID) + tabMethod: str = inputs.itemById(BIN_TAB_METHOD_INPUT_ID).selectedItem.name isHollow = binTypeDropdownInput.selectedItem.name == BIN_TYPE_HOLLOW isSolid = binTypeDropdownInput.selectedItem.name == BIN_TYPE_SOLID @@ -950,6 +1005,15 @@ def generateBin(args: adsk.core.CommandEventArgs): binBodyInput.tabWidth = binTabWidth.value binBodyInput.tabPosition = binTabPosition.value binBodyInput.tabOverhangAngle = binTabAngle.value + binBodyInput.tabMethod = tabMethod + binBodyInput.rootThickness = commandUIState.getState(BIN_TAB_ROOT_THICKNESS_INPUT_ID) + + isUniform = commandUIState.getState(BIN_TAB_UNIFORM_THICKNESS_INPUT_ID) + if isUniform: + binBodyInput.tipThickness = binBodyInput.rootThickness + else: + binBodyInput.tipThickness = commandUIState.getState(BIN_TAB_TIP_THICKNESS_INPUT_ID) + binBodyInput.compartmentsByX = compartmentsX.value binBodyInput.compartmentsByY = compartmentsY.value @@ -1027,6 +1091,17 @@ def generateBin(args: adsk.core.CommandEventArgs): compartmentTabInput.width = binBodyInput.tabWidth compartmentTabInput.overhangAngle = binBodyInput.tabOverhangAngle compartmentTabInput.topClearance = const.BIN_TAB_TOP_CLEARANCE + + # Populate new fields + compartmentTabInput.tabMethod = commandUIState.getState(BIN_TAB_METHOD_INPUT_ID) + compartmentTabInput.rootThickness = commandUIState.getState(BIN_TAB_ROOT_THICKNESS_INPUT_ID) + + isUniform = commandUIState.getState(BIN_TAB_UNIFORM_THICKNESS_INPUT_ID) + if isUniform: + compartmentTabInput.tipThickness = compartmentTabInput.rootThickness + else: + compartmentTabInput.tipThickness = commandUIState.getState(BIN_TAB_TIP_THICKNESS_INPUT_ID) + tabBody = createGridfinityBinBodyTab(compartmentTabInput, gridfinityBinComponent) combineInput = combineFeatures.createInput(tabBody, commonUtils.objectCollectionFromList([binBody])) combineInput.operation = adsk.fusion.FeatureOperations.CutFeatureOperation diff --git a/lib/gridfinityUtils/binBodyGenerator.py b/lib/gridfinityUtils/binBodyGenerator.py index fa13ecc..0eda17f 100644 --- a/lib/gridfinityUtils/binBodyGenerator.py +++ b/lib/gridfinityUtils/binBodyGenerator.py @@ -144,6 +144,9 @@ def createGridfinityBinBody( compartmentTabInput.width = input.tabWidth compartmentTabInput.overhangAngle = input.tabOverhangAngle compartmentTabInput.topClearance = const.BIN_TAB_TOP_CLEARANCE + compartmentTabInput.tabMethod = input.tabMethod + compartmentTabInput.rootThickness = input.rootThickness + compartmentTabInput.tipThickness = input.tipThickness [compartmentMerges, compartmentCuts] = createCompartment( input.wallThickness, @@ -257,12 +260,17 @@ def createCompartment( if hasTab: tabBody = createGridfinityBinBodyTab(tabInput, targetComponent) - intersectTabInput = targetComponent.features.combineFeatures.createInput( - tabBody, - commonUtils.objectCollectionFromList([innerCutoutBody]), - ) - intersectTabInput.operation = adsk.fusion.FeatureOperations.IntersectFeatureOperation - intersectTabInput.isKeepToolBodies = True - intersectTabFeature = targetComponent.features.combineFeatures.add(intersectTabInput) - bodiesToMerge = bodiesToMerge + [body for body in list(intersectTabFeature.bodies) if not body.revisionId == innerCutoutBody.revisionId] + try: + intersectTabInput = targetComponent.features.combineFeatures.createInput( + tabBody, + commonUtils.objectCollectionFromList([innerCutoutBody]), + ) + intersectTabInput.operation = adsk.fusion.FeatureOperations.IntersectFeatureOperation + intersectTabInput.isKeepToolBodies = True + intersectTabFeature = targetComponent.features.combineFeatures.add(intersectTabInput) + bodiesToMerge = bodiesToMerge + [body for body in list(intersectTabFeature.bodies) if not body.revisionId == innerCutoutBody.revisionId] + except: + # Fallback if intersection fails (e.g. Dimensions method might not overlap cutout exactly or logic differs) + # Just add the tab body itself + bodiesToMerge.append(tabBody) return (bodiesToMerge, bodiesToSubtract) \ No newline at end of file diff --git a/lib/gridfinityUtils/binBodyGeneratorInput.py b/lib/gridfinityUtils/binBodyGeneratorInput.py index 40eed7c..e20fadb 100644 --- a/lib/gridfinityUtils/binBodyGeneratorInput.py +++ b/lib/gridfinityUtils/binBodyGeneratorInput.py @@ -62,8 +62,11 @@ def __init__(self): self.tabOverhangAngle = const.BIN_TAB_OVERHANG_ANGLE self.tabPosition = 0 self.tabLength = 1 - self.tabWidth = const.BIN_TAB_WIDTH - self.compartments = [BinBodyCompartmentDefinition()] + self.tabOverhangAngle = 45 + self.tabMethod = const.BIN_TAB_METHOD_ANGLE + self.rootThickness = const.BIN_TAB_DEFAULT_ROOT_THICKNESS + self.tipThickness = const.BIN_TAB_DEFAULT_TIP_THICKNESS + self.compartments: list[BinBodyCompartmentDefinition] = [] self.compartmentsByX = 1 self.compartmentsByY = 1 self.binCornerFilletRadius = const.BIN_CORNER_FILLET_RADIUS diff --git a/lib/gridfinityUtils/binBodyTabGenerator.py b/lib/gridfinityUtils/binBodyTabGenerator.py index 14a8b80..16eb1aa 100644 --- a/lib/gridfinityUtils/binBodyTabGenerator.py +++ b/lib/gridfinityUtils/binBodyTabGenerator.py @@ -37,64 +37,160 @@ def createGridfinityBinBodyTab( tabProfilePlane = targetComponent.constructionPlanes.add(tabProfilePlaneInput) tabSketch: adsk.fusion.Sketch = targetComponent.sketches.add(tabProfilePlane) tabSketch.name = "label tab sketch" + tabSketchLine = tabSketch.sketchCurves.sketchLines + constraints = tabSketch.geometricConstraints + dimensions = tabSketch.sketchDimensions + + # Calculate tab top edge start point + # tab is centered on the bin width + # tab is positioned on the bin length (y) + # tab starts at bin top heightput.origin.z - input.topClearance tabTopEdgeHeight = input.origin.z - input.topClearance - actualTabWidth = input.width + BIN_TAB_EDGE_FILLET_RADIUS / math.tan((math.radians(90) - input.overhangAngle) / 2) - actualTabHeight = actualTabWidth / math.tan(input.overhangAngle) - line1 = tabSketchLine.addByTwoPoints( - tabSketch.modelToSketchSpace(adsk.core.Point3D.create(input.origin.x, input.origin.y, tabTopEdgeHeight)), - tabSketch.modelToSketchSpace(adsk.core.Point3D.create(input.origin.x, input.origin.y, tabTopEdgeHeight - actualTabHeight)), - ) - line2 = tabSketchLine.addByTwoPoints( - tabSketch.modelToSketchSpace(adsk.core.Point3D.create(input.origin.x, input.origin.y, tabTopEdgeHeight)), - tabSketch.modelToSketchSpace(adsk.core.Point3D.create(input.origin.x, input.origin.y - actualTabWidth, tabTopEdgeHeight)), - ) - line3 = tabSketchLine.addByTwoPoints( - tabSketch.modelToSketchSpace(adsk.core.Point3D.create(input.origin.x, input.origin.y, tabTopEdgeHeight - actualTabHeight)), - tabSketch.modelToSketchSpace(adsk.core.Point3D.create(input.origin.x, input.origin.y - actualTabWidth, tabTopEdgeHeight)), - ) + if input.tabMethod == const.BIN_TAB_METHOD_DIMENSIONS: + # Dimensions based generation + rootThickness = input.rootThickness + tipThickness = input.tipThickness + + # Points in sketch coordinates (Y seems to be width/depth, X is height/Z) + # Based on previous code: + # tabTopEdgeHeight is X in sketch (Z in model) + # input.origin.y is Y in sketch? No, see addByTwoPoints below. + + # Previous code: + # line1 (Vertical in sketch?): (origin.x, origin.y, tabTopEdgeHeight) -> (origin.x, origin.y, tabTopEdgeHeight - actualTabHeight) + # wait, modelToSketchSpace takes Point3D. + # The plane is offset from YZ plane by X. + # So Sketch Plane X = Model Y, Sketch Plane Y = Model Z. + + # Let's look at the previous code's points: + # P1: (origin.x, origin.y, tabTopEdgeHeight) + # P2: (origin.x, origin.y, tabTopEdgeHeight - actualTabHeight) + # P3: (origin.x, origin.y - actualTabWidth, tabTopEdgeHeight) + # P4: (origin.x, origin.y - actualTabWidth, tabTopEdgeHeight) (Used in line 3) + + # Model coords: + # origin.x is strictly X. + # origin.y varies by -actualTabWidth. + # tabTopEdgeHeight (Z) varies by -actualTabHeight. + + # So we want: + # Top-Back (Wall): (origin.x, origin.y, tabTopEdgeHeight) + # Top-Front (Tip): (origin.x, origin.y - input.width, tabTopEdgeHeight) + # Bottom-Front (Tip): (origin.x, origin.y - input.width, tabTopEdgeHeight - tipThickness) + # Bottom-Back (Wall): (origin.x, origin.y, tabTopEdgeHeight - rootThickness) + + # World Coordinates (X, Y, Z) + y_back = input.origin.y + y_front = input.origin.y - input.width + z_top = tabTopEdgeHeight + z_root_bottom = tabTopEdgeHeight - rootThickness + z_tip_bottom = tabTopEdgeHeight - tipThickness + + # P_top_back: At wall, top edge. + p_top_back = adsk.core.Point3D.create(input.origin.x, y_back, z_top) + # P_top_front: At tip, top edge. + p_top_front = adsk.core.Point3D.create(input.origin.x, y_front, z_top) + # P_bot_front: At tip, bottom edge. + p_bot_front = adsk.core.Point3D.create(input.origin.x, y_front, z_tip_bottom) + # P_bot_back: At wall, bottom edge. + p_bot_back = adsk.core.Point3D.create(input.origin.x, y_back, z_root_bottom) - constraints: adsk.fusion.GeometricConstraints = tabSketch.geometricConstraints - dimensions: adsk.fusion.SketchDimensions = tabSketch.sketchDimensions - - # horizontal/vertical relative to local sketch XY coordinates - constraints.addHorizontal(line1) - constraints.addVertical(line2) - constraints.addCoincident(line1.startSketchPoint, line2.startSketchPoint) - constraints.addCoincident(line2.endSketchPoint, line3.endSketchPoint) - constraints.addCoincident(line1.endSketchPoint, line3.startSketchPoint) - - dimensions.addDistanceDimension( - tabSketch.originPoint, - line1.startSketchPoint, - adsk.fusion.DimensionOrientations.VerticalDimensionOrientation, - line1.startSketchPoint.geometry, - True - ) + l_top = tabSketchLine.addByTwoPoints(tabSketch.modelToSketchSpace(p_top_back), tabSketch.modelToSketchSpace(p_top_front)) + l_front = tabSketchLine.addByTwoPoints(tabSketch.modelToSketchSpace(p_top_front), tabSketch.modelToSketchSpace(p_bot_front)) + l_bot = tabSketchLine.addByTwoPoints(tabSketch.modelToSketchSpace(p_bot_front), tabSketch.modelToSketchSpace(p_bot_back)) + l_back = tabSketchLine.addByTwoPoints(tabSketch.modelToSketchSpace(p_bot_back), tabSketch.modelToSketchSpace(p_top_back)) + + # Constraints + # Since we projected world points, we rely on standard constraints relative to the resulting geometry + # Top line should be horizontal (if Z is constant) + constraints.addHorizontal(l_top) + + # Back line (wall) should be vertical (if Y is constant) + constraints.addVertical(l_back) + + # Front line (tip) should be vertical + constraints.addVertical(l_front) + + constraints.addCoincident(l_top.endSketchPoint, l_front.startSketchPoint) + constraints.addCoincident(l_front.endSketchPoint, l_bot.startSketchPoint) + constraints.addCoincident(l_bot.endSketchPoint, l_back.startSketchPoint) + constraints.addCoincident(l_back.endSketchPoint, l_top.startSketchPoint) + # Let's verify standard Orientation. YZ plane. U=Y, V=Z. + # Horizontal constraint in Fusion sketch usually means parallel to X axis of sketch. + # Sketch X axis usually corresponds to Model Y for YZ plane. + - dimensions.addDistanceDimension( - tabSketch.originPoint, - line1.startSketchPoint, - adsk.fusion.DimensionOrientations.HorizontalDimensionOrientation, - line1.startSketchPoint.geometry, - True - ) + + # l_back (wall) should be vertical (Along Z/V, Perpendicular to Y/U) + + + # Dimensions + # Width (Top edge length) + dimensions.addDistanceDimension(l_top.startSketchPoint, l_top.endSketchPoint, adsk.fusion.DimensionOrientations.HorizontalDimensionOrientation, l_top.startSketchPoint.geometry, True) + + # Root Thickness (Back edge length) + dimensions.addDistanceDimension(l_back.startSketchPoint, l_back.endSketchPoint, adsk.fusion.DimensionOrientations.VerticalDimensionOrientation, l_back.startSketchPoint.geometry, True) + + # Tip Thickness (Front edge length) + dimensions.addDistanceDimension(l_front.startSketchPoint, l_front.endSketchPoint, adsk.fusion.DimensionOrientations.VerticalDimensionOrientation, l_front.startSketchPoint.geometry, True) - dimensions.addDistanceDimension( - line2.startSketchPoint, - line2.endSketchPoint, - adsk.fusion.DimensionOrientations.VerticalDimensionOrientation, - line2.endSketchPoint.geometry, - True + + else: + # Angle based (Default) + actualTabWidth = input.width + BIN_TAB_EDGE_FILLET_RADIUS / math.tan((math.radians(90) - input.overhangAngle) / 2) + actualTabHeight = actualTabWidth / math.tan(input.overhangAngle) + line1 = tabSketchLine.addByTwoPoints( + tabSketch.modelToSketchSpace(adsk.core.Point3D.create(input.origin.x, input.origin.y, tabTopEdgeHeight)), + tabSketch.modelToSketchSpace(adsk.core.Point3D.create(input.origin.x, input.origin.y, tabTopEdgeHeight - actualTabHeight)), + ) + line2 = tabSketchLine.addByTwoPoints( + tabSketch.modelToSketchSpace(adsk.core.Point3D.create(input.origin.x, input.origin.y, tabTopEdgeHeight)), + tabSketch.modelToSketchSpace(adsk.core.Point3D.create(input.origin.x, input.origin.y - actualTabWidth, tabTopEdgeHeight)), ) - - dimensions.addAngularDimension( - line1, - line3, - line1.endSketchPoint.geometry, - True, + line3 = tabSketchLine.addByTwoPoints( + tabSketch.modelToSketchSpace(adsk.core.Point3D.create(input.origin.x, input.origin.y, tabTopEdgeHeight - actualTabHeight)), + tabSketch.modelToSketchSpace(adsk.core.Point3D.create(input.origin.x, input.origin.y - actualTabWidth, tabTopEdgeHeight)), ) + # horizontal/vertical relative to local sketch XY coordinates + constraints.addHorizontal(line1) + constraints.addVertical(line2) + constraints.addCoincident(line1.startSketchPoint, line2.startSketchPoint) + constraints.addCoincident(line2.endSketchPoint, line3.endSketchPoint) + constraints.addCoincident(line1.endSketchPoint, line3.startSketchPoint) + + dimensions.addDistanceDimension( + tabSketch.originPoint, + line1.startSketchPoint, + adsk.fusion.DimensionOrientations.VerticalDimensionOrientation, + line1.startSketchPoint.geometry, + True + ) + + dimensions.addDistanceDimension( + tabSketch.originPoint, + line1.startSketchPoint, + adsk.fusion.DimensionOrientations.HorizontalDimensionOrientation, + line1.startSketchPoint.geometry, + True + ) + + dimensions.addDistanceDimension( + line2.startSketchPoint, + line2.endSketchPoint, + adsk.fusion.DimensionOrientations.VerticalDimensionOrientation, + line2.endSketchPoint.geometry, + True + ) + + dimensions.addAngularDimension( + line1, + line3, + line1.endSketchPoint.geometry, + True, + ) + tabExtrudeFeature = extrudeUtils.simpleDistanceExtrude( tabSketch.profiles.item(0), adsk.fusion.FeatureOperations.NewBodyFeatureOperation, @@ -107,6 +203,11 @@ def createGridfinityBinBodyTab( tabBody.name = 'label tab' tabTopFace = faceUtils.getTopFace(tabBody) + + # Fillet logic might need adjustment if top face edges change + # But for now, we assume standard behavior. + # We need to find the edge that is collinear to X (length of tab) and has min Y (inner edge? no, outer edge of top face) + roundedEdge = min([edge for edge in tabTopFace.edges if geometryUtils.isCollinearToX(edge)], key=lambda x: x.boundingBox.minPoint.y) fillet = filletUtils.createFillet( [roundedEdge], diff --git a/lib/gridfinityUtils/binBodyTabGeneratorInput.py b/lib/gridfinityUtils/binBodyTabGeneratorInput.py index d523108..364a2c7 100644 --- a/lib/gridfinityUtils/binBodyTabGeneratorInput.py +++ b/lib/gridfinityUtils/binBodyTabGeneratorInput.py @@ -7,6 +7,9 @@ def __init__(self): self.overhangAngle = const.BIN_TAB_OVERHANG_ANGLE self.labelAngle = const.BIN_TAB_LABEL_ANGLE self.position = 0 + self._tabMethod = const.BIN_TAB_METHOD_ANGLE + self._rootThickness = const.BIN_TAB_DEFAULT_ROOT_THICKNESS + self._tipThickness = const.BIN_TAB_DEFAULT_TIP_THICKNESS @property def topClearance(self) -> float: @@ -56,4 +59,28 @@ def labelAngle(self) -> float: def labelAngle(self, value: float): self._tablabelAngle = value + @property + def tabMethod(self) -> str: + return self._tabMethod + + @tabMethod.setter + def tabMethod(self, value: str): + self._tabMethod = value + + @property + def rootThickness(self) -> float: + return self._rootThickness + + @rootThickness.setter + def rootThickness(self, value: float): + self._rootThickness = value + + @property + def tipThickness(self) -> float: + return self._tipThickness + + @tipThickness.setter + def tipThickness(self, value: float): + self._tipThickness = value + \ No newline at end of file diff --git a/lib/gridfinityUtils/const.py b/lib/gridfinityUtils/const.py index fefb27e..7f106e7 100644 --- a/lib/gridfinityUtils/const.py +++ b/lib/gridfinityUtils/const.py @@ -17,11 +17,23 @@ BIN_COMPARTMENT_BOTTOM_THICKNESS = 0.1 BIN_BODY_CUTOUT_BOTTOM_FILLET_RADIUS = 0.2 + BIN_TAB_WIDTH = 1.3 BIN_TAB_OVERHANG_ANGLE = 45 BIN_TAB_LABEL_ANGLE = 0 BIN_TAB_EDGE_FILLET_RADIUS = 0.06 BIN_TAB_TOP_CLEARANCE = 0.05 +BIN_TAB_METHOD_ANGLE = 'Angle' +BIN_TAB_METHOD_DIMENSIONS = 'Dimensions' +BIN_TAB_METHOD_INPUT_ID = 'bin_tab_method' +BIN_TAB_ROOT_THICKNESS_INPUT_ID = 'bin_tab_root_thickness' +BIN_TAB_UNIFORM_THICKNESS_INPUT_ID = 'bin_tab_uniform_thickness' +BIN_TAB_TIP_THICKNESS_INPUT_ID = 'bin_tab_tip_thickness' + +# Default values (Internal units are cm, so 0.14 = 1.4mm) +BIN_TAB_DEFAULT_ROOT_THICKNESS = 0.14 +BIN_TAB_DEFAULT_TIP_THICKNESS = 0.14 +BIN_TAB_DEFAULT_IS_UNIFORM = True BIN_SCOOP_MAX_RADIUS = 2.5 From 5ff369701b76e58dee0c10e338154df287008f9c Mon Sep 17 00:00:00 2001 From: me Date: Tue, 30 Dec 2025 23:55:16 -0500 Subject: [PATCH 2/8] test: Add new test for tabbed bin generation and remove conflicting geometric constraints from tab sketch lines. --- lib/gridfinityUtils/binBodyTabGenerator.py | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/lib/gridfinityUtils/binBodyTabGenerator.py b/lib/gridfinityUtils/binBodyTabGenerator.py index 16eb1aa..ead739e 100644 --- a/lib/gridfinityUtils/binBodyTabGenerator.py +++ b/lib/gridfinityUtils/binBodyTabGenerator.py @@ -102,16 +102,11 @@ def createGridfinityBinBodyTab( l_back = tabSketchLine.addByTwoPoints(tabSketch.modelToSketchSpace(p_bot_back), tabSketch.modelToSketchSpace(p_top_back)) # Constraints - # Since we projected world points, we rely on standard constraints relative to the resulting geometry - # Top line should be horizontal (if Z is constant) - constraints.addHorizontal(l_top) - - # Back line (wall) should be vertical (if Y is constant) - constraints.addVertical(l_back) - - # Front line (tip) should be vertical - constraints.addVertical(l_front) + # Removing geometric constraints (Horizontal/Vertical) to avoid conflict with Sketch Plane orientation. + # The points are calculated in World Space to be correct, so we rely on their explicit positions. + # We still chain the lines to ensure a closed profile (Coincident is usually implicit with shared points in API, + # but explicit Coincident on end points guarantees connectivity for Profile creation). constraints.addCoincident(l_top.endSketchPoint, l_front.startSketchPoint) constraints.addCoincident(l_front.endSketchPoint, l_bot.startSketchPoint) constraints.addCoincident(l_bot.endSketchPoint, l_back.startSketchPoint) From abcafba0db410e58a662b7391a60abe407c4eab2 Mon Sep 17 00:00:00 2001 From: me Date: Wed, 31 Dec 2025 00:04:25 -0500 Subject: [PATCH 3/8] feat: Add test script for dimension-based Gridfinity tabs and update tab generator to fillet both top and bottom front edges. --- TestGridfinityTab.py | 80 ++++++++++++++++++++++ lib/gridfinityUtils/binBodyTabGenerator.py | 30 ++++---- 2 files changed, 97 insertions(+), 13 deletions(-) create mode 100644 TestGridfinityTab.py diff --git a/TestGridfinityTab.py b/TestGridfinityTab.py new file mode 100644 index 0000000..dc00dec --- /dev/null +++ b/TestGridfinityTab.py @@ -0,0 +1,80 @@ +import adsk.core, adsk.fusion, traceback +import math + +# Import the generator modules +# Note: We need to adjust path or copying/mocking might be needed if running as a separate script. +# Assuming this script is placed in the root of the add-in or similar location where it can import lib. + +from .lib.gridfinityUtils import binBodyGenerator, binBodyGeneratorInput, const +from .lib import fusion360utils as futil + +def run(context): + ui = None + try: + app = adsk.core.Application.get() + ui = app.userInterface + design = adsk.fusion.Design.cast(app.activeProduct) + + # Create a new document to avoid messing up the current one + doc = app.documents.add(adsk.core.DocumentTypes.FusionDesignDocumentType) + design = adsk.fusion.Design.cast(app.activeProduct) + root = design.rootComponent + + # Setup Input + binInput = binBodyGeneratorInput.BinBodyGeneratorInput() + binInput.binWidth = 42.0 # 1 unit + binInput.binLength = 42.0 # 1 unit + binInput.binHeight = 42.0 # 6 units (6*7) + binInput.isSolid = False + binInput.wallThickness = 1.2 + + # Tab Settings + binInput.hasTab = True + binInput.tabMethod = const.BIN_TAB_METHOD_DIMENSIONS + binInput.rootThickness = 0.14 # 1.4mm + binInput.tipThickness = 0.14 + binInput.tabLength = 12.0 # 12mm + binInput.tabWidth = 12.0 # Width of tab (along X axis? No, width is usually "Depth" of label area) + # Wait, tabWidth in UI maps to 'width' property in generatorInput? + # In entry.py: binBodyInput.tabWidth = binTabWidth.value + + # Run Generator + # We need a dummy 'innerCutoutBody' if we were running the full generator, + # but createGridfinityBinBody does the whole thing. + # However, createGridfinityBinBody requires 'baseGeneratorInput' context usually? + # Actually createGridfinityBinBody creates the bin body. + + # Let's call the high-level function that creates everything? + # No, let's call createGridfinityBinBody which is what generateBin calls. + + # Note: createGridfinityBinBody creates the bin, cutout, and tab. + binBody = binBodyGenerator.createGridfinityBinBody( + binInput, + root + ) + + # Assertions + # Check if we have bodies + if root.bRepBodies.count == 0: + ui.messageBox('Test Failed: No bodies created.') + return + + # Check Dimensions + # Bounding Box of the bin + bbox = binBody.boundingBox + # Check Height + height = bbox.maxPoint.z - bbox.minPoint.z + # Expected: 42.0 + if abs(height - 42.0) > 0.01: + ui.messageBox(f'Test Failed: Height mismatch. Expected 42.0, got {height}') + return + + # Check for Tab Feature + # The tab should add volume. + # Easier to visually verify or check if specific face exists. + + ui.messageBox('Test Passed: Bin created with Dimensions Tab.') + + except: + if ui: + ui.messageBox('Failed:\n{}'.format(traceback.format_exc())) diff --git a/lib/gridfinityUtils/binBodyTabGenerator.py b/lib/gridfinityUtils/binBodyTabGenerator.py index ead739e..fb3807f 100644 --- a/lib/gridfinityUtils/binBodyTabGenerator.py +++ b/lib/gridfinityUtils/binBodyTabGenerator.py @@ -197,19 +197,23 @@ def createGridfinityBinBodyTab( tabBody = tabExtrudeFeature.bodies.item(0) tabBody.name = 'label tab' - tabTopFace = faceUtils.getTopFace(tabBody) + # Fillet logic + # User wants both Top and Bottom edges of the tip to be filleted. + # We find all X-collinear edges. + x_edges = [edge for edge in tabBody.edges if geometryUtils.isCollinearToX(edge)] - # Fillet logic might need adjustment if top face edges change - # But for now, we assume standard behavior. - # We need to find the edge that is collinear to X (length of tab) and has min Y (inner edge? no, outer edge of top face) - - roundedEdge = min([edge for edge in tabTopFace.edges if geometryUtils.isCollinearToX(edge)], key=lambda x: x.boundingBox.minPoint.y) - fillet = filletUtils.createFillet( - [roundedEdge], - BIN_TAB_EDGE_FILLET_RADIUS, - False, - targetComponent - ) - fillet.name = 'label tab fillet' + # Find the minimum Y (Front of the tab) + if x_edges: + min_y = min(x_edges, key=lambda e: e.boundingBox.minPoint.y).boundingBox.minPoint.y + # Select all edges at that min_y (within float tolerance) + front_edges = [edge for edge in x_edges if abs(edge.boundingBox.minPoint.y - min_y) < 0.001] + + fillet = filletUtils.createFillet( + front_edges, + BIN_TAB_EDGE_FILLET_RADIUS, + False, + targetComponent + ) + fillet.name = 'label tab fillet' return tabBody From 66b95cf66c72ceb3f5ca7d5e87e52871ee120ad7 Mon Sep 17 00:00:00 2001 From: me Date: Wed, 31 Dec 2025 00:17:05 -0500 Subject: [PATCH 4/8] feat: Add configurable top and bottom fillets to bin tabs with separate UI inputs and generation logic. --- commands/commandCreateBin/entry.py | 35 +++++++++++++++ lib/gridfinityUtils/binBodyGeneratorInput.py | 2 + lib/gridfinityUtils/binBodyTabGenerator.py | 44 ++++++++++++++++--- .../binBodyTabGeneratorInput.py | 18 ++++++++ lib/gridfinityUtils/const.py | 5 +++ 5 files changed, 97 insertions(+), 7 deletions(-) diff --git a/commands/commandCreateBin/entry.py b/commands/commandCreateBin/entry.py index 4cd4446..57decd4 100644 --- a/commands/commandCreateBin/entry.py +++ b/commands/commandCreateBin/entry.py @@ -100,6 +100,10 @@ BIN_TAB_ROOT_THICKNESS_INPUT_ID = 'bin_tab_root_thickness' BIN_TAB_TIP_THICKNESS_INPUT_ID = 'bin_tab_tip_thickness' BIN_TAB_UNIFORM_THICKNESS_INPUT_ID = 'bin_tab_uniform_thickness' +BIN_TAB_FILLET_UNIFORM_INPUT_ID = 'bin_tab_fillet_uniform' +BIN_TAB_FILLET_TOP_INPUT_ID = 'bin_tab_fillet_top' +BIN_TAB_FILLET_BOTTOM_INPUT_ID = 'bin_tab_fillet_bottom' + BIN_WITH_LIP_INPUT_ID = 'with_lip' BIN_WITH_LIP_NOTCHES_INPUT_ID = 'with_lip_notches' BIN_COMPARTMENT_REAL_DIMENSIONS_TABLE = "compartment_real_dimensions" @@ -189,6 +193,10 @@ def initDefaultUiState(): commandUIState.initValue(BIN_TAB_ROOT_THICKNESS_INPUT_ID, const.BIN_TAB_DEFAULT_ROOT_THICKNESS, adsk.core.ValueCommandInput.classType()) commandUIState.initValue(BIN_TAB_UNIFORM_THICKNESS_INPUT_ID, const.BIN_TAB_DEFAULT_IS_UNIFORM, adsk.core.BoolValueCommandInput.classType()) commandUIState.initValue(BIN_TAB_TIP_THICKNESS_INPUT_ID, const.BIN_TAB_DEFAULT_TIP_THICKNESS, adsk.core.ValueCommandInput.classType()) + # Fillets default to half the tip thickness (0.07) + commandUIState.initValue(BIN_TAB_FILLET_TOP_INPUT_ID, const.BIN_TAB_DEFAULT_TIP_THICKNESS / 2, adsk.core.ValueCommandInput.classType()) + commandUIState.initValue(BIN_TAB_FILLET_BOTTOM_INPUT_ID, const.BIN_TAB_DEFAULT_TIP_THICKNESS / 2, adsk.core.ValueCommandInput.classType()) + commandUIState.initValue(BIN_TAB_FILLET_UNIFORM_INPUT_ID, const.BIN_TAB_DEFAULT_IS_FILLET_UNIFORM, adsk.core.BoolValueCommandInput.classType()) commandUIState.initValue(BIN_GENERATE_BASE_INPUT_ID, True, adsk.core.BoolValueCommandInput.classType()) commandUIState.initValue(BIN_SCREW_HOLES_INPUT_ID, False, adsk.core.BoolValueCommandInput.classType()) @@ -653,6 +661,16 @@ def command_created(args: adsk.core.CommandCreatedEventArgs): tipThicknessInput.isMinimumInclusive = True commandUIState.registerCommandInput(tipThicknessInput) + # Fillet Inputs + topFilletInput = binTabFeaturesGroup.children.addValueInput(BIN_TAB_FILLET_TOP_INPUT_ID, 'Top front fillet (mm)', defaultLengthUnits, adsk.core.ValueInput.createByReal(commandUIState.getState(BIN_TAB_FILLET_TOP_INPUT_ID))) + commandUIState.registerCommandInput(topFilletInput) + + uniformFilletInput = binTabFeaturesGroup.children.addBoolValueInput(BIN_TAB_FILLET_UNIFORM_INPUT_ID, 'Uniform fillet', True, '', commandUIState.getState(BIN_TAB_FILLET_UNIFORM_INPUT_ID)) + commandUIState.registerCommandInput(uniformFilletInput) + + bottomFilletInput = binTabFeaturesGroup.children.addValueInput(BIN_TAB_FILLET_BOTTOM_INPUT_ID, 'Bottom front fillet (mm)', defaultLengthUnits, adsk.core.ValueInput.createByReal(commandUIState.getState(BIN_TAB_FILLET_BOTTOM_INPUT_ID))) + commandUIState.registerCommandInput(bottomFilletInput) + for input in binTabFeaturesGroup.children: if not input.id == BIN_HAS_TAB_INPUT_ID: input.isEnabled = commandUIState.getState(BIN_HAS_TAB_INPUT_ID) @@ -887,6 +905,16 @@ def onChangeValidate(): commandUIState.getInput(BIN_TAB_TIP_THICKNESS_INPUT_ID).isVisible = isDimensionsMethod commandUIState.getInput(BIN_TAB_TIP_THICKNESS_INPUT_ID).isEnabled = generateTab and isDimensionsMethod and not isUniformThickness + + isFilletUniform = commandUIState.getState(BIN_TAB_FILLET_UNIFORM_INPUT_ID) + commandUIState.getInput(BIN_TAB_FILLET_TOP_INPUT_ID).isVisible = generateTab + commandUIState.getInput(BIN_TAB_FILLET_TOP_INPUT_ID).isEnabled = generateTab + + commandUIState.getInput(BIN_TAB_FILLET_UNIFORM_INPUT_ID).isVisible = generateTab + commandUIState.getInput(BIN_TAB_FILLET_UNIFORM_INPUT_ID).isEnabled = generateTab + + commandUIState.getInput(BIN_TAB_FILLET_BOTTOM_INPUT_ID).isVisible = generateTab + commandUIState.getInput(BIN_TAB_FILLET_BOTTOM_INPUT_ID).isEnabled = generateTab and not isFilletUniform compartmentsGridType: str = commandUIState.getState(BIN_COMPARTMENTS_GRID_TYPE_ID) commandUIState.getInput(BIN_COMPARTMENTS_TABLE_ID).isVisible = compartmentsGridType == BIN_COMPARTMENTS_GRID_TYPE_CUSTOM @@ -1014,6 +1042,13 @@ def generateBin(args: adsk.core.CommandEventArgs): else: binBodyInput.tipThickness = commandUIState.getState(BIN_TAB_TIP_THICKNESS_INPUT_ID) + isFilletUniform = commandUIState.getState(BIN_TAB_FILLET_UNIFORM_INPUT_ID) + binBodyInput.tabFilletTop = commandUIState.getState(BIN_TAB_FILLET_TOP_INPUT_ID) + if isFilletUniform: + binBodyInput.tabFilletBottom = binBodyInput.tabFilletTop + else: + binBodyInput.tabFilletBottom = commandUIState.getState(BIN_TAB_FILLET_BOTTOM_INPUT_ID) + binBodyInput.compartmentsByX = compartmentsX.value binBodyInput.compartmentsByY = compartmentsY.value diff --git a/lib/gridfinityUtils/binBodyGeneratorInput.py b/lib/gridfinityUtils/binBodyGeneratorInput.py index e20fadb..03caff9 100644 --- a/lib/gridfinityUtils/binBodyGeneratorInput.py +++ b/lib/gridfinityUtils/binBodyGeneratorInput.py @@ -66,6 +66,8 @@ def __init__(self): self.tabMethod = const.BIN_TAB_METHOD_ANGLE self.rootThickness = const.BIN_TAB_DEFAULT_ROOT_THICKNESS self.tipThickness = const.BIN_TAB_DEFAULT_TIP_THICKNESS + self.tabFilletTop = const.BIN_TAB_DEFAULT_TIP_THICKNESS / 2 + self.tabFilletBottom = const.BIN_TAB_DEFAULT_TIP_THICKNESS / 2 self.compartments: list[BinBodyCompartmentDefinition] = [] self.compartmentsByX = 1 self.compartmentsByY = 1 diff --git a/lib/gridfinityUtils/binBodyTabGenerator.py b/lib/gridfinityUtils/binBodyTabGenerator.py index fb3807f..b84e6a5 100644 --- a/lib/gridfinityUtils/binBodyTabGenerator.py +++ b/lib/gridfinityUtils/binBodyTabGenerator.py @@ -208,12 +208,42 @@ def createGridfinityBinBodyTab( # Select all edges at that min_y (within float tolerance) front_edges = [edge for edge in x_edges if abs(edge.boundingBox.minPoint.y - min_y) < 0.001] - fillet = filletUtils.createFillet( - front_edges, - BIN_TAB_EDGE_FILLET_RADIUS, - False, - targetComponent - ) - fillet.name = 'label tab fillet' + if len(front_edges) >= 2: + # Sort by Z (height) desc. Top edge is first. + front_edges_sorted = sorted(front_edges, key=lambda e: e.boundingBox.minPoint.z, reverse=True) + top_edge = front_edges_sorted[0] + bottom_edge = front_edges_sorted[1] + + # Apply Top Fillet + if input.tabFilletTop > 0.001: + filletTop = filletUtils.createFillet( + [top_edge], + input.tabFilletTop, + False, + targetComponent + ) + filletTop.name = 'label tab fillet top' + + # Apply Bottom Fillet + if input.tabFilletBottom > 0.001: + filletBottom = filletUtils.createFillet( + [bottom_edge], + input.tabFilletBottom, + False, + targetComponent + ) + filletBottom.name = 'label tab fillet bottom' + elif len(front_edges) == 1: + # Maybe Angle method makes a sharp tip with only one edge? + # If so, just apply top fillet? Or maybe the user sees it as one edge. + # Let's apply Top radius. + if input.tabFilletTop > 0.001: + fillet = filletUtils.createFillet( + front_edges, + input.tabFilletTop, + False, + targetComponent + ) + fillet.name = 'label tab fillet' return tabBody diff --git a/lib/gridfinityUtils/binBodyTabGeneratorInput.py b/lib/gridfinityUtils/binBodyTabGeneratorInput.py index 364a2c7..26a09a3 100644 --- a/lib/gridfinityUtils/binBodyTabGeneratorInput.py +++ b/lib/gridfinityUtils/binBodyTabGeneratorInput.py @@ -10,6 +10,8 @@ def __init__(self): self._tabMethod = const.BIN_TAB_METHOD_ANGLE self._rootThickness = const.BIN_TAB_DEFAULT_ROOT_THICKNESS self._tipThickness = const.BIN_TAB_DEFAULT_TIP_THICKNESS + self._tabFilletTop = const.BIN_TAB_DEFAULT_TIP_THICKNESS / 2 + self._tabFilletBottom = const.BIN_TAB_DEFAULT_TIP_THICKNESS / 2 @property def topClearance(self) -> float: @@ -83,4 +85,20 @@ def tipThickness(self) -> float: def tipThickness(self, value: float): self._tipThickness = value + @property + def tabFilletTop(self) -> float: + return self._tabFilletTop + + @tabFilletTop.setter + def tabFilletTop(self, value: float): + self._tabFilletTop = value + + @property + def tabFilletBottom(self) -> float: + return self._tabFilletBottom + + @tabFilletBottom.setter + def tabFilletBottom(self, value: float): + self._tabFilletBottom = value + \ No newline at end of file diff --git a/lib/gridfinityUtils/const.py b/lib/gridfinityUtils/const.py index 7f106e7..9953858 100644 --- a/lib/gridfinityUtils/const.py +++ b/lib/gridfinityUtils/const.py @@ -30,10 +30,15 @@ BIN_TAB_UNIFORM_THICKNESS_INPUT_ID = 'bin_tab_uniform_thickness' BIN_TAB_TIP_THICKNESS_INPUT_ID = 'bin_tab_tip_thickness' +BIN_TAB_FILLET_UNIFORM_INPUT_ID = 'bin_tab_fillet_uniform' +BIN_TAB_FILLET_TOP_INPUT_ID = 'bin_tab_fillet_top' +BIN_TAB_FILLET_BOTTOM_INPUT_ID = 'bin_tab_fillet_bottom' + # Default values (Internal units are cm, so 0.14 = 1.4mm) BIN_TAB_DEFAULT_ROOT_THICKNESS = 0.14 BIN_TAB_DEFAULT_TIP_THICKNESS = 0.14 BIN_TAB_DEFAULT_IS_UNIFORM = True +BIN_TAB_DEFAULT_IS_FILLET_UNIFORM = True BIN_SCOOP_MAX_RADIUS = 2.5 From f99a24a38c5f67a62a6872ca44e15d179c1314d3 Mon Sep 17 00:00:00 2001 From: me Date: Wed, 31 Dec 2025 00:24:08 -0500 Subject: [PATCH 5/8] feat: Include tab fillet top and bottom in compartment tab input. --- lib/gridfinityUtils/binBodyGenerator.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/gridfinityUtils/binBodyGenerator.py b/lib/gridfinityUtils/binBodyGenerator.py index 0eda17f..41ec801 100644 --- a/lib/gridfinityUtils/binBodyGenerator.py +++ b/lib/gridfinityUtils/binBodyGenerator.py @@ -147,6 +147,8 @@ def createGridfinityBinBody( compartmentTabInput.tabMethod = input.tabMethod compartmentTabInput.rootThickness = input.rootThickness compartmentTabInput.tipThickness = input.tipThickness + compartmentTabInput.tabFilletTop = input.tabFilletTop + compartmentTabInput.tabFilletBottom = input.tabFilletBottom [compartmentMerges, compartmentCuts] = createCompartment( input.wallThickness, From e1131be55b149759e1818a9aed821592639910c1 Mon Sep 17 00:00:00 2001 From: me Date: Wed, 31 Dec 2025 00:31:28 -0500 Subject: [PATCH 6/8] Remove a test script, fix typos in tab input variable names, and refine minimum value constraints for tab position and thickness inputs. --- TestGridfinityTab.py | 80 ------------------------------ commands/commandCreateBin/entry.py | 22 ++++---- 2 files changed, 12 insertions(+), 90 deletions(-) delete mode 100644 TestGridfinityTab.py diff --git a/TestGridfinityTab.py b/TestGridfinityTab.py deleted file mode 100644 index dc00dec..0000000 --- a/TestGridfinityTab.py +++ /dev/null @@ -1,80 +0,0 @@ -import adsk.core, adsk.fusion, traceback -import math - -# Import the generator modules -# Note: We need to adjust path or copying/mocking might be needed if running as a separate script. -# Assuming this script is placed in the root of the add-in or similar location where it can import lib. - -from .lib.gridfinityUtils import binBodyGenerator, binBodyGeneratorInput, const -from .lib import fusion360utils as futil - -def run(context): - ui = None - try: - app = adsk.core.Application.get() - ui = app.userInterface - design = adsk.fusion.Design.cast(app.activeProduct) - - # Create a new document to avoid messing up the current one - doc = app.documents.add(adsk.core.DocumentTypes.FusionDesignDocumentType) - design = adsk.fusion.Design.cast(app.activeProduct) - root = design.rootComponent - - # Setup Input - binInput = binBodyGeneratorInput.BinBodyGeneratorInput() - binInput.binWidth = 42.0 # 1 unit - binInput.binLength = 42.0 # 1 unit - binInput.binHeight = 42.0 # 6 units (6*7) - binInput.isSolid = False - binInput.wallThickness = 1.2 - - # Tab Settings - binInput.hasTab = True - binInput.tabMethod = const.BIN_TAB_METHOD_DIMENSIONS - binInput.rootThickness = 0.14 # 1.4mm - binInput.tipThickness = 0.14 - binInput.tabLength = 12.0 # 12mm - binInput.tabWidth = 12.0 # Width of tab (along X axis? No, width is usually "Depth" of label area) - # Wait, tabWidth in UI maps to 'width' property in generatorInput? - # In entry.py: binBodyInput.tabWidth = binTabWidth.value - - # Run Generator - # We need a dummy 'innerCutoutBody' if we were running the full generator, - # but createGridfinityBinBody does the whole thing. - # However, createGridfinityBinBody requires 'baseGeneratorInput' context usually? - # Actually createGridfinityBinBody creates the bin body. - - # Let's call the high-level function that creates everything? - # No, let's call createGridfinityBinBody which is what generateBin calls. - - # Note: createGridfinityBinBody creates the bin, cutout, and tab. - binBody = binBodyGenerator.createGridfinityBinBody( - binInput, - root - ) - - # Assertions - # Check if we have bodies - if root.bRepBodies.count == 0: - ui.messageBox('Test Failed: No bodies created.') - return - - # Check Dimensions - # Bounding Box of the bin - bbox = binBody.boundingBox - # Check Height - height = bbox.maxPoint.z - bbox.minPoint.z - # Expected: 42.0 - if abs(height - 42.0) > 0.01: - ui.messageBox(f'Test Failed: Height mismatch. Expected 42.0, got {height}') - return - - # Check for Tab Feature - # The tab should add volume. - # Easier to visually verify or check if specific face exists. - - ui.messageBox('Test Passed: Bin created with Dimensions Tab.') - - except: - if ui: - ui.messageBox('Failed:\n{}'.format(traceback.format_exc())) diff --git a/commands/commandCreateBin/entry.py b/commands/commandCreateBin/entry.py index 57decd4..c13c2a6 100644 --- a/commands/commandCreateBin/entry.py +++ b/commands/commandCreateBin/entry.py @@ -633,14 +633,16 @@ def command_created(args: adsk.core.CommandCreatedEventArgs): commandUIState.registerCommandInput(binTabLengthInput) binTabWidthInput = binTabFeaturesGroup.children.addValueInput(BIN_TAB_WIDTH_INPUT_ID, 'Tab width (mm)', defaultLengthUnits, adsk.core.ValueInput.createByReal(commandUIState.getState(BIN_TAB_WIDTH_INPUT_ID))) commandUIState.registerCommandInput(binTabWidthInput) - binTabPostionInput = binTabFeaturesGroup.children.addValueInput(BIN_TAB_POSITION_INPUT_ID, 'Tab offset (u)', '', adsk.core.ValueInput.createByReal(commandUIState.getState(BIN_TAB_POSITION_INPUT_ID))) - commandUIState.registerCommandInput(binTabPostionInput) - tabObverhangAngleInput = binTabFeaturesGroup.children.addValueInput(BIN_TAB_ANGLE_INPUT_ID, 'Tab overhang angle', 'deg', adsk.core.ValueInput.createByString(str(commandUIState.getState(BIN_TAB_ANGLE_INPUT_ID)))) - tabObverhangAngleInput.minimumValue = math.radians(30) - tabObverhangAngleInput.isMinimumInclusive = True - tabObverhangAngleInput.maximumValue = math.radians(65) - tabObverhangAngleInput.isMaximumInclusive = True - commandUIState.registerCommandInput(tabObverhangAngleInput) + binTabPositionInput = binTabFeaturesGroup.children.addValueInput(BIN_TAB_POSITION_INPUT_ID, 'Tab offset (u)', '', adsk.core.ValueInput.createByReal(commandUIState.getState(BIN_TAB_POSITION_INPUT_ID))) + binTabPositionInput.minimumValue = 0.0 + binTabPositionInput.isMinimumInclusive = True + commandUIState.registerCommandInput(binTabPositionInput) + tabOverhangAngleInput = binTabFeaturesGroup.children.addValueInput(BIN_TAB_ANGLE_INPUT_ID, 'Tab overhang angle', 'deg', adsk.core.ValueInput.createByString(str(commandUIState.getState(BIN_TAB_ANGLE_INPUT_ID)))) + tabOverhangAngleInput.minimumValue = math.radians(30) + tabOverhangAngleInput.isMinimumInclusive = True + tabOverhangAngleInput.maximumValue = math.radians(65) + tabOverhangAngleInput.isMaximumInclusive = True + commandUIState.registerCommandInput(tabOverhangAngleInput) tabMethodDropdown = binTabFeaturesGroup.children.addDropDownCommandInput(BIN_TAB_METHOD_INPUT_ID, 'Tab construction method', adsk.core.DropDownStyles.LabeledIconDropDownStyle) tabMethodDropdownDefaultValue = commandUIState.getState(BIN_TAB_METHOD_INPUT_ID) @@ -649,7 +651,7 @@ def command_created(args: adsk.core.CommandCreatedEventArgs): commandUIState.registerCommandInput(tabMethodDropdown) rootThicknessInput = binTabFeaturesGroup.children.addValueInput(BIN_TAB_ROOT_THICKNESS_INPUT_ID, 'Root thickness (mm)', defaultLengthUnits, adsk.core.ValueInput.createByReal(commandUIState.getState(BIN_TAB_ROOT_THICKNESS_INPUT_ID))) - rootThicknessInput.minimumValue = 0.1 + rootThicknessInput.minimumValue = 0.001 rootThicknessInput.isMinimumInclusive = True commandUIState.registerCommandInput(rootThicknessInput) @@ -657,7 +659,7 @@ def command_created(args: adsk.core.CommandCreatedEventArgs): commandUIState.registerCommandInput(uniformThicknessInput) tipThicknessInput = binTabFeaturesGroup.children.addValueInput(BIN_TAB_TIP_THICKNESS_INPUT_ID, 'Tip thickness (mm)', defaultLengthUnits, adsk.core.ValueInput.createByReal(commandUIState.getState(BIN_TAB_TIP_THICKNESS_INPUT_ID))) - tipThicknessInput.minimumValue = 0.1 + tipThicknessInput.minimumValue = 0.001 tipThicknessInput.isMinimumInclusive = True commandUIState.registerCommandInput(tipThicknessInput) From a33429b972b092d29240f7e868a68ceccac72311 Mon Sep 17 00:00:00 2001 From: me Date: Wed, 31 Dec 2025 08:06:20 -0500 Subject: [PATCH 7/8] feat: Add back fillet option to bin tabs, including UI controls and generation logic. --- commands/commandCreateBin/entry.py | 10 +++ lib/gridfinityUtils/binBodyGenerator.py | 1 + lib/gridfinityUtils/binBodyGeneratorInput.py | 1 + lib/gridfinityUtils/binBodyTabGenerator.py | 77 ++++++++++++++++--- .../binBodyTabGeneratorInput.py | 9 +++ lib/gridfinityUtils/const.py | 2 + 6 files changed, 89 insertions(+), 11 deletions(-) diff --git a/commands/commandCreateBin/entry.py b/commands/commandCreateBin/entry.py index c13c2a6..621c282 100644 --- a/commands/commandCreateBin/entry.py +++ b/commands/commandCreateBin/entry.py @@ -103,6 +103,7 @@ BIN_TAB_FILLET_UNIFORM_INPUT_ID = 'bin_tab_fillet_uniform' BIN_TAB_FILLET_TOP_INPUT_ID = 'bin_tab_fillet_top' BIN_TAB_FILLET_BOTTOM_INPUT_ID = 'bin_tab_fillet_bottom' +BIN_TAB_FILLET_BACK_INPUT_ID = 'bin_tab_fillet_back' BIN_WITH_LIP_INPUT_ID = 'with_lip' BIN_WITH_LIP_NOTCHES_INPUT_ID = 'with_lip_notches' @@ -196,6 +197,7 @@ def initDefaultUiState(): # Fillets default to half the tip thickness (0.07) commandUIState.initValue(BIN_TAB_FILLET_TOP_INPUT_ID, const.BIN_TAB_DEFAULT_TIP_THICKNESS / 2, adsk.core.ValueCommandInput.classType()) commandUIState.initValue(BIN_TAB_FILLET_BOTTOM_INPUT_ID, const.BIN_TAB_DEFAULT_TIP_THICKNESS / 2, adsk.core.ValueCommandInput.classType()) + commandUIState.initValue(BIN_TAB_FILLET_BACK_INPUT_ID, 0.0, adsk.core.ValueCommandInput.classType()) commandUIState.initValue(BIN_TAB_FILLET_UNIFORM_INPUT_ID, const.BIN_TAB_DEFAULT_IS_FILLET_UNIFORM, adsk.core.BoolValueCommandInput.classType()) commandUIState.initValue(BIN_GENERATE_BASE_INPUT_ID, True, adsk.core.BoolValueCommandInput.classType()) @@ -673,6 +675,9 @@ def command_created(args: adsk.core.CommandCreatedEventArgs): bottomFilletInput = binTabFeaturesGroup.children.addValueInput(BIN_TAB_FILLET_BOTTOM_INPUT_ID, 'Bottom front fillet (mm)', defaultLengthUnits, adsk.core.ValueInput.createByReal(commandUIState.getState(BIN_TAB_FILLET_BOTTOM_INPUT_ID))) commandUIState.registerCommandInput(bottomFilletInput) + backFilletInput = binTabFeaturesGroup.children.addValueInput(BIN_TAB_FILLET_BACK_INPUT_ID, 'Back bottom fillet (mm)', defaultLengthUnits, adsk.core.ValueInput.createByReal(commandUIState.getState(BIN_TAB_FILLET_BACK_INPUT_ID))) + commandUIState.registerCommandInput(backFilletInput) + for input in binTabFeaturesGroup.children: if not input.id == BIN_HAS_TAB_INPUT_ID: input.isEnabled = commandUIState.getState(BIN_HAS_TAB_INPUT_ID) @@ -918,6 +923,9 @@ def onChangeValidate(): commandUIState.getInput(BIN_TAB_FILLET_BOTTOM_INPUT_ID).isVisible = generateTab commandUIState.getInput(BIN_TAB_FILLET_BOTTOM_INPUT_ID).isEnabled = generateTab and not isFilletUniform + commandUIState.getInput(BIN_TAB_FILLET_BACK_INPUT_ID).isVisible = generateTab + commandUIState.getInput(BIN_TAB_FILLET_BACK_INPUT_ID).isEnabled = generateTab + compartmentsGridType: str = commandUIState.getState(BIN_COMPARTMENTS_GRID_TYPE_ID) commandUIState.getInput(BIN_COMPARTMENTS_TABLE_ID).isVisible = compartmentsGridType == BIN_COMPARTMENTS_GRID_TYPE_CUSTOM @@ -1050,6 +1058,8 @@ def generateBin(args: adsk.core.CommandEventArgs): binBodyInput.tabFilletBottom = binBodyInput.tabFilletTop else: binBodyInput.tabFilletBottom = commandUIState.getState(BIN_TAB_FILLET_BOTTOM_INPUT_ID) + + binBodyInput.tabFilletBack = commandUIState.getState(BIN_TAB_FILLET_BACK_INPUT_ID) binBodyInput.compartmentsByX = compartmentsX.value binBodyInput.compartmentsByY = compartmentsY.value diff --git a/lib/gridfinityUtils/binBodyGenerator.py b/lib/gridfinityUtils/binBodyGenerator.py index 41ec801..872fd68 100644 --- a/lib/gridfinityUtils/binBodyGenerator.py +++ b/lib/gridfinityUtils/binBodyGenerator.py @@ -149,6 +149,7 @@ def createGridfinityBinBody( compartmentTabInput.tipThickness = input.tipThickness compartmentTabInput.tabFilletTop = input.tabFilletTop compartmentTabInput.tabFilletBottom = input.tabFilletBottom + compartmentTabInput.tabFilletBack = input.tabFilletBack [compartmentMerges, compartmentCuts] = createCompartment( input.wallThickness, diff --git a/lib/gridfinityUtils/binBodyGeneratorInput.py b/lib/gridfinityUtils/binBodyGeneratorInput.py index 03caff9..6371e31 100644 --- a/lib/gridfinityUtils/binBodyGeneratorInput.py +++ b/lib/gridfinityUtils/binBodyGeneratorInput.py @@ -68,6 +68,7 @@ def __init__(self): self.tipThickness = const.BIN_TAB_DEFAULT_TIP_THICKNESS self.tabFilletTop = const.BIN_TAB_DEFAULT_TIP_THICKNESS / 2 self.tabFilletBottom = const.BIN_TAB_DEFAULT_TIP_THICKNESS / 2 + self.tabFilletBack = const.BIN_TAB_DEFAULT_FILLET_BACK self.compartments: list[BinBodyCompartmentDefinition] = [] self.compartmentsByX = 1 self.compartmentsByY = 1 diff --git a/lib/gridfinityUtils/binBodyTabGenerator.py b/lib/gridfinityUtils/binBodyTabGenerator.py index b84e6a5..2597a82 100644 --- a/lib/gridfinityUtils/binBodyTabGenerator.py +++ b/lib/gridfinityUtils/binBodyTabGenerator.py @@ -99,18 +99,73 @@ def createGridfinityBinBodyTab( l_top = tabSketchLine.addByTwoPoints(tabSketch.modelToSketchSpace(p_top_back), tabSketch.modelToSketchSpace(p_top_front)) l_front = tabSketchLine.addByTwoPoints(tabSketch.modelToSketchSpace(p_top_front), tabSketch.modelToSketchSpace(p_bot_front)) l_bot = tabSketchLine.addByTwoPoints(tabSketch.modelToSketchSpace(p_bot_front), tabSketch.modelToSketchSpace(p_bot_back)) - l_back = tabSketchLine.addByTwoPoints(tabSketch.modelToSketchSpace(p_bot_back), tabSketch.modelToSketchSpace(p_top_back)) + # Debug logging + futil.log(f"DEBUG: Tab Back Fillet Value: {input.tabFilletBack}") - # Constraints - # Removing geometric constraints (Horizontal/Vertical) to avoid conflict with Sketch Plane orientation. - # The points are calculated in World Space to be correct, so we rely on their explicit positions. - - # We still chain the lines to ensure a closed profile (Coincident is usually implicit with shared points in API, - # but explicit Coincident on end points guarantees connectivity for Profile creation). - constraints.addCoincident(l_top.endSketchPoint, l_front.startSketchPoint) - constraints.addCoincident(l_front.endSketchPoint, l_bot.startSketchPoint) - constraints.addCoincident(l_bot.endSketchPoint, l_back.startSketchPoint) - constraints.addCoincident(l_back.endSketchPoint, l_top.startSketchPoint) + if input.tabFilletBack > 0.001: + futil.log("DEBUG: Generating Back Fillet Geometry") + # Back Fillet (Support) Logic + # We want to fillet the corner between l_bot and the Wall (downwards). + # Epsilon shift to ensure the wedge is 'inside' the cutout void for Intersection. + epsilon = 0.0001 + y_back_shifted = y_back - epsilon + + # 1. Create temporary wall extension line downwards. + p_wall_low = adsk.core.Point3D.create(input.origin.x, y_back_shifted, z_root_bottom - input.tabFilletBack * 10.0 - 1.0) # sufficiently low + l_wall_temp = tabSketchLine.addByTwoPoints(tabSketch.modelToSketchSpace(p_bot_back), tabSketch.modelToSketchSpace(p_wall_low)) + + # 2. Add Fillet between l_bot and l_wall_temp + # Note: l_bot ends at p_bot_back. l_wall_temp starts at p_bot_back. + # addFillet needs the lines to share the point or intersect. + arc = tabSketch.sketchCurves.sketchArcs.addFillet(l_bot, l_bot.endSketchPoint.geometry, l_wall_temp, l_wall_temp.startSketchPoint.geometry, input.tabFilletBack) + + # 3. Clean up + # l_bot is now trimmed. arc connects l_bot end to l_wall_temp start (shifted). + # The segment of l_wall_temp from 'tangent point' to 'p_wall_low' remains. We assume we don't want a "tail". + # We want the profile to close along the wall from the arc end to the top. + # The 'arc' ends at the wall tangent. + # We delete l_wall_temp. + # We create l_back from arc.endSketchPoint (or start, depending on geometry) to p_top_back. + + # Identify the point on the wall. The arc connects approximate l_bot (slope) to l_wall_temp (vertical). + # One endpoint of arc is on the wall vertical line. + # We can check X coordinate in sketch space? Sketch X = Model Y = y_back. + # Sketch Y = Model Z. + # Check p_top_back (y=y_back). + + p1 = arc.startSketchPoint + p2 = arc.endSketchPoint + # The wall point is the one with the LARGER X (closer to back/y_back). + p_wall_tan = p1 if p1.geometry.x > p2.geometry.x else p2 + + sp_top_back = tabSketch.modelToSketchSpace(p_top_back) + + l_wall_temp.deleteMe() + + # 4. Create Back Line + # We connect p_wall_tan to p_top_back. + # Note: p_top_back is at y_back. p_wall_tan is at y_back_shifted. + # This creates a tiny slope on the back wall of the tab. + l_back = tabSketchLine.addByTwoPoints(p_wall_tan, sp_top_back) + + # Constraint Chain needs update because l_bot end changed and l_back start changed. + constraints.addCoincident(l_top.endSketchPoint, l_front.startSketchPoint) + constraints.addCoincident(l_front.endSketchPoint, l_bot.startSketchPoint) + # constraints.addCoincident(l_bot.endSketchPoint, arc.generatedPoint) -> Not needed, addFillet handles this. + + # Make sure l_back (Wall) is tangent to the arc (Smooth transition) + constraints.addTangent(l_back, arc) + + # Close the loop at the top + constraints.addCoincident(l_back.endSketchPoint, l_top.startSketchPoint) + + else: + l_back = tabSketchLine.addByTwoPoints(tabSketch.modelToSketchSpace(p_bot_back), tabSketch.modelToSketchSpace(p_top_back)) + + constraints.addCoincident(l_top.endSketchPoint, l_front.startSketchPoint) + constraints.addCoincident(l_front.endSketchPoint, l_bot.startSketchPoint) + constraints.addCoincident(l_bot.endSketchPoint, l_back.startSketchPoint) + constraints.addCoincident(l_back.endSketchPoint, l_top.startSketchPoint) # Let's verify standard Orientation. YZ plane. U=Y, V=Z. # Horizontal constraint in Fusion sketch usually means parallel to X axis of sketch. # Sketch X axis usually corresponds to Model Y for YZ plane. diff --git a/lib/gridfinityUtils/binBodyTabGeneratorInput.py b/lib/gridfinityUtils/binBodyTabGeneratorInput.py index 26a09a3..8bd28a9 100644 --- a/lib/gridfinityUtils/binBodyTabGeneratorInput.py +++ b/lib/gridfinityUtils/binBodyTabGeneratorInput.py @@ -12,6 +12,7 @@ def __init__(self): self._tipThickness = const.BIN_TAB_DEFAULT_TIP_THICKNESS self._tabFilletTop = const.BIN_TAB_DEFAULT_TIP_THICKNESS / 2 self._tabFilletBottom = const.BIN_TAB_DEFAULT_TIP_THICKNESS / 2 + self._tabFilletBack = const.BIN_TAB_DEFAULT_FILLET_BACK @property def topClearance(self) -> float: @@ -100,5 +101,13 @@ def tabFilletBottom(self) -> float: @tabFilletBottom.setter def tabFilletBottom(self, value: float): self._tabFilletBottom = value + + @property + def tabFilletBack(self) -> float: + return self._tabFilletBack + + @tabFilletBack.setter + def tabFilletBack(self, value: float): + self._tabFilletBack = value \ No newline at end of file diff --git a/lib/gridfinityUtils/const.py b/lib/gridfinityUtils/const.py index 9953858..6d1138e 100644 --- a/lib/gridfinityUtils/const.py +++ b/lib/gridfinityUtils/const.py @@ -33,12 +33,14 @@ BIN_TAB_FILLET_UNIFORM_INPUT_ID = 'bin_tab_fillet_uniform' BIN_TAB_FILLET_TOP_INPUT_ID = 'bin_tab_fillet_top' BIN_TAB_FILLET_BOTTOM_INPUT_ID = 'bin_tab_fillet_bottom' +BIN_TAB_FILLET_BACK_INPUT_ID = 'bin_tab_fillet_back' # Default values (Internal units are cm, so 0.14 = 1.4mm) BIN_TAB_DEFAULT_ROOT_THICKNESS = 0.14 BIN_TAB_DEFAULT_TIP_THICKNESS = 0.14 BIN_TAB_DEFAULT_IS_UNIFORM = True BIN_TAB_DEFAULT_IS_FILLET_UNIFORM = True +BIN_TAB_DEFAULT_FILLET_BACK = 0.0 BIN_SCOOP_MAX_RADIUS = 2.5 From 0f5609a9f2146a0c5e3aceeb20421d840f2d538c Mon Sep 17 00:00:00 2001 From: me Date: Wed, 31 Dec 2025 10:54:58 -0500 Subject: [PATCH 8/8] refactor: remove verbose comments and debug logs from bin body tab generation. --- lib/gridfinityUtils/binBodyTabGenerator.py | 93 +--------------------- 1 file changed, 2 insertions(+), 91 deletions(-) diff --git a/lib/gridfinityUtils/binBodyTabGenerator.py b/lib/gridfinityUtils/binBodyTabGenerator.py index 2597a82..f16d676 100644 --- a/lib/gridfinityUtils/binBodyTabGenerator.py +++ b/lib/gridfinityUtils/binBodyTabGenerator.py @@ -42,121 +42,50 @@ def createGridfinityBinBodyTab( constraints = tabSketch.geometricConstraints dimensions = tabSketch.sketchDimensions - # Calculate tab top edge start point - # tab is centered on the bin width - # tab is positioned on the bin length (y) - # tab starts at bin top heightput.origin.z - input.topClearance tabTopEdgeHeight = input.origin.z - input.topClearance if input.tabMethod == const.BIN_TAB_METHOD_DIMENSIONS: - # Dimensions based generation rootThickness = input.rootThickness tipThickness = input.tipThickness - # Points in sketch coordinates (Y seems to be width/depth, X is height/Z) - # Based on previous code: - # tabTopEdgeHeight is X in sketch (Z in model) - # input.origin.y is Y in sketch? No, see addByTwoPoints below. - - # Previous code: - # line1 (Vertical in sketch?): (origin.x, origin.y, tabTopEdgeHeight) -> (origin.x, origin.y, tabTopEdgeHeight - actualTabHeight) - # wait, modelToSketchSpace takes Point3D. - # The plane is offset from YZ plane by X. - # So Sketch Plane X = Model Y, Sketch Plane Y = Model Z. - - # Let's look at the previous code's points: - # P1: (origin.x, origin.y, tabTopEdgeHeight) - # P2: (origin.x, origin.y, tabTopEdgeHeight - actualTabHeight) - # P3: (origin.x, origin.y - actualTabWidth, tabTopEdgeHeight) - # P4: (origin.x, origin.y - actualTabWidth, tabTopEdgeHeight) (Used in line 3) - - # Model coords: - # origin.x is strictly X. - # origin.y varies by -actualTabWidth. - # tabTopEdgeHeight (Z) varies by -actualTabHeight. - - # So we want: - # Top-Back (Wall): (origin.x, origin.y, tabTopEdgeHeight) - # Top-Front (Tip): (origin.x, origin.y - input.width, tabTopEdgeHeight) - # Bottom-Front (Tip): (origin.x, origin.y - input.width, tabTopEdgeHeight - tipThickness) - # Bottom-Back (Wall): (origin.x, origin.y, tabTopEdgeHeight - rootThickness) - - # World Coordinates (X, Y, Z) y_back = input.origin.y y_front = input.origin.y - input.width z_top = tabTopEdgeHeight z_root_bottom = tabTopEdgeHeight - rootThickness z_tip_bottom = tabTopEdgeHeight - tipThickness - # P_top_back: At wall, top edge. p_top_back = adsk.core.Point3D.create(input.origin.x, y_back, z_top) - # P_top_front: At tip, top edge. p_top_front = adsk.core.Point3D.create(input.origin.x, y_front, z_top) - # P_bot_front: At tip, bottom edge. p_bot_front = adsk.core.Point3D.create(input.origin.x, y_front, z_tip_bottom) - # P_bot_back: At wall, bottom edge. p_bot_back = adsk.core.Point3D.create(input.origin.x, y_back, z_root_bottom) l_top = tabSketchLine.addByTwoPoints(tabSketch.modelToSketchSpace(p_top_back), tabSketch.modelToSketchSpace(p_top_front)) l_front = tabSketchLine.addByTwoPoints(tabSketch.modelToSketchSpace(p_top_front), tabSketch.modelToSketchSpace(p_bot_front)) l_bot = tabSketchLine.addByTwoPoints(tabSketch.modelToSketchSpace(p_bot_front), tabSketch.modelToSketchSpace(p_bot_back)) - # Debug logging - futil.log(f"DEBUG: Tab Back Fillet Value: {input.tabFilletBack}") if input.tabFilletBack > 0.001: - futil.log("DEBUG: Generating Back Fillet Geometry") - # Back Fillet (Support) Logic - # We want to fillet the corner between l_bot and the Wall (downwards). - # Epsilon shift to ensure the wedge is 'inside' the cutout void for Intersection. epsilon = 0.0001 y_back_shifted = y_back - epsilon - # 1. Create temporary wall extension line downwards. - p_wall_low = adsk.core.Point3D.create(input.origin.x, y_back_shifted, z_root_bottom - input.tabFilletBack * 10.0 - 1.0) # sufficiently low + p_wall_low = adsk.core.Point3D.create(input.origin.x, y_back_shifted, z_root_bottom - input.tabFilletBack * 10.0 - 1.0) l_wall_temp = tabSketchLine.addByTwoPoints(tabSketch.modelToSketchSpace(p_bot_back), tabSketch.modelToSketchSpace(p_wall_low)) - # 2. Add Fillet between l_bot and l_wall_temp - # Note: l_bot ends at p_bot_back. l_wall_temp starts at p_bot_back. - # addFillet needs the lines to share the point or intersect. arc = tabSketch.sketchCurves.sketchArcs.addFillet(l_bot, l_bot.endSketchPoint.geometry, l_wall_temp, l_wall_temp.startSketchPoint.geometry, input.tabFilletBack) - # 3. Clean up - # l_bot is now trimmed. arc connects l_bot end to l_wall_temp start (shifted). - # The segment of l_wall_temp from 'tangent point' to 'p_wall_low' remains. We assume we don't want a "tail". - # We want the profile to close along the wall from the arc end to the top. - # The 'arc' ends at the wall tangent. - # We delete l_wall_temp. - # We create l_back from arc.endSketchPoint (or start, depending on geometry) to p_top_back. - - # Identify the point on the wall. The arc connects approximate l_bot (slope) to l_wall_temp (vertical). - # One endpoint of arc is on the wall vertical line. - # We can check X coordinate in sketch space? Sketch X = Model Y = y_back. - # Sketch Y = Model Z. - # Check p_top_back (y=y_back). - p1 = arc.startSketchPoint p2 = arc.endSketchPoint - # The wall point is the one with the LARGER X (closer to back/y_back). p_wall_tan = p1 if p1.geometry.x > p2.geometry.x else p2 sp_top_back = tabSketch.modelToSketchSpace(p_top_back) l_wall_temp.deleteMe() - # 4. Create Back Line - # We connect p_wall_tan to p_top_back. - # Note: p_top_back is at y_back. p_wall_tan is at y_back_shifted. - # This creates a tiny slope on the back wall of the tab. l_back = tabSketchLine.addByTwoPoints(p_wall_tan, sp_top_back) - # Constraint Chain needs update because l_bot end changed and l_back start changed. constraints.addCoincident(l_top.endSketchPoint, l_front.startSketchPoint) constraints.addCoincident(l_front.endSketchPoint, l_bot.startSketchPoint) - # constraints.addCoincident(l_bot.endSketchPoint, arc.generatedPoint) -> Not needed, addFillet handles this. - # Make sure l_back (Wall) is tangent to the arc (Smooth transition) constraints.addTangent(l_back, arc) - # Close the loop at the top constraints.addCoincident(l_back.endSketchPoint, l_top.startSketchPoint) else: @@ -166,26 +95,12 @@ def createGridfinityBinBodyTab( constraints.addCoincident(l_front.endSketchPoint, l_bot.startSketchPoint) constraints.addCoincident(l_bot.endSketchPoint, l_back.startSketchPoint) constraints.addCoincident(l_back.endSketchPoint, l_top.startSketchPoint) - # Let's verify standard Orientation. YZ plane. U=Y, V=Z. - # Horizontal constraint in Fusion sketch usually means parallel to X axis of sketch. - # Sketch X axis usually corresponds to Model Y for YZ plane. - - - - # l_back (wall) should be vertical (Along Z/V, Perpendicular to Y/U) - # Dimensions - # Width (Top edge length) dimensions.addDistanceDimension(l_top.startSketchPoint, l_top.endSketchPoint, adsk.fusion.DimensionOrientations.HorizontalDimensionOrientation, l_top.startSketchPoint.geometry, True) - - # Root Thickness (Back edge length) dimensions.addDistanceDimension(l_back.startSketchPoint, l_back.endSketchPoint, adsk.fusion.DimensionOrientations.VerticalDimensionOrientation, l_back.startSketchPoint.geometry, True) - - # Tip Thickness (Front edge length) dimensions.addDistanceDimension(l_front.startSketchPoint, l_front.endSketchPoint, adsk.fusion.DimensionOrientations.VerticalDimensionOrientation, l_front.startSketchPoint.geometry, True) - else: # Angle based (Default) actualTabWidth = input.width + BIN_TAB_EDGE_FILLET_RADIUS / math.tan((math.radians(90) - input.overhangAngle) / 2) @@ -252,8 +167,7 @@ def createGridfinityBinBodyTab( tabBody = tabExtrudeFeature.bodies.item(0) tabBody.name = 'label tab' - # Fillet logic - # User wants both Top and Bottom edges of the tip to be filleted. + # Fillet both Top and Bottom edges. # We find all X-collinear edges. x_edges = [edge for edge in tabBody.edges if geometryUtils.isCollinearToX(edge)] @@ -289,9 +203,6 @@ def createGridfinityBinBodyTab( ) filletBottom.name = 'label tab fillet bottom' elif len(front_edges) == 1: - # Maybe Angle method makes a sharp tip with only one edge? - # If so, just apply top fillet? Or maybe the user sees it as one edge. - # Let's apply Top radius. if input.tabFilletTop > 0.001: fillet = filletUtils.createFillet( front_edges,