diff --git a/VirtualSatelliteCAD/Init.py b/VirtualSatelliteCAD/Init.py index 1e61532..f4074cb 100644 --- a/VirtualSatelliteCAD/Init.py +++ b/VirtualSatelliteCAD/Init.py @@ -24,9 +24,11 @@ # SPDX-License-Identifier: LGPL-3.0-or-later # import FreeCAD -from os.path import isdir +import os import sys +MOD_DIR = os.path.join(FreeCAD.ConfigGet("UserAppData"), "Mod") +APPDATA_DIR = os.path.join(MOD_DIR, "VirtualSatelliteCAD") # FreeCAD seems to load modules differently once they are stored in the User Home directory. # We try to load the whole folder if it exists @@ -37,7 +39,7 @@ Log("See if the directory " + freecad_user_mod + "exists...") -if isdir(freecad_user_mod): +if os.path.isdir(freecad_user_mod): Log("Directory Exists... Check if it is already on the path...") if (freecad_user_mod in sys.path): Log("Directory is already on the path...") @@ -45,5 +47,11 @@ Log("Directory will be appended to system path...") sys.path.append(freecad_user_mod) +# Create an appdata directory +if not os.path.isdir(MOD_DIR): + os.mkdir(MOD_DIR) +if not os.path.isdir(APPDATA_DIR): + os.mkdir(APPDATA_DIR) + # Finally register the unit test for being executed with all other FreeCAD tests FreeCAD.__unit_test__ += ["TestVirtualSatelliteApp"] diff --git a/VirtualSatelliteCAD/InitGui.py b/VirtualSatelliteCAD/InitGui.py index ff4ccea..9dc35be 100644 --- a/VirtualSatelliteCAD/InitGui.py +++ b/VirtualSatelliteCAD/InitGui.py @@ -45,10 +45,10 @@ def Initialize(self): import commands.command_export # NOQA @UnusedImport from commands.command_definitions import COMMAND_ID_EXPORT_2_VIRTUAL_SATELLITE from commands.command_definitions import COMMAND_ID_IMPORT_2_FREECAD - self.appendToolbar('VirtualSatelliteMod', [COMMAND_ID_EXPORT_2_VIRTUAL_SATELLITE]) - self.appendMenu('VirtualSatelliteMod', [COMMAND_ID_EXPORT_2_VIRTUAL_SATELLITE]) self.appendToolbar('VirtualSatelliteMod', [COMMAND_ID_IMPORT_2_FREECAD]) self.appendMenu('VirtualSatelliteMod', [COMMAND_ID_IMPORT_2_FREECAD]) + self.appendToolbar('VirtualSatelliteMod', [COMMAND_ID_EXPORT_2_VIRTUAL_SATELLITE]) + self.appendMenu('VirtualSatelliteMod', [COMMAND_ID_EXPORT_2_VIRTUAL_SATELLITE]) def GetClassName(self): # Required method by FreeCAD framework diff --git a/VirtualSatelliteCAD/Resources/Tests/VisCube2.json b/VirtualSatelliteCAD/Resources/Tests/VisCube2.json new file mode 100644 index 0000000..bf4ca17 --- /dev/null +++ b/VirtualSatelliteCAD/Resources/Tests/VisCube2.json @@ -0,0 +1,158 @@ +{ + "Products": { + "children": [{ + "posX": 0.0, + "posY": 0.0, + "posZ": 1.0, + "rotX": 0.0, + "children": [], + "rotZ": 0.0, + "rotY": 0.0, + "name": "Top", + "uuid": "cc14e2c7-9d7e-4cf2-8d6d-9b8cf5e96d56", + "partUuid": "cc14e2c7-9d7e-4cf2-8d6d-9b8cf5e96d56", + "partName": "Top" + }, { + "posX": 0.0, + "posY": 0.0, + "posZ": 0.0, + "rotX": 0.0, + "children": [], + "rotZ": 0.0, + "rotY": 0.0, + "name": "Bottom", + "uuid": "61db0622-6fef-4f12-932d-a00fdb9d0848", + "partUuid": "00f430a6-6311-4a33-961b-41ded4cf57d5", + "partName": "Plate" + }, { + "posX": 0.5, + "posY": 0.0, + "posZ": 0.5, + "rotX": 0.0, + "children": [], + "rotZ": 0.0, + "rotY": 1.5707963267948966, + "name": "Front", + "uuid": "e6af9d3f-8ad6-4488-b3d0-d35549be9a1e", + "partUuid": "e6af9d3f-8ad6-4488-b3d0-d35549be9a1e", + "partName": "Front" + }, { + "posX": -0.5, + "posY": 0.0, + "posZ": 0.5, + "rotX": 0.0, + "children": [], + "rotZ": 0.0, + "rotY": 1.5707963267948966, + "name": "Back", + "uuid": "a3c9c547-8fd3-40d5-97a1-a3f9a3a9c337", + "partUuid": "a3c9c547-8fd3-40d5-97a1-a3f9a3a9c337", + "partName": "Back" + }, { + "posX": 0.0, + "posY": 0.0, + "posZ": 0.5, + "rotX": 0.0, + "children": [{ + "posX": 0.0, + "posY": 0.5, + "posZ": 0.0, + "rotX": 1.5707963267948966, + "children": [], + "rotZ": 0.0, + "rotY": 0.0, + "name": "Left", + "uuid": "615985c0-73fd-48db-8f8b-e11b7cbb2ee8", + "partUuid": "615985c0-73fd-48db-8f8b-e11b7cbb2ee8", + "partName": "Left" + }, { + "posX": 0.0, + "posY": -0.5, + "posZ": 0.0, + "rotX": 1.5707963267948966, + "children": [], + "rotZ": 0.0, + "rotY": 0.0, + "name": "Right", + "uuid": "882a0b35-7da8-4555-903d-fd6b5cbec392", + "partUuid": "882a0b35-7da8-4555-903d-fd6b5cbec392", + "partName": "Right" + } + ], + "rotZ": 0.0, + "rotY": 0.0, + "name": "BeamStructure", + "uuid": "2afb23c9-f458-4bdb-a4e7-fc863364644f", + "partUuid": "2afb23c9-f458-4bdb-a4e7-fc863364644f", + "partName": "BeamStructure" + } + ], + "name": "SpaceCube", + "uuid": "a3533e02-125c-4066-bffe-d046d8d8342a" + }, + "Parts": [{ + "color": 16744448, + "shape": "CYLINDER", + "name": "BeamStructure", + "lengthY": 1.0, + "lengthX": 1.0, + "radius": 0.05, + "uuid": "2afb23c9-f458-4bdb-a4e7-fc863364644f", + "lengthZ": 1.0 + }, { + "color": 8388608, + "shape": "BOX", + "name": "Right", + "lengthY": 1.0, + "lengthX": 1.0, + "radius": 0.05, + "uuid": "882a0b35-7da8-4555-903d-fd6b5cbec392", + "lengthZ": 0.02 + }, { + "color": 32832, + "shape": "BOX", + "name": "Front", + "lengthY": 1.0, + "lengthX": 1.0, + "radius": 0.05, + "uuid": "e6af9d3f-8ad6-4488-b3d0-d35549be9a1e", + "lengthZ": 0.02 + }, { + "color": 16711680, + "shape": "BOX", + "name": "Left", + "lengthY": 1.0, + "lengthX": 1.0, + "radius": 0.05, + "uuid": "615985c0-73fd-48db-8f8b-e11b7cbb2ee8", + "lengthZ": 0.02 + }, { + "color": 65280, + "shape": "BOX", + "name": "Plate", + "lengthY": 1.0, + "lengthX": 1.0, + "radius": 0.05, + "uuid": "00f430a6-6311-4a33-961b-41ded4cf57d5", + "lengthZ": 0.02 + }, { + "color": 16776960, + "shape": "BOX", + "name": "Back", + "lengthY": 1.0, + "lengthX": 1.0, + "radius": 0.05, + "uuid": "a3c9c547-8fd3-40d5-97a1-a3f9a3a9c337", + "lengthZ": 0.02 + }, { + "color": 32768, + "shape": "BOX", + "name": "Top", + "lengthY": 1.0, + "lengthX": 1.0, + "radius": 0.05, + "uuid": "cc14e2c7-9d7e-4cf2-8d6d-9b8cf5e96d56", + "lengthZ": 0.02 + } + ] +} \ No newline at end of file diff --git a/VirtualSatelliteCAD/Resources/Tests/VisCube2_update.json b/VirtualSatelliteCAD/Resources/Tests/VisCube2_update.json new file mode 100644 index 0000000..082262e --- /dev/null +++ b/VirtualSatelliteCAD/Resources/Tests/VisCube2_update.json @@ -0,0 +1,31 @@ +{ + "Products": { + "children": [{ + "posX": 0.0, + "posY": 0.0, + "posZ": 1.0, + "rotX": 0.0, + "children": [], + "rotZ": 0.0, + "rotY": 0.0, + "name": "Top", + "uuid": "cc14e2c7-9d7e-4cf2-8d6d-9b8cf5e96d56", + "partUuid": "cc14e2c7-9d7e-4cf2-8d6d-9b8cf5e96d57", + "partName": "Top2" + } + ], + "name": "SpaceCube", + "uuid": "a3533e02-125c-4066-bffe-d046d8d8342a" + }, + "Parts": [{ + "color": 32768, + "shape": "BOX", + "name": "Top2", + "lengthY": 1.0, + "lengthX": 1.0, + "radius": 0.05, + "uuid": "cc14e2c7-9d7e-4cf2-8d6d-9b8cf5e96d57", + "lengthZ": 0.02 + } + ] +} \ No newline at end of file diff --git a/VirtualSatelliteCAD/TestVirtualSatelliteApp.py b/VirtualSatelliteCAD/TestVirtualSatelliteApp.py index 8ebab03..abf8191 100644 --- a/VirtualSatelliteCAD/TestVirtualSatelliteApp.py +++ b/VirtualSatelliteCAD/TestVirtualSatelliteApp.py @@ -36,4 +36,5 @@ from test.json_io.products.test_json_product import TestJsonProduct # NOQA from test.json_io.products.test_json_product_assembly import TestJsonProductAssembly # NOQA from test.json_io.products.test_json_product_child import TestJsonProductChild # NOQA +from test.json_io.products.test_json_product_assembly_tree_traverser import TestJsonProductAssemblyTreeTraverser # NOQA from test.freecad.test_actice_document import TestActiveDocument # NOQA diff --git a/VirtualSatelliteCAD/commands/command_export.py b/VirtualSatelliteCAD/commands/command_export.py index 24ccddd..5f94ffd 100644 --- a/VirtualSatelliteCAD/commands/command_export.py +++ b/VirtualSatelliteCAD/commands/command_export.py @@ -26,21 +26,21 @@ import FreeCAD import FreeCADGui -from module.environment import Environment, ICON_IMPORT +from module.environment import Environment, ICON_EXPORT from commands.command_definitions import COMMAND_ID_EXPORT_2_VIRTUAL_SATELLITE class CommandExport: def Activated(self): - FreeCAD.Console.PrintMessage("Calling the importer\n") + FreeCAD.Console.PrintMessage("Calling the exporter\n") def IsActive(self): return True def GetResources(self): - return {'Pixmap': Environment().get_icon_path(ICON_IMPORT), - 'MenuText': 'Import from Virtual Satellite', - 'ToolTip': 'Open the dialog for the Virtual Satellite json import.'} + return {'Pixmap': Environment().get_icon_path(ICON_EXPORT), + 'MenuText': 'Export from Virtual Satellite', + 'ToolTip': 'Open the dialog for the Virtual Satellite json export.'} FreeCADGui.addCommand(COMMAND_ID_EXPORT_2_VIRTUAL_SATELLITE, CommandExport()) # @UndefinedVariable diff --git a/VirtualSatelliteCAD/commands/command_import.py b/VirtualSatelliteCAD/commands/command_import.py index 1dc2713..62bdc40 100644 --- a/VirtualSatelliteCAD/commands/command_import.py +++ b/VirtualSatelliteCAD/commands/command_import.py @@ -28,19 +28,46 @@ import FreeCADGui from module.environment import Environment, ICON_IMPORT from commands.command_definitions import COMMAND_ID_IMPORT_2_FREECAD +from PySide2.QtWidgets import QFileDialog +from json_io.json_importer import JsonImporter +import os +import json +Log = FreeCAD.Console.PrintMessage -class CommandExport: + +class CommandImport: def Activated(self): - FreeCAD.Console.PrintMessage("Calling the json_io\n") + Log("Calling the importer\n") + + # call pyqt dialog: returns (filename, filter) + filename = QFileDialog.getOpenFileName( + None, # ui parent + "Open JSON file", # dialog caption + Environment.get_appdata_module_path(), + "JSON(*.json)")[0] # filter + + if filename != '': + (f"Selected file '{filename}'\n") + + with open(filename, 'r') as f: + try: + json_object = json.load(f) + except ValueError as error: + Log(f"ERROR: Invalid JSON found: '{error}'\n") + Log("Please provide a valid JSON\n") + return + + json_importer = JsonImporter(Environment.get_appdata_module_path() + os.sep) + json_importer.full_import(json_object) def IsActive(self): return True def GetResources(self): return {'Pixmap': Environment().get_icon_path(ICON_IMPORT), - 'MenuText': 'Export to Virtual Satellite', - 'ToolTip': 'Open the dialog for the Virtual Satellite json export.'} + 'MenuText': 'Import to Virtual Satellite', + 'ToolTip': 'Open the dialog for the Virtual Satellite json import.'} -FreeCADGui.addCommand(COMMAND_ID_IMPORT_2_FREECAD, CommandExport()) # @UndefinedVariable +FreeCADGui.addCommand(COMMAND_ID_IMPORT_2_FREECAD, CommandImport()) # @UndefinedVariable diff --git a/VirtualSatelliteCAD/freecad/active_document.py b/VirtualSatelliteCAD/freecad/active_document.py index a49d343..1cf324b 100644 --- a/VirtualSatelliteCAD/freecad/active_document.py +++ b/VirtualSatelliteCAD/freecad/active_document.py @@ -70,6 +70,14 @@ def open_set_and_get_document(self, file_name_without_extension): return self + def clear_if_open_document(self, file_name_without_extension): + documents = list(App.listDocuments().keys()) + + if documents.count(file_name_without_extension) != 0: + Log('Delete and recreate new FreeCAD file...\n') + App.closeDocument(file_name_without_extension) + App.newDocument(file_name_without_extension) + def set_active_documents(self, file_name_without_extension): App.setActiveDocument(file_name_without_extension) diff --git a/VirtualSatelliteCAD/json_io/json_definitions.py b/VirtualSatelliteCAD/json_io/json_definitions.py index baf6832..2eca28b 100644 --- a/VirtualSatelliteCAD/json_io/json_definitions.py +++ b/VirtualSatelliteCAD/json_io/json_definitions.py @@ -30,6 +30,9 @@ RAD_TO_DEG = 180.0 / math.pi +JSON_PARTS = "Parts" +JSON_PRODUCTS = "Products" + JSON_ELEMENT_COLOR = "color" JSON_ELEMENT_SHAPE = "shape" @@ -64,6 +67,9 @@ JSON_ELEMNT_CHILDREN = "children" +PART_IDENTIFIER = "part_" +PRODUCT_IDENTIFIER = "assembly_" + def _get_combined_name_uuid(name, uuid): return str(name + "_" + uuid.replace("-", "_")) diff --git a/VirtualSatelliteCAD/json_io/json_importer.py b/VirtualSatelliteCAD/json_io/json_importer.py index 281f94e..f31ba9e 100644 --- a/VirtualSatelliteCAD/json_io/json_importer.py +++ b/VirtualSatelliteCAD/json_io/json_importer.py @@ -28,7 +28,8 @@ import FreeCADGui from freecad.active_document import ActiveDocument from json_io.parts.json_part_factory import JsonPartFactory -from json_io.json_definitions import get_part_name_uuid +from json_io.products.json_product_assembly_tree_traverser import JsonProductAssemblyTreeTraverser +from json_io.json_definitions import get_part_name_uuid, JSON_PRODUCTS, JSON_PARTS, PART_IDENTIFIER App = FreeCAD Gui = FreeCADGui @@ -40,22 +41,24 @@ class JsonImporter(object): ''' - classdocs + Provides functionality to import a JSON created by Virtual Satellite into FreeCAD ''' - def __init__(self, working_ouput_directory): - self.working_output_directory = working_ouput_directory + def __init__(self, working_output_directory): + self.working_output_directory = working_output_directory def create_or_update_part(self, json_object): Log("Creating or Updating a part...\n") json_part = JsonPartFactory().create_from_json(json_object) + part_file_name = "" + if json_part is not None: # Use the name to create the part document # should be careful in case the name already exists. # thus it is combined with the uuid. not really nice # but definitely efficient - part_file_name = get_part_name_uuid(json_object) + part_file_name = PART_IDENTIFIER + get_part_name_uuid(json_object) active_document = ActiveDocument(self.working_output_directory).open_set_and_get_document(part_file_name) @@ -65,3 +68,24 @@ def create_or_update_part(self, json_object): Log("Saved part to file: " + part_file_name + "\n") else: Log("Visualization shape is most likely NONE, therefore no file is created\n") + + return part_file_name + + def full_import(self, json_object): + ''' + Import a whole json file's products and parts into a FreeCAD document + ''' + Log(f"Calling the importer'\n") + + json_parts = json_object[JSON_PARTS] + + part_file_names = [] + for part in json_parts: + part_file_names.append(self.create_or_update_part(part)) + + traverser = JsonProductAssemblyTreeTraverser(self.working_output_directory) + json_product, active_document = traverser.traverse_and_parse_from_json(json_object[JSON_PRODUCTS]) + + Log(f"Import successful\n") + + return part_file_names, json_product, active_document diff --git a/VirtualSatelliteCAD/json_io/json_spread_sheet.py b/VirtualSatelliteCAD/json_io/json_spread_sheet.py index 600630d..fe7626e 100644 --- a/VirtualSatelliteCAD/json_io/json_spread_sheet.py +++ b/VirtualSatelliteCAD/json_io/json_spread_sheet.py @@ -67,14 +67,18 @@ def write_to_freecad(self, active_document): sheet_line = FREECAD_PART_SHEET_ATTRIBUTE_START_LINE for json_part_attribute_name in list(self._json_part_or_product.attributes.keys()): - json_part_attribute_value = str(getattr(self._json_part_or_product, json_part_attribute_name)) - json_part_attribute_unit = self._json_part_or_product.attributes[json_part_attribute_name] - - sheet.set("A" + str(sheet_line), json_part_attribute_name) - sheet.set("B" + str(sheet_line), json_part_attribute_value) - sheet.set("C" + str(sheet_line), json_part_attribute_unit) - - sheet_line += 1 + # TODO: added this try catch because some children would not have part_name and attribute (because they are assemblies) + try: + json_part_attribute_value = str(getattr(self._json_part_or_product, json_part_attribute_name)) + json_part_attribute_unit = self._json_part_or_product.attributes[json_part_attribute_name] + + sheet.set("A" + str(sheet_line), json_part_attribute_name) + sheet.set("B" + str(sheet_line), json_part_attribute_value) + sheet.set("C" + str(sheet_line), json_part_attribute_unit) + + sheet_line += 1 + except AttributeError as e: + print(e) # Recompute the sheet, so that all properties are correctly written # if not recomputed accessing the properties will result in none objects diff --git a/VirtualSatelliteCAD/json_io/parts/json_part.py b/VirtualSatelliteCAD/json_io/parts/json_part.py index d0305e2..70192e6 100644 --- a/VirtualSatelliteCAD/json_io/parts/json_part.py +++ b/VirtualSatelliteCAD/json_io/parts/json_part.py @@ -27,7 +27,7 @@ from json_io.json_definitions import JSON_ELEMENT_NAME, JSON_ELEMENT_SHAPE,\ JSON_ELEMENT_UUID, JSON_ELEMENT_LENGTH_X, JSON_ELEMENT_LENGTH_Y,\ JSON_ELEMENT_LENGTH_Z, JSON_ELEMENT_RADIUS, JSON_ELEMENT_COLOR, M_TO_MM,\ - _get_combined_name_uuid + _get_combined_name_uuid, PART_IDENTIFIER from json_io.json_spread_sheet import JsonSpreadSheet @@ -138,4 +138,4 @@ def get_shape_type(self): return shape_type def get_unique_name(self): - return _get_combined_name_uuid(self.name, self.uuid) + return PART_IDENTIFIER + _get_combined_name_uuid(self.name, self.uuid) diff --git a/VirtualSatelliteCAD/json_io/products/json_product.py b/VirtualSatelliteCAD/json_io/products/json_product.py index b456e9c..61033e5 100644 --- a/VirtualSatelliteCAD/json_io/products/json_product.py +++ b/VirtualSatelliteCAD/json_io/products/json_product.py @@ -28,7 +28,7 @@ JSON_ELEMENT_POS_Y, JSON_ELEMENT_POS_X,\ JSON_ELEMENT_POS_Z, JSON_ELEMENT_ROT_X, JSON_ELEMENT_ROT_Y,\ JSON_ELEMENT_ROT_Z, JSON_ELEMENT_PART_UUID, JSON_ELEMENT_PART_NAME, M_TO_MM,\ - RAD_TO_DEG, _get_combined_name_uuid, JSON_ELEMNT_CHILDREN + RAD_TO_DEG, _get_combined_name_uuid, JSON_ELEMNT_CHILDREN, PART_IDENTIFIER from json_io.json_spread_sheet import JsonSpreadSheet from A2plus.a2p_importpart import importPartFromFile from freecad.active_document import VECTOR_X, VECTOR_Y, VECTOR_Z, VECTOR_ZERO @@ -58,6 +58,9 @@ def __init__(self): self.rot_y = 0.0 self.rot_z = 0.0 + self.name = None + self.uuid = None + def _parse_name_and_uuid_from_json(self, json_object): self.name = str(json_object[JSON_ELEMENT_NAME]) self.uuid = str(json_object[JSON_ELEMENT_UUID]).replace("-", "_") @@ -97,19 +100,24 @@ def _create_or_update_freecad_part(self, active_document): This method imports the part referenced by the product. The referenced part will be placed under the product part name into the assembly. E.g. A BasePlate will be added as BasePlateBottom to the - assembly. In case the object already exists, nothing special will happen. + assembly. In case the object already exists, it will be recreated. ''' import_part_file_name = self.get_part_unique_name() import_part_name_in_product = self.get_unique_name() import_part_full_path = active_document.get_file_full_path(import_part_file_name) + import_part_ref = active_document.app_active_document.getObjectsByLabel(import_part_name_in_product) + + # print(f"Called with '{import_part_name_in_product}'") + # TODO: CRUD + # If the part doesn't exists (the returned list is not empty) update (delete and recreate) it + if import_part_ref: + active_document.app_active_document.removeObject(import_part_ref[0].Name) + imported_product_part = importPartFromFile( active_document.app_active_document, import_part_full_path) imported_product_part.Label = import_part_name_in_product - def _set_freecad_name_and_color(self, active_document): - pass - def _set_freecad_position_and_rotation(self, active_document): product_part_name = self.get_unique_name() @@ -122,7 +130,7 @@ def _set_freecad_position_and_rotation(self, active_document): vector_rotation_y = active_document.app.Rotation(VECTOR_Y, self.rot_y) vector_rotation_z = active_document.app.Rotation(VECTOR_Z, self.rot_z) - placement = product_part.Placement + placement = product_part.Placement # Placement() placement_translation = active_document.app.Placement( vector_translation, @@ -167,7 +175,7 @@ def get_part_unique_name(self): ''' Returns the unique name of the referenced part ''' - return _get_combined_name_uuid(self.part_name, self.part_uuid) + return PART_IDENTIFIER + _get_combined_name_uuid(self.part_name, self.part_uuid) def is_part_reference(self): ''' @@ -178,3 +186,22 @@ def is_part_reference(self): has_part_name = hasattr(self, "part_name") return has_part_uuid and has_part_name + + def has_equal_values(self, other): + """ + Compares values with another AJsonProduct + """ + if(isinstance(other, AJsonProduct)): + return ( + self.pos_x == other.pos_x and + self.pos_y == other.pos_y and + self.pos_z == other.pos_z and + + self.rot_x == other.rot_x and + self.rot_y == other.rot_y and + self.rot_z == other.rot_z and + + self.name == other.name and + self.uuid == other.uuid) + + return NotImplemented diff --git a/VirtualSatelliteCAD/json_io/products/json_product_assembly.py b/VirtualSatelliteCAD/json_io/products/json_product_assembly.py index 426d984..fdca5d5 100644 --- a/VirtualSatelliteCAD/json_io/products/json_product_assembly.py +++ b/VirtualSatelliteCAD/json_io/products/json_product_assembly.py @@ -25,7 +25,7 @@ # from json_io.products.json_product import AJsonProduct -from json_io.json_definitions import JSON_ELEMNT_CHILDREN +from json_io.json_definitions import JSON_ELEMNT_CHILDREN, PRODUCT_IDENTIFIER, _get_combined_name_uuid from json_io.products.json_product_child import JsonProductChild @@ -70,6 +70,7 @@ class without implementation. Additionally this method starts parsing self.children = [] for json_object_child in json_object_children: json_product_child = JsonProductChild().parse_from_json(json_object_child) + # json_product_child.propagate_pos_and_rot_from_parent(self) self.children.append(json_product_child) # Don't hand back an assembly if there are no children @@ -89,3 +90,6 @@ def write_to_freecad(self, active_document): # part or a product for child in self.children: child.write_to_freecad(active_document) + + def get_product_unique_name(self): + return PRODUCT_IDENTIFIER + _get_combined_name_uuid(self.name, self.uuid) diff --git a/VirtualSatelliteCAD/json_io/products/json_product_assembly_tree_traverser.py b/VirtualSatelliteCAD/json_io/products/json_product_assembly_tree_traverser.py new file mode 100644 index 0000000..b78f02a --- /dev/null +++ b/VirtualSatelliteCAD/json_io/products/json_product_assembly_tree_traverser.py @@ -0,0 +1,88 @@ +# -*- coding: utf-8 -*- +# +# Virtual Satellite 4 - FreeCAD module +# +# Copyright (C) 2019 by +# +# DLR (German Aerospace Center), +# Software for Space Systems and interactive Visualization +# Braunschweig, Germany +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program. If not, see . +# +# SPDX-License-Identifier: LGPL-3.0-or-later + +from json_io.products.json_product_assembly import JsonProductAssembly +from json_io.json_definitions import JSON_ELEMNT_CHILDREN, JSON_ELEMENT_NAME +from freecad.active_document import ActiveDocument +import FreeCAD +Log = FreeCAD.Console.PrintLog + + +class JsonProductAssemblyTreeTraverser(object): + ''' + This class provides functionality to traverse a product tree to parse the product assemblies in the right order + ''' + + def __init__(self, working_output_directory): + self._lst_of_depths = [] + self.working_output_directory = working_output_directory + + def traverse(self, json_object, depth=0): + """ + Recursive traverse the tree and create a list containing the found depths, + that for each depth contains a list of found assembly (NOT child) nodes at that depth + """ + + # only look for products that have children + if(JSON_ELEMNT_CHILDREN in json_object and json_object[JSON_ELEMNT_CHILDREN] != []): + + # if the current depth has no list in the _lst_of_depths, add it + if(len(self._lst_of_depths) < depth + 1): + self._lst_of_depths.append([]) + Log(f"Added depth {depth} to _lst_of_depths\n") + + # append found assembly to the list + self._lst_of_depths[depth].append(json_object) + Log(f"Found assembly '{json_object[JSON_ELEMENT_NAME]}' at {depth}\n") + + # recursive call on all children + for child in json_object[JSON_ELEMNT_CHILDREN]: + self.traverse(child, depth+1) + + def parse_from_json(self): + """ + Iterate through the list created by traversing the tree in reverse and parse the found product assemblies + """ + json_product, active_document = None, None + + # parse in reverse order + for depth in reversed(self._lst_of_depths): + for assembly in depth: + Log(f"Parsing '{assembly[JSON_ELEMENT_NAME]}'\n") + + json_product = JsonProductAssembly().parse_from_json(assembly) + active_document = ActiveDocument(self.working_output_directory).open_set_and_get_document(json_product.get_product_unique_name()) + json_product.write_to_freecad(active_document) + active_document.save_and_close_active_document(json_product.get_product_unique_name()) + + # the last json_product is the root of the assembly, open it again for the UI + if(json_product is not None): + active_document = ActiveDocument(self.working_output_directory).open_set_and_get_document(json_product.get_product_unique_name()) + + return json_product, active_document + + def traverse_and_parse_from_json(self, json_object): + self.traverse(json_object) + return self.parse_from_json() diff --git a/VirtualSatelliteCAD/json_io/products/json_product_child.py b/VirtualSatelliteCAD/json_io/products/json_product_child.py index 56583ad..13e5994 100644 --- a/VirtualSatelliteCAD/json_io/products/json_product_child.py +++ b/VirtualSatelliteCAD/json_io/products/json_product_child.py @@ -26,6 +26,7 @@ from json_io.products.json_product import AJsonProduct from json_io.json_definitions import _get_combined_name_uuid +from json_io.json_definitions import PART_IDENTIFIER, PRODUCT_IDENTIFIER class JsonProductChild(AJsonProduct): @@ -37,6 +38,6 @@ def get_part_unique_name(self): In this case the file name of the product has to be returned ''' if self.has_children: - return _get_combined_name_uuid(self.name, self.uuid) + return PRODUCT_IDENTIFIER + _get_combined_name_uuid(self.name, self.uuid) else: - return _get_combined_name_uuid(self.part_name, self.part_uuid) + return PART_IDENTIFIER + _get_combined_name_uuid(self.part_name, self.part_uuid) diff --git a/VirtualSatelliteCAD/module/environment.py b/VirtualSatelliteCAD/module/environment.py index dadf9b7..4fe05d1 100644 --- a/VirtualSatelliteCAD/module/environment.py +++ b/VirtualSatelliteCAD/module/environment.py @@ -39,7 +39,7 @@ class Environment: ''' This class helps to understand the environment where the module is executed in. - E.g. knwoing which is the directory of the module and so on + E.g. knowing which is the directory of the module and so on ''' @classmethod @@ -90,3 +90,11 @@ def get_test_resource_path(cls, test_resource_name): ''' path = os.path.join(cls.get_tests_resource_path(), test_resource_name) return path + + # TODO: Update user file handling + @classmethod + def get_appdata_module_path(cls): + ''' + This method hands back the module path of the local Appdata directory. + ''' + return Init.APPDATA_DIR diff --git a/VirtualSatelliteCAD/test/json_io/parts/test_json_part.py b/VirtualSatelliteCAD/test/json_io/parts/test_json_part.py index 7ebe2bc..e5d2335 100644 --- a/VirtualSatelliteCAD/test/json_io/parts/test_json_part.py +++ b/VirtualSatelliteCAD/test/json_io/parts/test_json_part.py @@ -32,6 +32,7 @@ import FreeCAD import FreeCADGui from test.json_io.test_json_data import TEST_JSON_PART_BOX +from json_io.json_definitions import PART_IDENTIFIER App = FreeCAD Gui = FreeCADGui @@ -105,4 +106,4 @@ def test_get_part_unique_name(self): json_part = AJsonPart() json_part.parse_from_json(json_object) - self.assertEquals(json_part.get_unique_name(), "Beam_6201a731_d703_43f8_ab37_6a0581dfe022", "Correct unique name") + self.assertEquals(json_part.get_unique_name(), PART_IDENTIFIER + "Beam_6201a731_d703_43f8_ab37_6a0581dfe022", "Correct unique name") diff --git a/VirtualSatelliteCAD/test/json_io/products/test_json_product.py b/VirtualSatelliteCAD/test/json_io/products/test_json_product.py index ef880d1..4f3961b 100644 --- a/VirtualSatelliteCAD/test/json_io/products/test_json_product.py +++ b/VirtualSatelliteCAD/test/json_io/products/test_json_product.py @@ -31,6 +31,7 @@ import FreeCADGui from json_io.products.json_product import AJsonProduct from test.json_io.test_json_data import TEST_JSON_PRODUCT_WITHOUT_CHILDREN +from json_io.json_definitions import PART_IDENTIFIER App = FreeCAD @@ -76,7 +77,7 @@ def test_get_unique_names(self): json_product = AJsonProduct().parse_from_json(json_object) self.assertEquals(json_product.get_unique_name(), "BasePlateBottom_e8794f3d_86ec_44c5_9618_8b7170c45484", "Correct unique name") - self.assertEquals(json_product.get_part_unique_name(), "BasePlate_3d3708fd_5c6c_4af9_b710_d68778466084", "Correct unique name") + self.assertEquals(json_product.get_part_unique_name(), PART_IDENTIFIER + "BasePlate_3d3708fd_5c6c_4af9_b710_d68778466084", "Correct unique name") def test_is_part_reference(self): json_data = """{ diff --git a/VirtualSatelliteCAD/test/json_io/products/test_json_product_assembly.py b/VirtualSatelliteCAD/test/json_io/products/test_json_product_assembly.py index 4fcee99..b94431a 100644 --- a/VirtualSatelliteCAD/test/json_io/products/test_json_product_assembly.py +++ b/VirtualSatelliteCAD/test/json_io/products/test_json_product_assembly.py @@ -32,8 +32,8 @@ from json_io.products.json_product_assembly import JsonProductAssembly from freecad.active_document import ActiveDocument from test.json_io.test_json_data import TEST_JSON_PRODUCT_WITH_CHILDREN,\ - TEST_JSON_PRODUCT_WITHOUT_CHILDREN - + TEST_JSON_PRODUCT_WITHOUT_CHILDREN, TEST_JSON_PRODUCT_WITH_CHILDREN_WITH_CHILD +from json_io.json_definitions import JSON_ELEMNT_CHILDREN, PRODUCT_IDENTIFIER App = FreeCAD Gui = FreeCADGui @@ -79,6 +79,23 @@ def test_parse_with_children(self): self.assertEqual(json_product_child_1.name, "BasePlateBottom2", "Parsed correct child") self.assertEqual(json_product_child_2.name, "BasePlateTop", "Parsed correct child") + # Check that position and rotation from parent are propagated correctly + self.assertEqual(json_product_child_1.pos_x, 0, "Property is correctly set") + self.assertEqual(json_product_child_1.pos_y, 0, "Property is correctly set") + self.assertEqual(json_product_child_1.pos_z, 0, "Property is correctly set") + + self.assertEqual(json_product_child_1.rot_x, 0.0, "Property is correctly set") + self.assertEqual(json_product_child_1.rot_y, 0.0, "Property is correctly set") + self.assertEqual(json_product_child_1.rot_z, 0.0, "Property is correctly set") + + self.assertEqual(json_product_child_2.pos_x, 0, "Property is correctly set") + self.assertEqual(json_product_child_2.pos_y, 0, "Property is correctly set") + self.assertEqual(json_product_child_2.pos_z, 500.0, "Property is correctly set") + + self.assertEqual(json_product_child_2.rot_x, 0.0, "Property is correctly set") + self.assertEqual(json_product_child_2.rot_y, 0.0, "Property is correctly set") + self.assertEqual(json_product_child_2.rot_z, 0.0, "Property is correctly set") + def test_parse_with_no_children(self): json_data = TEST_JSON_PRODUCT_WITHOUT_CHILDREN @@ -93,7 +110,6 @@ def test_create_part_product_assembly_with_root_part(self): active_document = ActiveDocument(self._WORKING_DIRECTORY).open_set_and_get_document("ProductAssemblyRootPart") json_object = json.loads(self.json_data) json_product = JsonProductAssembly().parse_from_json(json_object) - active_document.save_as("ProductAssemblyRootPart") json_product.write_to_freecad(active_document) @@ -115,3 +131,53 @@ def test_create_part_product_assembly_with_root_part(self): product_child2_part_name = json_product.children[1].get_unique_name() product_object = active_document.app_active_document.getObjectsByLabel(product_child2_part_name)[0] self.assertIsNotNone(product_object, "Found an object under the given part name") + + def test_create_part_product_subassembly_with_root_part(self): + json_data = TEST_JSON_PRODUCT_WITH_CHILDREN_WITH_CHILD + self.create_Test_Part() + + active_document = ActiveDocument(self._WORKING_DIRECTORY).open_set_and_get_document("ProductSubassemblyRootPart") + + json_object = json.loads(json_data) + + subassembly = json_object[JSON_ELEMNT_CHILDREN][0] + json_product = JsonProductAssembly().parse_from_json(subassembly) + + json_product.write_to_freecad(active_document) + active_document.save_as("ProductSubassemblyRootPart") + + self.assertEquals(len(json_product.children), 1, "correct amount of children") + self.assertEquals(len(active_document.app_active_document.RootObjects), 4, "Found correct amount of root objects 2 objects plus 2 sheets") + + product_part_name = json_product.get_unique_name() + product_object = active_document.app_active_document.getObjectsByLabel(product_part_name)[0] + self.assertIsNotNone(product_object, "Found an object under the given part name") + + product_child1_part_name = json_product.children[0].get_unique_name() + product_object = active_document.app_active_document.getObjectsByLabel(product_child1_part_name)[0] + self.assertIsNotNone(product_object, "Found an object under the given part name") + + def test_create_part_product_assembly_and_subassembly_with_root_part_manual(self): + json_data = TEST_JSON_PRODUCT_WITH_CHILDREN_WITH_CHILD + self.create_Test_Part() + + json_object = json.loads(json_data) + + subassembly = json_object[JSON_ELEMNT_CHILDREN][0] + + active_document = ActiveDocument(self._WORKING_DIRECTORY).open_set_and_get_document( + PRODUCT_IDENTIFIER + "BasePlateBottom2_e8794f3d_86ec_44c5_9618_8b7170c45484") + + json_product = JsonProductAssembly().parse_from_json(subassembly) + json_product.write_to_freecad(active_document) + active_document.save_as(PRODUCT_IDENTIFIER + "BasePlateBottom2_e8794f3d_86ec_44c5_9618_8b7170c45484") + + self.assertEquals(len(active_document.app_active_document.RootObjects), 4, "Found correct amount of root objects 2 objects plus 2 sheets") + + active_document = ActiveDocument(self._WORKING_DIRECTORY).open_set_and_get_document("ProductAssemblyAndSubassemblyRootPart") + + json_product = JsonProductAssembly().parse_from_json(json_object) + json_product.write_to_freecad(active_document) + active_document.save_as("ProductAssemblyAndSubassemblyRootPart") + + self.assertEquals(len(active_document.app_active_document.RootObjects), 6, "Found correct amount of root objects 3 objects plus 3 sheets") diff --git a/VirtualSatelliteCAD/test/json_io/products/test_json_product_assembly_tree_traverser.py b/VirtualSatelliteCAD/test/json_io/products/test_json_product_assembly_tree_traverser.py new file mode 100644 index 0000000..ec593c9 --- /dev/null +++ b/VirtualSatelliteCAD/test/json_io/products/test_json_product_assembly_tree_traverser.py @@ -0,0 +1,149 @@ +# +# DLR (German Aerospace Center), +# Software for Space Systems and interactive Visualization +# Braunschweig, Germany +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program. If not, see . +# +# SPDX-License-Identifier: LGPL-3.0-or-later +# +from test.test_setup import AWorkingDirectoryTest +from test.json_io.test_json_data import TEST_JSON_PRODUCT_WITH_CHILDREN_WITH_CHILD, TEST_JSON_PRODUCT_WITHOUT_CHILDREN, \ + TEST_JSON_PRODUCT_WITH_CHILDREN_WITH_CHILD_SUBASSEMBLY_IS_NO_PART, TEST_JSON_PRODUCT_SUBASSEMBLY_WITH_SAME_PART +import json +from json_io.products.json_product_assembly_tree_traverser import JsonProductAssemblyTreeTraverser +from json_io.json_definitions import JSON_ELEMENT_NAME, PRODUCT_IDENTIFIER +from freecad.active_document import ActiveDocument +import glob +import os + + +class TestJsonProductAssemblyTreeTraverser(AWorkingDirectoryTest): + + @classmethod + def setUpClass(cls): + cls.setUpDirectory("ProductAssemblyTreeTraverser/") + cls._WORKING_DIRECTORY = cls.getDirectoryFullPath() + + def tearDown(self): + super().tearDown() + + def clearWorkingDirectory(self): + filelist = glob.glob(os.path.join(self._WORKING_DIRECTORY, "*")) + for f in filelist: + os.remove(f) + + def test_traverse_json(self): + json_data = TEST_JSON_PRODUCT_WITH_CHILDREN_WITH_CHILD + self.create_Test_Part() + + json_object = json.loads(json_data) + + traverser = JsonProductAssemblyTreeTraverser(self._WORKING_DIRECTORY) + traverser.traverse(json_object) + lst_of_depths = traverser._lst_of_depths + + self.assertEqual(len(lst_of_depths), 2, "Found the right amount of 2 depths") + self.assertEqual(len(lst_of_depths[0]), 1, "Found the right amount of 1 assembly at depth 0") + self.assertEqual(len(lst_of_depths[1]), 1, "Found the right amount of 1 assembly at depth 1") + self.assertEqual(lst_of_depths[0][0][JSON_ELEMENT_NAME], "BasePlateBottom1", "Found the right element at depth 0") + self.assertEqual(lst_of_depths[1][0][JSON_ELEMENT_NAME], "BasePlateBottom2", "Found the right element at depth 1") + + def test_parse_json_from_tree_without_traversing(self): + + traverser = JsonProductAssemblyTreeTraverser(self._WORKING_DIRECTORY) + self.assertIsNone(traverser.parse_from_json()[0], "Parsing no read in json object will result in 'None'") + + def test_traverse_and_parse_json_tree(self): + json_data = TEST_JSON_PRODUCT_WITH_CHILDREN_WITH_CHILD + self.create_Test_Part() + + json_object = json.loads(json_data) + + traverser = JsonProductAssemblyTreeTraverser(self._WORKING_DIRECTORY) + traverser.traverse(json_object) + lst_of_depths = traverser._lst_of_depths + + self.assertEqual(len(lst_of_depths), 2, "Found the right amount of 2 assemblies") + + traverser.parse_from_json() + + # this should have similar results to test_create_part_product_assembly_and_subassembly_with_root_part_manual in TestJsonProductAssembly + active_document = ActiveDocument(self._WORKING_DIRECTORY).open_set_and_get_document( + PRODUCT_IDENTIFIER + "BasePlateBottom2_e8794f3d_86ec_44c5_9618_8b7170c45484") + self.assertEquals(len(active_document.app_active_document.RootObjects), 4, "Found correct amount of root objects 2 objects plus 2 sheets") + + active_document = ActiveDocument(self._WORKING_DIRECTORY).open_set_and_get_document( + PRODUCT_IDENTIFIER + "BasePlateBottom1_e8794f3d_86ec_44c5_9618_8b7170c45484") + self.assertEquals(len(active_document.app_active_document.RootObjects), 6, "Found correct amount of root objects 3 objects plus 3 sheets") + + def test_traverse_and_parse_json_tree_subassembly_no_part(self): + self.clearWorkingDirectory() + json_data = TEST_JSON_PRODUCT_WITH_CHILDREN_WITH_CHILD_SUBASSEMBLY_IS_NO_PART + self.create_Test_Part() + + json_object = json.loads(json_data) + + traverser = JsonProductAssemblyTreeTraverser(self._WORKING_DIRECTORY) + traverser.traverse(json_object) + lst_of_depths = traverser._lst_of_depths + + self.assertEqual(len(lst_of_depths), 2, "Found the right amount of 2 assemblies") + + traverser.parse_from_json() + + # in this test case the product assembly "BasePlateBottom2" only has a child and not a part reference + active_document = ActiveDocument(self._WORKING_DIRECTORY).open_set_and_get_document( + PRODUCT_IDENTIFIER + "BasePlateBottom2_e8794f3d_86ec_44c5_9618_8b7170c45484") + self.assertEquals(len(active_document.app_active_document.RootObjects), 2, "Found correct amount of root objects 1 objects plus 1 sheets") + + active_document = ActiveDocument(self._WORKING_DIRECTORY).open_set_and_get_document( + PRODUCT_IDENTIFIER + "BasePlateBottom1_e8794f3d_86ec_44c5_9618_8b7170c45484") + self.assertEquals(len(active_document.app_active_document.RootObjects), 6, "Found correct amount of root objects 3 objects plus 3 sheets") + + def test_traverse_and_parse_json_tree_subassembly_same_part(self): + self.clearWorkingDirectory() + json_data = TEST_JSON_PRODUCT_SUBASSEMBLY_WITH_SAME_PART + self.create_Test_Part() + + json_object = json.loads(json_data) + + traverser = JsonProductAssemblyTreeTraverser(self._WORKING_DIRECTORY) + traverser.traverse(json_object) + lst_of_depths = traverser._lst_of_depths + + self.assertEqual(len(lst_of_depths), 2, "Found the right amount of 2 assemblies") + + traverser.parse_from_json() + + # in this test case the product assembly "BasePlate" refers a part "BasePlate" with the same name and uuid + active_document = ActiveDocument(self._WORKING_DIRECTORY).open_set_and_get_document( + PRODUCT_IDENTIFIER + "BasePlate_3d3708fd_5c6c_4af9_b710_d68778466084") + self.assertEquals(len(active_document.app_active_document.RootObjects), 4, "Found correct amount of root objects 2 objects plus 2 sheets") + + # the root assembly should only have a part and the product assembly "BasePlate" + active_document = ActiveDocument(self._WORKING_DIRECTORY).open_set_and_get_document( + PRODUCT_IDENTIFIER + "BasePlateBottom1_e8794f3d_86ec_44c5_9618_8b7170c45484") + self.assertEquals(len(active_document.app_active_document.RootObjects), 4, "Found correct amount of root objects 2 objects plus 2 sheets") + + def test_traverse_and_parse_json_tree_rootassembly_without_children(self): + json_data = TEST_JSON_PRODUCT_WITHOUT_CHILDREN + self.create_Test_Part() + + json_object = json.loads(json_data) + + traverser = JsonProductAssemblyTreeTraverser(self._WORKING_DIRECTORY) + traverser.traverse_and_parse_from_json(json_object) + + self.assertIsNone(traverser.parse_from_json()[0], "Parsing a json object without children") diff --git a/VirtualSatelliteCAD/test/json_io/products/test_json_product_child.py b/VirtualSatelliteCAD/test/json_io/products/test_json_product_child.py index 7be5aed..2f2dbc4 100644 --- a/VirtualSatelliteCAD/test/json_io/products/test_json_product_child.py +++ b/VirtualSatelliteCAD/test/json_io/products/test_json_product_child.py @@ -31,7 +31,7 @@ import FreeCADGui from json_io.products.json_product_child import JsonProductChild from freecad.active_document import ActiveDocument -from json_io.json_definitions import get_product_name_uuid +from json_io.json_definitions import get_product_name_uuid, PART_IDENTIFIER, PRODUCT_IDENTIFIER from test.json_io.test_json_data import TEST_JSON_PRODUCT_WITHOUT_CHILDREN,\ TEST_JSON_PRODUCT_WITH_ONE_CHILD @@ -73,14 +73,16 @@ def test_parse(self): self.assertAlmostEqual(json_product.rot_z, 60, 5, "Property is correctly set") self.assertFalse(json_product.has_children, "Current product has no children") - self.assertEquals(json_product.get_part_unique_name(), "BasePlate_3d3708fd_5c6c_4af9_b710_d68778466084", "No children thus references the part") + self.assertEquals(json_product.get_part_unique_name(), + PART_IDENTIFIER + "BasePlate_3d3708fd_5c6c_4af9_b710_d68778466084", "No children thus references the part") def test_parse_with_children(self): json_object = json.loads(self.json_data_with_child) json_product = JsonProductChild().parse_from_json(json_object) self.assertTrue(json_product.has_children, "Current product has children") - self.assertEquals(json_product.get_part_unique_name(), "BasePlateBottom_e8794f3d_86ec_44c5_9618_8b7170c45484", "No children thus references the part") + self.assertEquals(json_product.get_part_unique_name(), + PRODUCT_IDENTIFIER + "BasePlateBottom_e8794f3d_86ec_44c5_9618_8b7170c45484", "No children thus references the part") def test_create_part_product_child(self): self.create_Test_Part() diff --git a/VirtualSatelliteCAD/test/json_io/test_json_data.py b/VirtualSatelliteCAD/test/json_io/test_json_data.py index 22c4375..15e9637 100644 --- a/VirtualSatelliteCAD/test/json_io/test_json_data.py +++ b/VirtualSatelliteCAD/test/json_io/test_json_data.py @@ -97,9 +97,9 @@ "uuid": "e8794f3d-86ec-44c5-9618-8b7170c45484", "partUuid": "3d3708fd-5c6c-4af9-b710-d68778466084", "partName": "BasePlate", - "posX": 10.0, - "posY": 15.0, - "posZ": 20.0, + "posX": 1.0, + "posY": 2.0, + "posZ": 3.0, "rotX": 0.349, "rotY": 0.698, "rotZ": 1.046, @@ -136,6 +136,165 @@ } """ +TEST_JSON_PRODUCT_WITH_CHILDREN_WITH_CHILD = """{ + "name": "BasePlateBottom1", + "uuid": "e8794f3d-86ec-44c5-9618-8b7170c45484", + "partUuid": "3d3708fd-5c6c-4af9-b710-d68778466084", + "partName": "BasePlate", + "posX": 1.0, + "posY": 2.0, + "posZ": 3.0, + "rotX": 0.349, + "rotY": 0.698, + "rotZ": 1.046, + "children": [ + { + "posX": 0.0, + "posY": 0.0, + "posZ": 0.0, + "rotX": 0.0, + "children": [ + { + "posX": 0.5, + "posY": 0.5, + "posZ": 0.5, + "rotX": 0.0, + "children": [ + ], + "rotZ": 0.0, + "rotY": 0.0, + "name": "BasePlateBottom3", + "uuid": "e8794f3d-86ec-44c5-9618-8b7170c45485", + "partUuid": "3d3708fd-5c6c-4af9-b710-d68778466084", + "partName": "BasePlate" + } + ], + "rotZ": 0.3490659, + "rotY": 0.0, + "name": "BasePlateBottom2", + "uuid": "e8794f3d-86ec-44c5-9618-8b7170c45484", + "partUuid": "3d3708fd-5c6c-4af9-b710-d68778466084", + "partName": "BasePlate" + }, + { + "posX": 0.0, + "posY": 0.0, + "posZ": 0.5, + "rotX": 0.0, + "children": [ + ], + "rotZ": 0.0, + "rotY": 0.0, + "name": "BasePlateTop", + "uuid": "a199e3bd-3bc1-426d-8321-e9bd829339b3", + "partUuid": "3d3708fd-5c6c-4af9-b710-d68778466084", + "partName": "BasePlate" + } + ] + } + """ + +TEST_JSON_PRODUCT_WITH_CHILDREN_WITH_CHILD_SUBASSEMBLY_IS_NO_PART = """{ + "name": "BasePlateBottom1", + "uuid": "e8794f3d-86ec-44c5-9618-8b7170c45484", + "partUuid": "3d3708fd-5c6c-4af9-b710-d68778466084", + "partName": "BasePlate", + "posX": 1.0, + "posY": 2.0, + "posZ": 3.0, + "rotX": 0.349, + "rotY": 0.698, + "rotZ": 1.046, + "children": [ + { + "posX": 0.0, + "posY": 0.0, + "posZ": 0.0, + "rotX": 0.0, + "children": [ + { + "posX": 0.5, + "posY": 0.5, + "posZ": 0.5, + "rotX": 0.0, + "children": [ + ], + "rotZ": 0.0, + "rotY": 0.0, + "name": "BasePlateBottom3", + "uuid": "e8794f3d-86ec-44c5-9618-8b7170c45485", + "partUuid": "3d3708fd-5c6c-4af9-b710-d68778466084", + "partName": "BasePlate" + } + ], + "rotZ": 0.3490659, + "rotY": 0.0, + "name": "BasePlateBottom2", + "uuid": "e8794f3d-86ec-44c5-9618-8b7170c45484" + }, + { + "posX": 0.0, + "posY": 0.0, + "posZ": 0.5, + "rotX": 0.0, + "children": [ + ], + "rotZ": 0.0, + "rotY": 0.0, + "name": "BasePlateTop", + "uuid": "a199e3bd-3bc1-426d-8321-e9bd829339b3", + "partUuid": "3d3708fd-5c6c-4af9-b710-d68778466084", + "partName": "BasePlate" + } + ] + } + """ + +TEST_JSON_PRODUCT_SUBASSEMBLY_WITH_SAME_PART = """{ + "name": "BasePlateBottom1", + "uuid": "e8794f3d-86ec-44c5-9618-8b7170c45484", + "partUuid": "3d3708fd-5c6c-4af9-b710-d68778466084", + "partName": "BasePlate", + "posX": 1.0, + "posY": 2.0, + "posZ": 3.0, + "rotX": 0.349, + "rotY": 0.698, + "rotZ": 1.046, + "children": [ + { + "posX": 0.0, + "posY": 0.0, + "posZ": 0.0, + "rotX": 0.0, + "children": [ + { + "posX": 0.5, + "posY": 0.5, + "posZ": 0.5, + "rotX": 0.0, + "children": [ + ], + "rotZ": 0.0, + "rotY": 0.0, + "name": "BasePlateBottom3", + "uuid": "e8794f3d-86ec-44c5-9618-8b7170c45485", + "partUuid": "3d3708fd-5c6c-4af9-b710-d68778466084", + "partName": "BasePlate" + } + ], + "rotZ": 0.3490659, + "rotY": 0.0, + "name": "BasePlate", + "uuid": "3d3708fd-5c6c-4af9-b710-d68778466084", + "partUuid": "3d3708fd-5c6c-4af9-b710-d68778466084", + "partName": "BasePlate" + } + ] + } + """ + + TEST_JSON_PRODUCT_WITHOUT_CHILDREN = """{ "name": "BasePlateBottom", "uuid": "e8794f3d-86ec-44c5-9618-8b7170c45484", @@ -181,3 +340,162 @@ ] } """ +TEST_JSON_FULL_VISCUBE = """{ + "Products": { + "children": [{ + "posX": 0.0, + "posY": 0.0, + "posZ": 1.0, + "rotX": 0.0, + "children": [], + "rotZ": 0.0, + "rotY": 0.0, + "name": "Top", + "uuid": "cc14e2c7-9d7e-4cf2-8d6d-9b8cf5e96d56", + "partUuid": "cc14e2c7-9d7e-4cf2-8d6d-9b8cf5e96d56", + "partName": "Top" + }, { + "posX": 0.0, + "posY": 0.0, + "posZ": 0.0, + "rotX": 0.0, + "children": [], + "rotZ": 0.0, + "rotY": 0.0, + "name": "Bottom", + "uuid": "61db0622-6fef-4f12-932d-a00fdb9d0848", + "partUuid": "00f430a6-6311-4a33-961b-41ded4cf57d5", + "partName": "Plate" + }, { + "posX": 0.5, + "posY": 0.0, + "posZ": 0.5, + "rotX": 0.0, + "children": [], + "rotZ": 0.0, + "rotY": 1.5707963267948966, + "name": "Front", + "uuid": "e6af9d3f-8ad6-4488-b3d0-d35549be9a1e", + "partUuid": "e6af9d3f-8ad6-4488-b3d0-d35549be9a1e", + "partName": "Front" + }, { + "posX": -0.5, + "posY": 0.0, + "posZ": 0.5, + "rotX": 0.0, + "children": [], + "rotZ": 0.0, + "rotY": 1.5707963267948966, + "name": "Back", + "uuid": "a3c9c547-8fd3-40d5-97a1-a3f9a3a9c337", + "partUuid": "a3c9c547-8fd3-40d5-97a1-a3f9a3a9c337", + "partName": "Back" + }, { + "posX": 0.0, + "posY": 0.0, + "posZ": 0.5, + "rotX": 0.0, + "children": [{ + "posX": 0.0, + "posY": 0.5, + "posZ": 0.0, + "rotX": 1.5707963267948966, + "children": [], + "rotZ": 0.0, + "rotY": 0.0, + "name": "Left", + "uuid": "615985c0-73fd-48db-8f8b-e11b7cbb2ee8", + "partUuid": "615985c0-73fd-48db-8f8b-e11b7cbb2ee8", + "partName": "Left" + }, { + "posX": 0.0, + "posY": -0.5, + "posZ": 0.0, + "rotX": 1.5707963267948966, + "children": [], + "rotZ": 0.0, + "rotY": 0.0, + "name": "Right", + "uuid": "882a0b35-7da8-4555-903d-fd6b5cbec392", + "partUuid": "882a0b35-7da8-4555-903d-fd6b5cbec392", + "partName": "Right" + } + ], + "rotZ": 0.0, + "rotY": 0.0, + "name": "BeamStructure", + "uuid": "2afb23c9-f458-4bdb-a4e7-fc863364644f", + "partUuid": "2afb23c9-f458-4bdb-a4e7-fc863364644f", + "partName": "BeamStructure" + } + ], + "name": "SpaceCube", + "uuid": "a3533e02-125c-4066-bffe-d046d8d8342a" + }, + "Parts": [{ + "color": 16744448, + "shape": "CYLINDER", + "name": "BeamStructure", + "lengthY": 1.0, + "lengthX": 1.0, + "radius": 0.05, + "uuid": "2afb23c9-f458-4bdb-a4e7-fc863364644f", + "lengthZ": 1.0 + }, { + "color": 8388608, + "shape": "BOX", + "name": "Right", + "lengthY": 1.0, + "lengthX": 1.0, + "radius": 0.05, + "uuid": "882a0b35-7da8-4555-903d-fd6b5cbec392", + "lengthZ": 0.02 + }, { + "color": 32832, + "shape": "BOX", + "name": "Front", + "lengthY": 1.0, + "lengthX": 1.0, + "radius": 0.05, + "uuid": "e6af9d3f-8ad6-4488-b3d0-d35549be9a1e", + "lengthZ": 0.02 + }, { + "color": 16711680, + "shape": "BOX", + "name": "Left", + "lengthY": 1.0, + "lengthX": 1.0, + "radius": 0.05, + "uuid": "615985c0-73fd-48db-8f8b-e11b7cbb2ee8", + "lengthZ": 0.02 + }, { + "color": 65280, + "shape": "BOX", + "name": "Plate", + "lengthY": 1.0, + "lengthX": 1.0, + "radius": 0.05, + "uuid": "00f430a6-6311-4a33-961b-41ded4cf57d5", + "lengthZ": 0.02 + }, { + "color": 16776960, + "shape": "BOX", + "name": "Back", + "lengthY": 1.0, + "lengthX": 1.0, + "radius": 0.05, + "uuid": "a3c9c547-8fd3-40d5-97a1-a3f9a3a9c337", + "lengthZ": 0.02 + }, { + "color": 32768, + "shape": "BOX", + "name": "Top", + "lengthY": 1.0, + "lengthX": 1.0, + "radius": 0.05, + "uuid": "cc14e2c7-9d7e-4cf2-8d6d-9b8cf5e96d56", + "lengthZ": 0.02 + } + ] + } + """ diff --git a/VirtualSatelliteCAD/test/json_io/test_json_importer.py b/VirtualSatelliteCAD/test/json_io/test_json_importer.py index 469a5ba..c95c1d4 100644 --- a/VirtualSatelliteCAD/test/json_io/test_json_importer.py +++ b/VirtualSatelliteCAD/test/json_io/test_json_importer.py @@ -32,9 +32,11 @@ import FreeCAD import FreeCADGui from test.test_setup import AWorkingDirectoryTest -from freecad.active_document import FREECAD_FILE_EXTENSION +from freecad.active_document import FREECAD_FILE_EXTENSION, ActiveDocument from module.environment import Environment -from json_io.json_definitions import JSON_ELEMENT_STL_PATH +from json_io.json_definitions import JSON_ELEMENT_STL_PATH, PART_IDENTIFIER, PRODUCT_IDENTIFIER +import unittest +from test.json_io.test_json_data import TEST_JSON_FULL_VISCUBE App = FreeCAD Gui = FreeCADGui @@ -71,7 +73,7 @@ def test_create_part(self): json_importer.create_or_update_part(json_object) # Check the file got created - test_file_name = self._WORKING_DIRECTORY + "Beam_6201a731_d703_43f8_ab37_6a0581dfe022" + FREECAD_FILE_EXTENSION + test_file_name = self._WORKING_DIRECTORY + PART_IDENTIFIER + "Beam_6201a731_d703_43f8_ab37_6a0581dfe022" + FREECAD_FILE_EXTENSION self.assertTrue(os.path.isfile(test_file_name), "File exists on drive") App.open(test_file_name) @@ -126,7 +128,7 @@ def test_create_part_update_uuid(self): json_importer.create_or_update_part(json_object) # Check the file got created - test_file_name = self._WORKING_DIRECTORY + "Beam_6201a731_d703_43f8_ab37_6a0581dfe022" + FREECAD_FILE_EXTENSION + test_file_name = self._WORKING_DIRECTORY + PART_IDENTIFIER + "Beam_6201a731_d703_43f8_ab37_6a0581dfe022" + FREECAD_FILE_EXTENSION self.assertTrue(os.path.isfile(test_file_name), "File exists on drive") App.open(test_file_name) @@ -149,7 +151,7 @@ def test_create_part_update_uuid(self): json_importer.create_or_update_part(json_object) # Check the file got created - test_file_name = self._WORKING_DIRECTORY + "Beam_6201a731_d703_43f8_ab37_6a0666dfe022" + FREECAD_FILE_EXTENSION + test_file_name = self._WORKING_DIRECTORY + PART_IDENTIFIER + "Beam_6201a731_d703_43f8_ab37_6a0666dfe022" + FREECAD_FILE_EXTENSION App.open(test_file_name) self.assertEquals(len(App.ActiveDocument.RootObjects), TEST_ALLOWED_AMOUNT_OF_PART_OBJECTS, "Correct amount of objects in file") @@ -176,7 +178,7 @@ def test_create_part_update_value(self): json_importer.create_or_update_part(json_object) # Check the file got created - test_file_name = self._WORKING_DIRECTORY + "Beam_6201a731_d703_43f8_ab37_6a0581dfe022" + FREECAD_FILE_EXTENSION + test_file_name = self._WORKING_DIRECTORY + PART_IDENTIFIER + "Beam_6201a731_d703_43f8_ab37_6a0581dfe022" + FREECAD_FILE_EXTENSION self.assertTrue(os.path.isfile(test_file_name), "File exists on drive") App.open(test_file_name) @@ -200,7 +202,7 @@ def test_create_part_update_value(self): json_importer.create_or_update_part(json_object) # Check the file got created - test_file_name = self._WORKING_DIRECTORY + "Beam_6201a731_d703_43f8_ab37_6a0581dfe022" + FREECAD_FILE_EXTENSION + test_file_name = self._WORKING_DIRECTORY + PART_IDENTIFIER + "Beam_6201a731_d703_43f8_ab37_6a0581dfe022" + FREECAD_FILE_EXTENSION self.assertTrue(os.path.isfile(test_file_name), "File exists on drive") App.open(test_file_name) @@ -232,12 +234,12 @@ def test_create_part_change_shape(self): json_importer.create_or_update_part(json_object) # Check the file got created - test_file_name = self._WORKING_DIRECTORY + "Beam_6201a731_d703_43f8_ab37_6a7171dfe022" + FREECAD_FILE_EXTENSION + test_file_name = self._WORKING_DIRECTORY + PART_IDENTIFIER + "Beam_6201a731_d703_43f8_ab37_6a7171dfe022" + FREECAD_FILE_EXTENSION App.open(test_file_name) # Check that there is the correct object inside self.assertIsNotNone(App.ActiveDocument.getObject("Box"), "Got correct object") - App.closeDocument("Beam_6201a731_d703_43f8_ab37_6a7171dfe022") + App.closeDocument(PART_IDENTIFIER + "Beam_6201a731_d703_43f8_ab37_6a7171dfe022") # Now start cyling the objects json_object["shape"] = "CYLINDER" @@ -247,7 +249,7 @@ def test_create_part_change_shape(self): App.open(test_file_name) self.assertIsNone(App.ActiveDocument.getObject("Box"), "Removed previous object") self.assertIsNotNone(App.ActiveDocument.getObject("Cylinder"), "Got correct object") - App.closeDocument("Beam_6201a731_d703_43f8_ab37_6a7171dfe022") + App.closeDocument(PART_IDENTIFIER + "Beam_6201a731_d703_43f8_ab37_6a7171dfe022") # Next object json_object["shape"] = "SPHERE" @@ -257,7 +259,7 @@ def test_create_part_change_shape(self): App.open(test_file_name) self.assertIsNone(App.ActiveDocument.getObject("Cylinder"), "Removed previous object") self.assertIsNotNone(App.ActiveDocument.getObject("Sphere"), "Got correct object") - App.closeDocument("Beam_6201a731_d703_43f8_ab37_6a7171dfe022") + App.closeDocument(PART_IDENTIFIER + "Beam_6201a731_d703_43f8_ab37_6a7171dfe022") # Next object json_object["shape"] = "GEOMETRY" @@ -267,7 +269,7 @@ def test_create_part_change_shape(self): App.open(test_file_name) self.assertIsNone(App.ActiveDocument.getObject("Sphere"), "Removed previous object") self.assertIsNotNone(App.ActiveDocument.getObject("Geometry"), "Got correct object") - App.closeDocument("Beam_6201a731_d703_43f8_ab37_6a7171dfe022") + App.closeDocument(PART_IDENTIFIER + "Beam_6201a731_d703_43f8_ab37_6a7171dfe022") # Next object json_object["shape"] = "CONE" @@ -277,4 +279,112 @@ def test_create_part_change_shape(self): App.open(test_file_name) self.assertIsNone(App.ActiveDocument.getObject("Geometry"), "Removed previous object") self.assertIsNotNone(App.ActiveDocument.getObject("Cone"), "Got correct object") - App.closeDocument("Beam_6201a731_d703_43f8_ab37_6a7171dfe022") + App.closeDocument(PART_IDENTIFIER + "Beam_6201a731_d703_43f8_ab37_6a7171dfe022") + + def test_full_import(self): + """ + Full JSON import test + """ + + json_importer = JsonImporter(self._WORKING_DIRECTORY) + json_object = json.loads(TEST_JSON_FULL_VISCUBE) + part_file_names, json_product, active_document = json_importer.full_import(json_object) + + # ========================= + # Check parts + + # Check that the right number of parts was found + self.assertEqual(len(part_file_names), 7, "Found 7 files") + + # Check each part + for part_file_name in part_file_names: + test_file_name = self._WORKING_DIRECTORY + part_file_name + FREECAD_FILE_EXTENSION + + # Check the file got created + self.assertTrue(os.path.isfile(test_file_name), "File exists on drive") + + # ========================= + # Check product + + # Check that the right number of children and root objects got created + self.assertEquals(len(json_product.children), 5, "Correct amount of children") + self.assertEquals(len(active_document.app_active_document.RootObjects), 10, "Found correct amount of root objects 5 plus 5 sheets") + + active_document = ActiveDocument(self._WORKING_DIRECTORY).open_set_and_get_document( + PRODUCT_IDENTIFIER + "BeamStructure_2afb23c9_f458_4bdb_a4e7_fc863364644f") + self.assertEquals(len(active_document.app_active_document.RootObjects), 6, "Found correct amount of root objects 3 objects plus 3 sheets") + + @unittest.SkipTest + def test_full_import_again(self): + """ + Importing the same file again should not result in changes + """ + + json_test_resource_path = Environment.get_test_resource_path("VisCube2.json") + json_importer = JsonImporter(self._WORKING_DIRECTORY) + + # ========================= + # First import + part_file_names, json_product, active_document = json_importer.full_import(json_test_resource_path) + + # Check that the right number of parts was found + self.assertEqual(len(part_file_names), 7, "Found 7 files") + + # Check that the right number of children and root objects got created + self.assertEqual(len(json_product.children), 5, "Correct amount of children") + self.assertEqual(len(active_document.app_active_document.RootObjects), 14, "Found correct amount of root objects 7 plus 7 sheets") + + # ========================= + # Second import + part_file_names2, json_product2, active_document2 = json_importer.full_import(json_test_resource_path) + + # Check that the right number of parts was found + self.assertEqual(len(part_file_names2), 7, "Found 7 files") + + # Check that the right number of children and root objects got created + self.assertEqual(len(json_product2.children), 5, "Correct amount of children") + self.assertEqual(len(active_document2.app_active_document.RootObjects), 14, "Found correct amount of root objects 7 plus 7 sheets") + + # ========================= + # Check equality + self.assertEquals(part_file_names, part_file_names2) + + for i, child1 in enumerate(json_product.children): + child2 = json_product2.children[i] + child1.has_equal_values(child2) + + @unittest.SkipTest + def test_full_import_again_with_changes(self): + """ + If two files with the same name get imported: we assume that the VirSat side used CRUD, that means: + - new parts/products could be created, so add them + - old parts/products could be replaced/updated, so use the new information + - old parts/products could be deleted, so delete all not updated files + -> this means instead of merging, simply the information of the old files get replaced by the newer one + """ + + json_importer = JsonImporter(self._WORKING_DIRECTORY) + + # ========================= + # First import + json_test_resource_path = Environment.get_test_resource_path("VisCube2.json") + part_file_names, json_product, active_document = json_importer.full_import(json_test_resource_path) + + # Check that the right number of parts was found + self.assertEqual(len(part_file_names), 7, "Found 7 files") + + # Check that the right number of children and root objects got created + self.assertEqual(len(json_product.children), 5, "Correct amount of children") + self.assertEqual(len(active_document.app_active_document.RootObjects), 14, "Found correct amount of root objects 7 plus 7 sheets") + + # ========================= + # Second import + json_test_resource_path2 = Environment.get_test_resource_path("VisCube2_update.json") + part_file_names2, json_product2, active_document2 = json_importer.full_import(json_test_resource_path2) + + # Check that the right number of parts was found + self.assertEqual(len(part_file_names2), 1, "Found 1 files") + + # Check that the right number of children and root objects got created + self.assertEquals(len(json_product2.children), 1, "Correct amount of children") + self.assertEquals(len(active_document2.app_active_document.RootObjects), 2, "Found correct amount of root objects 1 plus 1 sheets")