diff --git a/.gitmodules b/.gitmodules index 00d28c789f..7b6a09edab 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,3 +4,6 @@ [submodule "jolt"] path = jolt url = https://github.com/azaleacolburn/JoltPhysics.js.git +[submodule "protobuf"] + path = protobuf + url = https://github.com/protocolbuffers/protobuf.git diff --git a/isotope/.clang-format b/isotope/.clang-format new file mode 100644 index 0000000000..b7b2746b01 --- /dev/null +++ b/isotope/.clang-format @@ -0,0 +1,40 @@ +BasedOnStyle: LLVM +AccessModifierOffset: -4 +AlignAfterOpenBracket: DontAlign +AlignConsecutiveAssignments: Consecutive +AlignConsecutiveMacros: Consecutive +AlignEscapedNewlines: Left +AlignOperands: AlignAfterOperator +AlignTrailingComments: + Kind: Never +AllowShortFunctionsOnASingleLine: Empty +AlwaysBreakTemplateDeclarations: Yes +BreakBeforeBinaryOperators: NonAssignment +ColumnLimit: 120 +IncludeIsMainRegex: '' +SortIncludes: true +IncludeBlocks: Regroup +IncludeCategories: + - Regex: '^<(Fusion|Core)/' + Priority: 1 + - Regex: '^<[^/]+>$' + Priority: 2 + - Regex: '^]$' + Priority: 4 + - Regex: '^"(?!.+\.pb\.h").*"$' + Priority: 5 + - Regex: '.*' + Priority: 6 +IndentCaseLabels: true +IndentWidth: 4 +IndentWrappedFunctionNames: true +InsertBraces: true +InsertNewlineAtEOF: true +LineEnding: LF +MaxEmptyLinesToKeep: 1 +NamespaceIndentation: None +PointerAlignment: Left +SpaceAfterCStyleCast: true +SpaceBeforeParens: true diff --git a/isotope/.gitignore b/isotope/.gitignore new file mode 100644 index 0000000000..2c806a14bc --- /dev/null +++ b/isotope/.gitignore @@ -0,0 +1,2 @@ +compile_commands.json +build* diff --git a/isotope/CMakeLists.txt b/isotope/CMakeLists.txt new file mode 100644 index 0000000000..ecd4748e65 --- /dev/null +++ b/isotope/CMakeLists.txt @@ -0,0 +1,96 @@ +cmake_minimum_required(VERSION 3.23) +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +project(isotope LANGUAGES CXX) + +if(NOT APPLE) + message(FATAL_ERROR "This project is only supported on macOS.") +endif() + +set(USER_APP_DATA_DIR "$ENV{HOME}/Library/Application Support") +set(USER_APP_DATA_RELATIVE_PATH "Autodesk/Autodesk Fusion 360/API") +set(FUSION_API_DIR "${USER_APP_DATA_DIR}/${USER_APP_DATA_RELATIVE_PATH}") +set(FUSION_API_INCLUDE_DIR "${FUSION_API_DIR}/CPP/include") +set(FUSION_API_LIBRARY_DIR "${FUSION_API_DIR}/CPP/lib") +set(FUSION_API_VERSION_FILE "${FUSION_API_DIR}/version.txt") + +set(FUSION_API_ADDINS_DIR "${FUSION_API_DIR}/Addins") +set(PROJECT_INSTALL_DIR "${FUSION_API_ADDINS_DIR}/${PROJECT_NAME}") + +file(READ ${FUSION_API_VERSION_FILE} FUSION_API_VERSION) +message(STATUS "Fusion 360 API Version: ${FUSION_API_VERSION}") + +set(FUSION_API_TARGET "fusion360") +set(FUSION_API_NAMESPACE "autodesk") +set(FUSION_API_LIBRARY_TARGETS "core" "fusion") + +foreach(target IN LISTS FUSION_API_LIBRARY_TARGETS) + set(libpath "${FUSION_API_LIBRARY_DIR}/${target}${CMAKE_SHARED_LIBRARY_SUFFIX}") + add_library(${target} SHARED IMPORTED) + set_target_properties(${target} PROPERTIES IMPORTED_LOCATION ${libpath}) +endforeach() + +add_library(${FUSION_API_TARGET} INTERFACE) +add_library(${FUSION_API_NAMESPACE}::${FUSION_API_TARGET} ALIAS ${FUSION_API_TARGET}) +target_include_directories(${FUSION_API_TARGET} SYSTEM INTERFACE ${FUSION_API_INCLUDE_DIR}) +target_link_libraries(${FUSION_API_TARGET} INTERFACE ${FUSION_API_LIBRARY_TARGETS}) + +file(GLOB SOURCES "${CMAKE_CURRENT_LIST_DIR}/src/*.cpp") +add_library(${PROJECT_NAME} SHARED ${SOURCES}) + +set_target_properties(${PROJECT_NAME} PROPERTIES PREFIX "") +target_link_libraries(${PROJECT_NAME} PRIVATE ${FUSION_API_NAMESPACE}::${FUSION_API_TARGET}) +target_include_directories(${PROJECT_NAME} PRIVATE "${CMAKE_CURRENT_LIST_DIR}/inc") + +set(ABSL_PROPAGATE_CXX_STD ON) +set(protobuf_BUILD_TESTS OFF CACHE BOOL "" FORCE) +set(protobuf_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE) +set(protobuf_BUILD_CONFORMANCE OFF CACHE BOOL "" FORCE) +set(protobuf_INSTALL OFF CACHE BOOL "" FORCE) + +set(GOOGLE_PROTOBUF_SUBMODULE_DIR "${CMAKE_CURRENT_LIST_DIR}/../protobuf") +if(NOT EXISTS "${GOOGLE_PROTOBUF_SUBMODULE_DIR}/.git") + message(FATAL_ERROR "google-protobuf is not checked out; make sure to run\n\tgit submodule update --init ../protobuf") +endif() + +add_subdirectory(${GOOGLE_PROTOBUF_SUBMODULE_DIR} "${CMAKE_BINARY_DIR}/protobuf") + +set(Protobuf_PROTOC_EXECUTABLE $) +target_link_libraries(${PROJECT_NAME} PRIVATE protobuf::libprotobuf) + +set(PROTO_GEN_DIR "${CMAKE_CURRENT_BINARY_DIR}/protobuf_gen") +set(MIRABUF_SUBMODULE_DIR "${CMAKE_CURRENT_LIST_DIR}/../mirabuf") +file(MAKE_DIRECTORY ${PROTO_GEN_DIR}) +file(GLOB PROTO_FILES "${MIRABUF_SUBMODULE_DIR}/*.proto") + +set(PROTO_SRCS "") +set(PROTO_HDRS "") +foreach(_P IN LISTS PROTO_FILES) + get_filename_component(_NWE "${_P}" NAME_WE) + list(APPEND PROTO_SRCS "${PROTO_GEN_DIR}/${_NWE}.pb.cc") + list(APPEND PROTO_HDRS "${PROTO_GEN_DIR}/${_NWE}.pb.h") +endforeach() + +add_custom_command( + OUTPUT ${PROTO_SRCS} ${PROTO_HDRS} + COMMAND ${Protobuf_PROTOC_EXECUTABLE} + --cpp_out=${PROTO_GEN_DIR} + -I ${MIRABUF_SUBMODULE_DIR} + ${PROTO_FILES} + DEPENDS ${PROTO_FILES} + COMMENT "Generating protobuf sources" +) + +add_custom_target(generate_protos ALL DEPENDS ${PROTO_SRCS} ${PROTO_HDRS}) +add_dependencies(${PROJECT_NAME} generate_protos) +target_sources(${PROJECT_NAME} + PRIVATE + ${PROTO_SRCS} + ${PROTO_HDRS} +) +target_include_directories(${PROJECT_NAME} SYSTEM PRIVATE ${PROTO_GEN_DIR}) + +install(TARGETS ${PROJECT_NAME} DESTINATION ${PROJECT_INSTALL_DIR}) +install(FILES "${CMAKE_CURRENT_LIST_DIR}/isotope.manifest" DESTINATION ${PROJECT_INSTALL_DIR}) +install(DIRECTORY "${CMAKE_CURRENT_LIST_DIR}/resources/" DESTINATION "${PROJECT_INSTALL_DIR}/resources" FILES_MATCHING PATTERN "*.png") diff --git a/isotope/Makefile b/isotope/Makefile new file mode 100644 index 0000000000..1faeda9d4d --- /dev/null +++ b/isotope/Makefile @@ -0,0 +1,48 @@ +BUILD_DIR ?= build +XCODE_BUILD_DIR ?= build-xcode +CMAKE_GENERATOR ?= Unix Makefiles +BUILD_TYPE ?= Debug # Release | RelWithDebInfo ... +JOBS ?= $(shell nproc 2>/dev/null || sysctl -n hw.ncpu) + +CMAKE_FLAGS = \ + -G "$(CMAKE_GENERATOR)" \ + -DCMAKE_BUILD_TYPE=$(BUILD_TYPE) \ + -DCMAKE_EXPORT_COMPILE_COMMANDS=ON + +configure: + @mkdir -p "$(BUILD_DIR)" + @cmake -S . -B "$(BUILD_DIR)" $(CMAKE_FLAGS) + @ln -sf "$(BUILD_DIR)/compile_commands.json" . + @ln -sf "./isotope.manifest" "$(BUILD_DIR)" + +build: configure + @cmake --build "$(BUILD_DIR)" -- -j$(JOBS) + +build-xcode: + @mkdir -p "$(XCODE_BUILD_DIR)" + @cmake -G Xcode -S . -B "$(XCODE_BUILD_DIR)" -DCMAKE_BUILD_TYPE=Debug + +open-xcode: build-xcode + @open build-xcode/isotope.xcodeproj + +format: + @clang-format -i -style=file $(shell git ls-files '*.h' '*.cpp') + +install: build + @cmake --install "$(BUILD_DIR)" + +clean: + @rm -rf "$(BUILD_DIR)" + @rm -rf "$(XCODE_BUILD_DIR)" + @rm -f compile_commands.json + +help: + @echo "Targets:" + @echo " configure - configure the project and generate the compilation rules json" + @echo " build - configure and build" + @echo " install - install the built project into your fusion instillation" + @echo " build-xcode - configure and build the xcode project for debugging" + @echo " open-xcode - open the built xcode project in xcode" + @echo " clean - remove build directory and prep for a fresh build" + +.DEFAULT_GOAL := build diff --git a/isotope/README.md b/isotope/README.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/isotope/inc/components.h b/isotope/inc/components.h new file mode 100644 index 0000000000..d77927696e --- /dev/null +++ b/isotope/inc/components.h @@ -0,0 +1,20 @@ +#pragma once +#include +#include +#ifndef ISOTOPE_COMPONENTS_H_ +#define ISOTOPE_COMPONENTS_H_ + +#include +#include + +#include "assembly.pb.h" +#include "types.pb.h" + +mirabuf::Parts map_all_parts(const adsk::core::Ptr& components, + const mirabuf::material::Materials& materials); // TODO: Replace parameter with appearance map + +mirabuf::Node parse_component_root(const adsk::core::Ptr& component, mirabuf::Parts* parts); + +void map_rigid_groups(const adsk::core::Ptr& root, mirabuf::joint::Joints* joints); + +#endif // ISOTOPE_COMPONENTS_H_ diff --git a/isotope/inc/config_command.h b/isotope/inc/config_command.h new file mode 100644 index 0000000000..29ad93b4b1 --- /dev/null +++ b/isotope/inc/config_command.h @@ -0,0 +1,30 @@ +#pragma once +#ifndef ISOTOPE_CONFIG_COMMAND_H_ +#define ISOTOPE_CONFIG_COMMAND_H_ + +#include +#include + +#include "context.h" + +class ConfigureCommandCreatedHandler : public adsk::core::CommandCreatedEventHandler { +private: + const GlobalContext& gctx; + +public: + ConfigureCommandCreatedHandler(const GlobalContext& context) : gctx(context) {} + + void notify(const adsk::core::Ptr& args) override; +}; + +class ConfigureCommandExecutedHandler : public adsk::core::CommandEventHandler { +private: + const GlobalContext& gctx; + +public: + ConfigureCommandExecutedHandler(const GlobalContext& context) : gctx(context) {} + + void notify(const adsk::core::Ptr& eventArgs) override; +}; + +#endif // ISOTOPE_CONFIG_COMMAND_H_ diff --git a/isotope/inc/context.h b/isotope/inc/context.h new file mode 100644 index 0000000000..dd7ae716c0 --- /dev/null +++ b/isotope/inc/context.h @@ -0,0 +1,17 @@ +#pragma once +#ifndef ISOTOPE_CONTEXT_H_ +#define ISOTOPE_CONTEXT_H_ + +#include +#include +#include + +struct GlobalContext { + adsk::core::Ptr app; + adsk::core::Ptr ui; + + bool configure(); + bool isValid() const; +}; + +#endif // ISOTOPE_CONTEXT_H_ diff --git a/isotope/inc/joints.h b/isotope/inc/joints.h new file mode 100644 index 0000000000..b1931968be --- /dev/null +++ b/isotope/inc/joints.h @@ -0,0 +1,19 @@ +#pragma once +#ifndef ISOTOPE_JOINTS_H_ +#define ISOTOPE_JOINTS_H_ + +#include +#include + +#include "joint.pb.h" +#include "signal.pb.h" +#include "types.pb.h" + +std::pair populate_joints( + const adsk::core::Ptr& design); + +mirabuf::GraphContainer create_joint_graph(const mirabuf::joint::Joints& joints); + +void build_joint_part_hierarchy(mirabuf::joint::Joints* joints, const adsk::core::Ptr& design); + +#endif // ISOTOPE_JOINTS_H_ diff --git a/isotope/inc/materials.h b/isotope/inc/materials.h new file mode 100644 index 0000000000..01a192ceb0 --- /dev/null +++ b/isotope/inc/materials.h @@ -0,0 +1,12 @@ +#pragma once +#ifndef ISOTOPE_MATERIALS_H_ +#define ISOTOPE_MATERIALS_H_ + +#include + +#include "material.pb.h" + +mirabuf::material::Materials map_all_materials(const adsk::core::Ptr& design_appearances, + const adsk::core::Ptr& design_materials); + +#endif // ISOTOPE_MATERIALS_H_ diff --git a/isotope/inc/parser.h b/isotope/inc/parser.h new file mode 100644 index 0000000000..62bf083861 --- /dev/null +++ b/isotope/inc/parser.h @@ -0,0 +1,9 @@ +#pragma once +#ifndef ISOTOPE_PARSER_H_ +#define ISOTOPE_PARSER_H_ + +#include "context.h" + +void export_design(const GlobalContext& gctx); + +#endif // ISOTOPE_PARSER_H_ diff --git a/isotope/inc/util.h b/isotope/inc/util.h new file mode 100644 index 0000000000..540d32490f --- /dev/null +++ b/isotope/inc/util.h @@ -0,0 +1,119 @@ +#pragma once +#ifndef ISOTOPE_UTILITY_H_ +#define ISOTOPE_UTILITY_H_ + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "types.pb.h" + +template +struct FusionTypeName; + +#define DEFINE_FUSION_TYPE_NAME(type) \ + template <> \ + struct FusionTypeName { \ + static constexpr std::string_view value = #type; \ + } + +DEFINE_FUSION_TYPE_NAME(adsk::fusion::JointGeometry); +DEFINE_FUSION_TYPE_NAME(adsk::fusion::JointOrigin); +DEFINE_FUSION_TYPE_NAME(adsk::fusion::BRepEdge); +DEFINE_FUSION_TYPE_NAME(adsk::fusion::BRepFace); + +template +const T* fusion_try_cast(const adsk::core::Base* base) { + if (!base) { + return nullptr; + } else if (std::string_view(base->objectType()) == FusionTypeName::value) { + return dynamic_cast(base); + } else { + return nullptr; + } +} + +template +VariantT fusion_base_to_variant_impl(const adsk::core::Base* raw) { + if (auto* p = fusion_try_cast(raw)) { + return VariantT{std::in_place_type, p}; + } else if constexpr (sizeof...(Ts) > 0) { + return fusion_base_to_variant_impl(raw); + } else { + return VariantT{std::in_place_type}; + } +} + +template +std::variant fusion_base_to_variant(const adsk::core::Base* base) { + using VariantT = std::variant; + if (!base) { + return std::variant{}; + } + + return fusion_base_to_variant_impl(base); +} + +template +struct overloaded : Ts... { + using Ts::operator()...; +}; + +template +overloaded(Ts...) -> overloaded; + +template +struct has_name : std::false_type {}; + +template +struct has_name()->name())>> : std::true_type {}; + +template +struct has_entity_token : std::false_type {}; + +template +struct has_entity_token()->entityToken())>> : std::true_type {}; + +template +struct has_id : std::false_type {}; + +template +struct has_id()->id())>> : std::true_type {}; + +template +mirabuf::Info create_info_from_fus_obj(const FusObjPtr& obj, const std::string& override_guid = "") { + mirabuf::Info info; + + // The python exporter sets all version numbers to 5. + // This version number can be used to differentiate between robot exports from + // the C++ and python exporters respectively. + info.set_version(1); + + if constexpr (has_name::value) { + info.set_name(obj->name()); + } + + if (!override_guid.length()) { + if constexpr (has_entity_token::value) { + info.set_guid(obj->entityToken()); + } else if constexpr (has_id::value) { + info.set_guid(obj->id()); + } + } else { + info.set_guid(override_guid); + } + + return info; +} + +std::string guid_component(const adsk::core::Ptr& component); +std::string guid_occurrence(const adsk::core::Ptr& occurrence); + +#endif // ISOTOPE_UTILITY_H_ diff --git a/isotope/isotope.manifest b/isotope/isotope.manifest new file mode 100644 index 0000000000..d1313dab7e --- /dev/null +++ b/isotope/isotope.manifest @@ -0,0 +1,13 @@ +{ + "autodeskProduct": "Fusion360", + "type": "addin", + "id": "d07927b2-1d3a-44a0-8797-560e1bea09d0", + "author": "ADSK FRC", + "description": { + "": "Isotope Synthesis Exporter" + }, + "version": "1.0.0", + "runOnStartup": false, + "supportedOS": "windows|mac", + "editEnabled": true +} diff --git a/isotope/resources/isotope_exporter/16x16-disabled.png b/isotope/resources/isotope_exporter/16x16-disabled.png new file mode 100644 index 0000000000..f4ba1b8f83 Binary files /dev/null and b/isotope/resources/isotope_exporter/16x16-disabled.png differ diff --git a/isotope/resources/isotope_exporter/16x16-normal.png b/isotope/resources/isotope_exporter/16x16-normal.png new file mode 100644 index 0000000000..34948454a9 Binary files /dev/null and b/isotope/resources/isotope_exporter/16x16-normal.png differ diff --git a/isotope/resources/isotope_exporter/32x32-disabled.png b/isotope/resources/isotope_exporter/32x32-disabled.png new file mode 100644 index 0000000000..770208ec0e Binary files /dev/null and b/isotope/resources/isotope_exporter/32x32-disabled.png differ diff --git a/isotope/resources/isotope_exporter/32x32-normal.png b/isotope/resources/isotope_exporter/32x32-normal.png new file mode 100644 index 0000000000..77458fce7d Binary files /dev/null and b/isotope/resources/isotope_exporter/32x32-normal.png differ diff --git a/isotope/resources/isotope_exporter/64x64-normal.png b/isotope/resources/isotope_exporter/64x64-normal.png new file mode 100644 index 0000000000..3076b744ca Binary files /dev/null and b/isotope/resources/isotope_exporter/64x64-normal.png differ diff --git a/isotope/src/components.cpp b/isotope/src/components.cpp new file mode 100644 index 0000000000..11304e81a9 --- /dev/null +++ b/isotope/src/components.cpp @@ -0,0 +1,272 @@ +#include "components.h" + +#include + +#include + +#include "assembly.pb.h" +#include "joint.pb.h" +#include "types.pb.h" + +#include "util.h" + +namespace { + +mirabuf::PhysicalProperties map_physical_properties( + const adsk::core::Ptr& properties) { + mirabuf::PhysicalProperties new_properties; + new_properties.set_mass(properties->mass()); + new_properties.set_volume(properties->volume()); + new_properties.set_density(properties->density()); + new_properties.set_area(properties->area()); + if (auto com = properties->centerOfMass()) { + if (auto vec = com->asVector()) { + new_properties.mutable_com()->set_x(vec->x()); + new_properties.mutable_com()->set_y(vec->y()); + new_properties.mutable_com()->set_z(vec->z()); + } + } + + return new_properties; +} + +mirabuf::TriangleMesh map_b_rep_body(const adsk::core::Ptr& body) { + // auto calc = body->meshManager()->createMeshCalculator(); + auto mesh_mgr = body->meshManager(); + if (!mesh_mgr) { + return {}; + } + + auto calc = mesh_mgr->createMeshCalculator(); + if (!calc) { + return {}; + } + + calc->setQuality(adsk::fusion::TriangleMeshQualityOptions::LowQualityTriangleMesh); + auto fus_mesh = calc->calculate(); + if (!fus_mesh) { + return {}; + } + + mirabuf::TriangleMesh mesh; + mesh.mutable_info()->CopyFrom(create_info_from_fus_obj(body)); + + mesh.set_has_volume(true); + + std::vector coords = fus_mesh->nodeCoordinatesAsFloat(); + mesh.mutable_mesh()->mutable_verts()->Add(coords.begin(), coords.end()); + + std::vector normals = fus_mesh->normalVectorsAsFloat(); + mesh.mutable_mesh()->mutable_normals()->Add(normals.begin(), normals.end()); + + std::vector node_indicies = fus_mesh->nodeIndices(); + mesh.mutable_mesh()->mutable_indices()->Add(node_indicies.begin(), node_indicies.end()); + + std::vector texture_coords = fus_mesh->textureCoordinatesAsFloat(); + mesh.mutable_mesh()->mutable_uv()->Add(texture_coords.begin(), texture_coords.end()); + + return mesh; +} + +mirabuf::TriangleMesh map_mesh_body(const adsk::core::Ptr& body) { + auto fus_mesh = body->displayMesh(); + + mirabuf::TriangleMesh mesh; + mesh.mutable_info()->CopyFrom(create_info_from_fus_obj(body)); + mesh.set_has_volume(true); + + std::vector coords = fus_mesh->nodeCoordinatesAsFloat(); + mesh.mutable_mesh()->mutable_verts()->Add(coords.begin(), coords.end()); + + std::vector normals = fus_mesh->normalVectorsAsFloat(); + mesh.mutable_mesh()->mutable_normals()->Add(normals.begin(), normals.end()); + + std::vector node_indicies = fus_mesh->nodeIndices(); + mesh.mutable_mesh()->mutable_indices()->Add(node_indicies.begin(), node_indicies.end()); + + std::vector texture_coords = fus_mesh->textureCoordinatesAsFloat(); + mesh.mutable_mesh()->mutable_uv()->Add(texture_coords.begin(), texture_coords.end()); + + return mesh; +} + +adsk::core::Ptr get_matrix_world(const adsk::core::Ptr& occurrence) { + if (!occurrence) { + return nullptr; + } + + auto matrix = occurrence->transform2()->copy(); + auto next_occurrence = occurrence; + while (next_occurrence->assemblyContext()) { + matrix->transformBy(next_occurrence->assemblyContext()->transform2()); + next_occurrence = next_occurrence->assemblyContext(); + } + + return matrix; +} + +mirabuf::Node parse_child_occurrence( + const adsk::core::Ptr& occurrence, mirabuf::Parts* parts) { + assert(occurrence->isLightBulbOn()); + + mirabuf::Node node; + + // TODO: Really explicit typing for this sort of thing would be great. + const std::string map_constant = guid_occurrence(occurrence); + node.set_value(map_constant); + + if (parts->part_instances().find(map_constant) != parts->part_instances().end()) { + assert(false); + } + + auto& part = (*parts->mutable_part_instances())[map_constant]; + part.mutable_info()->CopyFrom(create_info_from_fus_obj(occurrence, map_constant)); + if (occurrence->appearance()) { + part.set_appearance(occurrence->appearance()->id()); // TODO: Check if this is correct. + } else { + part.set_appearance("default"); + } + + if (auto material = occurrence->component()->material()) { + part.set_physical_material(material->id()); + } + + auto& part_defs = parts->part_definitions(); + const std::string component_ref = guid_component(occurrence->component()); + if (part_defs.find(component_ref) != part_defs.end()) { + part.set_part_definition_reference(component_ref); + } + + auto transform_array = occurrence->transform()->asArray(); + part.mutable_transform()->mutable_spatial_matrix()->Add(transform_array.begin(), transform_array.end()); + + auto world_transform = get_matrix_world(occurrence)->asArray(); + part.mutable_global_transform()->mutable_spatial_matrix()->Add(world_transform.begin(), world_transform.end()); + + // final recursive step to parse child occurrences + std::vector> child_occurrences; + occurrence->childOccurrences()->copyTo(std::back_inserter(child_occurrences)); + for (const auto& child_occurrence : child_occurrences) { + if (!child_occurrence->isLightBulbOn()) { + continue; + } + + auto child_node = parse_child_occurrence(child_occurrence, parts); + node.mutable_children()->Add()->CopyFrom(child_node); + } + + return node; +} + +} // namespace + +mirabuf::Parts map_all_parts( + const adsk::core::Ptr& components, const mirabuf::material::Materials& materials) { + mirabuf::Parts parts; + + std::vector> fusion_components; + components->copyTo(std::back_inserter(fusion_components)); + for (const auto& component : fusion_components) { + const std::string component_ref = guid_component(component); + if (parts.part_definitions().find(component_ref) != parts.part_definitions().end()) { + assert(false); + } + + auto& part = (*parts.mutable_part_definitions())[component_ref]; + part.mutable_info()->CopyFrom(create_info_from_fus_obj(component, component_ref)); + part.set_dynamic(true); + + if (auto props = component->physicalProperties()) { + *part.mutable_physical_data() = map_physical_properties(props); + } + + std::vector> b_rep_bodies; + component->bRepBodies()->copyTo(std::back_inserter(b_rep_bodies)); + for (const auto& body : b_rep_bodies) { + if (!body->isLightBulbOn()) { + continue; + } + + auto& part_body = *part.mutable_bodies()->Add(); + part_body.mutable_info()->CopyFrom(create_info_from_fus_obj(body)); + part_body.mutable_triangle_mesh()->CopyFrom(map_b_rep_body(body)); + + if (auto appearances = materials.appearances(); // TODO: Replace the parameter + appearances.find(body->appearance()->id()) != appearances.end()) { + part_body.set_appearance_override(body->appearance()->id()); + } else { + part_body.set_appearance_override("default"); + } + } + + std::vector> mesh_bodies; + component->meshBodies()->copyTo(std::back_inserter(mesh_bodies)); + for (const auto& body : mesh_bodies) { + if (!body->isLightBulbOn()) { + continue; + } + + auto& part_body = *part.mutable_bodies()->Add(); + part_body.mutable_info()->CopyFrom(create_info_from_fus_obj(body)); + part_body.mutable_triangle_mesh()->CopyFrom(map_mesh_body(body)); + + if (auto appearances = materials.appearances(); // TODO: Replace the parameter + appearances.find(body->appearance()->id()) != appearances.end()) { + part_body.set_appearance_override(body->appearance()->id()); + } else { + part_body.set_appearance_override("default"); + } + } + } + + return parts; +} + +mirabuf::Node parse_component_root(const adsk::core::Ptr& component, mirabuf::Parts* parts) { + mirabuf::Node root_node; + const std::string map_constant = guid_component(component); + root_node.set_value(map_constant); + + // TODO: Info stuff + if (parts->part_instances().find(map_constant) != parts->part_instances().end()) { + assert(false); + } + + auto& part = (*parts->mutable_part_instances())[map_constant]; + part.mutable_info()->CopyFrom(create_info_from_fus_obj(component, map_constant)); + auto& part_defs = parts->part_definitions(); + if (part_defs.find(map_constant) != part_defs.end()) { + part.set_part_definition_reference(map_constant); + } + + std::vector> child_occurrences; + component->occurrences()->copyTo(std::back_inserter(child_occurrences)); + for (const auto& child_occurrence : child_occurrences) { + if (!child_occurrence->isLightBulbOn()) { + continue; + } + + auto child_node = parse_child_occurrence(child_occurrence, parts); + root_node.mutable_children()->Add()->CopyFrom(child_node); + } + + return root_node; +} + +void map_rigid_groups(const adsk::core::Ptr& root, mirabuf::joint::Joints* joints) { + for (const auto& fus_group : root->allRigidGroups()) { + auto mira_group = mirabuf::joint::RigidGroup(); + mira_group.set_name(fus_group->entityToken()); + for (const auto& occurrence : fus_group->occurrences()) { + if (!occurrence || !occurrence->isLightBulbOn()) { + continue; + } + + mira_group.mutable_occurrences()->Add(occurrence->entityToken()); + } + + if (mira_group.occurrences().size()) { + joints->mutable_rigid_groups()->Add()->CopyFrom(mira_group); + } + } +} diff --git a/isotope/src/config_command.cpp b/isotope/src/config_command.cpp new file mode 100644 index 0000000000..048109428b --- /dev/null +++ b/isotope/src/config_command.cpp @@ -0,0 +1,23 @@ +#include "config_command.h" + +#include +#include +#include + +#include "parser.h" + +void ConfigureCommandCreatedHandler::notify(const adsk::core::Ptr& args) { + assert(this->gctx.isValid()); + adsk::core::Ptr command = args->command(); + if (!command || !command->isValid()) { + this->gctx.ui->messageBox("Invalid command in ConfigureCommandCreatedHandler."); + return; + } + + command->execute()->add(new ConfigureCommandExecutedHandler(this->gctx)); +} + +void ConfigureCommandExecutedHandler::notify(const adsk::core::Ptr& eventArgs) { + assert(this->gctx.isValid()); + export_design(this->gctx); +} diff --git a/isotope/src/context.cpp b/isotope/src/context.cpp new file mode 100644 index 0000000000..e8825691ba --- /dev/null +++ b/isotope/src/context.cpp @@ -0,0 +1,19 @@ +#include "context.h" + +bool GlobalContext::configure() { + this->app = adsk::core::Application::get(); + if (!this->app) { + return false; + } + + this->ui = app->userInterface(); + if (!this->ui) { + return false; + } + + return true; +} + +bool GlobalContext::isValid() const { + return this->app && this->ui && this->app->isValid() && this->ui->isValid(); +} diff --git a/isotope/src/isotope.cpp b/isotope/src/isotope.cpp new file mode 100644 index 0000000000..88e84054a4 --- /dev/null +++ b/isotope/src/isotope.cpp @@ -0,0 +1,80 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "config_command.h" +#include "context.h" + +GlobalContext gctx; + +extern "C" XI_EXPORT bool run(const char* context) { + if (!gctx.configure()) { + return false; + } + + adsk::core::Ptr workspace = gctx.ui->workspaces()->itemById("FusionSolidEnvironment"); + adsk::core::Ptr tab = workspace->toolbarTabs()->itemById("ToolsTab"); + assert(workspace); + assert(tab); + + tab->activate(); + tab->toolbarPanels()->add("isotope_tool_tab", "Isotope"); + + auto button = gctx.ui->commandDefinitions()->addButtonDefinition( + "isotope_command", "Isotope", "This command does something interesting.", "./resources/isotope_exporter/"); + + if (!button || !button->isValid()) { + gctx.ui->messageBox("Failed to create command definition for Isotope."); + return false; + } + + button->commandCreated()->add(new ConfigureCommandCreatedHandler(gctx)); + + auto panel = gctx.ui->allToolbarPanels()->itemById("isotope_tool_tab"); + + if (!panel || !panel->isValid()) { + gctx.ui->messageBox("Failed to find toolbar panel for Isotope."); + return false; + } + + auto button_control = panel->controls()->addCommand(button); + + if (!button_control || !button_control->isValid()) { + gctx.ui->messageBox("Failed to add command control for Isotope."); + return false; + } + + button_control->isPromoted(true); + button_control->isPromotedByDefault(true); + + return true; +} + +extern "C" XI_EXPORT bool stop() { + auto panel = gctx.ui->allToolbarPanels()->itemById("isotope_tool_tab"); + if (panel && panel->isValid()) { + panel->deleteMe(); + } + + auto button_def = gctx.ui->commandDefinitions()->itemById("isotope_command"); + if (button_def && button_def->isValid()) { + button_def->deleteMe(); + } + + return true; +} diff --git a/isotope/src/joints.cpp b/isotope/src/joints.cpp new file mode 100644 index 0000000000..fcb563f7cb --- /dev/null +++ b/isotope/src/joints.cpp @@ -0,0 +1,653 @@ +#include "joints.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "assembly.pb.h" +#include "joint.pb.h" +#include "signal.pb.h" +#include "types.pb.h" + +#include "util.h" + +namespace { + +mirabuf::joint::RigidGroup map_rigid_group( + const adsk::fusion::Joint* /* adsk::fusion::joint | adsk::fusion::AsBuiltJoint */ joint) { + assert(joint); + assert(joint->jointMotion()->jointType() == adsk::fusion::JointTypes::RigidJointType); + + if (!joint->occurrenceOne()->isLightBulbOn() || !joint->occurrenceTwo()->isLightBulbOn()) { + return {}; + } + + mirabuf::joint::RigidGroup group; + std::string group_name = "group_" + joint->occurrenceOne()->name() + "_" + joint->occurrenceTwo()->name(); + group.set_name(group_name); + group.add_occurrences(guid_occurrence(joint->occurrenceOne())); + group.add_occurrences(guid_occurrence(joint->occurrenceTwo())); + + return group; +} + +void fill_revolute_joint_motion( + const adsk::core::Ptr& motion, mirabuf::joint::Joint* proto_joint) { + assert(motion); + assert(proto_joint); + + proto_joint->set_joint_motion_type(mirabuf::joint::JointMotion::REVOLUTE); + auto dof = proto_joint->mutable_rotational()->mutable_rotational_freedom(); + dof->set_name("Rotational Joint"); + dof->set_value(motion->rotationValue()); + if (motion->rotationLimits()) { + dof->mutable_limits()->set_lower(motion->rotationLimits()->minimumValue()); + dof->mutable_limits()->set_upper(motion->rotationLimits()->maximumValue()); + } + + auto rotation_axis_vector = motion->rotationAxisVector(); + if (rotation_axis_vector) { + dof->mutable_axis()->set_x(rotation_axis_vector->x()); + dof->mutable_axis()->set_y(rotation_axis_vector->y()); + dof->mutable_axis()->set_z(rotation_axis_vector->z()); + } else { + auto rotation_axis = motion->rotationAxis(); + assert(rotation_axis); + dof->mutable_axis()->set_x((int) rotation_axis == 0); + dof->mutable_axis()->set_y((int) rotation_axis == 2); + dof->mutable_axis()->set_z((int) rotation_axis == 1); + } +} + +void fill_slider_joint_motion( + const adsk::core::Ptr& motion, mirabuf::joint::Joint* proto_joint) { + assert(motion); + assert(proto_joint); + + proto_joint->set_joint_motion_type(mirabuf::joint::JointMotion::SLIDER); + auto dof = proto_joint->mutable_prismatic()->mutable_prismatic_freedom(); + dof->mutable_axis()->set_x(-motion->slideDirectionVector()->x()); + dof->mutable_axis()->set_y(-motion->slideDirectionVector()->y()); + dof->mutable_axis()->set_z(-motion->slideDirectionVector()->z()); + + switch (motion->slideDirection()) { + case adsk::fusion::JointDirections::XAxisJointDirection: + dof->set_pivotdirection(mirabuf::Axis::X); + break; + case adsk::fusion::JointDirections::YAxisJointDirection: + dof->set_pivotdirection(mirabuf::Axis::Y); + break; + case adsk::fusion::JointDirections::ZAxisJointDirection: + dof->set_pivotdirection(mirabuf::Axis::Z); + break; + case adsk::fusion::JointDirections::CustomJointDirection: + default: + break; + } + + if (motion->slideLimits()) { + dof->mutable_limits()->set_lower(motion->slideLimits()->minimumValue()); + dof->mutable_limits()->set_upper(motion->slideLimits()->maximumValue()); + } + + dof->set_value(motion->slideValue()); +} + +void fill_motion_from_joint( + const adsk::core::Ptr& motion, mirabuf::joint::Joint* proto_joint) { + assert(motion); + assert(proto_joint); + + switch (motion->jointType()) { + case adsk::fusion::JointTypes::RevoluteJointType: + fill_revolute_joint_motion(adsk::core::Ptr(motion), proto_joint); + break; + case adsk::fusion::JointTypes::SliderJointType: + fill_slider_joint_motion(adsk::core::Ptr(motion), proto_joint); + break; + case adsk::fusion::JointTypes::RigidJointType: + proto_joint->set_joint_motion_type(mirabuf::joint::JointMotion::RIGID); + break; + case adsk::fusion::JointTypes::CylindricalJointType: + case adsk::fusion::JointTypes::BallJointType: + case adsk::fusion::JointTypes::PinSlotJointType: + case adsk::fusion::JointTypes::PlanarJointType: + case adsk::fusion::JointTypes::InferredJointType: + default: + break; + } +} + +adsk::core::Ptr origin_from_joint_geometry( + const adsk::fusion::JointGeometry* geometry, const adsk::core::Ptr occurrence) { + if (!geometry) { + return adsk::core::Point3D::create(); + } + + auto entity_one = geometry->entityOne(); + if (!entity_one) { + return adsk::core::Point3D::create(); + } + + auto edge_or_face = fusion_base_to_variant(entity_one.get()); + adsk::core::Ptr result = + std::visit(overloaded{[&geometry](std::monostate) -> auto { return geometry->origin(); }, + [&geometry, &occurrence](const adsk::fusion::BRepEdge* edge) -> auto { + if (!edge->assemblyContext()) { + auto new_entity = edge->createForAssemblyContext(occurrence); + auto min = new_entity->boundingBox()->minPoint(); + auto max = new_entity->boundingBox()->maxPoint(); + auto org = adsk::core::Point3D::create((max->x() + min->x()) / 2.0f, + (max->y() + min->y()) / 2.0f, (max->z() + min->z()) / 2.0f); + return org; + } + + return geometry->origin(); + }, + [&geometry, &occurrence](const adsk::fusion::BRepFace* face) -> auto { + if (!face->assemblyContext()) { + auto new_entity = face->createForAssemblyContext(occurrence); + return new_entity->centroid(); + } + + return geometry->origin(); + }}, + edge_or_face); + + return result; +} + +adsk::core::Ptr origin_from_joint_origin(const adsk::fusion::JointOrigin* joint_origin) { + if (!joint_origin) { + return adsk::core::Point3D::create(); + } + + auto origin = joint_origin->geometry()->origin(); + double offset_x = joint_origin->offsetX() ? joint_origin->offsetX()->value() : 0; + double offset_y = joint_origin->offsetY() ? joint_origin->offsetY()->value() : 0; + double offset_z = joint_origin->offsetZ() ? joint_origin->offsetZ()->value() : 0; + return adsk::core::Point3D::create(origin->x() + offset_x, origin->y() + offset_y, origin->z() + offset_z); +} + +adsk::core::Ptr get_joint_origin(const adsk::fusion::Joint* fusion_joint) { + assert(fusion_joint); + auto raw_geo_test = fusion_joint->geometryOrOriginOne(); + auto geometry_or_origin = + fusion_base_to_variant(raw_geo_test.get()); + if (std::holds_alternative(geometry_or_origin)) { + return adsk::core::Point3D::create(); + } + + adsk::core::Ptr result = std::visit( + overloaded{[](std::monostate) -> auto { return adsk::core::Point3D::create(); }, + [&fusion_joint](const adsk::fusion::JointGeometry* geometry) -> auto { + return origin_from_joint_geometry(geometry, fusion_joint->occurrenceOne()); + }, + [](const adsk::fusion::JointOrigin* origin) -> auto { return origin_from_joint_origin(origin); }}, + geometry_or_origin); + + return result; +} + +adsk::core::Ptr search_for_grounded( + const adsk::core::Ptr& occurrence) { + if (occurrence->isGrounded()) { + return occurrence; + } + + for (const auto occ : occurrence->childOccurrences()) { + auto searched = search_for_grounded(occ); + + if (searched) { + return searched; + } + } + + return nullptr; +} + +adsk::core::Ptr search_for_grounded(const adsk::core::Ptr& root) { + for (const auto occ : root->allOccurrences()) { + auto searched = search_for_grounded(occ); + + if (searched) { + return searched; + } + } + + return nullptr; +} + +enum OccurrenceRelationship { + TRANSFORM, // Hierarchy parenting + CONNECTION, // A rigid joint or other designator + GROUP, // A rigid grouping + NEXT, // The next joint in a list + END, // Orphaned child relationship + NONE, +}; + +struct GraphEdge; + +// TODO: Should maybe separate this out into multiple structs +// overlapping purpose +struct GraphNode { + adsk::core::Ptr data = nullptr; + std::shared_ptr previous = nullptr; + std::vector> edges{}; + + adsk::core::Ptr joint = nullptr; +}; + +struct GraphEdge { + OccurrenceRelationship relationship = NONE; + std::shared_ptr node = nullptr; +}; + +std::optional> populate_node(const adsk::core::Ptr& occurrence, + std::shared_ptr prev, OccurrenceRelationship relationship, bool is_ground, + std::unordered_set& visited_occurrence_entity_tokens, + const std::unordered_map>& dynamic_joints) { + if (occurrence->isGrounded() && !is_ground) { + return std::nullopt; + } + + if (relationship == NEXT && prev) { + auto node = GraphNode{occurrence}; + auto edge = GraphEdge{relationship, std::make_shared(node)}; + prev->edges.push_back(std::make_shared(edge)); + return std::nullopt; + } + + if (prev && dynamic_joints.find(occurrence->entityToken()) != dynamic_joints.end()) { + return std::nullopt; + } + + if (visited_occurrence_entity_tokens.count(occurrence->entityToken())) { + return std::nullopt; + } + + visited_occurrence_entity_tokens.insert(occurrence->entityToken()); + auto node = std::make_shared(GraphNode{occurrence, prev}); + for (auto occ : occurrence->childOccurrences()) { + populate_node(occ, node, TRANSFORM, is_ground, visited_occurrence_entity_tokens, dynamic_joints); + } + + for (auto joint : occurrence->joints()) { + if (!joint || !joint->occurrenceOne() || !joint->occurrenceTwo()) { + continue; + } + + bool is_rigid = joint->jointMotion()->jointType() == adsk::fusion::RigidJointType; + adsk::core::Ptr connection = nullptr; + if (is_rigid) { + if (joint->occurrenceOne() == occurrence) { + connection = joint->occurrenceTwo(); + } else if (joint->occurrenceTwo() == occurrence) { + connection = joint->occurrenceOne(); + } + } else { + if (joint->occurrenceOne() != occurrence) { + connection = joint->occurrenceOne(); + } + } + + if (!connection) { + continue; + } + + if (!prev || connection->entityToken() != prev->data->entityToken()) { + populate_node(connection, node, is_rigid ? CONNECTION : NEXT, is_ground, visited_occurrence_entity_tokens, + dynamic_joints); + } + } + + if (prev) { + prev->edges.push_back(std::make_shared(GraphEdge{relationship, node})); + } + + return node; +} + +std::optional create_tree_parts( + std::shared_ptr occurrence_node, OccurrenceRelationship relationship) { + if (relationship == NEXT || !occurrence_node->data->isLightBulbOn()) { + return std::nullopt; + } + + mirabuf::Node node; + node.set_value(guid_occurrence(occurrence_node->data)); + for (auto edge : occurrence_node->edges) { + auto dyn_node = std::dynamic_pointer_cast(edge->node); + auto child_node = create_tree_parts(dyn_node, edge->relationship); + if (child_node) { + node.mutable_children()->Add()->CopyFrom(child_node.value()); + } + } + + return node; +} + +void populate_joint(std::shared_ptr sim_node, mirabuf::joint::Joints* joints) { + mirabuf::joint::JointInstance* joint = nullptr; + if (!sim_node->joint) { + joint = &(*joints->mutable_joint_instances())["grounded"]; + } else { + joint = &(*joints->mutable_joint_instances())[sim_node->joint->entityToken()]; + } + + assert(joint); + auto root = create_tree_parts(sim_node, CONNECTION); + if (root) { + joint->mutable_parts()->mutable_nodes()->Add()->CopyFrom(root.value()); + } + + for (auto edge : sim_node->edges) { + populate_joint(edge->node, joints); + } +} + +void get_all_joints(adsk::core::Ptr root_component, + adsk::core::Ptr grounded, + std::vector>& grounded_connections, + std::unordered_map>& dynamic_joints) { + auto process_joint = [&](const auto /* adsk::fusion::joint | adsk::fusion::AsBuiltJoint */ joint) -> void { + assert(joint); + if (!joint->occurrenceOne() || !joint->occurrenceTwo()) { + return; + } + + if (joint->jointMotion()->jointType() != adsk::fusion::RigidJointType) { + if (dynamic_joints.find(joint->occurrenceOne()->entityToken()) == dynamic_joints.end()) { + dynamic_joints[joint->occurrenceOne()->entityToken()] = joint; + } + } else { + if (joint->occurrenceOne()->entityToken() == grounded->entityToken()) { + grounded_connections.push_back(joint->occurrenceTwo()); + } else if (joint->occurrenceTwo()->entityToken() == grounded->entityToken()) { + grounded_connections.push_back(joint->occurrenceOne()); + } + } + }; + + for (const auto& j : root_component->allJoints()) { + process_joint(j); + } + + for (const auto& j : root_component->allAsBuiltJoints()) { + process_joint(j); + } +} + +void look_for_grounded_joints(const std::vector>& grounded_connections, + const std::unordered_map>& dynamic_joints, + std::shared_ptr root_node) { + for (auto& grounded_connection : grounded_connections) { + std::unordered_set visited; + populate_node(grounded_connection, root_node, CONNECTION, false, visited, dynamic_joints); + } +} + +void populate_axis(const adsk::core::Ptr& design, + std::unordered_map>& simulation_nodes, + const std::unordered_map>& dynamic_joints, + const std::string& occurrence_token, const adsk::core::Ptr& joint) { + auto result = design->findEntityByToken(occurrence_token); + if (result.empty() || !result.at(0)) { + return; + } + + auto occurrence = static_cast>(result[0]); + if (!occurrence) { + return; + } + + std::unordered_set visited; + auto node = populate_node(occurrence, nullptr, NONE, false, visited, dynamic_joints); + if (node) { + node.value()->joint = joint; + simulation_nodes[occurrence_token] = node.value(); + } +} + +std::vector get_connected_axis_tokens(std::shared_ptr start) { + std::vector tokens; + std::unordered_set visited_nodes; + std::unordered_set visited_tokens; + + std::stack stack; + stack.push(start.get()); + + while (!stack.empty()) { + const GraphNode* node = stack.top(); + stack.pop(); + if (!visited_nodes.insert(node).second) { + continue; + } + + for (const auto& edge : node->edges) { + if (edge->relationship == NEXT) { + std::string token = edge->node->data->entityToken(); + if (visited_tokens.insert(token).second) { + tokens.emplace_back(std::move(token)); + } + } else { + stack.push(edge->node.get()); + } + } + } + + return tokens; +} + +void recurse_link_node_axis(std::shared_ptr root_node, + const std::unordered_map>& simulation_nodes) { + const std::vector tokens = get_connected_axis_tokens(root_node); + for (const auto& key : tokens) { + auto it = simulation_nodes.find(key); + if (it == simulation_nodes.end()) { + continue; + } + + // The original python exporter has separate enums for tracking + // both occurrence relationships and joint relationships. + // + // This, when transitioning to C++, made the types very complex as each + // node would contain either a occurrence relationship or a joint + // relationship label. + // + // Within this rewrite of the exporter this was omitted as the original + // functionality and necessity for these two distinct label types was + // unclear. + // + // Joint relationships are not tracked, only occurrence relationships are. + // + // For more information visit: + // https://github.com/Autodesk/synthesis/blob/f9bc9be63e21a705d7c8f5be9607f912764e0aa0/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/JointHierarchy.py#L54-L67 + root_node->edges.push_back(std::make_shared(GraphEdge{NONE, it->second})); + recurse_link_node_axis(it->second, simulation_nodes); + } +} + +} // namespace + +std::pair populate_joints( + const adsk::core::Ptr& design) { + assert(design); + mirabuf::joint::Joints joints; + joints.mutable_info()->set_name(""); + joints.mutable_info()->set_guid("joints-guid"); + joints.mutable_info()->set_version(1); + + mirabuf::signal::Signals signals; + + auto& joint_definition_ground = (*joints.mutable_joint_definitions())["grounded"]; + joint_definition_ground.mutable_info()->set_name("grounded"); + // TODO: Add comment + joint_definition_ground.mutable_info()->set_guid("grounded"); + joint_definition_ground.mutable_info()->set_version(1); + + auto& joint_instance_ground = (*joints.mutable_joint_instances())["grounded"]; + joint_instance_ground.mutable_info()->set_name("grounded"); + joint_instance_ground.mutable_info()->set_guid("grounded-inst-guid"); + joint_instance_ground.mutable_info()->set_version(1); + + joint_instance_ground.set_joint_reference(joint_definition_ground.info().guid()); + + auto process_joint = [&joints, &signals]( + const adsk::fusion::Joint* /* adsk::fusion::joint | adsk::fusion::AsBuiltJoint */ joint) { + assert(joint); + if (joint->isSuppressed()) { + return; + } + + auto motion = joint->jointMotion(); + if (motion->jointType() == adsk::fusion::JointTypes::RigidJointType) { + auto rigidGroup = map_rigid_group(joint); + if (!rigidGroup.occurrences().empty()) { + joints.mutable_rigid_groups()->Add()->CopyFrom(rigidGroup); + } + } + + auto& signal = (*signals.mutable_signal_map())[joint->entityToken()]; + signal.mutable_info()->CopyFrom(create_info_from_fus_obj(joint)); + signal.set_io(mirabuf::signal::IOType::OUTPUT); + signal.set_device_type(mirabuf::signal::DeviceType::PWM); + + auto& joint_instance = (*joints.mutable_joint_instances())[joint->entityToken()]; + joint_instance.mutable_info()->CopyFrom(create_info_from_fus_obj(joint)); + joint_instance.set_signal_reference(signal.info().guid()); + joint_instance.set_joint_reference(joint_instance.info().guid()); + joint_instance.set_parent_part(guid_occurrence(joint->occurrenceOne())); + joint_instance.set_child_part(guid_occurrence(joint->occurrenceTwo())); + + // TODO: Wheel logic should go here + + auto& joint_definition = (*joints.mutable_joint_definitions())[joint->entityToken()]; + joint_definition.set_motor_reference(signal.info().guid()); + joint_definition.mutable_info()->CopyFrom(create_info_from_fus_obj(joint)); + + auto joint_origin = get_joint_origin(joint); + + if (joint_origin) { + joint_definition.mutable_origin()->set_x(joint_origin->x()); + joint_definition.mutable_origin()->set_y(joint_origin->y()); + joint_definition.mutable_origin()->set_z(joint_origin->z()); + } else { + joint_definition.mutable_origin()->set_x(0.0f); + joint_definition.mutable_origin()->set_y(0.0f); + joint_definition.mutable_origin()->set_z(0.0f); + } + + joint_definition.set_break_magnitude(0.0f); + + auto& motor = (*joints.mutable_motor_definitions())[joint->entityToken()]; + motor.mutable_info()->CopyFrom(create_info_from_fus_obj(joint)); + auto simple_motor = motor.mutable_simple_motor(); + + // These are values I just chose on a whim, they need to be checked and changed to make sure + // everything works correctly. + simple_motor->set_stall_torque(0.5f); + simple_motor->set_max_velocity(1.0f); + simple_motor->set_braking_constant(0.8f); + + fill_motion_from_joint(motion, &joint_definition); + }; + + for (const auto& joint : design->rootComponent()->allJoints()) { + process_joint(joint.get()); + } + + for (const auto& asBuiltJoint : design->rootComponent()->allAsBuiltJoints()) { + // TODO: Replace adsk::fusion::Joint* with auto to make this function call valid + // the compiler will make two instances of the lambda, one for each type + // process_joint(asBuiltJoint.get()); + } + + return {joints, signals}; +} + +mirabuf::GraphContainer create_joint_graph(const mirabuf::joint::Joints& joints) { + std::unordered_map nodes; + auto ground_node = mirabuf::Node(); + ground_node.set_value("ground"); + nodes[ground_node.value()] = ground_node; + + for (const auto& [_, joint] : joints.joint_definitions()) { + if (joint.info().guid().length()) { + auto new_node = mirabuf::Node(); + new_node.set_value(joint.info().guid()); + nodes[new_node.value()] = new_node; + } + } + + for (const auto& [_, joint] : joints.joint_definitions()) { + if (joint.info().guid().length()) { + nodes["ground"].mutable_children()->Add()->CopyFrom(nodes[joint.info().guid()]); + } + } + + mirabuf::GraphContainer joint_tree; + for (const auto& [_, node] : nodes) { + joint_tree.mutable_nodes()->Add()->CopyFrom(node); + } + + return joint_tree; +} + +void build_joint_part_hierarchy(mirabuf::joint::Joints* joints, const adsk::core::Ptr& design) { + std::unordered_set visited_occurrence_entity_tokens; + std::unordered_map> dynamic_joints; + std::unordered_map> simulation_nodes; + std::vector> grounded_connections; + + auto grounded = search_for_grounded(design->rootComponent()); + + // If there was anything that represented that the C++ exporter is currently + // experimental it would be this. Not having a grounded node is a very common + // user facing problem and simply asserting this will cause fusion to crash. + // In the future if we want to actually support this section of the project + // we will need to update this into an actual error system. + // + // Note for future development: + // All instances of `assert(..)` need to be removed as Fusion simply cannot catch + // these errors and will crash. + assert(grounded); + + get_all_joints(design->rootComponent(), grounded, grounded_connections, dynamic_joints); + + auto root_node = + populate_node(grounded, nullptr, NONE, true, visited_occurrence_entity_tokens, dynamic_joints).value(); + simulation_nodes["ground"] = root_node; + + look_for_grounded_joints(grounded_connections, dynamic_joints, root_node); + + for (const auto& [key, value] : dynamic_joints) { + populate_axis(design, simulation_nodes, dynamic_joints, key, value); + } + + recurse_link_node_axis(root_node, simulation_nodes); + + populate_joint(root_node, joints); +} diff --git a/isotope/src/materials.cpp b/isotope/src/materials.cpp new file mode 100644 index 0000000000..55f5ab2d65 --- /dev/null +++ b/isotope/src/materials.cpp @@ -0,0 +1,217 @@ +#include "materials.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "material.pb.h" + +#include "util.h" + +namespace { + +mirabuf::material::Appearance default_appearance() { + mirabuf::material::Appearance appearance; + appearance.mutable_info()->set_name("Default Appearance"); + appearance.mutable_info()->set_guid("default"); + appearance.mutable_info()->set_version(1); + appearance.set_roughness(0.5f); + appearance.set_metallic(0.5f); + appearance.set_specular(0.5f); + + appearance.mutable_albedo()->set_r(127); + appearance.mutable_albedo()->set_g(127); + appearance.mutable_albedo()->set_b(127); + appearance.mutable_albedo()->set_a(255); + + return appearance; +} + +mirabuf::material::Appearance map_appearance(const adsk::core::Ptr& appearance) { + mirabuf::material::Appearance new_appearance = default_appearance(); + new_appearance.mutable_info()->CopyFrom(create_info_from_fus_obj(appearance)); + + new_appearance.set_roughness(0.9f); + new_appearance.set_metallic(0.3f); + new_appearance.set_specular(0.5f); + + new_appearance.mutable_albedo()->set_r(10); + new_appearance.mutable_albedo()->set_g(10); + new_appearance.mutable_albedo()->set_b(10); + new_appearance.mutable_albedo()->set_a(127); + + auto properties = appearance->appearanceProperties(); + if (auto roughness_property = properties->itemById("surface_roughness")) { + new_appearance.set_roughness(dynamic_cast(roughness_property.get())->value()); + } + + adsk::core::Ptr model_item = properties->itemById("interior_model"); + if (!model_item) { + return new_appearance; + } + + adsk::core::Ptr base_color = nullptr; + + int mat_model_type = model_item->value(); + switch (mat_model_type) { + case 0: { + if (auto reflectance_property = properties->itemById("opaque_f0")) { + new_appearance.set_metallic( + dynamic_cast(reflectance_property.get())->value()); + } + + adsk::core::Ptr color = properties->itemById("opaque_albedo"); + if (color && color->value()) { + base_color = color->value(); + base_color->opacity(255); + } + + break; + } + case 1: { + new_appearance.set_metallic(0.8); + + adsk::core::Ptr color = properties->itemById("opaque_albedo"); + if (color && color->value()) { + base_color = color->value(); + base_color->opacity(255); + } + + break; + } + case 2: { + adsk::core::Ptr color = properties->itemById("layered_diffuse"); + if (color && color->value()) { + base_color = color->value(); + base_color->opacity(255); + } + + break; + } + case 3: { + adsk::core::Ptr color = properties->itemById("layered_diffuse"); + adsk::core::Ptr transparent_distance = + properties->itemById("transparent_distance"); + + constexpr float OPACITY_RAMPING_CONSTANT = 14.0f; + float opacity = + (255.0f * transparent_distance->value()) / (transparent_distance->value() + OPACITY_RAMPING_CONSTANT); + if (opacity > 255) { + opacity = 255; + } else if (opacity < 0) { + opacity = 0; + } + + if (color && color->value()) { + base_color = color->value(); + base_color->opacity(static_cast(opacity)); + } + break; + } + } + + if (base_color) { + new_appearance.mutable_albedo()->set_r(base_color->red()); + new_appearance.mutable_albedo()->set_g(base_color->green()); + new_appearance.mutable_albedo()->set_b(base_color->blue()); + new_appearance.mutable_albedo()->set_a(base_color->opacity()); + } else { + for (auto prop : appearance->appearanceProperties()) { + if (prop->name() == "Color") { + auto color_property = dynamic_cast(prop.get()); + if (color_property->value() && color_property->id() != "surface_albedo") { + new_appearance.mutable_albedo()->set_r(color_property->value()->red()); + new_appearance.mutable_albedo()->set_g(color_property->value()->green()); + new_appearance.mutable_albedo()->set_b(color_property->value()->blue()); + new_appearance.mutable_albedo()->set_a(color_property->value()->opacity()); + break; + } + } + } + } + + return new_appearance; +} + +mirabuf::material::PhysicalMaterial default_physical_material() { + mirabuf::material::PhysicalMaterial physical_material; + physical_material.mutable_info()->set_name("Default Physical Material"); + physical_material.mutable_info()->set_guid("default-physical-material-guid"); + physical_material.mutable_info()->set_version(1); + physical_material.set_dynamic_friction(0.5f); + physical_material.set_static_friction(0.5f); + physical_material.set_restitution(0.5f); + physical_material.set_deformable(false); + physical_material.set_mattype(mirabuf::material::PhysicalMaterial_MaterialType_METAL); + + return physical_material; +} + +#define SET_FROM_PROP(props, id, obj, method) \ + do { \ + if (auto p = (props)->itemById(id)) { \ + if (auto fp = dynamic_cast(p.get())) { \ + (obj)->method(fp->value()); \ + } \ + } \ + } while (0) + +mirabuf::material::PhysicalMaterial map_physical_material(const adsk::core::Ptr& material) { + mirabuf::material::PhysicalMaterial new_physical_material = default_physical_material(); + new_physical_material.mutable_info()->CopyFrom(create_info_from_fus_obj(material)); + + new_physical_material.set_deformable(false); + new_physical_material.set_mattype(mirabuf::material::PhysicalMaterial_MaterialType_METAL); + + new_physical_material.set_dynamic_friction(0.5f); + new_physical_material.set_static_friction(0.5f); + new_physical_material.set_restitution(0.5f); + + auto mat_props = material->materialProperties(); + auto mechanical_properties = new_physical_material.mutable_mechanical(); + auto strength_properties = new_physical_material.mutable_strength(); + + SET_FROM_PROP(mat_props, "structural_Young_modulus", mechanical_properties, set_young_mod); + SET_FROM_PROP(mat_props, "structural_Poisson_ratio", mechanical_properties, set_poisson_ratio); + SET_FROM_PROP(mat_props, "structural_Shear_modulus", mechanical_properties, set_shear_mod); + SET_FROM_PROP(mat_props, "structural_Density", mechanical_properties, set_density); + SET_FROM_PROP(mat_props, "structural_Damping_coefficient", mechanical_properties, set_damping_coefficient); + SET_FROM_PROP(mat_props, "structural_Minimum_yield_stress", strength_properties, set_yield_strength); + SET_FROM_PROP(mat_props, "structural_Minimum_tensile_strength", strength_properties, set_tensile_strength); + + return new_physical_material; +} + +} // namespace + +mirabuf::material::Materials map_all_materials(const adsk::core::Ptr& design_appearances, + const adsk::core::Ptr& design_materials) { + mirabuf::material::Materials materials; + (*materials.mutable_appearances())["default"] = default_appearance(); + + std::vector> appearances; + design_appearances->copyTo(std::back_inserter(appearances)); + for (const auto& appearance : appearances) { + auto& new_appearance = (*materials.mutable_appearances())[appearance->id()]; + new_appearance = map_appearance(appearance); + new_appearance.mutable_info()->CopyFrom(create_info_from_fus_obj(appearance)); + } + + std::vector> physical_materials; + design_materials->copyTo(std::back_inserter(physical_materials)); + for (const auto& material : physical_materials) { + auto& new_physical_material = (*materials.mutable_physicalmaterials())[material->id()]; + new_physical_material = map_physical_material(material); + new_physical_material.mutable_info()->CopyFrom(create_info_from_fus_obj(material)); + } + + return materials; +} diff --git a/isotope/src/parser.cpp b/isotope/src/parser.cpp new file mode 100644 index 0000000000..459a8bca7a --- /dev/null +++ b/isotope/src/parser.cpp @@ -0,0 +1,87 @@ +#include "parser.h" + +#include +#include +#include +#include +#include +#include +#include + +#include + +#include + +#include "assembly.pb.h" +#include "types.pb.h" + +#include "components.h" +#include "joints.h" +#include "materials.h" +#include "util.h" + +void export_design(const GlobalContext& gctx) { + assert(gctx.isValid()); + auto document = gctx.app->activeDocument(); + auto design = document->query()->design(); + + mirabuf::Assembly assembly; + assembly.mutable_info()->CopyFrom(create_info_from_fus_obj(design->rootComponent())); + assembly.mutable_info()->set_guid(design->parentDocument()->name()); + + // Determines if the exported design should be treated as a robot or field + // assembly. Currently since there is no UI present in Isotope we default to + // robot always. This could be changed if either 1) a UI is added to Isotope + // or 2) some sort of algorithmic detection is added to determine if the + // design is a robot or field assembly. + assembly.set_dynamic(true); + + auto appearances = design->appearances(); + auto materials = design->materials(); + assembly.mutable_data()->mutable_materials()->CopyFrom(map_all_materials(appearances, materials)); + + auto components = design->allComponents(); + assembly.mutable_data()->mutable_parts()->CopyFrom(map_all_parts(components, assembly.data().materials())); + + mirabuf::Node root_node = parse_component_root(design->rootComponent(), assembly.mutable_data()->mutable_parts()); + assembly.mutable_design_hierarchy()->mutable_nodes()->Add()->CopyFrom(root_node); + + const auto [joints, signals] = populate_joints(design); + + assembly.mutable_data()->mutable_joints()->CopyFrom(joints); + assembly.mutable_data()->mutable_signals()->CopyFrom(signals); + + map_rigid_groups(design->rootComponent(), assembly.mutable_data()->mutable_joints()); + + auto joint_hierarchy = create_joint_graph(joints); + assembly.mutable_joint_hierarchy()->CopyFrom(joint_hierarchy); + + build_joint_part_hierarchy(assembly.mutable_data()->mutable_joints(), design); + + // Print assembly as JSON + std::string json_output; + auto _ = google::protobuf::util::MessageToJsonString(assembly, &json_output); + + std::ofstream output_file(std::getenv("HOME") + std::string("/Desktop/assembly_debug.json")); + if (!output_file.is_open()) { + gctx.app->userInterface()->messageBox("Failed to open output file for writing."); + return; + } + + output_file << json_output; + output_file.close(); + + std::ofstream binary_output( + std::getenv("HOME") + std::string("/Desktop/test_dozer.mira"), std::ios::out | std::ios::binary); + if (!binary_output.is_open()) { + gctx.app->userInterface()->messageBox("Failed to open output file for writing."); + return; + } + + if (!assembly.SerializeToOstream(&binary_output)) { + gctx.app->userInterface()->messageBox("Failed to write binary."); + return; + } + + gctx.app->userInterface()->messageBox("Exported assembly:\n" + json_output); +} diff --git a/isotope/src/util.cpp b/isotope/src/util.cpp new file mode 100644 index 0000000000..613744f986 --- /dev/null +++ b/isotope/src/util.cpp @@ -0,0 +1,20 @@ +#include "util.h" + +#include +#include + +std::string guid_component(const adsk::core::Ptr& component) { + std::string output; + output += component->entityToken(); + output += "_"; + output += component->id(); + return output; +} + +std::string guid_occurrence(const adsk::core::Ptr& occurrence) { + std::string output; + output += occurrence->entityToken(); + output += "_"; + output += guid_component(occurrence->component()); + return output; +} diff --git a/protobuf b/protobuf new file mode 160000 index 0000000000..ad4a58499d --- /dev/null +++ b/protobuf @@ -0,0 +1 @@ +Subproject commit ad4a58499dc00c6e8eb51d79b31e714fd6043c74