├── freecad └── cross │ ├── py.typed │ ├── version.py │ ├── exceptions.py │ ├── pyproject.toml │ ├── sensors │ ├── sensor_proxy_link.py │ ├── sensor_proxy_joint.py │ ├── sensor.py │ └── sensor_factory.py │ ├── gui_utils.py │ ├── controller.py │ ├── workcell.py │ ├── xacro_object.py │ ├── link.py │ ├── observer.py │ ├── trajectory.py │ ├── planning_scene.py │ ├── attached_collision_object.py │ ├── pose.py │ ├── ros │ ├── node.py │ └── planning_scene.py │ ├── wb_globals.py │ ├── ui │ ├── command_new_pose.py │ ├── command_new_link.py │ ├── command_sphere_from_bounding_box.py │ ├── command_new_workcell.py │ ├── command_new_observer.py │ ├── command_new_trajectory.py │ ├── command_new_xacro_object.py │ ├── command_box_from_bounding_box.py │ ├── command_create_collision_copy_obj.py │ ├── command_cylinder_x_aligned_from_bounding_box.py │ ├── command_cylinder_y_aligned_from_bounding_box.py │ ├── command_cylinder_z_aligned_from_bounding_box.py │ ├── command_open_models_library.py │ ├── command_new_joint.py │ ├── command_new_sensor.py │ ├── command_set_joints.py │ ├── duplicate_robot_dialog.py │ ├── command_new_controller.py │ ├── kk_model.py │ ├── command_wb_settings.py │ ├── dh_model.py │ ├── command_new_attached_collision_object.py │ ├── command_get_planning_scene.py │ ├── command_robot_from_urdf.py │ ├── command_rotate_joint_x.py │ ├── command_rotate_joint_y.py │ ├── command_rotate_joint_z.py │ ├── command_new_joints_filled_spider_connect.py │ ├── command_assembly_from_urdf.py │ ├── command_new_joints_filled.py │ ├── command_new_links_filled.py │ ├── command_set_placement_fast_parent_to_child.py │ ├── command_world_generator.py │ ├── command_set_placement_fast_child_to_parent.py │ ├── command_simplify_mesh.py │ ├── command_explode_links.py │ ├── command_set_placement_fast.py │ ├── command_update_planning_scene.py │ ├── command_kk_edit.py │ ├── command_set_material.py │ ├── kk_dialog.py │ ├── command_new_robot.py │ ├── command_set_placement_fast_sensor.py │ ├── command_new_lcs_at_robot_link_body.py │ └── command_set_placement_by_orienteer_with_hold_chain.py │ ├── robot.py │ ├── joint.py │ ├── import_urdf.py │ ├── vendor │ └── fcapi │ │ └── README.md │ ├── geometry_helpers.py │ ├── assembly4_utils.py │ └── sdf │ └── sdf_parser │ └── sdf_tree.py ├── MANIFEST.in ├── docker ├── freecad │ ├── freecad_custom_appimage_dir │ │ └── .gitkeep │ └── freecad_projects_save_dir │ │ └── .gitkeep ├── ros2_ws │ └── src │ │ └── freecad_cross_rosdep │ │ ├── freecad_cross_rosdep │ │ └── __init__.py │ │ ├── resource │ │ └── freecad_cross_rosdep │ │ ├── setup.cfg │ │ ├── setup.py │ │ ├── test │ │ ├── test_pep257.py │ │ ├── test_flake8.py │ │ └── test_copyright.py │ │ └── package.xml ├── .dockerignore ├── .gitignore └── README.md ├── resources ├── icons │ ├── assembly_from_urdf.svg.rst │ ├── media-playback-start.svg.rst │ ├── set_joints.svg.rst │ ├── world_generator.svg.rst │ ├── update_planning_scene.svg.rst │ ├── link.svg │ ├── joint.svg │ ├── calculate_mass_and_inertia.svg │ ├── sensor.svg │ ├── ros_9dotslogo_color.svg.rst │ ├── collision_copy_obj.svg │ ├── links_to_joints.svg │ ├── links_to_joints_spider.svg │ ├── explode_links.svg │ ├── cylinder_x_from_bbox.svg │ ├── cylinder_y_from_bbox.svg │ ├── cylinder_z_from_bbox.svg │ ├── body_to_link.svg │ ├── cylinder_from_bbox.svg │ ├── planning_scene.svg │ ├── observer.svg │ ├── pose.svg │ ├── robot.svg │ └── controller.svg ├── templates │ ├── urdf │ │ ├── sensors_template.urdf.xacro │ │ └── xacro_wrapper_template.urdf.xacro │ ├── package.xml │ ├── CMakeLists.txt │ ├── rviz │ │ └── robot_description.rviz │ └── launch │ │ ├── description.launch.py │ │ └── display.launch.py ├── sensors │ ├── TODO │ │ ├── model │ │ │ └── logical_audio_sensor_plugin.sdf │ │ └── link │ │ │ └── optical_tactile.sdf │ ├── link │ │ ├── navsat.sdf │ │ ├── imu.sdf │ │ ├── contact.sdf │ │ ├── air_speed.sdf │ │ ├── logical_camera.sdf │ │ ├── air_pressure.sdf │ │ ├── camera.sdf │ │ ├── rgbd_camera.sdf │ │ ├── depth_camera.sdf │ │ ├── thermal_camera.sdf │ │ ├── boundingbox_camera_3d.sdf │ │ ├── altimeter.sdf │ │ ├── boundingbox_camera_full_2d.sdf │ │ ├── boundingbox_camera_visible_2d.sdf │ │ ├── segmentation_camera_panoptic.sdf │ │ ├── segmentation_camera_semantic.sdf │ │ ├── gpu_lidar.sdf │ │ ├── magnetometer.sdf │ │ └── wideangle_camera.sdf │ └── joint │ │ └── force_torque.sdf └── ui │ ├── task_panel_link.ui │ ├── task_panel_robot.ui │ ├── duplicate_robot_dialog.ui │ ├── kk_dialog.ui │ └── file_overwrite_confirmation_dialog.ui ├── .vimspector.json ├── .gitmodules ├── notes.md ├── .pre-commit-config.yaml ├── setup.py ├── .github └── FUNDING.yml ├── package.xml ├── .vscode └── launch.json ├── .gitignore ├── load_workbench.py.txt └── docs └── commands.md /freecad/cross/py.typed: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-include resources * 2 | -------------------------------------------------------------------------------- /docker/freecad/freecad_custom_appimage_dir/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docker/freecad/freecad_projects_save_dir/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /freecad/cross/version.py: -------------------------------------------------------------------------------- 1 | __version__ = '1.0.0' 2 | -------------------------------------------------------------------------------- /docker/ros2_ws/src/freecad_cross_rosdep/freecad_cross_rosdep/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docker/ros2_ws/src/freecad_cross_rosdep/resource/freecad_cross_rosdep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /freecad/cross/exceptions.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | class NoPartWrapperOfObject(Exception): 4 | pass 5 | 6 | class CallRecursion(Exception): 7 | pass 8 | -------------------------------------------------------------------------------- /resources/icons/assembly_from_urdf.svg.rst: -------------------------------------------------------------------------------- 1 | The "Model" icon is taken from the `Assembly4 workbench`_. 2 | -------------------------------------------------------------------------------- /resources/icons/media-playback-start.svg.rst: -------------------------------------------------------------------------------- 1 | The `media-playback-start.svg` icon is taken from FreeCAD sources and is licensed under CC BY-SA 2.0. 2 | -------------------------------------------------------------------------------- /resources/templates/urdf/sensors_template.urdf.xacro: -------------------------------------------------------------------------------- 1 | 2 | 3 | {sensors_urdf} 4 | 5 | 6 | -------------------------------------------------------------------------------- /docker/ros2_ws/src/freecad_cross_rosdep/setup.cfg: -------------------------------------------------------------------------------- 1 | [develop] 2 | script_dir=$base/lib/freecad_cross_rosdep 3 | [install] 4 | install_scripts=$base/lib/freecad_cross_rosdep 5 | -------------------------------------------------------------------------------- /resources/icons/set_joints.svg.rst: -------------------------------------------------------------------------------- 1 | The robotic-arm symbol was originally designed by Inkie30 and obtained from `OpenClipard`_. 2 | -------------------------------------------------------------------------------- /docker/.dockerignore: -------------------------------------------------------------------------------- 1 | freecad/freecad_projects_save_dir/* 2 | !freecad/freecad_projects_save_dir/.gitkeep 3 | 4 | ros2_ws/build_* 5 | ros2_ws/src/* 6 | !ros2_ws/src/freecad_cross_rosdep -------------------------------------------------------------------------------- /docker/.gitignore: -------------------------------------------------------------------------------- 1 | FreeCAD* 2 | freecad/freecad_projects_save_dir/* 3 | !freecad/freecad_projects_save_dir/.gitkeep 4 | 5 | ros2_ws/build_* 6 | ros2_ws/src/* 7 | !ros2_ws/src/freecad_cross_rosdep -------------------------------------------------------------------------------- /resources/icons/world_generator.svg.rst: -------------------------------------------------------------------------------- 1 | The related icon was originally designed by Royyan Wijaya and obtained from `icon-icons`_. 2 | -------------------------------------------------------------------------------- /freecad/cross/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.pylint.'FORMAT'] 2 | max-line-length = 90 3 | 4 | [tool.pylsp-mypy] 5 | enabled = true 6 | live_mode = true 7 | report_progress = true 8 | strict = false 9 | # exclude = ["tests/*"] 10 | -------------------------------------------------------------------------------- /resources/icons/update_planning_scene.svg.rst: -------------------------------------------------------------------------------- 1 | ======================================== 2 | Logo for the UpdatePlanningScene command 3 | ======================================== 4 | 5 | The double-arrow symbol is from `view-refresh.svg` of FreeCAD. 6 | -------------------------------------------------------------------------------- /resources/templates/urdf/xacro_wrapper_template.urdf.xacro: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /freecad/cross/sensors/sensor_proxy_link.py: -------------------------------------------------------------------------------- 1 | from .sensor_proxy import SensorProxy 2 | 3 | 4 | class SensorProxyLink(SensorProxy): 5 | """The proxy for Sensor of link objects.""" 6 | 7 | # The member is often used in workbenches, particularly in the Draft 8 | # workbench, to identify the object type. 9 | Type = 'Cross::SensorLink' 10 | -------------------------------------------------------------------------------- /freecad/cross/sensors/sensor_proxy_joint.py: -------------------------------------------------------------------------------- 1 | from .sensor_proxy import SensorProxy 2 | 3 | 4 | class SensorProxyJoint(SensorProxy): 5 | """The proxy for Sensor of joint objects.""" 6 | 7 | # The member is often used in workbenches, particularly in the Draft 8 | # workbench, to identify the object type. 9 | Type = 'Cross::SensorJoint' 10 | -------------------------------------------------------------------------------- /freecad/cross/sensors/sensor.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import List 4 | 5 | import FreeCAD as fc 6 | 7 | DO = fc.DocumentObject 8 | DOList = List[DO] 9 | 10 | 11 | class Sensor(DO): 12 | _Type: str 13 | 14 | def addObject(self, object_to_add: fc.DocumentObject) -> None: ... 15 | def removeObject(self, object_to_remove: fc.DocumentObject) -> DOList: ... 16 | -------------------------------------------------------------------------------- /freecad/cross/gui_utils.py: -------------------------------------------------------------------------------- 1 | """Utility functions to work with FreeCAD's Gui.""" 2 | 3 | from __future__ import annotations 4 | 5 | import FreeCAD as fc 6 | 7 | if hasattr(fc, 'GuiUp') and fc.GuiUp: 8 | from PySide import QtGui # FreeCAD's PySide! 9 | 10 | def tr(text: str) -> str: 11 | return QtGui.QApplication.translate('cross', text) 12 | else: 13 | def tr(text: str) -> str: 14 | return text 15 | -------------------------------------------------------------------------------- /freecad/cross/controller.py: -------------------------------------------------------------------------------- 1 | # Stub for a CROSS::Robot. 2 | 3 | from __future__ import annotations 4 | 5 | from typing import List 6 | 7 | import FreeCAD as fc 8 | 9 | DO = fc.DocumentObject 10 | DOList = List[DO] 11 | 12 | 13 | class Controller(DO): 14 | _Type: str 15 | 16 | def addObject(self, object_to_add: fc.DocumentObject) -> None: ... 17 | def removeObject(self, object_to_remove: fc.DocumentObject) -> DOList: ... 18 | -------------------------------------------------------------------------------- /resources/icons/link.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /.vimspector.json: -------------------------------------------------------------------------------- 1 | { 2 | "configurations": { 3 | "Python: Remote Debug": { 4 | "adapter": "multi-session", 5 | "configuration": { 6 | "name": "Python: Remote Debug", 7 | "request": "attach", 8 | "connect": { 9 | "host": "localhost", 10 | "port": 5678 11 | }, 12 | "pathMappings": [ 13 | { 14 | "localRoot": "${workspaceFolder}", 15 | "remoteRoot": "${workspaceFolder}" 16 | } 17 | ] 18 | } 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /resources/sensors/TODO/model/logical_audio_sensor_plugin.sdf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 1 8 | 0 0 0 0 0 0 9 | .1 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /resources/sensors/link/navsat.sdf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 9 | 10 | true 11 | navsat 12 | 30 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /resources/sensors/joint/force_torque.sdf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | 10 | 11 | true 12 | force_torque 13 | 10 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /freecad/cross/workcell.py: -------------------------------------------------------------------------------- 1 | # Stub for a CROSS::Robot. 2 | 3 | from __future__ import annotations 4 | 5 | from typing import NewType, Union 6 | 7 | import FreeCAD as fc 8 | 9 | # Implementation note: The following import is necessary to avoid a circular 10 | # dependency. 11 | Joint = NewType('Joint', fc.DocumentObject) 12 | XacroObject = NewType('XacroObject', fc.DocumentObject) 13 | 14 | JointOrXacroObject = Union[Joint, XacroObject] 15 | 16 | 17 | class Workcell(fc.DocumentObject): 18 | Group: list[JointOrXacroObject] 19 | OutputPath: str 20 | RootLink: str 21 | _Type: str 22 | -------------------------------------------------------------------------------- /freecad/cross/xacro_object.py: -------------------------------------------------------------------------------- 1 | # Stub for a CROSS::XacroObject. 2 | 3 | from __future__ import annotations 4 | 5 | from typing import List 6 | 7 | import FreeCAD as fc 8 | 9 | # Typing hints 10 | DO = fc.DocumentObject 11 | DOList = List[DO] 12 | 13 | 14 | class XacroObject(fc.DocumentObject): 15 | Group: list[fc.DocumentObject] 16 | InputFile: str 17 | MainMacro: str 18 | Placement: fc.Placement 19 | _Type: str 20 | 21 | def addObject(self, object_to_add: fc.DocumentObject) -> None: ... 22 | def removeObject(self, object_to_remove: fc.DocumentObject) -> DOList: ... 23 | -------------------------------------------------------------------------------- /freecad/cross/link.py: -------------------------------------------------------------------------------- 1 | # Stub for a CROSS::Link. 2 | 3 | from __future__ import annotations 4 | 5 | import FreeCAD as fc 6 | 7 | 8 | class Link(fc.DocumentObject): 9 | Collision: list[fc.DocumentObject] 10 | Group: list[fc.DocumentObject] 11 | Mass: float 12 | MountedPlacement: fc.Placement 13 | Placement: fc.Placement 14 | Real: list[fc.DocumentObject] 15 | Visual: list[fc.DocumentObject] 16 | MaterialCardName: str 17 | MaterialCardPath: str 18 | MaterialDensity: str 19 | MaterialNotCalculate: bool 20 | CalculateInertiaBasedOnMass:bool 21 | _Type: str 22 | -------------------------------------------------------------------------------- /freecad/cross/observer.py: -------------------------------------------------------------------------------- 1 | # Stub for a CROSS::Observer. 2 | 3 | from __future__ import annotations 4 | 5 | from typing import NewType, Optional 6 | 7 | import FreeCAD as fc 8 | 9 | # Typing hints 10 | DO = fc.DocumentObject 11 | ObserverProxy = NewType('ObserverProxy', object) 12 | VPObserverProxy = NewType('VPObserverProxy', object) 13 | 14 | 15 | class Observer(DO): 16 | Formula: int 17 | Proxy: ObserverProxy 18 | ViewObject: Optional[ViewProviderObserver] 19 | _Type: str 20 | 21 | 22 | class ViewProviderObserver: 23 | Object: Observer 24 | Proxy: VPObserverProxy 25 | Visibility: bool 26 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "modules/ros2_controllers"] 2 | path = modules/ros2_controllers 3 | url = https://github.com/ros-controls/ros2_controllers 4 | branch = jazzy 5 | [submodule "modules/sdformat"] 6 | path = modules/sdformat 7 | url = https://github.com/gazebosim/sdformat 8 | branch = sdf13 9 | [submodule "modules/robot_descriptions"] 10 | path = modules/robot_descriptions 11 | url = https://github.com/drfenixion/robot_descriptions.py 12 | branch = main 13 | [submodule "modules/Dynamic_World_Generator"] 14 | path = modules/Dynamic_World_Generator 15 | url = https://github.com/drfenixion/Dynamic_World_Generator 16 | branch = main 17 | -------------------------------------------------------------------------------- /resources/sensors/link/imu.sdf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 9 | 10 | true 11 | 100 12 | true 13 | imu 14 | true 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /notes.md: -------------------------------------------------------------------------------- 1 | Initialization of objects 2 | ========================= 3 | 4 | New object (using our `make_...` functions) 5 | ------------------------------------------- 6 | 7 | - Object is created with `doc.addObject()`. 8 | - ViewObject is created 9 | - `Object.Proxy.__init__()` 10 | - `ViewObject.Proxy.__init__()` 11 | - `ViewObject.Proxy.attach()`, triggered by `ViewObject.Proxy = self` 12 | 13 | When opening an existing document 14 | --------------------------------- 15 | 16 | - Object exists 17 | - loads() 18 | - ViewObject exists 19 | - `ViewObject.Proxy.attach()` 20 | - Properties are restored (onChanged()) 21 | - `Object.onDocumentRestored()` 22 | -------------------------------------------------------------------------------- /resources/sensors/link/contact.sdf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 9 | 10 | 11 | collision 12 | /contact_sensor 13 | 14 | true 15 | 100 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # See https://pre-commit.com for more information 2 | # See https://pre-commit.com/hooks.html for more hooks 3 | repos: 4 | - repo: https://github.com/pre-commit/pre-commit-hooks 5 | rev: v5.0.0 6 | hooks: 7 | - id: check-added-large-files 8 | - id: check-byte-order-marker 9 | - id: check-yaml 10 | - id: end-of-file-fixer 11 | exclude: resources/icons/.*\.svg 12 | - id: trailing-whitespace 13 | exclude: resources/icons/.*\.svg 14 | - id: check-merge-conflict 15 | - id: check-symlinks 16 | - id: check-xml 17 | - repo: https://github.com/asottile/add-trailing-comma 18 | rev: v3.1.0 19 | hooks: 20 | - id: add-trailing-comma 21 | exclude: resources/icons/.*\.svg 22 | -------------------------------------------------------------------------------- /resources/templates/package.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {package_name} 5 | 0.0.0 6 | TODO: Package description 7 | TODO 8 | TODO: License declaration 9 | 10 | ament_cmake 11 | 12 | joint_state_publisher_gui 13 | ros_gz 14 | 15 | ament_lint_auto 16 | ament_lint_common 17 | 18 | 19 | ament_cmake 20 | 21 | 22 | -------------------------------------------------------------------------------- /freecad/cross/trajectory.py: -------------------------------------------------------------------------------- 1 | # Stub for a CROSS::Trajectory. 2 | 3 | from __future__ import annotations 4 | 5 | from typing import NewType, Optional 6 | 7 | import FreeCAD as fc 8 | 9 | # Typing hints 10 | from .robot import Robot as CrossRobot # A Cross::Robot, i.e. a DocumentObject with Proxy "Robot". # noqa: E501 11 | DO = fc.DocumentObject 12 | TrajectoryProxy = NewType('TrajectoryProxy', object) 13 | VPTrajectoryProxy = NewType('VPTrajectoryProxy', object) 14 | 15 | 16 | class Trajectory(DO): 17 | PointIndex: int 18 | Proxy: TrajectoryProxy 19 | Robot: CrossRobot 20 | ViewObject: Optional[ViewProviderTrajectory] 21 | _Type: str 22 | 23 | 24 | class ViewProviderTrajectory: 25 | Object: Trajectory 26 | Proxy: VPTrajectoryProxy 27 | Visibility: bool 28 | -------------------------------------------------------------------------------- /resources/ui/task_panel_link.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | TaskPanelLinkForm 4 | 5 | 6 | 7 | 0 8 | 0 9 | 400 10 | 300 11 | 12 | 13 | 14 | Link Definition 15 | 16 | 17 | 18 | 19 | 20 | No need to do anything here 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /freecad/cross/planning_scene.py: -------------------------------------------------------------------------------- 1 | # Stub for a CROSS::PlanningScene. 2 | 3 | from __future__ import annotations 4 | 5 | from typing import ForwardRef 6 | 7 | import FreeCAD as fc 8 | 9 | # Typing hints 10 | DO = fc.DocumentObject 11 | from .robot import Robot as CrossRobot # A Cross::Robot, i.e. a DocumentObject with Proxy "Robot". # noqa: E501 12 | VPPlanningSceneProxy = ForwardRef('VPPlanningSceneProxy') 13 | 14 | 15 | class PlanningScene(DO): 16 | _Type: str 17 | 18 | 19 | class ViewProviderPlanningScene: 20 | Object: PlanningScene 21 | PlaneSides: float 22 | Robot: CrossRobot 23 | Proxy: VPPlanningSceneProxy 24 | SubframeSize: float 25 | Visibility: bool 26 | 27 | # This is missing inFreeCAD's stubs. 28 | def addDisplayMode(self, separator, mode: str) -> None: ... 29 | -------------------------------------------------------------------------------- /resources/sensors/link/air_speed.sdf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | true 7 | 30 8 | true 9 | air_speed 10 | true 11 | 12 | 13 | 14 | 0.2 15 | 0.1 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /docker/ros2_ws/src/freecad_cross_rosdep/setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import find_packages, setup 2 | 3 | package_name = 'freecad_cross_rosdep' 4 | 5 | setup( 6 | name=package_name, 7 | version='0.0.0', 8 | packages=find_packages(exclude=['test']), 9 | data_files=[ 10 | ( 11 | 'share/ament_index/resource_index/packages', 12 | ['resource/' + package_name], 13 | ), 14 | ('share/' + package_name, ['package.xml']), 15 | ], 16 | install_requires=['setuptools'], 17 | zip_safe=True, 18 | maintainer='', 19 | maintainer_email='', 20 | description='Package for store freecad.cross workbench dependencies', 21 | license='LGPL-2.1', 22 | tests_require=['pytest'], 23 | entry_points={ 24 | 'console_scripts': [ 25 | ], 26 | }, 27 | ) 28 | -------------------------------------------------------------------------------- /resources/icons/joint.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /resources/templates/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.8) 2 | project({package_name}) 3 | 4 | find_package(ament_cmake REQUIRED) 5 | 6 | if(BUILD_TESTING) 7 | find_package(ament_lint_auto REQUIRED) 8 | # the following line skips the linter which checks for copyrights 9 | # comment the line when a copyright and license is added to all source files 10 | set(ament_cmake_copyright_FOUND TRUE) 11 | # the following line skips cpplint (only works in a git repo) 12 | # comment the line when this package is in a git repo and when 13 | # a copyright and license is added to all source files 14 | set(ament_cmake_cpplint_FOUND TRUE) 15 | ament_lint_auto_find_test_dependencies() 16 | endif() 17 | 18 | install(DIRECTORY launch {meshes_dir}rviz urdf worlds 19 | DESTINATION share/${{PROJECT_NAME}} 20 | ) 21 | 22 | ament_package() 23 | -------------------------------------------------------------------------------- /resources/icons/calculate_mass_and_inertia.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | KG 8 | 9 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from setuptools import setup 4 | 5 | # name: this is the name of the distribution. 6 | # Packages using the same name here cannot be installed together 7 | 8 | version_path = os.path.join( 9 | os.path.abspath(os.path.dirname(__file__)), 10 | 'freecad', 'cross', 'version.py', 11 | ) 12 | with open(version_path) as fp: 13 | exec(fp.read()) 14 | 15 | setup( 16 | name='freecad.cross', 17 | version=str(__version__), 18 | packages=[ 19 | 'freecad', 20 | 'freecad.cross', 21 | ], 22 | maintainer='Vasily S', 23 | maintainer_email='it.project.devel@gmail.com', 24 | url='https://github.com/drfenixion/freecad.overcross.git', 25 | description='RobotCAD (FreeCAD OVERCROSS) is a workbench to work with ROS in FreeCAD', 26 | install_requires=[], 27 | include_package_data=True, 28 | ) 29 | 30 | # install_requires should be ['xacro']. 31 | -------------------------------------------------------------------------------- /freecad/cross/attached_collision_object.py: -------------------------------------------------------------------------------- 1 | # Stub for a CROSS::AttachedCollisionObject. 2 | 3 | from __future__ import annotations 4 | 5 | from typing import NewType, Optional 6 | 7 | import FreeCAD as fc 8 | 9 | # Typing hints 10 | from .link import Link as CrossLink # A Cross::Link, i.e. a DocumentObject with Proxy "Link". # noqa: E501 11 | DO = fc.DocumentObject 12 | AttachedCollisionObjectProxy = NewType('AttachedCollisionObjectProxy', object) 13 | VPAttachedCollisionObjectProxy = NewType('VPAttachedCollisionObjectProxy', object) 14 | 15 | 16 | class AttachedCollisionObject(DO): 17 | Link: CrossLink 18 | Objects: list[DO] 19 | Proxy: AttachedCollisionObjectProxy 20 | ViewObject: Optional[ViewProviderAttachedCollisionObject] 21 | _Type: str 22 | 23 | 24 | class ViewProviderAttachedCollisionObject: 25 | Object: AttachedCollisionObject 26 | Proxy: VPAttachedCollisionObjectProxy 27 | Visibility: bool 28 | -------------------------------------------------------------------------------- /docker/ros2_ws/src/freecad_cross_rosdep/test/test_pep257.py: -------------------------------------------------------------------------------- 1 | # Copyright 2015 Open Source Robotics Foundation, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from ament_pep257.main import main 16 | import pytest 17 | 18 | 19 | @pytest.mark.linter 20 | @pytest.mark.pep257 21 | def test_pep257(): 22 | rc = main(argv=['.', 'test']) 23 | assert rc == 0, 'Found code style errors / warnings' 24 | -------------------------------------------------------------------------------- /freecad/cross/pose.py: -------------------------------------------------------------------------------- 1 | # Stub for a CROSS::Pose. 2 | 3 | from __future__ import annotations 4 | 5 | from typing import NewType, Optional, Union 6 | 7 | import FreeCAD as fc 8 | 9 | # Typing hints 10 | from .robot import Robot as CrossRobot # A Cross::Robot, i.e. a DocumentObject with Proxy "Robot". # noqa: E501 11 | DO = fc.DocumentObject 12 | PoseProxy = NewType('PoseProxy', object) 13 | VPPoseProxy = NewType('VPPoseProxy', object) 14 | 15 | 16 | class Pose(DO): 17 | AllowNonLeafLink: bool 18 | EndEffector: Union[str, list[str]] # Both an enum and a string. 19 | Placement: fc.Placement 20 | Proxy: PoseProxy 21 | Robot: CrossRobot 22 | ViewObject: Optional[ViewProviderPose] 23 | _Type: str 24 | 25 | 26 | class ViewProviderPose: 27 | AxisLength: float 28 | Object: Pose 29 | Proxy: VPPoseProxy 30 | ShowEndEffector: bool 31 | Visibility: bool 32 | 33 | def addDisplayMode(self, separator, mode: str) -> None: ... 34 | -------------------------------------------------------------------------------- /freecad/cross/ros/node.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import ForwardRef, Optional, Tuple 4 | 5 | try: 6 | import rclpy 7 | from rclpy.node import Node 8 | from rclpy.executors import MultiThreadedExecutor 9 | imports_ok = True 10 | except ImportError: 11 | Node = ForwardRef('Node') 12 | MultiThreadedExecutor = ForwardRef('MultiThreadedExecutor') 13 | imports_ok = False 14 | 15 | 16 | def get_node() -> Optional[Node]: 17 | if not imports_ok: 18 | return None 19 | 20 | node = Node('cross') 21 | return node 22 | 23 | 24 | def get_node_and_executor() -> Tuple[Optional[MultiThreadedExecutor], Optional[Node]]: 25 | if not imports_ok: 26 | return None, None 27 | 28 | if not rclpy.ok(): 29 | rclpy.init() 30 | node = get_node() 31 | if node is None: 32 | return None, None 33 | executor = rclpy.executors.MultiThreadedExecutor() 34 | executor.add_node(node) 35 | return node, executor 36 | -------------------------------------------------------------------------------- /freecad/cross/wb_globals.py: -------------------------------------------------------------------------------- 1 | """Global workbench configuration.""" 2 | 3 | from pathlib import Path 4 | 5 | from .ros.utils import add_ros_library_path 6 | from .ros.utils import get_ros_distro_from_env_or_default 7 | from .ros.utils import get_ros_workspace_from_env 8 | from . import wb_constants 9 | 10 | # Constants. 11 | PREFS_CATEGORY = wb_constants.PREFS_CATEGORY # Category in the preferences dialog. 12 | PREF_VHACD_PATH = wb_constants.PREF_VHACD_PATH # Path to the V-HACD executable. 13 | PREF_OVERCROSS_TOKEN = wb_constants.PREF_OVERCROSS_TOKEN # Auth token for external code generator 14 | 15 | # Session-wide globals. 16 | g_ros_distro = get_ros_distro_from_env_or_default() 17 | 18 | add_ros_library_path(g_ros_distro) 19 | # Must be imported after the call to `add_ros_library_path`. 20 | from .ros.node import get_node_and_executor # noqa: E402. 21 | g_ros_node, g_ros_executor = get_node_and_executor() 22 | 23 | # Can be changed in the GUI. 24 | g_ros_workspace: Path = get_ros_workspace_from_env() 25 | -------------------------------------------------------------------------------- /resources/ui/task_panel_robot.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | TaskPanelRobotForm 4 | 5 | 6 | 7 | 0 8 | 0 9 | 367 10 | 45 11 | 12 | 13 | 14 | Robot Definition 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | &Add Part 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /docker/ros2_ws/src/freecad_cross_rosdep/test/test_flake8.py: -------------------------------------------------------------------------------- 1 | # Copyright 2017 Open Source Robotics Foundation, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from ament_flake8.main import main_with_errors 16 | import pytest 17 | 18 | 19 | @pytest.mark.flake8 20 | @pytest.mark.linter 21 | def test_flake8(): 22 | rc, errors = main_with_errors(argv=[]) 23 | assert rc == 0, \ 24 | 'Found %d code style errors / warnings:\n' % len(errors) + \ 25 | '\n'.join(errors) 26 | -------------------------------------------------------------------------------- /resources/sensors/link/logical_camera.sdf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 9 | ogre2 10 | 11 | 10 12 | 13 | 0.55 14 | 5 15 | 1.04719755 16 | 1.778 17 | 18 | true 19 | true 20 | camera_logical 21 | true 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /freecad/cross/ui/command_new_pose.py: -------------------------------------------------------------------------------- 1 | import FreeCAD as fc 2 | 3 | import FreeCADGui as fcgui 4 | 5 | from ..gui_utils import tr 6 | 7 | 8 | class _NewPoseCommand: 9 | """The command definition to create a new CROSS::Pose object.""" 10 | 11 | def GetResources(self): 12 | return { 13 | 'Pixmap': 'pose.svg', 14 | 'MenuText': tr('New Pose'), 15 | 'Accel': 'N, P', 16 | 'ToolTip': tr('Create a Pose.'), 17 | } 18 | 19 | def IsActive(self): 20 | return fc.activeDocument() is not None 21 | 22 | def Activated(self): 23 | doc = fc.activeDocument() 24 | doc.openTransaction('Create Pose') 25 | fcgui.doCommand('') 26 | fcgui.addModule('freecad.cross.pose_proxy') 27 | fcgui.doCommand("_pose = freecad.cross.pose_proxy.make_pose('Pose')") 28 | # fcgui.doCommand('FreeCADGui.ActiveDocument.setEdit(_pose.Name)') 29 | doc.commitTransaction() 30 | doc.recompute() 31 | 32 | 33 | fcgui.addCommand('NewPose', _NewPoseCommand()) 34 | -------------------------------------------------------------------------------- /freecad/cross/ui/command_new_link.py: -------------------------------------------------------------------------------- 1 | import FreeCAD as fc 2 | 3 | import FreeCADGui as fcgui 4 | 5 | from ..gui_utils import tr 6 | 7 | 8 | class _NewLinkCommand: 9 | """The command definition to create a new Link object.""" 10 | 11 | def GetResources(self): 12 | return { 13 | 'Pixmap': 'link.svg', 14 | 'MenuText': tr('New Link'), 15 | 'Accel': 'N, L', 16 | 'ToolTip': tr('Create a Link container.'), 17 | } 18 | 19 | def IsActive(self): 20 | return fc.activeDocument() is not None 21 | 22 | def Activated(self): 23 | doc = fc.activeDocument() 24 | doc.openTransaction(tr('Create Link')) 25 | fcgui.doCommand('') 26 | fcgui.addModule('freecad.cross.link_proxy') 27 | fcgui.doCommand("_link = freecad.cross.link_proxy.make_link('Link')") 28 | fcgui.doCommand('FreeCADGui.ActiveDocument.setEdit(_link.Name)') 29 | doc.commitTransaction() 30 | doc.recompute() 31 | 32 | 33 | fcgui.addCommand('NewLink', _NewLinkCommand()) 34 | -------------------------------------------------------------------------------- /freecad/cross/robot.py: -------------------------------------------------------------------------------- 1 | # Stub for a CROSS::Robot. 2 | 3 | from __future__ import annotations 4 | 5 | from typing import NewType, List, Union 6 | 7 | import FreeCAD as fc 8 | 9 | # Typing hints 10 | DO = fc.DocumentObject 11 | # Implementation note: These cannot be imported because of circular 12 | # dependency. 13 | Joint = NewType('Joint', DO) 14 | Link = NewType('Link', DO) 15 | Controller = NewType('Controller', DO) 16 | AttachedCollisionObject = NewType('AttachedCollisionObject', DO) 17 | 18 | BasicElement = Union[AttachedCollisionObject, Joint, Link, Controller] 19 | DOList = List[DO] 20 | 21 | 22 | class Robot(DO): 23 | Group: list[BasicElement] 24 | OutputPath: str 25 | Placement: fc.Placement 26 | MaterialCardName: str 27 | MaterialCardPath: str 28 | MaterialDensity: str 29 | RobotType: dict 30 | GenerateCodeForRosVersion: dict 31 | _Type: str 32 | Mass: float 33 | 34 | def addObject(self, object_to_add: fc.DocumentObject) -> None: ... 35 | def removeObject(self, object_to_remove: fc.DocumentObject) -> DOList: ... 36 | -------------------------------------------------------------------------------- /docker/ros2_ws/src/freecad_cross_rosdep/package.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | freecad_cross_rosdep 5 | 1.0.0 6 | Package to store freecad.cross workbench dependencies 7 | Vasily S 8 | LGPL-2.1 9 | 10 | geometry_msgs 11 | moveit_msgs 12 | shape_msgs 13 | urdfdom_py 14 | xacro 15 | python3-requests 16 | python3-collada-pip 17 | ament_index_python 18 | 19 | ament_copyright 20 | ament_flake8 21 | ament_pep257 22 | python3-pytest 23 | 24 | 25 | ament_python 26 | 27 | 28 | -------------------------------------------------------------------------------- /resources/sensors/link/air_pressure.sdf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 9 | 10 | true 11 | 30 12 | true 13 | air_pressure 14 | true 15 | 16 | 123 17 | 18 | 19 | 0.2 20 | 0.1 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 12 | polar: # Replace with a single Polar username 13 | buy_me_a_coffee: # Replace with a single Buy Me a Coffee username 14 | custom: ['https://vk.com/robotforge?source=description&w=donut_payment-219386643', 'https://robotcad.ru/en/donate/', 'https://robotcad.ru/en/become_sponsor/', 'https://robotcad.ru/en/order_function/'] # Replace with up to 4 custom sponsorship URLs e.g., # 15 | -------------------------------------------------------------------------------- /freecad/cross/ui/command_sphere_from_bounding_box.py: -------------------------------------------------------------------------------- 1 | import FreeCAD as fc 2 | import FreeCADGui as fcgui 3 | 4 | from ..gui_utils import tr 5 | from ..freecadgui_utils import createBoundSphere 6 | from ..wb_gui_utils import createBoundObjects 7 | 8 | 9 | class SphereFromBoundingBoxCommand: 10 | def GetResources(self): 11 | return { 12 | 'Pixmap': 'sphere_from_bbox.svg', 13 | 'MenuText': tr('Sphere from bounding box'), 14 | 'ToolTip': tr( 15 | 'Add a Part::Sphere corresponding to the' 16 | ' bounding box of the selected objects.' 17 | '\n\nIf you select a link, an object based on Real element will be created. ' 18 | 'This object will then be bound to the link as a collision.', 19 | ), 20 | } 21 | 22 | def Activated(self): 23 | createBoundObjects(createBoundFunc = createBoundSphere) 24 | 25 | def IsActive(self): 26 | return bool(fcgui.Selection.getSelection()) 27 | 28 | 29 | fcgui.addCommand('SphereFromBoundingBox', SphereFromBoundingBoxCommand()) 30 | -------------------------------------------------------------------------------- /freecad/cross/ui/command_new_workcell.py: -------------------------------------------------------------------------------- 1 | import FreeCAD as fc 2 | 3 | import FreeCADGui as fcgui 4 | 5 | from ..gui_utils import tr 6 | 7 | 8 | class _NewWorkcellCommand: 9 | """The command definition to create a new Workcell object.""" 10 | 11 | def GetResources(self): 12 | return { 13 | 'Pixmap': 'workcell.svg', 14 | 'MenuText': tr('New Workcell'), 15 | 'Accel': 'N, W', 16 | 'ToolTip': tr('Create a Workcell.'), 17 | } 18 | 19 | def IsActive(self): 20 | return fc.activeDocument() is not None 21 | 22 | def Activated(self): 23 | doc = fc.activeDocument() 24 | doc.openTransaction('Create Workcell') 25 | fcgui.doCommand('') 26 | fcgui.addModule('freecad.cross.workcell_proxy') 27 | fcgui.doCommand("_workcell = freecad.cross.workcell_proxy.make_workcell('Workcell')") 28 | # fcgui.doCommand('FreeCADGui.ActiveDocument.setEdit(_workcell.Name)') 29 | doc.commitTransaction() 30 | doc.recompute() 31 | 32 | 33 | fcgui.addCommand('NewWorkcell', _NewWorkcellCommand()) 34 | -------------------------------------------------------------------------------- /docker/ros2_ws/src/freecad_cross_rosdep/test/test_copyright.py: -------------------------------------------------------------------------------- 1 | # Copyright 2015 Open Source Robotics Foundation, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from ament_copyright.main import main 16 | import pytest 17 | 18 | 19 | # Remove the `skip` decorator once the source file(s) have a copyright header 20 | @pytest.mark.skip(reason='No copyright header has been placed in the generated source file.') 21 | @pytest.mark.copyright 22 | @pytest.mark.linter 23 | def test_copyright(): 24 | rc = main(argv=['.', 'test']) 25 | assert rc == 0, 'Found errors' 26 | -------------------------------------------------------------------------------- /freecad/cross/ui/command_new_observer.py: -------------------------------------------------------------------------------- 1 | import FreeCAD as fc 2 | 3 | import FreeCADGui as fcgui 4 | 5 | from ..gui_utils import tr 6 | 7 | 8 | class _NewObserverCommand: 9 | """The command definition to create a new CROSS::Observer object.""" 10 | 11 | def GetResources(self): 12 | return { 13 | 'Pixmap': 'observer.svg', 14 | 'MenuText': tr('New Observer'), 15 | 'Accel': 'N, O', 16 | 'ToolTip': tr('Create an Observer.'), 17 | } 18 | 19 | def IsActive(self): 20 | return fc.activeDocument() is not None 21 | 22 | def Activated(self): 23 | doc = fc.activeDocument() 24 | doc.openTransaction('Create Observer') 25 | fcgui.doCommand('') 26 | fcgui.addModule('freecad.cross.observer_proxy') 27 | fcgui.doCommand("_observer = freecad.cross.observer_proxy.make_observer('Observer')") 28 | # fcgui.doCommand('FreeCADGui.ActiveDocument.setEdit(_observer.Name)') 29 | doc.commitTransaction() 30 | doc.recompute() 31 | 32 | 33 | fcgui.addCommand('NewObserver', _NewObserverCommand()) 34 | -------------------------------------------------------------------------------- /package.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | RobotCAD 5 | RobotCAD (FreeCAD OVERCROSS) is a FreeCAD workbench to generate robot description packages (xacro or URDF) for the Robot Operating System. 6 | 9.4.1 7 | 2025-09-18 8 | Vasily S 9 | LGPL-2.1-or-later 10 | https://github.com/drfenixion/freecad.robotcad 11 | https://github.com/drfenixion/freecad.robotcad/blob/main/README.md 12 | resources/icons/robotcad_overcross_joint.svg 13 | 14 | 0.21.2 15 | 16 | 17 | 18 | CrossWorkbench 19 | freecad/robotcad 20 | 3.12 21 | CROSS 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /resources/templates/rviz/robot_description.rviz: -------------------------------------------------------------------------------- 1 | Panels: 2 | - Class: rviz_common/Displays 3 | Name: Displays 4 | - Class: rviz_common/Views 5 | Name: Views 6 | Visualization Manager: 7 | Class: "" 8 | Displays: 9 | - Class: rviz_default_plugins/Grid 10 | Name: Grid 11 | Value: true 12 | Alpha: 0.5 13 | - Class: rviz_default_plugins/RobotModel 14 | Description Source: Topic 15 | Description Topic: 16 | Value: /robot_description 17 | Enabled: true 18 | Name: RobotModel 19 | Alpha: 1 20 | Value: true 21 | - Class: rviz_default_plugins/TF 22 | Name: TF 23 | Value: true 24 | Enabled: false 25 | Global Options: 26 | Fixed Frame: {fixed_frame} 27 | Frame Rate: 30 28 | Name: root 29 | Tools: 30 | - Class: rviz_default_plugins/MoveCamera 31 | Value: true 32 | Views: 33 | Current: 34 | Class: rviz_default_plugins/Orbit 35 | Distance: 5.0 36 | Name: Current View 37 | Pitch: 0.33 38 | Value: Orbit (rviz) 39 | Yaw: 5.5 40 | Window Geometry: 41 | Height: 800 42 | Width: 1200 43 | -------------------------------------------------------------------------------- /freecad/cross/ui/command_new_trajectory.py: -------------------------------------------------------------------------------- 1 | import FreeCAD as fc 2 | 3 | import FreeCADGui as fcgui 4 | 5 | from ..gui_utils import tr 6 | 7 | 8 | class _NewTrajectoryCommand: 9 | """The command definition to create a new CROSS::Trajectory object.""" 10 | 11 | def GetResources(self): 12 | return { 13 | 'Pixmap': 'trajectory.svg', 14 | 'MenuText': tr('New Trajectory'), 15 | 'Accel': 'N, T', 16 | 'ToolTip': tr('Create a Trajectory.'), 17 | } 18 | 19 | def IsActive(self): 20 | return fc.activeDocument() is not None 21 | 22 | def Activated(self): 23 | doc = fc.activeDocument() 24 | doc.openTransaction('Create Trajectory') 25 | fcgui.doCommand('') 26 | fcgui.addModule('freecad.cross.trajectory_proxy') 27 | fcgui.doCommand("_trajectory = freecad.cross.trajectory_proxy.make_trajectory('Trajectory')") 28 | # fcgui.doCommand('FreeCADGui.ActiveDocument.setEdit(_trajectory.Name)') 29 | doc.commitTransaction() 30 | doc.recompute() 31 | 32 | 33 | fcgui.addCommand('NewTrajectory', _NewTrajectoryCommand()) 34 | -------------------------------------------------------------------------------- /freecad/cross/ui/command_new_xacro_object.py: -------------------------------------------------------------------------------- 1 | import FreeCAD as fc 2 | 3 | import FreeCADGui as fcgui 4 | 5 | from ..gui_utils import tr 6 | 7 | 8 | class _NewXacroObjectCommand: 9 | """The command definition to create a new xacro object.""" 10 | 11 | def GetResources(self): 12 | return { 13 | 'Pixmap': 'xacro.svg', 14 | 'MenuText': tr('New xacro object'), 15 | 'Accel': 'N, X', 16 | 'ToolTip': tr('Create a robot from a xacro macro.'), 17 | } 18 | 19 | def IsActive(self): 20 | return (fc.activeDocument() is not None) 21 | 22 | def Activated(self): 23 | doc = fc.activeDocument() 24 | doc.openTransaction(tr('Create xacro object')) 25 | fcgui.doCommand('') 26 | fcgui.addModule('freecad.cross.xacro_object_proxy') 27 | fcgui.doCommand("_xacro = freecad.cross.xacro_object_proxy.make_xacro_object('Xacro')") 28 | # fcgui.doCommand('FreeCADGui.ActiveDocument.setEdit(_xacro.Name)') 29 | doc.commitTransaction() 30 | doc.recompute() 31 | 32 | 33 | fcgui.addCommand('NewXacroObject', _NewXacroObjectCommand()) 34 | -------------------------------------------------------------------------------- /freecad/cross/ui/command_box_from_bounding_box.py: -------------------------------------------------------------------------------- 1 | import FreeCAD as fc 2 | import FreeCADGui as fcgui 3 | 4 | from ..freecadgui_utils import createBoundBox 5 | from ..gui_utils import tr 6 | from ..wb_gui_utils import createBoundObjects 7 | from typing import List 8 | DO = fc.DocumentObject 9 | DOList = List[DO] 10 | 11 | 12 | class BoxFromBoundingBoxCommand: 13 | def GetResources(self): 14 | return { 15 | 'Pixmap': 'box_from_bbox.svg', 16 | 'MenuText': tr('Box from bounding box'), 17 | 'ToolTip': tr( 18 | 'Add a Part::Cube corresponding to the' 19 | ' bounding box of the selected objects.' 20 | '\n\nIf you select a link, an object based on Real element will be created. ' 21 | 'This object will then be bound to the link as a collision.', 22 | ), 23 | } 24 | 25 | def Activated(self): 26 | createBoundObjects(createBoundFunc = createBoundBox) 27 | 28 | def IsActive(self): 29 | return bool(fcgui.Selection.getSelection()) 30 | 31 | 32 | fcgui.addCommand('BoxFromBoundingBox', BoxFromBoundingBoxCommand()) 33 | -------------------------------------------------------------------------------- /freecad/cross/ui/command_create_collision_copy_obj.py: -------------------------------------------------------------------------------- 1 | import FreeCAD as fc 2 | import FreeCADGui as fcgui 3 | 4 | from ..freecadgui_utils import createCollisionCopyObj 5 | from ..gui_utils import tr 6 | from ..wb_gui_utils import createBoundObjects 7 | from typing import List 8 | DO = fc.DocumentObject 9 | DOList = List[DO] 10 | 11 | 12 | class CreateCollisionCopyObj: 13 | def GetResources(self): 14 | return { 15 | 'Pixmap': 'collision_copy_obj.svg', 16 | 'MenuText': tr('Create collision as copy of object'), 17 | 'ToolTip': tr( 18 | 'Add a Part::Feature copies of the selected objects.' 19 | '.' 20 | '\n\nIf selected a link, an object based on Real element will be created. ' 21 | 'This object will then be bound to the link as a collision.', 22 | ), 23 | } 24 | 25 | def Activated(self): 26 | createBoundObjects(createBoundFunc = createCollisionCopyObj) 27 | 28 | def IsActive(self): 29 | return bool(fcgui.Selection.getSelection()) 30 | 31 | 32 | fcgui.addCommand('CreateCollisionCopyObj', CreateCollisionCopyObj()) 33 | -------------------------------------------------------------------------------- /resources/icons/sensor.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /freecad/cross/ui/command_cylinder_x_aligned_from_bounding_box.py: -------------------------------------------------------------------------------- 1 | import FreeCAD as fc 2 | import FreeCADGui as fcgui 3 | 4 | from ..gui_utils import tr 5 | from ..freecadgui_utils import createBoundXAlignedCylinder 6 | from ..wb_gui_utils import createBoundObjects 7 | 8 | 9 | class XAlignedCylinderFromBoundingBoxCommand: 10 | def GetResources(self): 11 | return { 12 | 'Pixmap': 'cylinder_x_from_bbox.svg', 13 | 'MenuText': tr('Cylinder from bounding box'), 14 | 'ToolTip': tr( 15 | 'Add a x-aligned Part::Cylinder englobing the' 16 | ' bounding box of the selected objects.' 17 | '\n\nIf you select a link, an object based on Real element will be created. ' 18 | 'This object will then be bound to the link as a collision.', 19 | ), 20 | } 21 | 22 | def Activated(self): 23 | createBoundObjects(createBoundFunc = createBoundXAlignedCylinder) 24 | 25 | 26 | def IsActive(self): 27 | return bool(fcgui.Selection.getSelection()) 28 | 29 | 30 | fcgui.addCommand('XAlignedCylinderFromBoundingBox', XAlignedCylinderFromBoundingBoxCommand()) 31 | -------------------------------------------------------------------------------- /freecad/cross/ui/command_cylinder_y_aligned_from_bounding_box.py: -------------------------------------------------------------------------------- 1 | import FreeCAD as fc 2 | import FreeCADGui as fcgui 3 | 4 | from ..gui_utils import tr 5 | from ..freecadgui_utils import createBoundYAlignedCylinder 6 | from ..wb_gui_utils import createBoundObjects 7 | 8 | 9 | class YAlignedCylinderFromBoundingBoxCommand: 10 | def GetResources(self): 11 | return { 12 | 'Pixmap': 'cylinder_y_from_bbox.svg', 13 | 'MenuText': tr('Cylinder from bounding box'), 14 | 'ToolTip': tr( 15 | 'Add a y-aligned Part::Cylinder englobing the' 16 | ' bounding box of the selected objects.' 17 | '\n\nIf you select a link, an object based on Real element will be created. ' 18 | 'This object will then be bound to the link as a collision.', 19 | ), 20 | } 21 | 22 | def Activated(self): 23 | createBoundObjects(createBoundFunc = createBoundYAlignedCylinder) 24 | 25 | 26 | def IsActive(self): 27 | return bool(fcgui.Selection.getSelection()) 28 | 29 | 30 | fcgui.addCommand('YAlignedCylinderFromBoundingBox', YAlignedCylinderFromBoundingBoxCommand()) 31 | -------------------------------------------------------------------------------- /freecad/cross/ui/command_cylinder_z_aligned_from_bounding_box.py: -------------------------------------------------------------------------------- 1 | import FreeCAD as fc 2 | import FreeCADGui as fcgui 3 | 4 | from ..gui_utils import tr 5 | from ..freecadgui_utils import createBoundZAlignedCylinder 6 | from ..wb_gui_utils import createBoundObjects 7 | 8 | 9 | class ZAlignedCylinderFromBoundingBoxCommand: 10 | def GetResources(self): 11 | return { 12 | 'Pixmap': 'cylinder_z_from_bbox.svg', 13 | 'MenuText': tr('Cylinder from bounding box'), 14 | 'ToolTip': tr( 15 | 'Add a z-aligned Part::Cylinder englobing the' 16 | ' bounding box of the selected objects.' 17 | '\n\nIf you select a link, an object based on Real element will be created. ' 18 | 'This object will then be bound to the link as a collision.', 19 | ), 20 | } 21 | 22 | def Activated(self): 23 | createBoundObjects(createBoundFunc = createBoundZAlignedCylinder) 24 | 25 | 26 | def IsActive(self): 27 | return bool(fcgui.Selection.getSelection()) 28 | 29 | 30 | fcgui.addCommand('ZAlignedCylinderFromBoundingBox', ZAlignedCylinderFromBoundingBoxCommand()) 31 | -------------------------------------------------------------------------------- /resources/sensors/link/camera.sdf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 1.047 8 | 9 | 320 10 | 240 11 | 12 | 13 | 0.1 14 | 100 15 | 16 | 17 | /tmp/camera_data 18 | 19 | false 20 | camera/trigger 21 | 22 | true 23 | 30 24 | true 25 | camera 26 | true 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /resources/icons/ros_9dotslogo_color.svg.rst: -------------------------------------------------------------------------------- 1 | ======== 2 | ROS logo 3 | ======== 4 | 5 | Trademark of Open Source Robotics Foundation. 6 | Obtained from the official `ROS Press Kit `_. 7 | Original name ``ROS_9DotsLogo_color.svg``. 8 | 9 | Extract from the official web page: 10 | 11 | We provide a wide variety of visual assets that the ROS community can use for their own projects and events. Most of the images are provided under a Creative Commons Attribution-NonCommercial 4.0 International License. The ROS distribution images and illustrations were created by San Francisco based illustrator Joshua Ellingson 12 | 13 | High quality vector files of the ROS “nine dot” Logo can be found in the ROS Press Kit and Logos. Trademark and usage information for the ROS name and nine dot logo can be found in the ROS Brand Guide. The “ROS” name, the “Nine Dots” ROS logo, and other ROS trademarks are property of Open Source Robotics Foundation (“Open Robotics”). All creatives that include or reference ROS trademarks must be reviewed and fully approved by Open Robotics. Requests for information about the trademark, and applications for its use can be sent to info@openrobotics.org. 14 | -------------------------------------------------------------------------------- /resources/sensors/link/rgbd_camera.sdf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 1.047 8 | 9 | 320 10 | 240 11 | 12 | 13 | 0.1 14 | 100 15 | 16 | 17 | /tmp/camera_rgbd_data 18 | 19 | false 20 | camera_rgbd/trigger 21 | 22 | true 23 | 30 24 | true 25 | camera_rgbd 26 | true 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /freecad/cross/ui/command_open_models_library.py: -------------------------------------------------------------------------------- 1 | import FreeCADGui as fcgui 2 | import FreeCAD as fc 3 | 4 | from ..gui_utils import tr 5 | from ..freecad_utils import warn 6 | try: 7 | from .dynamic_ui.models_library import ModelsLibraryModalClass 8 | imports_ok = True 9 | except ImportError as e: 10 | # TODO: Warn the user more nicely. 11 | warn(str(e) + '. Models library tool is disabled.', gui=False) 12 | imports_ok = False 13 | 14 | 15 | class _OpenModelsLibraryCommand: 16 | """The command definition to open models library.""" 17 | 18 | def GetResources(self): 19 | return { 20 | 'Pixmap': 'models_library.svg', 21 | 'MenuText': tr('Open models library'), 22 | 'Accel': 'O, L', 23 | 'ToolTip': tr( 24 | 'library with models.', 25 | ), 26 | } 27 | 28 | def IsActive(self): 29 | return imports_ok 30 | 31 | def Activated(self): 32 | doc = fc.activeDocument() 33 | if not doc: 34 | doc = fc.newDocument() 35 | form = ModelsLibraryModalClass() 36 | form.exec_() 37 | 38 | 39 | fcgui.addCommand('OpenModelsLibrary', _OpenModelsLibraryCommand()) 40 | -------------------------------------------------------------------------------- /resources/sensors/link/depth_camera.sdf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 1.05 8 | 9 | 640 10 | 480 11 | R_FLOAT32 12 | 13 | 14 | 0.1 15 | 10.0 16 | 17 | 18 | /tmp/camera_depth_data 19 | 20 | false 21 | camera_depth/trigger 22 | 23 | true 24 | 10 25 | camera_depth 26 | true 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /resources/sensors/link/thermal_camera.sdf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 1.047 8 | 9 | 320 10 | 240 11 | 12 | 13 | 0.1 14 | 100 15 | 16 | 17 | /tmp/camera_thermal_data 18 | 19 | false 20 | camera_thermal/trigger 21 | 22 | true 23 | 30 24 | true 25 | camera_thermal 26 | true 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /freecad/cross/joint.py: -------------------------------------------------------------------------------- 1 | # Stub for a CROSS::Joint. 2 | 3 | from __future__ import annotations 4 | 5 | from typing import NewType 6 | 7 | import FreeCAD as fc 8 | 9 | # Implementation note: These cannot be imported because of circular 10 | # dependency. 11 | JointProxy = NewType('JointProxy', object) 12 | VPJointProxy = NewType('VPJointProxy', object) 13 | 14 | 15 | class Joint(fc.DocumentObject): 16 | Group: list[fc.DocumentObject] 17 | Child: str # Must name a CROSS::Link by its ROS name. 18 | Effort: float 19 | LowerLimit: float 20 | Mimic: bool 21 | MimickedJoint: Joint 22 | Multiplier: float 23 | Offset: float 24 | Origin: fc.Placement 25 | Parent: str # Must name a CROSS::Link by its ROS name. 26 | Placement: fc.Placement 27 | PlacementRelTotalCenterOfMass: fc.Placement 28 | Position: float 29 | Proxy: JointProxy 30 | Type: str 31 | UpperLimit: float 32 | Velocity: float 33 | JointSpecific: dict 34 | JoinRotationDirection: dict 35 | _Type: str 36 | 37 | 38 | class ViewProviderJoint: 39 | AxisLength: float 40 | Object: Joint 41 | Proxy: VPJointProxy 42 | Visibility: bool 43 | 44 | def addDisplayMode(self, separator, mode: str) -> None: ... 45 | -------------------------------------------------------------------------------- /resources/icons/collision_copy_obj.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /freecad/cross/import_urdf.py: -------------------------------------------------------------------------------- 1 | import FreeCAD as fc 2 | 3 | import FreeCADGui as fcgui 4 | 5 | from .freecad_utils import warn 6 | from .gui_utils import tr 7 | from .robot_from_urdf import robot_from_urdf 8 | 9 | try: 10 | from .urdf_loader import UrdfLoader 11 | imports_ok = True 12 | except ImportError as e: 13 | # TODO: Warn the user more nicely. 14 | warn(str(e), gui=False) 15 | imports_ok = False 16 | 17 | 18 | def open(filename: str): 19 | """Called when FreeCAD wants to open a file.""" 20 | doc = fc.newDocument() 21 | if not doc: 22 | return 23 | load_urdf(filename, doc) 24 | 25 | 26 | def insert(filename: str, docname: str): 27 | """Called when freecad wants to import a file.""" 28 | try: 29 | doc = fc.getDocument(docname) 30 | except NameError: 31 | doc = fc.newDocument(docname) 32 | load_urdf(filename, doc) 33 | 34 | 35 | def load_urdf(filename: str, doc: fc.Document) -> None: 36 | if doc is None: 37 | return 38 | urdf_robot = UrdfLoader.load_from_file(filename) 39 | doc.openTransaction(tr('Robot from URDF')) 40 | robot_from_urdf(doc, urdf_robot) 41 | doc.commitTransaction() 42 | doc.recompute() 43 | fcgui.SendMsgToActiveView('ViewFit') 44 | -------------------------------------------------------------------------------- /resources/sensors/link/boundingbox_camera_3d.sdf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | camera_boxes_3d 7 | 8 | 3d 9 | 1.047 10 | 11 | 800 12 | 600 13 | 14 | 15 | 0.1 16 | 10 17 | 18 | 19 | /tmp/camera_boxes_3d_data 20 | 21 | false 22 | camera_boxes_3d/trigger 23 | 24 | true 25 | 30 26 | true 27 | true 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /resources/sensors/link/altimeter.sdf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 9 | 10 | true 11 | 30 12 | true 13 | altimeter 14 | true 15 | 16 | 17 | 18 | 0.1 19 | 0.2 20 | 21 | 22 | 23 | 24 | 0.2 25 | 0.1 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /freecad/cross/ui/command_new_joint.py: -------------------------------------------------------------------------------- 1 | import FreeCAD as fc 2 | 3 | import FreeCADGui as fcgui 4 | 5 | from ..gui_utils import tr 6 | from ..wb_utils import is_link_selected 7 | from ..wb_utils import is_robot_selected 8 | from ..wb_utils import is_workcell_selected 9 | 10 | 11 | class _NewJointCommand: 12 | """The command definition to create a new Joint object.""" 13 | 14 | def GetResources(self): 15 | return { 16 | 'Pixmap': 'joint.svg', 17 | 'MenuText': tr('New Joint'), 18 | 'Accel': 'N, J', 19 | 'ToolTip': tr('Create a Joint.'), 20 | } 21 | 22 | def IsActive(self): 23 | return ( 24 | is_robot_selected() 25 | or is_link_selected() 26 | or is_workcell_selected() 27 | ) 28 | 29 | def Activated(self): 30 | doc = fc.activeDocument() 31 | doc.openTransaction('Create Joint') 32 | fcgui.doCommand('') 33 | fcgui.addModule('freecad.cross.joint_proxy') 34 | fcgui.doCommand("_joint = freecad.cross.joint_proxy.make_joint('Joint')") 35 | fcgui.doCommand('FreeCADGui.ActiveDocument.setEdit(_joint.Name)') 36 | doc.commitTransaction() 37 | doc.recompute() 38 | 39 | 40 | fcgui.addCommand('NewJoint', _NewJointCommand()) 41 | -------------------------------------------------------------------------------- /freecad/cross/ui/command_new_sensor.py: -------------------------------------------------------------------------------- 1 | import FreeCADGui as fcgui 2 | 3 | from ..gui_utils import tr 4 | from ..wb_utils import is_robot_selected, is_link_selected, is_joint_selected 5 | from ..wb_utils import git_init_submodules 6 | from ..wb_utils import SDFORMAT_PATH 7 | from .dynamic_ui.sensors_selector import SensorsSelectorModalClass 8 | 9 | 10 | class _NewSensorCommand: 11 | """The command definition to create a new Sensor object.""" 12 | 13 | def GetResources(self): 14 | return { 15 | 'Pixmap': 'sensor.svg', 16 | 'MenuText': tr('Create a Sensor'), 17 | 'Accel': 'N, C', 18 | 'ToolTip': tr( 19 | 'Create a Sensor.\n' 20 | '\n' 21 | 'Sensors are needed to receive data about environment.\n' 22 | '\n' 23 | 'See https://gazebosim.org/docs/latest/sensors/ documentation.', 24 | ), 25 | } 26 | 27 | def IsActive(self): 28 | return is_robot_selected() or is_link_selected() or is_joint_selected() 29 | 30 | def Activated(self): 31 | git_init_submodules(submodule_repo_path = SDFORMAT_PATH) 32 | form = SensorsSelectorModalClass() 33 | form.exec_() 34 | 35 | 36 | fcgui.addCommand('NewSensor', _NewSensorCommand()) 37 | -------------------------------------------------------------------------------- /resources/sensors/link/boundingbox_camera_full_2d.sdf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | camera_boxes_full_2d 7 | 8 | full_2d 9 | 1.047 10 | 11 | 800 12 | 600 13 | 14 | 15 | 0.1 16 | 10 17 | 18 | 19 | /tmp/camera_boxes_full_2d_data 20 | 21 | false 22 | camera_boxes_full_2d/trigger 23 | 24 | true 25 | 30 26 | true 27 | true 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /resources/sensors/link/boundingbox_camera_visible_2d.sdf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | camera_boxes_visible_2d 7 | 8 | visible_2d 9 | 1.047 10 | 11 | 800 12 | 600 13 | 14 | 15 | 0.1 16 | 10 17 | 18 | 19 | /tmp/camera_boxes_visible_2d_data 20 | 21 | false 22 | camera_boxes_visible_2d/trigger 23 | 24 | true 25 | 30 26 | true 27 | true 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /resources/sensors/link/segmentation_camera_panoptic.sdf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | camera_panoptic 7 | 8 | instance 9 | 1.57 10 | 11 | 800 12 | 600 13 | 14 | 15 | 0.1 16 | 100 17 | 18 | 19 | /tmp/camera_panoptic_data 20 | 21 | false 22 | camera_panoptic/trigger 23 | 24 | true 25 | 30 26 | true 27 | true 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /resources/sensors/link/segmentation_camera_semantic.sdf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | camera_semantic 7 | 8 | semantic 9 | 1.57 10 | 11 | 800 12 | 600 13 | 14 | 15 | 0.1 16 | 100 17 | 18 | 19 | /tmp/camera_semantic_data 20 | 21 | false 22 | camera_semantic/trigger 23 | 24 | true 25 | 30 26 | true 27 | true 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /resources/sensors/link/gpu_lidar.sdf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | lidar 7 | 10 8 | 9 | 10 | 11 | 640 12 | 1 13 | -1.396263 14 | 1.396263 15 | 16 | 17 | 16 18 | 1 19 | -0.261799 20 | 0.261799 21 | 22 | 23 | 24 | 0.08 25 | 10.0 26 | 0.01 27 | 28 | 29 | true 30 | true 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /freecad/cross/ui/command_set_joints.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import FreeCAD as fc 4 | 5 | import FreeCADGui as fcgui 6 | 7 | from ..gui_utils import tr 8 | from ..wb_utils import is_robot_selected 9 | from .set_joints_dialog import SetJointsDialog 10 | 11 | 12 | class _SetJointsCommand: 13 | """The command definition to create a set the joint values of a robot.""" 14 | 15 | def GetResources(self): 16 | return { 17 | 'Pixmap': 'set_joints.svg', 18 | 'MenuText': tr('Set Joints'), 19 | 'Accel': 'S, J', 20 | 'ToolTip': tr('Set the joint values of a robot.'), 21 | } 22 | 23 | def IsActive(self): 24 | return is_robot_selected() 25 | 26 | def Activated(self): 27 | doc = fc.activeDocument() 28 | doc.openTransaction(tr('Set Joints')) 29 | objs = fcgui.Selection.getSelection() 30 | if not objs: 31 | doc = fc.activeDocument() 32 | else: 33 | robot = objs[0] 34 | diag = SetJointsDialog(robot, fcgui.getMainWindow()) 35 | joint_values = diag.exec_() 36 | diag.close() 37 | if joint_values: 38 | robot.Proxy.set_joint_values(joint_values) 39 | doc.recompute() 40 | doc.commitTransaction() 41 | 42 | 43 | fcgui.addCommand('SetJoints', _SetJointsCommand()) 44 | -------------------------------------------------------------------------------- /freecad/cross/ui/duplicate_robot_dialog.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import FreeCADGui as fcgui 4 | 5 | 6 | class DuplicateRobotDialog: 7 | """A dialog to get the parameters for robot duplication. 8 | 9 | """ 10 | 11 | def __init__(self, base_name: str = 'robot'): 12 | """Constructor with a CROSS::Robot.""" 13 | # Import late to avoid slowing down workbench start-up. 14 | from ..wb_utils import UI_PATH 15 | 16 | self.number_of_duplicates = 0 17 | self.base_name = base_name 18 | 19 | self.form = fcgui.PySideUic.loadUi( 20 | str(UI_PATH / 'duplicate_robot_dialog.ui'), 21 | fcgui.getMainWindow(), 22 | ) 23 | 24 | self.form.line_edit_basename.setText(base_name) 25 | 26 | self.form.button_box.accepted.connect(self._on_accept) 27 | self.form.button_box.rejected.connect(self._on_cancel) 28 | 29 | def exec(self) -> int: 30 | self.form.exec() 31 | return self.number_of_duplicates 32 | 33 | def close(self) -> None: 34 | self.form.close() 35 | 36 | def _on_accept(self) -> None: 37 | self.number_of_duplicates = self.form.spinbox_number.value() 38 | self.base_name = self.form.line_edit_basename.text() 39 | self.form.close() 40 | 41 | def _on_cancel(self) -> None: 42 | self.form.close() 43 | -------------------------------------------------------------------------------- /freecad/cross/ui/command_new_controller.py: -------------------------------------------------------------------------------- 1 | import FreeCADGui as fcgui 2 | 3 | from ..gui_utils import tr 4 | from ..wb_utils import is_controller_selected, is_robot_selected 5 | from ..wb_utils import git_init_submodules 6 | from .dynamic_ui.controllers_selector import ControllersSelectorModalClass 7 | from ..freecad_utils import message 8 | 9 | 10 | class _NewControllerCommand: 11 | """The command definition to create a new Controller or Broadcaster object.""" 12 | 13 | def GetResources(self): 14 | return { 15 | 'Pixmap': 'controller.svg', 16 | 'MenuText': tr('Create a Controller or Broadcaster'), 17 | 'Accel': 'N, C', 18 | 'ToolTip': tr( 19 | 'Create a Controller or Broadcaster.\n' 20 | '\n' 21 | 'Controllers are needed to control joints.\n' 22 | 'Broadcasters are needed for receive and transfer data from sensors.\n' 23 | '\n' 24 | 'See ros2_controllers and ros2_control documentation.', 25 | ), 26 | } 27 | 28 | def IsActive(self): 29 | return is_robot_selected() or is_controller_selected() 30 | 31 | def Activated(self): 32 | git_init_submodules() 33 | form = ControllersSelectorModalClass() 34 | form.exec_() 35 | 36 | 37 | fcgui.addCommand('NewController', _NewControllerCommand()) 38 | -------------------------------------------------------------------------------- /docker/README.md: -------------------------------------------------------------------------------- 1 | # (optional) If you dont have docker installed 2 | 3 | ## Install Docker with Compose and Buildx plugins 4 | https://docs.docker.com/engine/install/ubuntu/#install-using-the-repository 5 | 6 | ### Add your user to docker group 7 | ``` 8 | sudo usermod -aG docker $USER 9 | ``` 10 | 11 | ### Reboot system 12 | ``` 13 | reboot 14 | ``` 15 | 16 | # Run FreeCAD with RobotCAD 17 | In RobotCAD root directory do commands 18 | ``` 19 | cd docker 20 | bash run.bash 21 | ``` 22 | 23 | It will build image and run FreeCAD in container then open FreeCAD window at host. Also it bind host FreeCAD mods to container. 24 | 25 | Folder bindings (cont <-> host): 26 | 27 | /docker/freecad/freecad_projects_save_place/ - folder for your FC projects 28 | 29 | /docker/freecad/freecad_appimage_dir - folder where you can place FreeCAD appImage if you want not to use default stable FreeCAD 30 | 31 | /docker/ros2_ws/src - ROS2 workspace src inside container 32 | 33 | /docker/ros2_ws/build_data - ROS2 workspace build folders (build, install, log, etc) inside container 34 | 35 | ~/.local/share/FreeCAD/Mod - mods of FreeCAD 36 | 37 | 38 | # Troubleshooting 39 | 40 | ## ERROS when run FreeCAD: 41 | "Program received signal SIGSEGV, Segmentation fault." 42 | Mean not enough rights with some folder that FreeCAD using. FreeCAD can be run with sudo or find folder (usualy ~/.local/share/FreeCAD) and "chown" it to current user 43 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Python: Remote Attach", 9 | "type": "debugpy", 10 | "request": "attach", 11 | "connect": { 12 | "host": "localhost", 13 | "port": 5678 14 | }, 15 | "pathMappings": [ 16 | { 17 | "localRoot": "${workspaceFolder}", 18 | "remoteRoot": "${userHome}/.local/share/FreeCAD/Mod/freecad.robotcad" 19 | } 20 | ], 21 | "justMyCode": true 22 | }, 23 | { 24 | "name": "Python Conda Mod: Remote Attach", 25 | "type": "debugpy", 26 | "request": "attach", 27 | "connect": { 28 | "host": "localhost", 29 | "port": 5678 30 | }, 31 | "pathMappings": [ 32 | { 33 | "localRoot": "${workspaceFolder}", 34 | "remoteRoot": "${userHome}/miniconda3/envs/freecad_1_0_312/share/Mod/freecad.robotcad" 35 | } 36 | ], 37 | "justMyCode": true 38 | } 39 | ] 40 | } 41 | -------------------------------------------------------------------------------- /freecad/cross/ui/kk_model.py: -------------------------------------------------------------------------------- 1 | """ 2 | The model for Khalil-Kleinfinger (KK) robot representation 3 | 4 | The model for Khalil-Kleinfinger (KK) robot representation 5 | for Qt's Model/View architecture. 6 | 7 | """ 8 | 9 | from ..kk_robot import KKRobot 10 | from .dh_kk_model import DHKKModel 11 | 12 | 13 | class KKModel(DHKKModel): 14 | 15 | columns = ( 16 | 'theta', 17 | 'r', 18 | 'd', 19 | 'alpha', 20 | 'gamma', 21 | 'epsilon', 22 | 'prismatic', 23 | ) 24 | column_headers = ( 25 | 'θ (rad)', 26 | 'r (m)', 27 | 'd (m)', 28 | 'α (rad)', 29 | 'ɣ (rad)', 30 | 'ε (m)', 31 | 'Prismatic?', 32 | ) 33 | 34 | column_tooltips = ( 35 | 'Θi: angle between X(i-1) and Xi about Zi, in radians.', 36 | 'ri: distance between Oi and X(i-1), in meters.', 37 | 'di: distance between O(i-1) and Zi, in meters.', 38 | 'ɑi: angle between Z(i-1) and Zi about X(i-1), in radians.', 39 | "γi: angle between Xi and X'i about Zi, in radians.", 40 | "εi: distance between Oi and O'i, in meters.", 41 | 'True if the joint is prismatic, False if revolute.', 42 | ) 43 | 44 | def __init__( 45 | self, 46 | kk_robot: KKRobot, 47 | parent=None, 48 | ) -> None: 49 | super().__init__(kk_robot, parent) 50 | -------------------------------------------------------------------------------- /resources/sensors/link/magnetometer.sdf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 9 | 10 | true 11 | 100 12 | true 13 | magnetometer 14 | true 15 | 16 | 17 | 18 | 0.0 19 | 0.1 20 | 21 | 22 | 23 | 24 | 0.0 25 | 0.1 26 | 27 | 28 | 29 | 30 | 0.0 31 | 0.1 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /freecad/cross/ui/command_wb_settings.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from pathlib import Path 4 | 5 | import FreeCADGui as fcgui 6 | 7 | from ..freecad_utils import warn 8 | from ..gui_utils import tr 9 | from ..wb_gui_utils import WbSettingsGetter 10 | from ..wb_utils import set_workbench_param, get_workbench_param 11 | from .. import wb_globals 12 | 13 | 14 | def _warn_if_not_workspace(path: str, gui: bool = True): 15 | p = Path(path) 16 | if not (p / 'install/setup.bash').exists(): 17 | warn(f'{path} does not appear to be a valid ROS workspace', gui) 18 | 19 | 20 | class _WbSettingsCommand: 21 | """The command definition to set up the workbench.""" 22 | 23 | def GetResources(self): 24 | return { 25 | 'Pixmap': 'wb_settings.svg', 26 | 'MenuText': tr('Workbench settings'), 27 | 'Accel': 'W, S', 28 | 'ToolTip': tr('Workbench settings'), 29 | } 30 | 31 | def IsActive(self): 32 | return True 33 | 34 | def Activated(self): 35 | settings_getter = WbSettingsGetter(wb_globals.g_ros_workspace) 36 | if settings_getter.get_settings(): 37 | wb_globals.g_ros_workspace = settings_getter.ros_workspace 38 | set_workbench_param(wb_globals.PREF_VHACD_PATH, str(settings_getter.vhacd_path)) 39 | set_workbench_param(wb_globals.PREF_OVERCROSS_TOKEN, str(settings_getter.overcross_token)) 40 | 41 | 42 | fcgui.addCommand('WbSettings', _WbSettingsCommand()) 43 | -------------------------------------------------------------------------------- /freecad/cross/ui/dh_model.py: -------------------------------------------------------------------------------- 1 | """ 2 | The model for Denavit-Hartenberg (DH) robot representation 3 | 4 | The model for Denavit-Hartenberg (DH) robot representation 5 | for Qt's Model/View architecture. 6 | 7 | """ 8 | 9 | from PySide import QtCore # FreeCAD's PySide! 10 | 11 | from ..kk_robot import KKRobot 12 | from .dh_kk_model import DHKKModel 13 | 14 | 15 | class DHModel(DHKKModel): 16 | 17 | columns = ( 18 | 'rz', 19 | 'tz', 20 | 'rx', 21 | 'tx', 22 | 'prismatic', 23 | ) 24 | column_headers = ( 25 | 'θ (rad)', 26 | 'r (m)', 27 | 'α (rad)', 28 | 'd (m)', 29 | 'Prismatic?', 30 | ) 31 | 32 | column_tooltips = ( 33 | 'Θi: angle between X(i-1) and Xi about Z(i-1), in radians.', 34 | 'ri: distance between Oi and X(i-1), in meters.', 35 | 'ɑi: angle between Z(i-1) and Zi about X(i-1), in radians.', 36 | 'di: distance between O(i-1) and Zi, in meters.', 37 | 'True if the joint is prismatic, False if revolute.', 38 | ) 39 | 40 | def __init__( 41 | self, 42 | kk_robot: KKRobot, 43 | parent=None, 44 | ) -> None: 45 | super().__init__(kk_robot, parent) 46 | 47 | def data(self, index, role): 48 | if role == QtCore.Qt.DisplayRole: 49 | if self.kk_robot.is_dh_compatible: 50 | return super().data(index, role) 51 | else: 52 | return 'N/A' 53 | -------------------------------------------------------------------------------- /freecad/cross/ui/command_new_attached_collision_object.py: -------------------------------------------------------------------------------- 1 | import FreeCAD as fc 2 | 3 | import FreeCADGui as fcgui 4 | 5 | from ..freecad_utils import warn 6 | from ..gui_utils import tr 7 | 8 | 9 | class _NewAttachedCollisionObjectCommand: 10 | """The command definition to create a new AttachedCollisionObject object.""" 11 | 12 | def GetResources(self): 13 | return { 14 | 'Pixmap': 'attached_collision_object.svg', 15 | 'MenuText': tr('New AttachedCollisionObject'), 16 | 'Accel': 'N, A', 17 | 'ToolTip': tr('Create an AttachedCollisionObject container.'), 18 | } 19 | 20 | def IsActive(self): 21 | return True 22 | 23 | def Activated(self): 24 | doc = fc.activeDocument() 25 | if not doc: 26 | doc = fc.newDocument() 27 | if not doc: 28 | warn('No active document and cannot create a new document, doing nothing', True) 29 | doc.openTransaction(tr('Create AttachedCollisionObject')) 30 | fcgui.doCommand('') 31 | fcgui.addModule('freecad.cross.attached_collision_object_proxy') 32 | fcgui.doCommand("_attached_collision_object = freecad.cross.attached_collision_object_proxy.make_attached_collision_object('AttachedCollisionObject')") 33 | # fcgui.doCommand('FreeCADGui.ActiveDocument.setEdit(_attached_collision_object.Name)') 34 | doc.commitTransaction() 35 | doc.recompute() 36 | 37 | 38 | fcgui.addCommand('NewAttachedCollisionObject', _NewAttachedCollisionObjectCommand()) 39 | -------------------------------------------------------------------------------- /resources/icons/links_to_joints.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /resources/templates/launch/description.launch.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | import launch 4 | from launch.substitutions import Command 5 | from launch.substitutions import LaunchConfiguration 6 | from launch.actions import DeclareLaunchArgument 7 | 8 | import launch_ros 9 | from launch_ros.parameter_descriptions import ParameterValue 10 | 11 | 12 | def generate_launch_description(): 13 | pkg_share = Path(launch_ros.substitutions.FindPackageShare(package='{package_name}').find('{package_name}')) 14 | default_model_path = pkg_share / 'urdf/{xacro_wrapper_file}' 15 | 16 | use_sim_time = LaunchConfiguration('use_sim_time') 17 | use_sim_time_launch_arg = DeclareLaunchArgument('use_sim_time', default_value='true') 18 | 19 | robot_state_publisher_node = launch_ros.actions.Node( 20 | package='robot_state_publisher', 21 | executable='robot_state_publisher', 22 | parameters=[ 23 | {{ 24 | # ParameterValue is required to avoid being interpreted as YAML. 25 | 'robot_description': ParameterValue(Command(['xacro ', LaunchConfiguration('model')]), value_type=str), 26 | 'use_sim_time': use_sim_time, 27 | }}, 28 | ], 29 | ) 30 | 31 | return launch.LaunchDescription([ 32 | launch.actions.DeclareLaunchArgument( 33 | name='model', 34 | default_value=str(default_model_path), 35 | description="Absolute path to the robot's URDF file", 36 | ), 37 | use_sim_time_launch_arg, 38 | robot_state_publisher_node, 39 | ]) 40 | -------------------------------------------------------------------------------- /freecad/cross/sensors/sensor_factory.py: -------------------------------------------------------------------------------- 1 | import FreeCAD as fc 2 | from typing import ForwardRef, List, Optional, Union, cast 3 | 4 | from .sensor_proxy import add_sensor_properties_block 5 | from .sensor_proxy import _ViewProviderSensor 6 | from .sensor_proxy_link import SensorProxyLink 7 | from .sensor_proxy_joint import SensorProxyJoint 8 | from ..freecad_utils import error, warn 9 | # Stubs and type hints. 10 | from .sensor import Sensor as CrossSensor # A Cross::Sensor, i.e. a DocumentObject with Proxy "Sensor". # noqa: E501 11 | 12 | 13 | def make_sensor(sensor_data: dict, sensor_dir_name: str, doc: Optional[fc.Document] = None) -> CrossSensor | None: 14 | """Add a Cross::SensorJoint or Cross::SensorLink to the current document.""" 15 | 16 | if doc is None: 17 | doc = fc.activeDocument() 18 | if doc is None: 19 | warn('No active document, doing nothing', False) 20 | return 21 | 22 | sensor: CrossSensor = doc.addObject('App::FeaturePython', sensor_data['name']) 23 | sensor_proxy_map = { 24 | 'link': lambda sensor: SensorProxyLink(sensor), 25 | 'joint': lambda sensor: SensorProxyJoint(sensor), 26 | } 27 | sensor_proxy_map[sensor_dir_name](sensor) # ex. SensorProxyLink(sensor) or SensorProxyJoint(sensor) 28 | 29 | add_sensor_properties_block(sensor, sensor_data) 30 | 31 | if hasattr(fc, 'GuiUp') and fc.GuiUp: 32 | _ViewProviderSensor(sensor.ViewObject) 33 | else: 34 | error('Parameters of ' + sensor_data['name'] + ' not received, interrupt.', True) 35 | doc.recompute() 36 | return None 37 | 38 | doc.recompute() 39 | return sensor 40 | -------------------------------------------------------------------------------- /resources/icons/links_to_joints_spider.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /resources/icons/explode_links.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *,cover 47 | .hypothesis/ 48 | 49 | # Linters 50 | .mypy_cache 51 | 52 | # Translations 53 | *.mo 54 | *.pot 55 | 56 | # Django stuff: 57 | *.log 58 | local_settings.py 59 | 60 | # Flask stuff: 61 | instance/ 62 | .webassets-cache 63 | 64 | # Scrapy stuff: 65 | .scrapy 66 | 67 | # Sphinx documentation 68 | docs/_build/ 69 | 70 | # PyBuilder 71 | target/ 72 | 73 | # Jupyter Notebook 74 | .ipynb_checkpoints 75 | 76 | # pyenv 77 | .python-version 78 | 79 | # celery beat schedule file 80 | celerybeat-schedule 81 | 82 | # SageMath parsed files 83 | *.sage.py 84 | 85 | # dotenv 86 | .env 87 | 88 | # virtualenv 89 | .venv 90 | venv/ 91 | ENV/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | 96 | # Rope project settings 97 | .ropeproject 98 | 99 | # enternal gitmodules md5 backup 100 | .gitmodules_md5 101 | -------------------------------------------------------------------------------- /resources/icons/cylinder_x_from_bbox.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | X 13 | -------------------------------------------------------------------------------- /resources/icons/cylinder_y_from_bbox.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | Y 13 | -------------------------------------------------------------------------------- /resources/icons/cylinder_z_from_bbox.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | Z 13 | -------------------------------------------------------------------------------- /freecad/cross/ui/command_get_planning_scene.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import FreeCAD as fc 4 | 5 | import FreeCADGui as fcgui 6 | 7 | from ..gui_utils import tr 8 | 9 | 10 | class _GetPlanningSceneCommand: 11 | """The command definition to get the planning scene from a service. 12 | 13 | The command definition to get the planning scene from a service 14 | (usually /get_planning_scene). 15 | 16 | """ 17 | 18 | def GetResources(self): 19 | return { 20 | 'Pixmap': 'planning_scene.svg', 21 | 'MenuText': tr('Get Planning Scene'), 22 | 'Accel': 'G, P', 23 | 'ToolTip': tr('Get the Current Planning Scene from the /get_planning_scene service.'), 24 | } 25 | 26 | def IsActive(self): 27 | return True 28 | 29 | def Activated(self): 30 | doc = fc.activeDocument() 31 | if not doc: 32 | doc = fc.newDocument() 33 | fcgui.addModule('freecad.cross.planning_scene_proxy') 34 | fcgui.addModule('freecad.cross.ros.planning_scene') 35 | fcgui.doCommand('_scene_msg = freecad.cross.ros.planning_scene.get_planning_scene(timeout_sec=10.0)') 36 | doc.openTransaction(tr('Get Planning Scene')) 37 | fcgui.doCommand( 38 | 'if _scene_msg is None:\n' 39 | ' _scene = None\n' 40 | 'else:\n' 41 | " _scene = freecad.cross.planning_scene_proxy.make_planning_scene(_scene_msg.name, _scene_msg)", 42 | ) 43 | # fcgui.doCommand('FreeCADGui.ActiveDocument.setEdit(_scene.Name)') 44 | doc.recompute() 45 | doc.commitTransaction() 46 | fcgui.doCommand("Gui.SendMsgToActiveView('ViewFit')") 47 | 48 | 49 | fcgui.addCommand('GetPlanningScene', _GetPlanningSceneCommand()) 50 | -------------------------------------------------------------------------------- /freecad/cross/ui/command_robot_from_urdf.py: -------------------------------------------------------------------------------- 1 | 2 | import FreeCAD as fc 3 | import FreeCADGui as fcgui 4 | 5 | from PySide import QtGui # FreeCAD's PySide! 6 | 7 | from ..freecad_utils import warn 8 | from ..gui_utils import tr 9 | from ..ros.utils import is_ros_found 10 | try: 11 | from ..robot_from_urdf import robot_from_urdf_path 12 | imports_ok = True 13 | except ImportError as e: 14 | # TODO: Warn the user more nicely. 15 | warn(str(e) + '. Robot from URDF tool is disabled.', gui=False) 16 | imports_ok = False 17 | 18 | 19 | class _UrdfImportCommand: 20 | def GetResources(self): 21 | return { 22 | 'Pixmap': 'robot_from_urdf.svg', 23 | 'MenuText': tr('Import a URDF or xacro file'), 24 | 'ToolTip': tr('Import a URDF or xacro file'), 25 | } 26 | 27 | def Activated(self): 28 | doc = fc.activeDocument() 29 | dialog = QtGui.QFileDialog( 30 | fcgui.getMainWindow(), 31 | 'Select URDF/xacro file to import part from', 32 | ) 33 | # set option "DontUseNativeDialog"=True, as native Filedialog shows 34 | # misbehavior on Ubuntu 18.04. It works case sensitively, what is not wanted... 35 | dialog.setNameFilter('Supported Formats *.urdf *.xacro;;All files (*.*)') 36 | if dialog.exec_(): 37 | if not doc: 38 | doc = fc.newDocument() 39 | urdf_filename = str(dialog.selectedFiles()[0]) 40 | robot_from_urdf_path( 41 | fc.activeDocument(), 42 | urdf_filename, 43 | remove_solid_splitter = True, 44 | ) 45 | fcgui.SendMsgToActiveView('ViewFit') 46 | 47 | 48 | def IsActive(self): 49 | return imports_ok 50 | 51 | 52 | fcgui.addCommand('UrdfImport', _UrdfImportCommand()) 53 | -------------------------------------------------------------------------------- /freecad/cross/ui/command_rotate_joint_x.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import FreeCAD as fc 4 | 5 | import FreeCADGui as fcgui 6 | 7 | from ..freecad_utils import message 8 | from ..freecad_utils import validate_types 9 | from ..freecad_utils import is_lcs 10 | from ..gui_utils import tr 11 | from ..wb_utils import rotate_origin 12 | 13 | 14 | # Stubs and type hints. 15 | from ..joint import Joint 16 | from ..link import Link 17 | DO = fc.DocumentObject 18 | CrossLink = Link 19 | CrossJoint = Joint 20 | LCS = DO # Local coordinate systen, TypeId == "PartDesign::CoordinateSystem" 21 | 22 | 23 | class _RotateJointXCommand: 24 | """Command to rotate joint by X axis. 25 | """ 26 | 27 | def GetResources(self): 28 | return { 29 | 'Pixmap': 'rotate_joint_x.svg', 30 | 'MenuText': tr('Rotate joint/link by X axis'), 31 | 'Accel': 'R, X', 32 | 'ToolTip': tr( 33 | 'Rotate joint or link by X axis.\n' 34 | '\n' 35 | 'Select: joint or link or subelement (face, edge, vertex) of link Real or LCS of link Real.\n' 36 | 'It rotates joint Origin or Link MountedPlacement dependent on selection.\n' 37 | 'If selected subelement or LCS or body it rotate link around it center\n' 38 | 'or center of gravity or concentric for curve and circle.\n', 39 | ), 40 | } 41 | 42 | def IsActive(self): 43 | return bool(fcgui.Selection.getSelection()) 44 | 45 | def Activated(self): 46 | doc = fc.activeDocument() 47 | 48 | doc.openTransaction(tr("Rotate joint origin by X axis")) 49 | rotate_origin(x = 90, y = None, z = None) 50 | doc.commitTransaction() 51 | 52 | doc.recompute() 53 | 54 | 55 | fcgui.addCommand('RotateJointX', _RotateJointXCommand()) 56 | -------------------------------------------------------------------------------- /freecad/cross/ui/command_rotate_joint_y.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import FreeCAD as fc 4 | 5 | import FreeCADGui as fcgui 6 | 7 | from ..freecad_utils import message 8 | from ..freecad_utils import validate_types 9 | from ..freecad_utils import is_lcs 10 | from ..gui_utils import tr 11 | from ..wb_utils import rotate_origin 12 | 13 | 14 | # Stubs and type hints. 15 | from ..joint import Joint 16 | from ..link import Link 17 | DO = fc.DocumentObject 18 | CrossLink = Link 19 | CrossJoint = Joint 20 | LCS = DO # Local coordinate systen, TypeId == "PartDesign::CoordinateSystem" 21 | 22 | 23 | class _RotateJointYCommand: 24 | """Command to rotate joint by Y axis. 25 | """ 26 | 27 | def GetResources(self): 28 | return { 29 | 'Pixmap': 'rotate_joint_y.svg', 30 | 'MenuText': tr('Rotate joint/link by Y axis'), 31 | 'Accel': 'R, Y', 32 | 'ToolTip': tr( 33 | 'Rotate joint or link by Y axis.\n' 34 | '\n' 35 | 'Select: joint or link or subelement (face, edge, vertex) of link Real or LCS of link Real.\n' 36 | 'It rotates joint Origin or Link MountedPlacement dependent on selection.\n' 37 | 'If selected subelement or LCS or body it rotate link around it center\n' 38 | 'or center of gravity or concentric for curve and circle.\n', 39 | ), 40 | } 41 | 42 | def IsActive(self): 43 | return bool(fcgui.Selection.getSelection()) 44 | 45 | def Activated(self): 46 | doc = fc.activeDocument() 47 | 48 | doc.openTransaction(tr("Rotate joint origin by Y axis")) 49 | rotate_origin(x = None, y = 90, z = None) 50 | doc.commitTransaction() 51 | 52 | doc.recompute() 53 | 54 | 55 | fcgui.addCommand('RotateJointY', _RotateJointYCommand()) 56 | -------------------------------------------------------------------------------- /freecad/cross/ui/command_rotate_joint_z.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import FreeCAD as fc 4 | 5 | import FreeCADGui as fcgui 6 | 7 | from ..freecad_utils import message 8 | from ..freecad_utils import validate_types 9 | from ..freecad_utils import is_lcs 10 | from ..gui_utils import tr 11 | from ..wb_utils import rotate_origin 12 | 13 | 14 | # Stubs and type hints. 15 | from ..joint import Joint 16 | from ..link import Link 17 | DO = fc.DocumentObject 18 | CrossLink = Link 19 | CrossJoint = Joint 20 | LCS = DO # Local coordinate systen, TypeId == "PartDesign::CoordinateSystem" 21 | 22 | 23 | class _RotateJointZCommand: 24 | """Command to rotate joint by Z axis. 25 | """ 26 | 27 | def GetResources(self): 28 | return { 29 | 'Pixmap': 'rotate_joint_z.svg', 30 | 'MenuText': tr('Rotate joint/link by Z axis'), 31 | 'Accel': 'R, Z', 32 | 'ToolTip': tr( 33 | 'Rotate joint or link by Z axis.\n' 34 | '\n' 35 | 'Select: joint or link or subelement (face, edge, vertex) of link Real or LCS of link Real.\n' 36 | 'It rotates joint Origin or Link MountedPlacement dependent on selection.\n' 37 | 'If selected subelement or LCS or body it rotate link around it center\n' 38 | 'or center of gravity or concentric for curve and circle.\n', 39 | ), 40 | } 41 | 42 | def IsActive(self): 43 | return bool(fcgui.Selection.getSelection()) 44 | 45 | def Activated(self): 46 | doc = fc.activeDocument() 47 | 48 | doc.openTransaction(tr("Rotate joint origin by Z axis")) 49 | rotate_origin(x = None, y = None, z = 90) 50 | doc.commitTransaction() 51 | 52 | doc.recompute() 53 | 54 | 55 | fcgui.addCommand('RotateJointZ', _RotateJointZCommand()) 56 | -------------------------------------------------------------------------------- /freecad/cross/vendor/fcapi/README.md: -------------------------------------------------------------------------------- 1 | # FreeCAD Scripting Modern APIs 2 | 3 | ## Audience 4 | 5 | This is for developers of FreeCAD extensions like Workbenches and Macros in pure 6 | python. 7 | 8 | This is a python API, it is expected that the readers are python developers with 9 | basic python coding skills. The API does not use any obscure language feature but 10 | it uses classes, functions, decorators, type hints, etc... 11 | 12 | It is also expected that the readers are FreeCAD users, and have a good understanding 13 | of the basic usage of it. 14 | 15 | ## Scripted Object API (Feature Python Object) 16 | 17 | This API is an overlay API that helps in definition of Feature Python Objects in 18 | a more declarative an pythonic way. 19 | 20 | * `fpo.py ` 21 | This is the API implementation 22 | * `examples/` 23 | contains basic usage examples of the fpo api 24 | * `docs/` 25 | contains source and generated documentation 26 | 27 | ### Documentation 28 | 29 | * [docs/documentation.md](docs/documentation.md) 30 | * [docs/documentation.pdf](docs/documentation.pdf) 31 | 32 | 33 | ## Declarative Qt/Gui API 34 | 35 | This API provides a simple way to create GUIs for the extensions without the 36 | complexity of raw Qt code or .ui files. Code layout reflect GUI structure making 37 | it readable and easy to maintain. The API covers the most common Widgets, Qt is 38 | a massive library so covering everything is virtually impossible for this project 39 | and largely unnecessary. 40 | 41 | * `fcui.py` 42 | This is the GUI API implementation 43 | * `examples/ui/` 44 | contains basic usage examples of the `fcui` api 45 | * `docs/` 46 | contains source and generated documentation 47 | 48 | 49 | ### Documentation 50 | * [docs/ui-documentation.md](docs/ui-documentation.md) 51 | * [docs/ui-documentation.pdf](docs/ui-documentation.pdf) 52 | 53 | 54 | ## Status 55 | 56 | This is a working draft as of Nov 10, 2024 -------------------------------------------------------------------------------- /freecad/cross/ui/command_new_joints_filled_spider_connect.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import FreeCAD as fc 4 | 5 | import FreeCADGui as fcgui 6 | 7 | from ..gui_utils import tr 8 | from ..joint_proxy import make_robot_joints_filled 9 | from ..wb_utils import is_link 10 | 11 | 12 | # Stubs and type hints. 13 | from ..joint import Joint 14 | from ..link import Link 15 | DO = fc.DocumentObject 16 | CrossLink = Link 17 | CrossJoint = Joint 18 | LCS = DO # Local coordinate systen, TypeId == "PartDesign::CoordinateSystem" 19 | 20 | 21 | class _NewJointsFilledSpiderCommand: 22 | """Command to create new joints by selected links with spider connection. 23 | 24 | Links must be in robot container before 25 | """ 26 | 27 | def GetResources(self): 28 | return { 29 | 'Pixmap': 'links_to_joints_spider.svg', 30 | 'MenuText': tr('New joints by selected links with spider connection'), 31 | 'Accel': 'J, S', 32 | 'ToolTip': tr( 33 | 'New joints by selected links with spider connection.\n' 34 | '\n' 35 | 'Select: robot links (minimum 2). Links must be in robot container.\n' 36 | '\n' 37 | 'Joints will be created by selected links and all links will be connected to first link.\n', 38 | ), 39 | } 40 | 41 | def IsActive(self): 42 | return bool(fcgui.Selection.getSelection()) and is_link(fcgui.Selection.getSelection()[0]) 43 | 44 | def Activated(self): 45 | doc = fc.activeDocument() 46 | 47 | doc.openTransaction(tr("New filled joints by selected links with spider connection")) 48 | make_robot_joints_filled(joints_group_connect_type = 'spider') 49 | doc.commitTransaction() 50 | 51 | doc.recompute() 52 | 53 | 54 | fcgui.addCommand('NewJointsFilledSpider', _NewJointsFilledSpiderCommand()) 55 | -------------------------------------------------------------------------------- /resources/icons/body_to_link.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /freecad/cross/ui/command_assembly_from_urdf.py: -------------------------------------------------------------------------------- 1 | 2 | import FreeCAD as fc 3 | import FreeCADGui as fcgui 4 | 5 | from PySide import QtGui 6 | 7 | 8 | from ..freecad_utils import warn 9 | from ..gui_utils import tr 10 | from ..ros.utils import is_ros_found 11 | try: 12 | from freecad.cross.robot_from_urdf import assembly_from_urdf_path # FreeCAD's PySide! 13 | imports_ok = True 14 | except ImportError as e: 15 | # TODO: Warn the user more nicely. 16 | warn(str(e) + '. Assembly from URDF tool is disabled.', gui=False) 17 | imports_ok = False 18 | 19 | 20 | class _AssemblyFromUrdfCommand: 21 | def GetResources(self): 22 | return { 23 | 'Pixmap': 'assembly_from_urdf.svg', 24 | 'MenuText': tr('Create an assembly from a URDF or xacro file'), 25 | 'ToolTip': tr('Create an assembly from a URDF or xacro file'), 26 | } 27 | 28 | def Activated(self): 29 | doc = fc.activeDocument() 30 | dialog = QtGui.QFileDialog( 31 | fcgui.getMainWindow(), 32 | 'Select URDF/xacro file to import part from', 33 | ) 34 | # set option "DontUseNativeDialog"=True, as native Filedialog shows 35 | # misbehavior on Ubuntu 18.04. It works case sensitively, what isn't wanted 36 | dialog.setNameFilter('Supported Formats *.urdf *.xacro;;All files (*.*)') 37 | if dialog.exec_(): 38 | if not doc: 39 | doc = fc.newDocument() 40 | filename = str(dialog.selectedFiles()[0]) 41 | doc.openTransaction(tr('Assembly from URDF')) 42 | assembly_from_urdf_path( 43 | doc, 44 | filename, 45 | ) 46 | doc.commitTransaction() 47 | doc.recompute() 48 | fcgui.SendMsgToActiveView('ViewFit') 49 | 50 | def IsActive(self): 51 | return imports_ok 52 | 53 | 54 | fcgui.addCommand('AssemblyFromUrdf', _AssemblyFromUrdfCommand()) 55 | -------------------------------------------------------------------------------- /freecad/cross/ui/command_new_joints_filled.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import FreeCAD as fc 4 | 5 | import FreeCADGui as fcgui 6 | 7 | from ..freecad_utils import message 8 | from ..freecad_utils import validate_types 9 | from ..freecad_utils import is_lcs 10 | from ..gui_utils import tr 11 | from ..joint_proxy import make_robot_joints_filled 12 | from ..wb_utils import is_link 13 | 14 | 15 | # Stubs and type hints. 16 | from ..joint import Joint 17 | from ..link import Link 18 | DO = fc.DocumentObject 19 | CrossLink = Link 20 | CrossJoint = Joint 21 | LCS = DO # Local coordinate systen, TypeId == "PartDesign::CoordinateSystem" 22 | 23 | 24 | class _NewJointsFilledCommand: 25 | """Command to create new joints by selected links. 26 | 27 | Links must be in robot container before 28 | """ 29 | 30 | def GetResources(self): 31 | return { 32 | 'Pixmap': 'links_to_joints.svg', 33 | 'MenuText': tr('New joints by selected links with chain connection'), 34 | 'Accel': 'J, F', 35 | 'ToolTip': tr( 36 | 'New joints by selected links with chain connection.\n' 37 | '\n' 38 | 'Select: robot links (minimum 2). Links must be in robot container.\n' 39 | '\n' 40 | 'Joints will be created by selected links with chain connection.\n' 41 | 'Order of links in selection is order of links in joints.\n', 42 | ), 43 | } 44 | 45 | def IsActive(self): 46 | return bool(fcgui.Selection.getSelection()) and is_link(fcgui.Selection.getSelection()[0]) 47 | 48 | def Activated(self): 49 | doc = fc.activeDocument() 50 | 51 | doc.openTransaction(tr("New filled joints by selected links")) 52 | make_robot_joints_filled() 53 | doc.commitTransaction() 54 | 55 | doc.recompute() 56 | 57 | 58 | fcgui.addCommand('NewJointsFilled', _NewJointsFilledCommand()) 59 | -------------------------------------------------------------------------------- /freecad/cross/ui/command_new_links_filled.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import FreeCAD as fc 4 | 5 | import FreeCADGui as fcgui 6 | 7 | from ..freecad_utils import message 8 | from ..freecad_utils import validate_types 9 | from ..freecad_utils import is_lcs 10 | from ..gui_utils import tr 11 | from ..link_proxy import make_robot_links_filled 12 | 13 | 14 | # Stubs and type hints. 15 | from ..joint import Joint 16 | from ..link import Link 17 | DO = fc.DocumentObject 18 | CrossLink = Link 19 | CrossJoint = Joint 20 | LCS = DO # Local coordinate systen, TypeId == "PartDesign::CoordinateSystem" 21 | 22 | 23 | class _NewLinksFilledCommand: 24 | """Command to create new robot links with filled Visual and Real by selected objects. 25 | """ 26 | 27 | def GetResources(self): 28 | return {'Pixmap': 'body_to_link.svg', 29 | 'MenuText': tr('New links with filled Real, Visual by selected objects'), 30 | 'Accel': 'L, F', 31 | 'ToolTip': tr('New links with filled Real, Visual by selected objects.\n' 32 | '\n' 33 | 'Select: object (part, body) or objects \n' 34 | '\n' 35 | 'Will be created robot links with filled Real and Visual properties by selected objects.\n' 36 | 'Each link for each object.\n' 37 | 'Will be created part-wrapper for each object if object is not a part.' 38 | )} 39 | 40 | def IsActive(self): 41 | return bool(fcgui.Selection.getSelection()) 42 | 43 | def Activated(self): 44 | doc = fc.activeDocument() 45 | 46 | doc.openTransaction(tr("New filled robot links by selected objects")) 47 | make_robot_links_filled() 48 | doc.commitTransaction() 49 | 50 | doc.recompute() 51 | 52 | 53 | fcgui.addCommand('NewLinksFilled', _NewLinksFilledCommand()) 54 | -------------------------------------------------------------------------------- /resources/icons/cylinder_from_bbox.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /resources/sensors/TODO/link/optical_tactile.sdf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | true 8 | optical_tactile_plugin 9 | true 10 | true 11 | true 12 | 15 13 | 0.01 14 | 0.001 15 | 16 | 1 17 | true 18 | depth_camera 19 | 0 0 0 0 0 0 20 | 21 | 22 | 640 23 | 480 24 | R_FLOAT32 25 | 26 | 27 | 0.030 28 | 10.0 29 | 30 | 31 | true 32 | 33 | 34 | 37 | 38 | true 39 | 40 | collision 41 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /resources/icons/planning_scene.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 18 | 36 | 40 | 47 | 54 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /resources/icons/observer.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 35 | 37 | 41 | 47 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /freecad/cross/ui/command_set_placement_fast_parent_to_child.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import FreeCAD as fc 4 | 5 | import FreeCADGui as fcgui 6 | 7 | from ..gui_utils import tr 8 | from ..wb_utils import set_placement_fast 9 | 10 | 11 | # Stubs and type hints. 12 | from ..joint import Joint 13 | from ..link import Link 14 | DO = fc.DocumentObject 15 | CrossLink = Link 16 | CrossJoint = Joint 17 | LCS = DO # Local coordinate systen, TypeId == "PartDesign::CoordinateSystem" 18 | 19 | 20 | class _SetCROSSPlacementFastParentToChildCommand: 21 | """Command to move parent kinematic tree to child branch. 22 | """ 23 | 24 | def GetResources(self): 25 | return { 26 | 'Pixmap': 'set_cross_placement_fast_parent_to_child.svg', 27 | 'MenuText': tr('Set placement - move parent kinematic tree to child branch'), 28 | 'Accel': 'P, P', 29 | 'ToolTip': tr( 30 | 'Move parent kinematic tree to child branch and connect references.\n' 31 | '\n' 32 | 'Select (with Ctlr): \n' 33 | ' 1) subelement (face, edge, vertex, LCS) of body (of Real) of robot link (first reference)\n' 34 | ' 2) subelement (face, edge, vertex, LCS) of body (of Real) of robot link (second reference)\n' 35 | '\n' 36 | 'Robot links must be near to each other in chain (parent, child) and have joint between.\n' 37 | '\n' 38 | 'This will connect parent kinematic tree and child branch in reference places.\n' 39 | 'Parent kinematic tree will be moved relatively. Child branch visually will be in same position\n' 40 | '\n' 41 | 'If selected subelement (face, edge, vertex) will be used temporary LCS underhood.\n' 42 | ), 43 | } 44 | 45 | def IsActive(self): 46 | return bool(fcgui.Selection.getSelection()) 47 | 48 | def Activated(self): 49 | set_placement_fast(parent_tree_to_child_branch=True) 50 | 51 | 52 | fcgui.addCommand('SetCROSSPlacementFastParentToChild', _SetCROSSPlacementFastParentToChildCommand()) 53 | -------------------------------------------------------------------------------- /freecad/cross/ui/command_world_generator.py: -------------------------------------------------------------------------------- 1 | import FreeCAD as fc 2 | 3 | import FreeCADGui as fcgui 4 | try: 5 | from PySide.QtWidgets import QApplication 6 | except: 7 | from PySide2.QtWidgets import QApplication 8 | 9 | from ..gui_utils import tr 10 | import sys 11 | from ..wb_utils import DYNAMIC_WORLD_GENERATOR_MODULE_PATH, DYNAMIC_WORLD_GENERATOR_REPO_PATH, git_init_submodules 12 | 13 | 14 | class _WorldGeneratorCommand: 15 | """The command definition to create a custom world.""" 16 | 17 | def GetResources(self): 18 | return { 19 | 'Pixmap': 'world_generator.svg', 20 | 'MenuText': tr('Create custom world'), 21 | 'Accel': 'N, W', 22 | 'ToolTip': tr('Create custom world with dynamic and static obstacles.' \ 23 | '\n\nDocs at https://github.com/drfenixion/Dynamic_World_Generator'), 24 | } 25 | 26 | def IsActive(self): 27 | return fc.activeDocument() is not None 28 | 29 | def Activated(self): 30 | 31 | def world_generator_run_wrapper(): 32 | # add module path to sys.path for working local path imports in module 33 | DYNAMIC_WORLD_GENERATOR_MODULE_PATH_str = str(DYNAMIC_WORLD_GENERATOR_MODULE_PATH) 34 | if DYNAMIC_WORLD_GENERATOR_MODULE_PATH_str not in sys.path: 35 | sys.path.insert(0, DYNAMIC_WORLD_GENERATOR_MODULE_PATH_str) 36 | 37 | from modules.Dynamic_World_Generator.code.dwg_wizard import run as world_generator_run 38 | from modules.Dynamic_World_Generator.code.dwg_wizard import dwg_wizard_close_emitter 39 | 40 | def close_callback(close_emitter_instance): 41 | QApplication.instance().aboutToQuit.connect(close_emitter_instance.on_app_quit) 42 | 43 | close_emitter = dwg_wizard_close_emitter(close_callback) 44 | 45 | world_generator_run(close_emitter) 46 | 47 | git_init_submodules( 48 | submodule_repo_path = DYNAMIC_WORLD_GENERATOR_REPO_PATH, 49 | callback = world_generator_run_wrapper 50 | ) 51 | 52 | 53 | fcgui.addCommand('WorldGenerator', _WorldGeneratorCommand()) 54 | -------------------------------------------------------------------------------- /freecad/cross/ros/planning_scene.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import ForwardRef, Optional 4 | import time 5 | 6 | try: 7 | from moveit_msgs.msg import PlanningScene as PlanningSceneMsg 8 | from moveit_msgs.srv import GetPlanningScene 9 | imports_ok = True 10 | except ImportError: 11 | PlanningSceneMsg = ForwardRef('PlanningSceneMsg') 12 | GetPlanningScene = ForwardRef('GetPlanningScene') 13 | imports_ok = False 14 | 15 | from .. import wb_globals 16 | from ..freecad_utils import message 17 | from ..freecad_utils import tr 18 | from ..freecad_utils import warn 19 | 20 | 21 | def get_planning_scene(timeout_sec=0.0) -> Optional[PlanningSceneMsg]: 22 | """Get the current planning scene by calling the ROS server. 23 | 24 | Parameters: 25 | - timeout_sec: Timeout to reach the server and then timeout 26 | to get the response. Will wait indefinitely if 27 | set to 0.0 (the default). 28 | 29 | """ 30 | if not imports_ok: 31 | warn(tr('ROS modules cannot be imported'), gui=True) 32 | return None 33 | 34 | node = wb_globals.g_ros_node 35 | if node is None: 36 | return None 37 | 38 | executor = wb_globals.g_ros_executor 39 | 40 | # TODO: configure the service name. 41 | service_name = 'get_planning_scene' 42 | client = node.create_client(GetPlanningScene, service_name) 43 | start_time = time.time() 44 | while not client.wait_for_service(timeout_sec=1.0): 45 | msg = f'service /{service_name} not available, waiting again...' 46 | message(msg) 47 | node.get_logger().info(msg) 48 | if (timeout_sec > 0.0) and (time.time() - start_time) > timeout_sec: 49 | warn(tr(f'The server /{service_name} was not reached within {timeout_sec} s'), gui=True) 50 | return None 51 | request = GetPlanningScene.Request() 52 | future = client.call_async(request) 53 | executor.spin_until_future_complete(future, timeout_sec=timeout_sec) 54 | if not future.done(): 55 | warn(tr('The server was reached but the request timed out'), gui=True) 56 | return None 57 | return future.result().scene 58 | -------------------------------------------------------------------------------- /freecad/cross/ui/command_set_placement_fast_child_to_parent.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import FreeCAD as fc 4 | 5 | import FreeCADGui as fcgui 6 | 7 | from ..gui_utils import tr 8 | from ..wb_utils import set_placement_fast 9 | 10 | # Stubs and type hints. 11 | from ..joint import Joint 12 | from ..link import Link 13 | DO = fc.DocumentObject 14 | CrossLink = Link 15 | CrossJoint = Joint 16 | LCS = DO # Local coordinate systen, TypeId == "PartDesign::CoordinateSystem" 17 | 18 | 19 | class _SetCROSSPlacementFastChildToParentCommand: 20 | """Command to set the Origin of a joint beetween links to reference placement. 21 | """ 22 | 23 | def GetResources(self): 24 | return { 25 | 'Pixmap': 'set_cross_placement_fast_child_to_parent.svg', 26 | 'MenuText': tr('Set placement - of child kinematic branch'), 27 | 'Accel': 'P, C', 28 | 'ToolTip': tr( 29 | 'Set the Origin of a joint beetween links to reference placement.\nIt moves child kinematic branch.\n' 30 | '\n' 31 | 'Select (with Ctlr): \n' 32 | ' 1) subelement (face, edge, vertex, LCS) of body (of Real) of robot link (first reference)\n' 33 | ' 2) subelement (face, edge, vertex, LCS) of body (of Real) of robot link (second reference)\n' 34 | '\n' 35 | 'Robot links must be near to each other in chain (parent, child) and have joint between.\n' 36 | '\n' 37 | 'This will connect 2 links (child to parent) in reference places.\n' 38 | 'Joint Origin will be moved to connection position.\n' 39 | '\n' 40 | 'If selected subelement (face, edge, vertex) will be used temporary LCS underhood.\n' 41 | '\n' 42 | 'This tool for moving kinematic branch root joint to reference placement\n', 43 | ), 44 | } 45 | 46 | def IsActive(self): 47 | return bool(fcgui.Selection.getSelection()) 48 | 49 | def Activated(self): 50 | set_placement_fast(child_branch_to_parent_tree = True) 51 | 52 | 53 | fcgui.addCommand('SetCROSSPlacementFastChildToParent', _SetCROSSPlacementFastChildToParentCommand()) 54 | -------------------------------------------------------------------------------- /freecad/cross/ui/command_simplify_mesh.py: -------------------------------------------------------------------------------- 1 | """Command to simplify a part or mesh into an approximate convex decomposition mesh.""" 2 | 3 | from __future__ import annotations 4 | 5 | import FreeCAD as fc 6 | import FreeCADGui as fcgui 7 | 8 | from ..gui_utils import tr 9 | from ..freecad_utils import warn 10 | from ..mesh_utils import get_simplified_mesh 11 | from ..placement_utils import get_global_placement 12 | 13 | 14 | # Typing hints. 15 | DO = fc.DocumentObject 16 | SO = 'FreeCADGui.SelectionObject' # Could not get the class from Python. 17 | 18 | 19 | class _SimplifyMeshCommand: 20 | """Command to make a simplified mesh from the selected objects.""" 21 | 22 | def GetResources(self): 23 | return { 24 | 'Pixmap': 'simplify_mesh.svg', 25 | 'MenuText': tr('Simplify mesh'), 26 | 'ToolTip': tr( 27 | 'Generate an approximate convex decomposition mesh' 28 | 'from the selected object with V-HACD from' 29 | ' https://github.com/kmammou/v-hacd/' 30 | ' configured in the workbench settings' 31 | ), 32 | } 33 | 34 | def Activated(self): 35 | selection = fcgui.Selection.getSelectionEx('', 0) 36 | if not selection: 37 | warn(tr('Nothing selected, nothing to do'), True) 38 | return 39 | for sel in selection: 40 | obj = sel.Object 41 | sub_fullpaths = sel.SubElementNames 42 | if not sub_fullpaths: 43 | # An object is selected, not a face, edge, vertex. 44 | placement = get_global_placement(obj, '') 45 | else: 46 | # One or more subelements are selected, only consider the first one. 47 | placement = get_global_placement(obj, sub_fullpaths[0]) 48 | mesh = get_simplified_mesh(obj) 49 | mesh_obj = obj.Document.addObject('Mesh::Feature', f'{obj.Name}_simplified') 50 | mesh_obj.Label = f'{obj.Label} simplified' 51 | mesh_obj.Mesh = mesh 52 | mesh_obj.Placement = placement 53 | 54 | def IsActive(self): 55 | return bool(fcgui.Selection.getSelection()) 56 | 57 | 58 | fcgui.addCommand('SimplifyMesh', _SimplifyMeshCommand()) 59 | -------------------------------------------------------------------------------- /freecad/cross/ui/command_explode_links.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import FreeCAD as fc 4 | 5 | import FreeCADGui as fcgui 6 | 7 | from ..freecad_utils import message 8 | from ..freecad_utils import validate_types 9 | from ..freecad_utils import is_lcs 10 | from ..gui_utils import tr 11 | from ..link_proxy import explode_link 12 | from ..wb_utils import is_robot 13 | 14 | 15 | # Stubs and type hints. 16 | from ..joint import Joint 17 | from ..link import Link 18 | DO = fc.DocumentObject 19 | CrossLink = Link 20 | CrossJoint = Joint 21 | LCS = DO # Local coordinate systen, TypeId == "PartDesign::CoordinateSystem" 22 | 23 | 24 | class _ExplodeLinksCommand: 25 | """Command to move links (explode view) to see hiden faces. 26 | """ 27 | 28 | def GetResources(self): 29 | return {'Pixmap': 'explode_links.svg', 30 | 'MenuText': tr('Move links (explode view) to see hiden faces.'), 31 | 'Accel': 'E, L', 32 | 'ToolTip': tr('Move links (explode view) to see hiden faces.\n' 33 | '\n' 34 | 'Select: link(s) or subobject of link \n' 35 | '\n' 36 | 'Link(s) must be in joint(s) for visual effect.\n' 37 | '\n' 38 | 'Link will be moved by changing MountedPlacement.\n' 39 | 'This useful for see hidden faces of link subobject\n' 40 | 'for select contact faces for placement tools.\n' 41 | )} 42 | 43 | def IsActive(self): 44 | return bool(fcgui.Selection.getSelection()) 45 | 46 | def Activated(self): 47 | doc = fc.activeDocument() 48 | 49 | selection = fcgui.Selection.getSelection() 50 | 51 | doc.openTransaction(tr("Explode links")) 52 | if len(selection) and is_robot(selection[0]): 53 | robot = selection[0] 54 | selection = robot.Proxy.get_links() 55 | 56 | for i in range(len(selection)): 57 | explode_link(selection[i], i) 58 | doc.commitTransaction() 59 | 60 | doc.recompute() 61 | 62 | 63 | fcgui.addCommand('ExplodeLinks', _ExplodeLinksCommand()) 64 | -------------------------------------------------------------------------------- /freecad/cross/ui/command_set_placement_fast.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import FreeCAD as fc 4 | 5 | import FreeCADGui as fcgui 6 | 7 | from ..gui_utils import tr 8 | from ..wb_utils import set_placement_fast 9 | 10 | 11 | # Stubs and type hints. 12 | from ..joint import Joint 13 | from ..link import Link 14 | DO = fc.DocumentObject 15 | CrossLink = Link 16 | CrossJoint = Joint 17 | LCS = DO # Local coordinate systen, TypeId == "PartDesign::CoordinateSystem" 18 | 19 | 20 | class _SetCROSSPlacementFastCommand: 21 | """Command to set the Origin of a joint and Mounted Placement of link and make LCS. 22 | """ 23 | 24 | def GetResources(self): 25 | return { 26 | 'Pixmap': 'set_cross_placement_fast.svg', 27 | 'MenuText': tr('Set placement - fast'), 28 | 'Accel': 'P, F', 29 | 'ToolTip': tr( 30 | 'Set the Origin of a joint and Mounted Placement of link.\n' 31 | 'Use it for current unplacement tip of kinematic branch.\n' 32 | '\n' 33 | 'Select (with Ctlr): \n' 34 | ' 1) subelement (face, edge, vertex, LCS) of body (of Real) of robot link (first reference)\n' 35 | ' 2) subelement (face, edge, vertex, LCS) of body (of Real) of robot link (second reference)\n' 36 | '\n' 37 | 'Robot links must be near to each other in chain (parent, child) and have joint between.\n' 38 | '\n' 39 | 'This will connect 2 links (child to parent) in reference places.\n' 40 | 'Joint Origin and link Mounted Placement will be moved to connection position.\n' 41 | '\n' 42 | 'If selected subelement (face, edge, vertex) will be used temporary LCS underhood.\n' 43 | '\n' 44 | 'Dont use this for moving kinematic branch because it also set MountedPlacement\n' 45 | 'that is may not be desirable in this case.\n', 46 | ), 47 | } 48 | 49 | def IsActive(self): 50 | return bool(fcgui.Selection.getSelection()) 51 | 52 | def Activated(self): 53 | set_placement_fast() 54 | 55 | 56 | fcgui.addCommand('SetCROSSPlacementFast', _SetCROSSPlacementFastCommand()) 57 | -------------------------------------------------------------------------------- /freecad/cross/ui/command_update_planning_scene.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import FreeCAD as fc 4 | 5 | import FreeCADGui as fcgui 6 | 7 | from ..freecad_utils import validate_types 8 | from ..freecad_utils import warn 9 | from ..gui_utils import tr 10 | from ..wb_utils import is_planning_scene_selected 11 | 12 | 13 | 14 | class _UpdatePlanningSceneCommand: 15 | """The command definition to get the planning scene from a service. 16 | 17 | The command definition to get the planning scene from a service 18 | (usually /get_planning_scene). 19 | 20 | """ 21 | 22 | def GetResources(self): 23 | return { 24 | 'Pixmap': 'update_planning_scene.svg', 25 | 'MenuText': tr('Update Planning Scene'), 26 | 'Accel': 'U, P', 27 | 'ToolTip': tr('Update the selected PlanningScene object with the current planning scene from the /get_planning_scene service.'), 28 | } 29 | 30 | def IsActive(self): 31 | return is_planning_scene_selected() 32 | 33 | def Activated(self): 34 | doc = fc.activeDocument() 35 | try: 36 | cross_planning_scene, = validate_types( 37 | fcgui.Selection.getSelection(), ['Cross::PlanningScene'], 38 | ) 39 | except RuntimeError: 40 | # The command is active only when a Cross::PlanningScene is active, 41 | # this should not happen. 42 | warn('Internal error, no Cross::PlanningScene selected', true) 43 | return 44 | 45 | fcgui.addModule('freecad.cross.ros.planning_scene') 46 | fcgui.doCommand('_scene_msg = freecad.cross.ros.planning_scene.get_planning_scene(timeout_sec=10.0)') 47 | doc.openTransaction(tr('Update Planning Scene')) 48 | fcgui.doCommand( 49 | 'if _scene_msg is not None:\n' 50 | f" FreeCAD.getDocument('{doc.Name}').getObject('{cross_planning_scene.Name}').Proxy.update_scene(_scene_msg)", 51 | ) 52 | # fcgui.doCommand('FreeCADGui.ActiveDocument.setEdit(_scene.Name)') 53 | doc.recompute() 54 | doc.commitTransaction() 55 | fcgui.doCommand("Gui.SendMsgToActiveView('ViewFit')") 56 | 57 | 58 | fcgui.addCommand('UpdatePlanningScene', _UpdatePlanningSceneCommand()) 59 | -------------------------------------------------------------------------------- /freecad/cross/geometry_helpers.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | EPSILON = 1.0e-5 4 | 5 | 6 | def are_parallel(vec1, vec2): 7 | """Determine if two vectors are parallel.""" 8 | vec1_unit = vec1 / np.linalg.norm(vec1) 9 | vec2_unit = vec2 / np.linalg.norm(vec2) 10 | 11 | return np.all(abs(np.cross(vec1_unit, vec2_unit)) < EPSILON) 12 | 13 | 14 | def are_collinear(point1, vec1, point2, vec2): 15 | """Determine if vectors are collinear.""" 16 | 17 | # To be collinear, vectors must be parallel 18 | if not are_parallel(vec1, vec2): 19 | return False 20 | 21 | # If parallel and point1 is coincident with point2, vectors are collinear 22 | if all(np.isclose(point1, point2)): 23 | return True 24 | 25 | # If vectors are parallel, point2 can be defined as p2 = p1 + t * v1 26 | t = np.zeros(3) 27 | for idx in range(0, 3): 28 | if vec1[idx] != 0: 29 | t[idx] = (point2[idx] - point1[idx]) / vec1[idx] 30 | p2 = point1 + t * vec1 31 | 32 | return np.allclose(p2, point2) 33 | 34 | 35 | def lines_intersect(point1, vec1, point2, vec2): 36 | """Determine if two lines intersect.""" 37 | epsilon = 1e-6 38 | x = np.zeros(2) 39 | 40 | # If lines are collinear, they have an infinite number of intersections. 41 | # Choose point1. 42 | if are_collinear(point1, vec1, point2, vec2): 43 | return True, np.atleast_1d(point1).copy() 44 | 45 | # If lines are parallel, they don't intersect. 46 | if are_parallel(vec1, vec2): 47 | return False, np.zeros_like(point1) 48 | 49 | # Test if lines intersect. Need to find non-singular 50 | # pair to solve for coefficients. 51 | for idx in range(3): 52 | i = idx 53 | j = (idx + 1) % 3 54 | A = np.array([[vec1[i], -vec2[i]], [vec1[j], -vec2[j]]]) 55 | b = np.array([[point2[i] - point1[i]], [point2[j] - point1[j]]]) 56 | 57 | # If singular matrix, go to next set 58 | if np.isclose(np.linalg.det(A), 0): 59 | continue 60 | else: 61 | x = np.linalg.solve(A, b) 62 | 63 | # Test if solution generates a point of intersection 64 | p1 = point1 + x[0] * vec1 65 | p2 = point2 + x[1] * vec2 66 | 67 | if all(np.less(np.abs(p1 - p2), epsilon * np.ones(3))): 68 | return True, x.flatten() 69 | 70 | return False, x 71 | -------------------------------------------------------------------------------- /freecad/cross/ui/command_kk_edit.py: -------------------------------------------------------------------------------- 1 | """Command to edit the selected robot in Khalil-Kleinfinger representation. 2 | 3 | Command to edit the selected robot in Khalil-Kleinfinger (KK) representation. 4 | Denavit-Hartenberg (DH) parameters can also be used to represent the robot. 5 | A new robot is created if nothing is selected. 6 | 7 | """ 8 | from __future__ import annotations 9 | 10 | import FreeCAD as fc 11 | import FreeCADGui as fcgui 12 | 13 | from ..gui_utils import tr 14 | 15 | 16 | def _supported_object_selected(): 17 | # Import late to avoid slowing down workbench start-up. 18 | from ..wb_utils import is_robot 19 | 20 | objs = fcgui.Selection.getSelection() 21 | if not objs: 22 | return True 23 | if is_robot(objs[0]): 24 | return True 25 | return False 26 | 27 | 28 | class _KKEditCommand: 29 | """Command to edit the selected robot in KK representation.""" 30 | 31 | def GetResources(self): 32 | return { 33 | 'Pixmap': 'kk_edit.svg', 34 | 'MenuText': tr('Edit DH or KK parameters'), 35 | 'ToolTip': tr('Edit the Denavit-Hartenberg or Khalil-Kleinfinger parameters of the selected robot'), 36 | } 37 | 38 | def Activated(self): 39 | # Import late to avoid slowing down workbench start-up. 40 | from ..freecad_utils import warn 41 | from ..robot_proxy import make_robot 42 | from ..wb_utils import is_robot 43 | from .kk_dialog import KKDialog 44 | 45 | objs = fcgui.Selection.getSelection() 46 | if not objs: 47 | doc = fc.activeDocument() 48 | if doc is None: 49 | doc = fc.newDocument() 50 | if not doc: 51 | warn('Could not create a document', True) 52 | return 53 | robot = make_robot('Robot') 54 | else: 55 | robot = objs[0] 56 | if not is_robot(robot): 57 | warn('Selected object is not a robot', True) 58 | return 59 | doc = robot.Document 60 | diag = KKDialog(robot) 61 | kk_robot = diag.exec_() 62 | diag.close() 63 | if not kk_robot: 64 | return 65 | doc.openTransaction(tr('Change robot from DH or KK')) 66 | kk_robot.transfer_to_robot(robot) 67 | doc.commitTransaction() 68 | 69 | def IsActive(self): 70 | return _supported_object_selected() 71 | 72 | 73 | fcgui.addCommand('KKEdit', _KKEditCommand()) 74 | -------------------------------------------------------------------------------- /load_workbench.py.txt: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import sys 4 | sys.path.append('/usr/lib/freecad-daily-python3/lib') 5 | sys.path.append('/usr/share/freecad-daily/Ext') 6 | sys.path.append('/usr/lib/python3/dist-packages') 7 | sys.path.append('$HOME/.local/share/FreeCAD/Mod/freecad.cross') 8 | sys.path.append('$HOME/.local/share/FreeCAD/Mod/MeshRemodel') 9 | sys.path.append('$HOME/.local/share/FreeCAD/Mod/Assembly4') 10 | sys.path.append('/usr/share/freecad-daily/Mod/PartDesign') 11 | sys.path.append('/usr/share/freecad-daily/Mod/MeshPart') 12 | sys.path.append('/usr/share/freecad-daily/Mod/Material') 13 | sys.path.append('/usr/share/freecad-daily/Mod/Draft') 14 | sys.path.append('/usr/share/freecad-daily/Mod/Mesh') 15 | sys.path.append('/usr/share/freecad-daily/Mod/Part') 16 | sys.path.append('/usr/share/freecad-daily/Mod/Arch') 17 | sys.path.append('/usr/lib/freecad-daily/Mod/Arch') 18 | sys.path.append('/usr/lib/freecad-daily/Mod/Draft') 19 | sys.path.append('$HOME/.local/share/FreeCAD/Macro/') 20 | sys.path.append('/usr/lib/freecad-daily/Macro') 21 | 22 | from freecad.cross.assembly4_utils import * 23 | from freecad.cross.assembly_from_urdf import * 24 | from freecad.cross.coin_utils import * 25 | from freecad.cross.deep_copy import * 26 | from freecad.cross.freecad_utils import * 27 | from freecad.cross.freecadgui_utils import * 28 | from freecad.cross.geometry_helpers import * 29 | from freecad.cross.gui_utils import * 30 | from freecad.cross.import_dae import * 31 | from freecad.cross.joint_proxy import * 32 | from freecad.cross.kk_robot import * 33 | from freecad.cross.link_proxy import * 34 | from freecad.cross.mesh_utils import * 35 | from freecad.cross.placement_utils import * 36 | from freecad.cross.pose_proxy import * 37 | from freecad.cross.planning_scene_proxy import * 38 | from freecad.cross.planning_scene_utils import * 39 | from freecad.cross.robot_proxy import * 40 | from freecad.cross.robot_from_urdf import * 41 | from freecad.cross.ros.node import * 42 | from freecad.cross.ros.planning_scene import * 43 | from freecad.cross.ros.utils import * 44 | from freecad.cross.trajectory_proxy import * 45 | from freecad.cross.urdf_loader import * 46 | from freecad.cross.urdf_parser_utils import * 47 | from freecad.cross.urdf_utils import * 48 | from freecad.cross.utils import * 49 | from freecad.cross.version import * 50 | from freecad.cross.wb_globals import * 51 | from freecad.cross.wb_utils import * 52 | from freecad.cross.workcell_proxy import * 53 | from freecad.cross.xacro_loader import * 54 | from freecad.cross.xacro_object_proxy import * 55 | -------------------------------------------------------------------------------- /freecad/cross/ui/command_set_material.py: -------------------------------------------------------------------------------- 1 | import FreeCAD as fc 2 | import FreeCADGui as fcgui 3 | import MaterialEditor 4 | 5 | from ..gui_utils import tr 6 | from ..wb_utils import is_robot_selected, is_link_selected 7 | from ..freecad_utils import error 8 | 9 | 10 | class _SetMaterialCommand: 11 | def GetResources(self): 12 | return { 13 | 'Pixmap': 'set_material.svg', 14 | 'MenuText': tr('Set the material to the whole robot or a link'), 15 | 'ToolTip': tr( 16 | 'Select a robot or a link and use this action to' 17 | ' select a material. The material will be used to' 18 | ' calculate mass and moments of inertia of the links.', 19 | ), 20 | } 21 | 22 | def Activated(self): 23 | doc = fc.activeDocument() 24 | doc.openTransaction(tr('Calculate mass and inertia')) 25 | objs = fcgui.Selection.getSelection() 26 | 27 | # TODO: apply to all selected robots and links. 28 | 29 | if not objs: 30 | doc = fc.activeDocument() 31 | else: 32 | obj = objs[0] 33 | 34 | try: 35 | card_path = obj.MaterialCardPath 36 | except (KeyError, AttributeError): 37 | card_path = '' 38 | 39 | material_editor = MaterialEditor.MaterialEditor(card_path=card_path) 40 | result = material_editor.exec_() 41 | 42 | if not result: 43 | # on cancel button an empty dict is returned. 44 | return 45 | 46 | try: 47 | card_name = material_editor.cards[material_editor.card_path] 48 | density = material_editor.materials[material_editor.card_path]['Density'] 49 | except (AttributeError, KeyError): 50 | card_name = '' 51 | density = '' 52 | 53 | if not density: 54 | error('Material without density. Choose other material or fill density.', True) 55 | elif fc.Units.Quantity(density) <= 0.0: 56 | error('Material density must be stringly positive. Correct material density or choose another material.', True) 57 | 58 | obj.MaterialCardName = card_name 59 | # TODO: make MaterialCardPath portable. 60 | obj.MaterialCardPath = material_editor.card_path 61 | obj.MaterialDensity = density 62 | 63 | doc.recompute() 64 | doc.commitTransaction() 65 | 66 | def IsActive(self): 67 | return is_robot_selected() or is_link_selected() 68 | 69 | 70 | fcgui.addCommand('SetMaterial', _SetMaterialCommand()) 71 | -------------------------------------------------------------------------------- /freecad/cross/ui/kk_dialog.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import Optional 4 | 5 | import FreeCAD as fc 6 | import FreeCADGui as fcgui 7 | 8 | from ..kk_robot import KKRobot 9 | from ..wb_utils import UI_PATH 10 | from .dh_model import DHModel 11 | from .kk_model import KKModel 12 | 13 | # Stubs and type hints. 14 | from ..robot import Robot as CrossRobot # A Cross::Robot, i.e. a DocumentObject with Proxy "Robot". # noqa: E501 15 | DO = fc.DocumentObject # A FreeCAD DocumentObject. 16 | 17 | 18 | class KKDialog: 19 | """A dialog to input DH and KK parameters. 20 | 21 | A dialog to input Denavit-Hartenberg (DH) and 22 | Khalil-Kleinfinger (KK) parameters. 23 | 24 | """ 25 | 26 | def __init__(self, robot: CrossRobot): 27 | """Constructor with a CROSS::Robot.""" 28 | self.robot = robot 29 | self.kk_robot = KKRobot() 30 | self.kk_robot.set_from_robot(robot) 31 | 32 | self.form = fcgui.PySideUic.loadUi( 33 | str(UI_PATH / 'kk_dialog.ui'), 34 | fcgui.getMainWindow(), 35 | ) 36 | self.dialog_confirmed = False 37 | 38 | # Disable the KK tab until supported. 39 | self.form.tab_widget.removeTab(1) 40 | 41 | self.dh_robot_model = DHModel(self.kk_robot) 42 | self.kk_robot_model = KKModel(self.kk_robot) 43 | self.form.table_view_dh.setModel(self.dh_robot_model) 44 | self.form.table_view_kk.setModel(self.kk_robot_model) 45 | 46 | self.form.push_button_add_joint.clicked.connect(self._on_add_joint) 47 | self.form.button_box.accepted.connect(self._on_accept) 48 | self.form.button_box.rejected.connect(self._on_cancel) 49 | 50 | def exec_(self) -> Optional[KKRobot]: 51 | self.form.exec_() 52 | if not self.dialog_confirmed: 53 | return 54 | return self.kk_robot 55 | 56 | def close(self) -> None: 57 | self.form.close() 58 | 59 | def _on_add_joint(self) -> None: 60 | # Call add_joint() only on one of self.kk_robot_model 61 | # or self.dh_robot_model because they share the same kk_robot. 62 | self.kk_robot_model.add_joint() 63 | # The modelReset() signal must be emitted on both, though. 64 | self.dh_robot_model.modelReset.emit() 65 | self.kk_robot_model.modelReset.emit() 66 | 67 | def _on_accept(self) -> None: 68 | self.dialog_confirmed = True 69 | self.form.close() 70 | 71 | def _on_cancel(self) -> None: 72 | self.dialog_confirmed = False 73 | self.form.close() 74 | -------------------------------------------------------------------------------- /resources/sensors/link/wideangle_camera.sdf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 3.14 8 | 9 | 800 10 | 600 11 | 12 | 13 | 0.1 14 | 100 15 | 16 | 17 | 18 | 19 | custom 20 | 21 | 22 | 23 | 1.05 24 | 25 | 4 26 | 27 | 1.0 28 | 29 | tan 30 | 31 | 32 | 33 | 34 | true 35 | 36 | 3.1415 37 | 38 | 512 39 | 40 | 41 | /tmp/camera_wide_angle_data 42 | 43 | false 44 | camera_wide_angle/trigger 45 | 46 | true 47 | 30 48 | camera_wide_angle 49 | true 50 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /freecad/cross/ui/command_new_robot.py: -------------------------------------------------------------------------------- 1 | import FreeCAD as fc 2 | 3 | import FreeCADGui as fcgui 4 | from freecad.cross.freecad_utils import is_assembly_from_assembly_wb 5 | 6 | from ..gui_utils import tr 7 | from ..robot_proxy import make_filled_robot, make_filled_robot_from_assembly 8 | 9 | 10 | class _NewRobotCommand: 11 | """The command definition to create a new Robot object.""" 12 | 13 | def GetResources(self): 14 | return { 15 | 'Pixmap': 'robot.svg', 16 | 'MenuText': tr('New Robot'), 17 | 'Accel': 'N, R', 18 | 'ToolTip': tr('Create a Robot container or Robot container with links and joints by selected objects \n' 19 | 'or Robot based on assembly from default Assembly WB. Choose an option:\n' 20 | '\n' 21 | '1) Select (objects): objects (parts, bodies, etc) in order of links in joints.\n' 22 | 'The order of objects selection will correspond to the order of links (parent, child) in the joints.\n' 23 | '\n' 24 | '2) Select (assembly): assembly from default Assembly workbench \n' 25 | '(closed loop assembly is not supported, break the ring before use).\n' 26 | '\n' 27 | '3) If no object is selected, an empty robot container will be created.' 28 | ), 29 | } 30 | 31 | def IsActive(self): 32 | return fc.activeDocument() is not None 33 | 34 | def Activated(self): 35 | doc = fc.activeDocument() 36 | 37 | 38 | robot_name = 'Robot' 39 | sel = fcgui.Selection.getSelection() 40 | if len(sel) and is_assembly_from_assembly_wb(sel[0]): 41 | doc.openTransaction(tr('Convert assembly to robot')) 42 | assembly = fcgui.Selection.getSelection()[0] 43 | make_filled_robot_from_assembly(assembly) 44 | doc.commitTransaction() 45 | elif len(sel): 46 | doc.openTransaction(tr('Make filled robot')) 47 | make_filled_robot(robot_name) 48 | doc.commitTransaction() 49 | else: 50 | doc.openTransaction(tr('Create Robot')) 51 | fcgui.doCommand('') 52 | fcgui.addModule('freecad.cross.robot_proxy') 53 | fcgui.doCommand(f"_robot = freecad.cross.robot_proxy.make_robot('{robot_name}')") 54 | fcgui.doCommand('FreeCADGui.ActiveDocument.setEdit(_robot.Name)') 55 | doc.commitTransaction() 56 | doc.recompute() 57 | 58 | 59 | fcgui.addCommand('NewRobot', _NewRobotCommand()) 60 | -------------------------------------------------------------------------------- /freecad/cross/assembly4_utils.py: -------------------------------------------------------------------------------- 1 | # Emulate some functions from module Asm4_libs v0.12.5. 2 | 3 | from typing import List, Optional, Tuple 4 | 5 | import FreeCAD as fc 6 | 7 | from .freecad_utils import add_property 8 | 9 | # Typing hints. 10 | DO = fc.DocumentObject 11 | LabelAndExpression = Tuple[str, str] 12 | ExpressionEngine = List[LabelAndExpression] 13 | 14 | 15 | def add_asm4_properties(obj: DO): 16 | """Render `obj` compatible with Assembly4.""" 17 | add_property(obj, 'App::PropertyString', 'AttachedBy', 'Assembly', '') 18 | add_property(obj, 'App::PropertyString', 'AttachedTo', 'Assembly', '') 19 | add_property( 20 | obj, 'App::PropertyPlacement', 'AttachmentOffset', 'Assembly', 21 | '', 22 | ) 23 | add_property(obj, 'App::PropertyString', 'SolverId', 'Assembly', '') 24 | obj.SolverId = 'Asm4EE' 25 | 26 | 27 | # TODO: Use `from Asm4_libs import createVariables as _new_variable_container` 28 | # (but fallback if not importable) 29 | # From Asm4_libs 30 | def new_variable_container( 31 | assembly: DO, 32 | ) -> DO: 33 | """Return a variable container for Assembly4.""" 34 | if ((not hasattr(assembly, 'TypeId')) 35 | or (assembly.TypeId != 'App::Part')): 36 | raise RuntimeError( 37 | 'First argument must be an `App::Part` FreeCAD object', 38 | ) 39 | # There is no object "Variables", so we create it. 40 | variables = assembly.Document.addObject('App::FeaturePython', 'Variables') 41 | if hasattr(variables, 'ViewObject') and variables.ViewObject: 42 | pass 43 | # Signature of a PropertyContainer. 44 | variables.addProperty('App::PropertyString', 'Type') 45 | variables.Type = 'App::PropertyContainer' 46 | assembly.addObject(variables) 47 | return variables 48 | 49 | 50 | def _get_placement_label_and_expression( 51 | expression_engine: ExpressionEngine, 52 | ) -> Optional[LabelAndExpression]: 53 | if not isinstance(expression_engine, list): 54 | return 55 | if not expression_engine: 56 | return 57 | for label_and_expr in expression_engine: 58 | if len(label_and_expr) < 2: 59 | continue 60 | if label_and_expr[0] == 'Placement': 61 | return label_and_expr 62 | 63 | 64 | def get_placement_expression( 65 | expression_engine: ExpressionEngine, 66 | ) -> Optional[str]: 67 | # Original name: placementEE. 68 | label_and_expr = _get_placement_label_and_expression(expression_engine) 69 | if label_and_expr: 70 | return label_and_expr[1] 71 | 72 | 73 | def update_placement_expression( 74 | obj: DO, 75 | expression: str, 76 | ) -> None: 77 | if not hasattr(obj, 'setExpression'): 78 | return 79 | obj.setExpression('Placement', expression) 80 | obj.setEditorMode('Placement', ['ReadOnly']) 81 | -------------------------------------------------------------------------------- /freecad/cross/ui/command_set_placement_fast_sensor.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import FreeCAD as fc 4 | 5 | import FreeCADGui as fcgui 6 | 7 | from ..gui_utils import tr 8 | from ..wb_utils import rotate_origin, rotate_placement, set_placement_fast 9 | 10 | 11 | # Stubs and type hints. 12 | from ..joint import Joint 13 | from ..link import Link 14 | DO = fc.DocumentObject 15 | CrossLink = Link 16 | CrossJoint = Joint 17 | LCS = DO # Local coordinate systen, TypeId == "PartDesign::CoordinateSystem" 18 | 19 | 20 | class _SetCROSSPlacementFastSensorCommand: 21 | """Command to set "Set placement - fast", and after use axis orietation correction for sensor (front of sensor is positive x-oriented). 22 | """ 23 | 24 | def GetResources(self): 25 | return { 26 | 'Pixmap': 'set_cross_placement_fast_sensor.svg', 27 | 'MenuText': tr('Set placement - sensor'), 28 | 'Accel': 'P, S', 29 | 'ToolTip': tr( 30 | 'Set the Origin of a joint and Mounted Placement of link.\n' 31 | '\n' 32 | 'Select (with Ctlr): \n' 33 | ' 1) subelement (face, edge, vertex, LCS) of body (of Real) of robot link (first reference)\n' 34 | ' 2) subelement (face, edge, vertex, LCS) of body (of Real) of robot link (second reference)\n' 35 | '\n' 36 | 'Works same way as "Set placement - fast", and after uses parent joint orietation correction for sensor link.\n' 37 | 'Front of sensor is positive x-oriented (red arrow) of parent joint. Y-axis (green arrow) is horizontal.\n' 38 | '\n' 39 | 'It may be necessary to ajust MountedPlacement after.\n' 40 | 'Use "Rotate joint/link" tool (select some face of sensor) or "Set placement - by orienteer" tool after that tool.\n', 41 | ), 42 | } 43 | 44 | def IsActive(self): 45 | return bool(fcgui.Selection.getSelection()) 46 | 47 | def Activated(self): 48 | doc = fc.activeDocument() 49 | 50 | # set placement of sensor link 51 | try: 52 | joint, child_link, parent_link = set_placement_fast() 53 | doc.recompute() 54 | except TypeError: 55 | return 56 | 57 | # make x-oriented sensor joint 58 | doc.openTransaction(tr("Rotate joint origin")) 59 | x = None 60 | y = 270 61 | z = None 62 | joint.Origin = rotate_placement(joint.Origin, x, y, z) 63 | doc.recompute() 64 | 65 | # correct mountedPlacement of sensor link 66 | set_placement_fast(joint_origin = False) 67 | # after correction link can have x-positive or x-negative direction dependent of object form 68 | # user should corrects MountedPlacement if wrong direction is set 69 | 70 | 71 | fcgui.addCommand('SetCROSSPlacementFastSensor', _SetCROSSPlacementFastSensorCommand()) 72 | -------------------------------------------------------------------------------- /resources/icons/pose.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 18 | 36 | 40 | 45 | 51 | 55 | 60 | 65 | 69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /resources/icons/robot.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /resources/ui/duplicate_robot_dialog.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | Dialog 4 | 5 | 6 | 7 | 0 8 | 0 9 | 287 10 | 129 11 | 12 | 13 | 14 | FreeCAD - CROSS - Duplicate Robot 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | Number of duplicates to create 24 | 25 | 26 | 27 | 28 | 29 | 30 | 1 31 | 32 | 33 | 9999 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | Base name 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | Qt::Horizontal 60 | 61 | 62 | QDialogButtonBox::Cancel|QDialogButtonBox::Ok 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | button_box 72 | accepted() 73 | Dialog 74 | accept() 75 | 76 | 77 | 248 78 | 254 79 | 80 | 81 | 157 82 | 274 83 | 84 | 85 | 86 | 87 | button_box 88 | rejected() 89 | Dialog 90 | reject() 91 | 92 | 93 | 316 94 | 260 95 | 96 | 97 | 286 98 | 274 99 | 100 | 101 | 102 | 103 | 104 | -------------------------------------------------------------------------------- /resources/ui/kk_dialog.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | Dialog 4 | 5 | 6 | 7 | 0 8 | 0 9 | 793 10 | 456 11 | 12 | 13 | 14 | Dialog 15 | 16 | 17 | 18 | 19 | 20 | true 21 | 22 | 23 | 0 24 | 25 | 26 | 27 | Denavit-Hartenberg 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | false 38 | 39 | 40 | Khalil-Kleinfinger 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | &Add a joint 54 | 55 | 56 | 57 | 58 | 59 | 60 | Qt::Horizontal 61 | 62 | 63 | QDialogButtonBox::Cancel|QDialogButtonBox::Ok 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | button_box 73 | accepted() 74 | Dialog 75 | accept() 76 | 77 | 78 | 248 79 | 254 80 | 81 | 82 | 157 83 | 274 84 | 85 | 86 | 87 | 88 | button_box 89 | rejected() 90 | Dialog 91 | reject() 92 | 93 | 94 | 316 95 | 260 96 | 97 | 98 | 286 99 | 274 100 | 101 | 102 | 103 | 104 | 105 | -------------------------------------------------------------------------------- /freecad/cross/ui/command_new_lcs_at_robot_link_body.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import FreeCAD as fc 4 | 5 | import FreeCADGui as fcgui 6 | 7 | from ..freecad_utils import message 8 | from ..freecad_utils import validate_types 9 | from ..freecad_utils import is_lcs 10 | from ..gui_utils import tr 11 | from ..wb_utils import get_parent_link_of_obj 12 | from ..wb_utils import get_chain 13 | from ..wb_utils import is_joint 14 | from ..wb_utils import is_link 15 | from ..wb_utils import set_placement_by_orienteer 16 | from ..wb_utils import move_placement 17 | from ..wb_utils import make_lcs_at_link_body 18 | 19 | 20 | # Stubs and type hints. 21 | from ..joint import Joint 22 | from ..link import Link 23 | DO = fc.DocumentObject 24 | CrossLink = Link 25 | CrossJoint = Joint 26 | LCS = DO # Local coordinate systen, TypeId == "PartDesign::CoordinateSystem" 27 | 28 | 29 | class _NewLCSAtRobotLinkBodyCommand: 30 | """Command to create LCS (Local Coordinate System) at face of body of link. 31 | """ 32 | 33 | def GetResources(self): 34 | return { 35 | 'Pixmap': 'set_new_lcs_at_robot_link_body.svg', 36 | 'MenuText': tr('New LCS at robot link body'), 37 | 'Accel': 'L, B', 38 | 'ToolTip': tr( 39 | 'Make LCS at robot link body (Real) subelement (face, edge, vertex).\n' 40 | '\n' 41 | 'Select: face or edge or vertex of body of robot link \n' 42 | '\n' 43 | 'LCS can be used with Set Placement tools. Be free with LCS params correction.\n' 44 | 'By default LCS will use InertialCS MapMode \n' 45 | 'and Translate MapMode for vertex and Concentric for curve and circle.', 46 | ), 47 | } 48 | 49 | def IsActive(self): 50 | return bool(fcgui.Selection.getSelection()) 51 | 52 | def Activated(self): 53 | doc = fc.activeDocument() 54 | selection_ok = False 55 | try: 56 | orienteer1, = validate_types( 57 | fcgui.Selection.getSelection(), 58 | ['Any'], 59 | ) 60 | selection_ok = True 61 | except RuntimeError: 62 | pass 63 | 64 | if not selection_ok: 65 | message('Select: face or edge or vertex of body of robot link.\n', gui=True) 66 | return 67 | 68 | 69 | link1 = get_parent_link_of_obj(orienteer1) 70 | 71 | if link1 == None: 72 | message('Can not get parent robot link of first selected object', gui=True) 73 | return 74 | 75 | sel = fcgui.Selection.getSelectionEx() 76 | orienteer1_sub_obj = sel[0] 77 | if not is_link(orienteer1) and not is_joint(orienteer1): 78 | orienteer1 = orienteer1_sub_obj 79 | 80 | doc.openTransaction(tr("Make LCS at link body")) 81 | lcs, body_lcs_wrapper, lcs_placement = make_lcs_at_link_body( 82 | orienteer1, 83 | delete_created_objects = False, 84 | deactivate_after_map_mode = True, 85 | ) 86 | doc.commitTransaction() 87 | 88 | doc.recompute() 89 | 90 | 91 | fcgui.addCommand('NewLCSAtRobotLinkBody', _NewLCSAtRobotLinkBodyCommand()) 92 | -------------------------------------------------------------------------------- /resources/templates/launch/display.launch.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | import launch 4 | from launch.substitutions import LaunchConfiguration, PathJoinSubstitution 5 | 6 | import launch_ros 7 | from launch_ros.substitutions import FindPackageShare 8 | from launch.actions import IncludeLaunchDescription 9 | from launch.launch_description_sources import PythonLaunchDescriptionSource 10 | 11 | 12 | def generate_launch_description(): 13 | pkg_share = Path(launch_ros.substitutions.FindPackageShare(package='{package_name}').find('{package_name}')) 14 | default_model_path = pkg_share / 'urdf/{xacro_wrapper_file}' 15 | default_rviz_config_path = pkg_share / 'rviz/robot_description.rviz' 16 | 17 | use_sim_time = LaunchConfiguration('use_sim_time') 18 | 19 | robot_state_publisher_node = IncludeLaunchDescription( 20 | PythonLaunchDescriptionSource([ 21 | PathJoinSubstitution([ 22 | FindPackageShare('{package_name}'), 23 | 'launch', 24 | 'description.launch.py', 25 | ]), 26 | ]), 27 | launch_arguments=dict(use_sim_time=use_sim_time).items(), 28 | ) 29 | 30 | joint_state_publisher_node = launch_ros.actions.Node( 31 | package='joint_state_publisher', 32 | executable='joint_state_publisher', 33 | name='joint_state_publisher', 34 | condition=launch.conditions.UnlessCondition(LaunchConfiguration('gui')), 35 | parameters=[{{ 36 | 'use_sim_time': use_sim_time, 37 | }}], 38 | ) 39 | joint_state_publisher_gui_node = launch_ros.actions.Node( 40 | package='joint_state_publisher_gui', 41 | executable='joint_state_publisher_gui', 42 | name='joint_state_publisher_gui', 43 | condition=launch.conditions.IfCondition(LaunchConfiguration('gui')), 44 | parameters=[{{ 45 | 'use_sim_time': use_sim_time, 46 | }}], 47 | ) 48 | rviz_node = launch_ros.actions.Node( 49 | package='rviz2', 50 | executable='rviz2', 51 | name='rviz2', 52 | output='screen', 53 | arguments=['-d', LaunchConfiguration('rvizconfig')], 54 | parameters=[{{ 55 | 'use_sim_time': use_sim_time, 56 | }}], 57 | ) 58 | 59 | return launch.LaunchDescription([ 60 | launch.actions.DeclareLaunchArgument( 61 | name='use_sim_time', 62 | default_value='false', 63 | description='Flag to enable usage of simulation time', 64 | ), 65 | launch.actions.DeclareLaunchArgument( 66 | name='gui', 67 | default_value='True', 68 | description='Flag to enable joint_state_publisher_gui', 69 | ), 70 | launch.actions.DeclareLaunchArgument( 71 | name='model', 72 | default_value=str(default_model_path), 73 | description='Absolute path to robot urdf file', 74 | ), 75 | launch.actions.DeclareLaunchArgument( 76 | name='rvizconfig', 77 | default_value=str(default_rviz_config_path), 78 | description='Absolute path to rviz config file', 79 | ), 80 | joint_state_publisher_node, 81 | joint_state_publisher_gui_node, 82 | robot_state_publisher_node, 83 | rviz_node, 84 | ]) 85 | -------------------------------------------------------------------------------- /freecad/cross/sdf/sdf_parser/sdf_tree.py: -------------------------------------------------------------------------------- 1 | from .sdf_schema_parser import sdf_schema_parser 2 | 3 | import xml.etree.ElementTree as ET 4 | import xmltodict 5 | 6 | from ...utils import dict_to_xml 7 | 8 | 9 | class sdf_tree: 10 | def __init__(self, sfile:str): 11 | '''sfile : file name to read the element properties from located in ../sdf \n 12 | use the get_elem property to get the initialized element ''' 13 | #initialize class 14 | self.struct_class=sdf_schema_parser(file=sfile) 15 | #get the dictionary structure 16 | self.structured=self.struct_class.data_structure 17 | #a stack of parent elements 18 | #this stack is provided to allow for having a parent key in the dictionary 19 | #to help when creating an xnl tree by adding subnode 20 | #get stuff started 21 | self.create_root() 22 | if len(self.structured["children"])==0: 23 | self._root_elem.text=self.structured["value"] 24 | else: 25 | self.construct_tree(self._root_elem, self.structured["children"]) 26 | 27 | self.e_tree=ET.ElementTree(self._root_elem) 28 | #create the root element 29 | #this does not need other properties as it does not have them ,this I'm sure of 30 | #so no need to add them 31 | def create_root(self)->ET.Element: 32 | self._root_elem=ET.Element(self.structured["tag"]) 33 | if self.structured["attributes"] is not None: 34 | for attr in self.structured["attributes"]: 35 | self._root_elem.set(attr.name,attr.attr_value) 36 | 37 | def construct_tree(self, parent_elem: ET.Element, st_lst)->ET.Element: 38 | 39 | for child in st_lst: 40 | # attr=dict() 41 | s=ET.SubElement(parent_elem,child["tag"]) 42 | if child["attributes"] is not None: 43 | for attr in child["attributes"]: 44 | #recall attributes are stored as class Element_attributes defined in RD_parse_sdf.py 45 | #create a dictionary with the attributes name as the key and attribute_value as the value 46 | #for each attribute in the list 47 | # attr[_att.name]=_att.attr_value 48 | s.set(attr.name,attr.attr_value) 49 | 50 | if child["value"] is not None: 51 | s.text=child["value"] 52 | if len(child["children"]) > 0: 53 | self.construct_tree(s,child["children"]) 54 | 55 | @property 56 | def get_tree(self)->ET.ElementTree: 57 | return self.e_tree 58 | @property 59 | def get_element(self)->ET.Element: 60 | return self._root_elem 61 | @property 62 | def get_element_as_dict(self) -> dict: 63 | """Convert root element (ET) to dictionary""" 64 | return xmltodict.parse(ET.tostring(self._root_elem)) 65 | 66 | 67 | def sdf_dict_to_xml(sdf_dict: dict, full_document: bool = True, pretty: bool = False) -> str: 68 | """ Convert (sdf_dict -> xml). Also removes meta attributes before converting. 69 | Meta attributes (ex. @meta_description) were added manually and should be removed""" 70 | 71 | return dict_to_xml( 72 | sdf_dict, 73 | keys_to_remove_before_convert = sdf_schema_parser.get_manually_added_technical_attributes(), 74 | remove_keys_recursively = True, 75 | full_document = full_document, 76 | pretty = pretty, 77 | ) 78 | -------------------------------------------------------------------------------- /freecad/cross/ui/command_set_placement_by_orienteer_with_hold_chain.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import FreeCAD as fc 4 | 5 | import FreeCADGui as fcgui 6 | 7 | from ..freecad_utils import message 8 | from ..freecad_utils import validate_types 9 | from ..freecad_utils import is_lcs 10 | from ..gui_utils import tr 11 | from ..wb_utils import set_placement_by_orienteer 12 | from ..wb_utils import move_placement 13 | from ..wb_utils import is_joint 14 | from ..wb_utils import get_chain 15 | from ..wb_utils import is_link 16 | 17 | 18 | # Stubs and type hints. 19 | from ..joint import Joint 20 | from ..link import Link 21 | DO = fc.DocumentObject 22 | CrossLink = Link 23 | CrossJoint = Joint 24 | LCS = DO # Local coordinate systen, TypeId == "PartDesign::CoordinateSystem" 25 | 26 | 27 | class _SetCROSSPlacementByOrienteerWithHoldChainCommand: 28 | """Command to set the placement of Joint. 29 | 30 | Command to set the Origin by orienteer placement with hold downstream kinamatic chain 31 | 32 | """ 33 | 34 | def GetResources(self): 35 | return { 36 | 'Pixmap': 'set_cross_placement_by_orienteer_with_hold_chain.svg', 37 | 'MenuText': tr('Set placement - by orienteer with hold chain'), 38 | 'Accel': 'P, H', 39 | 'ToolTip': tr( 40 | 'Set the Origin of a joint with hold downstream kinematic chain.\n' 41 | '\n' 42 | 'Select (with Ctlr): a CROSS::Joint, any (orienteer)\n' 43 | '\n' 44 | 'This will set Origin with hold downstream kinematic chain (persist same position) \n' 45 | 'including hold of Mounting Placement of link.\n' 46 | 'Placement of orienteer will be placement of join origin\n' 47 | '\n' 48 | 'LCS is convenient as reference because of configurable orientation.\n' 49 | 'Use this when you want just move Origin to some placement and dont touch other downstream chain.\n', 50 | ), 51 | } 52 | 53 | def IsActive(self): 54 | return bool(fcgui.Selection.getSelection()) 55 | 56 | def Activated(self): 57 | doc = fc.activeDocument() 58 | selection_ok = False 59 | selection_joint = False 60 | 61 | if not selection_ok: 62 | try: 63 | cross_joint, orienteer1 = validate_types( 64 | fcgui.Selection.getSelection(), 65 | ['Cross::Joint', 'Any'], 66 | ) 67 | selection_ok = True 68 | selection_joint = True 69 | except RuntimeError: 70 | pass 71 | 72 | if not selection_ok: 73 | message( 74 | 'Select (with Ctlr): a CROSS::Joint, any (orienteer) \n', 75 | gui=True, 76 | ) 77 | return 78 | 79 | # for work with subelement 80 | sel = fcgui.Selection.getSelectionEx() 81 | orienteer1_sub_element = sel[1] 82 | if not is_lcs(orienteer1) and not is_joint(orienteer1) and not is_link(orienteer1): 83 | orienteer1 = orienteer1_sub_element 84 | 85 | if selection_joint: 86 | doc.openTransaction(tr("Set joint's origin")) 87 | set_placement_by_orienteer(doc, cross_joint, 'Origin', orienteer1, hold_downstream_chain = True) 88 | doc.commitTransaction() 89 | doc.recompute() 90 | 91 | 92 | fcgui.addCommand('SetCROSSPlacementByOrienteerWithHoldChain', _SetCROSSPlacementByOrienteerWithHoldChainCommand()) 93 | -------------------------------------------------------------------------------- /resources/icons/controller.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /docs/commands.md: -------------------------------------------------------------------------------- 1 | # Code Snippets 2 | There is no way to add a button, menu entry from python to a workbench which is added with c++. So here is a comparison how to do that with python and with c++. 3 | 4 | ## Adding a command: 5 | This can be done either with python or c++. 6 | 7 | ### 1. python 8 | 9 | ```python 10 | import FreeCAD as App 11 | 12 | class MyCommand(object): 13 | def IsActive(self): 14 | """ 15 | availability of the command (eg.: check for existence of a document,...) 16 | if this function returns False, the menu/ buttons are ßdisabled (gray) 17 | """ 18 | if App.ActiveDocument is None: 19 | return False 20 | else: 21 | return True 22 | 23 | def GetResources(self): 24 | """ 25 | resources which are used by buttons and menu-items 26 | """ 27 | return {'Pixmap': 'path_to_icon.svg', 'MenuText': 'my command', 'ToolTip': 'very short description'} 28 | 29 | def Activated(self): 30 | """ 31 | the function to be handled, when a user starts the command 32 | """ 33 | ``` 34 | To register the command in FreeCAD: 35 | 36 | ```python 37 | import FreeCADGui as Gui 38 | Gui.addCommand('MyCommand', MyCommand()) 39 | ``` 40 | 41 | Adding a new toolbar/menu: 42 | ```python 43 | from FreeCADGui import Workbench 44 | class myWorkbench(Workbench): 45 | MenuText = "name_of_workbench" 46 | ToolTip = "short description of workbench" 47 | Icon = "path_to_icon.svg" 48 | 49 | def GetClassName(self): 50 | return "Gui::PythonWorkbench" 51 | 52 | def Initialize(self): 53 | self.appendToolbar("Gear", ["MyCommand"]) 54 | self.appendMenu("Gear", ["MyCommand"]) 55 | ``` 56 | 57 | ### 2. C++ 58 | 59 | ```c++ 60 | 61 | #include 62 | #include 63 | #include 64 | #include 65 | 66 | using namespace std; 67 | 68 | DEF_STD_CMD_A(MyCommand) 69 | 70 | MyCommand::MyCommand() 71 | : Command("MyCommand") 72 | { 73 | sAppModule = "module"; 74 | sGroup = QT_TR_NOOP("Mesh"); 75 | sMenuText = QT_TR_NOOP("my command"); 76 | sToolTipText = QT_TR_NOOP("very short description"); 77 | sWhatsThis = "MyCommand"; 78 | sStatusTip = sToolTipText; 79 | } 80 | 81 | void MyCommand::activated(int) 82 | { 83 | // the function to be handled, when a user starts the command 84 | } 85 | 86 | bool MyCommand::isActive(void) 87 | { 88 | // availability of the command (eg.: check for existence of a document,...) 89 | // if this function returns False, the menu/ buttons are ßdisabled (gray) 90 | return (hasActiveDocument() && !Gui::Control().activeDialog()); 91 | } 92 | ``` 93 | To register the command in FreeCAD: 94 | 95 | ```c++ 96 | #include 97 | 98 | Gui::CommandManager &rcCmdMgr = Gui::Application::Instance->commandManager(); 99 | rcCmdMgr.addCommand(new MyCommand()); 100 | ``` 101 | Adding a item to a menu/toolbar: 102 | 103 | if your command is added with python you have to run this code: 104 | in src/module/Gui/AppModuleGui.cpp add to PyMOD_INIT_FUNC: 105 | 106 | ```c++ 107 | // try to instantiate a python module 108 | try{ 109 | Base::Interpreter().runStringObject("import MyCommands"); 110 | } catch (Base::PyException &err){ 111 | err.ReportException(); 112 | } 113 | ``` 114 | 115 | and add the name of the command to a tooltip/menu in src/module/Gui/Workbench.cpp Workbench::setupToolBars 116 | 117 | 118 | ```c++ 119 | Gui::ToolBarItem* Workbench::setupToolBars() const 120 | { 121 | Gui::ToolBarItem* root = StdWorkbench::setupToolBars(); 122 | Gui::ToolBarItem* myToolbar = new Gui::ToolBarItem(root); 123 | myToolbar->setCommand("my_commands"); 124 | *myToolbar << "MyCommand"; 125 | return root; 126 | } 127 | ``` 128 | -------------------------------------------------------------------------------- /resources/ui/file_overwrite_confirmation_dialog.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | FileOverwriteConfirmationDialog 4 | 5 | 6 | 7 | 0 8 | 0 9 | 400 10 | 305 11 | 12 | 13 | 14 | Dialog 15 | 16 | 17 | 18 | 19 | 20 | Generated Files 21 | 22 | 23 | 24 | 25 | 26 | 27 | QFrame::StyledPanel 28 | 29 | 30 | QFrame::Raised 31 | 32 | 33 | 34 | 35 | 36 | Generated Package Save Path 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | Browse 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | Select &all 60 | 61 | 62 | false 63 | 64 | 65 | 66 | 67 | 68 | 69 | Files to be generated 70 | 71 | 72 | 73 | 74 | 75 | 76 | true 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | Qt::Horizontal 90 | 91 | 92 | 93 | 40 94 | 20 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | Generate Files 103 | 104 | 105 | 106 | 107 | 108 | 109 | Cancel 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | --------------------------------------------------------------------------------