Привет, друзья! Сегодня я расскажу вам, как своими руками написать небольшое расширение для известной САПР Fusion 360.
Хоть Autodesk и не работает на территории РФ, сам Fusion 360 вполне функционирует, да и бесплатную хоббийную лицензию на него получить все еще можно, так что, надеюсь, статья найдет своего читателя.
Немного предыстории: увлекаюсь я, кроме разработки, еще и робототехникой и 3-д печатью. А робототехника требует, если у вас конечно нет 100500 денег на готовые сервоприводы, изготовления механических редукторов. И редукторы те должны быть с большим передаточным числом, потому что моторы с большим моментом тоже зело недешевые. В основном все хоббийщики делают либо волновые, либо циклоидальные редукторы, особенно, конечно, циклоидальные — они почти идеальны для домашнего производства на 3-д принтере. Но, мне как-то на просторах интернета попался еще один тип редуктора — волновой с промежуточными телами качения (далее по тексту буду использовать аббревиатуру ВПТК). Так вот, если для построения циклоидных редукторов существует великое множество аддонов и скриптов для того же фьюжена, то для ВПТК таких не нашлось, нашелся только скрипт для генерации профиля циклоиды с сохранением в dxf. В принципе этого бы и хватило, но делать особо было нечего, и я решил сделать аддон для Fusion 360, который бы строил этот редуктор целиком.
Шаг первый. Создаем новый аддон
Для начала надо перейти на вкладку UTILITIES и найти там иконку «scripts and add-ins», если на нее нажать, то откроется такое окно:
где и нужно выбрать «create script or addin».
Заполнить название, автора и прочую информацию, если хотите. Fusion создаст шаблонный проект с приблизительно вот такой структурой:
Открываем этот проект своей любимой IDE (я использовал PyCahrm). После чего удаляем ненужные нам каталоги с учебными примерами команд, и создаем каталог для своей коменды (я ее назвал createWaveDrive). Скопипастим в него файлы init.py, entry.py и каталог resources из шаблонной папки commandDialog. Так же надо отредактировать файл init.py в каталоге commands убрав лишние импорты и добавив свой:
#это удаляем: from .commandDialog import entry as commandDialog from .paletteShow import entry as paletteShow from .paletteSend import entry as paletteSend #а это добавляем: from .createWaveDrive import entry as createWaveDrive commands = [ #и это удаляем: commandDialog, paletteShow, paletteSend #а это добавляем: createWaveDrive ]
по итогу структура проекта окажется примерно такой:
Шаг второй. Создаем пользовательский интерфейс
Теперь нам надо создать интрефейс нашего аддона: кнопку вызова диалога создания редуктора и сам диалог. Для этого нам нужно перейти в файл entry.py.
Отредактируем константы под наше расширение:
CMD_ID = f'{config.COMPANY_NAME}_{config.ADDIN_NAME}_waveDriveDialog' CMD_NAME = 'Wave Drive Creation Dialog' CMD_Description = 'Create wave drive with roller elements'
И удалим тела функций command_created, command_execute, command_input_changed, command_validate_input:
def command_created(args: adsk.core.CommandCreatedEventArgs): pass def command_execute(args: adsk.core.CommandEventArgs): pass def command_input_changed(args: adsk.core.InputChangedEventArgs): pass def command_validate_input(args: adsk.core.ValidateInputsEventArgs): pass
Они нам пока не нужны. Функцию command_destroy оставляем как есть — ее менять не потребуется.
Немного отредактируем функцию start (подробно на ней останавливаться не будем, она достаточно очевидна):
по умолчанию кнопка аддона добавляется рядом с кнопкой «scripts and addins», что не очень наглядно, перенесем ее на свою личную панель.
вместо
panel = workspace.toolbarPanels.itemById(PANEL_ID)
напишем
panels = workspace.toolbarPanels panel = panels.itemById(PANEL_ID) if panel: panel.deleteMe() panel = panels.add(PANEL_ID, 'ROLLER WAVE DRIVE', 'SelectPanel', False)
По желанию можно отредактировать иконки в каталоге resources, я заменил их на иконки передачи. Теперь при запуске расширения будет появляться кнопка на дополнительной панели справа.
Добавим реакцию на нажатие. Для начала добавим константы идентификаторов полей ввода диалога:
ID_ROLLER_DIAMETER = 'roller_diameter' ID_ROLLERS_NUMBER = 'rollers_number' ID_USE_BALLS = 'use_balls' ID_ROLLER_HEIGHT = 'roller_height' ID_USE_MINIMAL_DIAMETER = 'use_minimal_diameter' ID_CYCLOID_DIAMETER = 'cycloid_diameter' ID_INPUT_SHAFT_DIAMETER = 'input_shaft_diameter' ID_INPUT_PLANE = 'input_plane' ID_ROLLER_TOLERANCE = 'roller_tolerance' ID_BODY_DIAMETER = 'body_diameter' ID_BEARING_OUTER_DIAMETER = 'bearing_outer_diameter' ID_BEARING_INNER_DIAMETER = 'bearing_inner_diameter' ID_BEARING_HEIGHT = 'bearing_height'
после чего в функции command_created создадим, собственно, диалог:
#сохраним единицы длины выставленные в системе. можно указывать явно строками 'cm', 'mm', 'in' len_units = app.activeProduct.unitsManager.defaultLengthUnits #ссылка на контейнер элементов управления inputs = args.command.commandInputs # Создаем диалог # Сверху добавим небольшое схематическое изображение передачи inputs.addImageCommandInput('image', '', 'commands/createWaveDrive/resources/diagram.png') # Поле ввода для чисел, по умолчанию 6 inputs.addValueInput(ID_ROLLER_DIAMETER, 'Roller diameter', len_units, adsk.core.ValueInput.createByString('6')) # Поле ввода целых чисел из диапазона со стрелками инкремена и декремента inputs.addIntegerSpinnerCommandInput(ID_ROLLERS_NUMBER, 'Rollers number', 5, 100, 1, 17) # Чекбокс inputs.addBoolValueInput(ID_USE_BALLS, 'Use balls', True, '', False) inputs.addValueInput(ID_ROLLER_HEIGHT, 'Roller height', len_units, adsk.core.ValueInput.createByString('6')) inputs.addBoolValueInput(ID_USE_MINIMAL_DIAMETER, 'Use minimal cycloid diameter', True, '', False) inputs.addValueInput(ID_CYCLOID_DIAMETER, 'Cycloid outer diameter', len_units, adsk.core.ValueInput.createByString('75')) inputs.addValueInput(ID_BODY_DIAMETER, 'Body diameter', len_units, adsk.core.ValueInput.createByString('80')) inputs.addValueInput(ID_INPUT_SHAFT_DIAMETER, 'Input shaft diameter', len_units, adsk.core.ValueInput.createByString('5')) inputs.addValueInput(ID_ROLLER_TOLERANCE, 'Rollers tolerance', len_units, adsk.core.ValueInput.createByString('0.1')) inputs.addValueInput(ID_BEARING_OUTER_DIAMETER, 'Bearing outer diameter', len_units, adsk.core.ValueInput.createByString('21')) inputs.addValueInput(ID_BEARING_INNER_DIAMETER, 'Bearing inner diameter', len_units, adsk.core.ValueInput.createByString('12')) inputs.addValueInput(ID_BEARING_HEIGHT, 'Bearing height', len_units, adsk.core.ValueInput.createByString('5')) # Пикер объектов. В фильтре указываем, что можно выбирать плоскости и плоские грани # Позволить строить редуктор не только в дефолтной ориентации, но и на люой плоскости или грани plane_select = inputs.addSelectionInput(ID_INPUT_PLANE, 'Input plane', 'select a plane') plane_select.addSelectionFilter(adsk.core.SelectionCommandInput.PlanarFaces) plane_select.addSelectionFilter(adsk.core.SelectionCommandInput.ConstructionPlanes) plane_select.setSelectionLimits(1, 1) # В конце приаттачим остальные хендлеры к событиям диалога futil.add_handler(args.command.execute, command_execute, local_handlers=local_handlers) futil.add_handler(args.command.inputChanged, command_input_changed, local_handlers=local_handlers) futil.add_handler(args.command.validateInputs, command_validate_input, local_handlers=local_handlers) futil.add_handler(args.command.destroy, command_destroy, local_handlers=local_handlers)
Если теперь запустить аддон и нажать на кнопку получим вот такой красивый диалог, который пока ничего не делает.
Валидацию входных значений в рамках статьи опустим — она и так выходит не короткая. Так, что следующие это….
Шаг третий. Получаем значения параметров
Для начала, чтоб не кидаться куче отдельных параметров создадим небольшую ДТО для параметров:
class RollerWaveDriveParams: RESOLUTION = 8 ECCENTRICITY = 0.2 def __init__(self, roller_diameter: float, rollers_number: int, use_balls: bool, roller_height: float, use_minimal_diameter: bool, cycloid_diameter: float, shaft_diameter: float, roller_tolerance: float, body_diameter: float, bearing_outer_diameter: float, bearing_inner_diameter: float, bearing_height: float): self.roller_diameter = roller_diameter self.roller_number = rollers_number self.use_balls = use_balls self._roller_height = roller_height self.use_minimal_diameter = use_minimal_diameter self.cycloid_diameter = cycloid_diameter self.shaft_diameter = shaft_diameter self.roller_tolerance = roller_tolerance self._body_diameter = body_diameter self.bearing_outer_diameter = bearing_outer_diameter self.bearing_inner_diameter = bearing_inner_diameter self.bearing_height = bearing_height
а во вторую очередь добавим фенкцию, которая будет заполнять эту DTO на основе полей диалога
def get_params_from_inputs(inputs: adsk.core.CommandInputs) -> RollerWaveDriveParams: #получаем поля ввода по их идентификаторам roller_diameter_input: adsk.core.ValueCommandInput = inputs.itemById(ID_ROLLER_DIAMETER) rollers_number_input: adsk.core.IntegerSpinnerCommandInput = inputs.itemById(ID_ROLLERS_NUMBER) use_balls_input: adsk.core.BoolValueCommandInput = inputs.itemById(ID_USE_BALLS) roller_height_input: adsk.core.ValueCommandInput = inputs.itemById(ID_ROLLER_HEIGHT) use_minimal_diameter_input: adsk.core.BoolValueCommandInput = inputs.itemById(ID_USE_MINIMAL_DIAMETER) cycloid_diameter_input: adsk.core.ValueCommandInput = inputs.itemById(ID_CYCLOID_DIAMETER) shaft_diameter_input: adsk.core.ValueCommandInput = inputs.itemById(ID_INPUT_SHAFT_DIAMETER) rollers_tolerance_input: adsk.core.ValueCommandInput = inputs.itemById(ID_ROLLER_TOLERANCE) bearing_outer_diameter_input: adsk.core.ValueCommandInput = inputs.itemById(ID_BEARING_OUTER_DIAMETER) bearing_inner_diameter_input: adsk.core.ValueCommandInput = inputs.itemById(ID_BEARING_INNER_DIAMETER) bearing_height_input: adsk.core.ValueCommandInput = inputs.itemById(ID_BEARING_HEIGHT) body_diameter_input: adsk.core.ValueCommandInput = inputs.itemById(ID_BODY_DIAMETER) #И передаем их значения в конструктор return RollerWaveDriveParams( roller_diameter_input.value, rollers_number_input.value, use_balls_input.value, roller_height_input.value, use_minimal_diameter_input.value, cycloid_diameter_input.value, shaft_diameter_input.value, rollers_tolerance_input.value, body_diameter_input.value, bearing_outer_diameter_input.value, bearing_inner_diameter_input.value, bearing_height_input.value )
Шаг четвертый. Рисуем редуктор
Переходим к самому главному и интересному — будем рисовать, собственно, передачу. Идем в функцию command_execute, и добавляем в нее:
def command_execute(args: adsk.core.CommandEventArgs): #Получаем ссылку на контролы диалога inputs = args.command.commandInputs #и на корневой компонент дизайна root = design.rootComponent #При помощи фенкции из предыдущего шага получаем параметры передачи params = get_params_from_inputs(inputs) #И получаем плоскость для построения редуктора plane_input: adsk.core.SelectionCommandInput = inputs.itemById(ID_INPUT_PLANE) plane: ConstructionPlane = plane_input.selection(0).entity #Создадим новый компонент для редуктора. Некоторые расширения строят прямо в корневом #но мне так не удобно. component = root.occurrences.addNewComponent(adsk.core.Matrix3D.create()).component #Назовем его как-нибудь component.name = 'RollerWaveDrive-1-to-{}'.format(params.roller_number) #Сохраним размер таймлайна start_index = design.timeline.count - 1 #Нарисуем внешнюю циклоиду редуктора draw_gear(params, component, plane) #Нарисуем сепаратор draw_separator(params, component, plane) #Нарисуем эксцентрик draw_cam(params, component, plane) #И, если у нас стоит в параметрах "использовать шарики" if params.use_balls: #То нарисуем шарики draw_balls(params, component, plane) else: #А иначе ролики draw_rollers(params, component, plane) #Все что мы нарисовали свернем на таймлайне в группу, чтоб не захламлять его design.timeline.timelineGroups.add(start_index, design.timeline.count - 1)
Теперь перейдем к реализации функций рисования частей редуктора. Начнем с самого сложного — циклоиды.
def draw_gear(params: RollerWaveDriveParams, component: Component, plane: ConstructionPlane): #Число впадин на ободе num_dimples = params.roller_number + 1 #Радиус шарика ball_radius = params.roller_diameter / 2 eccentricity = params.eccentricity #Создадим новый скетч на выбранной плоскости profile_sketch = component.sketches.add(plane) profile_sketch.name = 'Wheel' #И массив точек для сплайна циклоиды points = adsk.core.ObjectCollection.create() for i in range(params.resolution): theta = math.pi * 2.0 * i / params.resolution S = math.sqrt( (ball_radius + params.cam_radius) ** 2 - math.pow(eccentricity * math.sin(num_dimples * theta), 2)) l = eccentricity * math.cos(num_dimples * theta) + S xi = math.atan2(eccentricity * num_dimples * math.sin(num_dimples * theta), S) x = l * math.sin(theta) + ball_radius * math.sin(theta + xi) y = l * math.cos(theta) + ball_radius * math.cos(theta + xi) #Добавляем новые точки циклоиды в коллекцию point = adsk.core.Point3D.create(x, y, 0) points.add(point) #Добавим перую точку, как последнюю, чтобы кривая вышла замкнутой points.add(points[0]) #Создадим сплайн из точек и сделаем его замкнутым profile_spline = profile_sketch.sketchCurves.sketchFittedSplines.add(points) profile_spline.isClosed = True #Нарисуем окружность внешней части колеса profile_sketch.sketchCurves.sketchCircles.addByCenterRadius(adsk.core.Point3D.create(0, 0, 0), params.body_diameter) #берем фичи выдавливания extrudes = component.features.extrudeFeatures #взьмем из нашего скетча профиль для выдавливания prof = profile_sketch.profiles.item(0) #расстояние выдавливания distance = adsk.core.ValueInput.createByReal(get_extrusion_height(params)) #и выдавливаем профиль. Получаем сплошной объект - внешнюю часть редуктора disk_extrude = extrudes.addSimple(prof, distance, adsk.fusion.FeatureOperations.NewBodyFeatureOperation) disk_extrude.bodies.item(0).name = "CycloidWheel"
Небольшая проблема кроется в получении профиля для операций выдавливания/вращения — я так и не смог понять по каком принципу они нумеруются, приходится пользоваться методом проб и ошибок. Благо тут всего 2 варианта. Функция получения высоты выдавливания тривиальна — берем высоту шарика/ролика и добавляем пару миллиметров «про запас»
def get_extrusion_height(params: RollerWaveDriveParams) -> float: return params.roller_height + 2 * params.roller_tolerance + 0.2
Теперь на очереди сепаратор:
def draw_separator(params: RollerWaveDriveParams, component: Component, plane: ConstructionPlane): #Снова создадим скетч sketch = component.sketches.add(plane) sketch.name = 'Separator' #И нарисуем 2 круга - внешнюю и внутренюю окружности сепаратора sketch.sketchCurves.sketchCircles.addByCenterRadius(adsk.core.Point3D.create(0, 0, 0), params.separator_inner_radius) sketch.sketchCurves.sketchCircles.addByCenterRadius(adsk.core.Point3D.create(0, 0, 0), params.separator_outer_radius) #Выдавим это как и в предыдущий раз extrudes = component.features.extrudeFeatures prof = sketch.profiles.item(1) distance = adsk.core.ValueInput.createByReal(get_extrusion_height(params)) separator_extrude = extrudes.addSimple(prof, distance, adsk.fusion.FeatureOperations.NewBodyFeatureOperation) #И сохрним тело сепаратора, присвоим ему имя separator_body = separator_extrude.bodies.item(0) separator_body.name = "Separator" #Построим ось вращения сепаратора axis = create_axis_from_cylindrical_body(component, separator_body) #Проделаем отверстие под шарик или ролик hole_feature = create_round_hole(params, component, plane) if params.use_balls else create_square_hole(params, component, plane) #Размножим отверстие по кругу create_circular_pattern(axis, hole_feature, params.roller_number)
Ось вращения получается как construction axis по цилиндрической поверхности, сепаратор у нас как раз их имеет аж 2 штуки.
def create_axis_from_cylindrical_body(component: Component, separator_body: BRepBody) -> ConstructionAxis: axis_input = component.constructionAxes.createInput() axis_input.setByCircularFace(find_cylindrical_face(separator_body)) axis = component.constructionAxes.add(axis_input) return axis #Находим циллиндрический фейс def find_cylindrical_face(body: BRepBody) -> BRepFace: for face in body.faces: geom = face.geometry if geom.surfaceType == adsk.core.SurfaceTypes.CylinderSurfaceType: return face
Отверстие под ролик (рассмотрим ролик, шарик делается плюс-минус аналогично, можно посмотреть в коде)
def create_square_hole(params: RollerWaveDriveParams, component: Component, plane: ConstructionPlane) -> Feature: extrudes = component.features.extrudeFeatures planes = component.constructionPlanes #создаем параметры для новой плоскости построения plane_input = planes.createInput() #Делаем новую плоскость как смещенную изначальную, на толщину "крышки" сепаратора, 1мм plane_input.setByOffset(plane, adsk.core.ValueInput.createByReal(0.1)) holes_plane = planes.add(plane_input) #создаем на этой плоскости новый скетч holes_sketch = component.sketches.add(holes_plane) holes_sketch.name = 'RollerHole' #Нарисуем прямоуголник, размером с ролик + запас holes_sketch.sketchCurves.sketchLines.addCenterPointRectangle( adsk.core.Point3D.create(0, params.separator_middle_radius, 0), adsk.core.Point3D.create(params.roller_diameter / 2 + params.roller_tolerance, params.separator_middle_radius + params.separator_thickness, 0), ) prof = holes_sketch.profiles.item(0) distance = adsk.core.ValueInput.createByReal(params.roller_height + 2 * params.roller_tolerance) #Выдавим профиль прямоуголника, но не как новое тело, а как "вырез" hole_extrude = extrudes.addSimple(prof, distance, adsk.fusion.FeatureOperations.CutFeatureOperation) return hole_extrude
И надо это отверстие размножить по кругу (circular pattern) вокруг полученной оси
def create_circular_pattern(axis: ConstructionAxis, feature: Feature, num_copies: int): #Создаем колелкцию для размножаемых фич collection = adsk.core.ObjectCollection.create() collection.add(feature) #Указываем параметры размножения - количество копий и угол. pattern_features = feature.parentComponent.features.circularPatternFeatures pattern_input = pattern_features.createInput(collection, axis) pattern_input.quantity = adsk.core.ValueInput.createByReal(num_copies) #можно указать как adsk.core.ValueInput.createByReal(2 * pi) pattern_input.totalAngle = adsk.core.ValueInput.createByString('360 deg') pattern_input.isSymmetric = False #строим круговой паттерн pattern_features.add(pattern_input)
Реализацию отрисовки эксцентрика и шариков/роликов оставим за рамками статьи — они не содержат ничего нового сверх уже рассмотренного выше, ознакомится, при желании, можно в коде на гитхабе.
По итогу у нас получается вот такой красивенький редуктор.
В итоге, конечно, этот тип редуктора оказался хуже циклоидального: изначально он меня подкупил «круглым» выходным звеном, не требующем пальцевой муфты или муфты Олдема, как циклоидальный. Но на этом его плюсы, пожалуй и заканчиваются: при работе издает много шума, особенно на высоких оборотах, и имеет явно более низкий КПД.
ссылка на оригинал статьи https://habr.com/ru/articles/919478/
Добавить комментарий