From 694b1dd250f70475dfe491a3ec0addc52dc469bd Mon Sep 17 00:00:00 2001 From: JAmmermann-DLR Date: Mon, 2 Dec 2019 18:13:49 +0100 Subject: [PATCH] Use traverser in importer and add test cases- (Task #5) Try to use the traverser in the importer and reactivating the full_import test case showed problems with assemblies having the same name and uuid as their part. To further investigate that, test cases were added in the traverser. --- Task #5: Implement Import Functionality --- VirtualSatelliteCAD/json_io/json_importer.py | 24 +- .../json_io/json_spread_sheet.py | 20 +- .../json_product_assembly_tree_traverser.py | 16 +- ...st_json_product_assembly_tree_traverser.py | 83 +++++- .../test/json_io/test_json_data.py | 260 ++++++++++++++++++ .../test/json_io/test_json_importer.py | 22 +- 6 files changed, 373 insertions(+), 52 deletions(-) diff --git a/VirtualSatelliteCAD/json_io/json_importer.py b/VirtualSatelliteCAD/json_io/json_importer.py index 5ae8848..04bc645 100644 --- a/VirtualSatelliteCAD/json_io/json_importer.py +++ b/VirtualSatelliteCAD/json_io/json_importer.py @@ -28,10 +28,11 @@ import FreeCADGui from freecad.active_document import ActiveDocument from json_io.parts.json_part_factory import JsonPartFactory -from json_io.products.json_product_assembly import JsonProductAssembly -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 import json +from freecad import active_document App = FreeCAD Gui = FreeCADGui @@ -89,27 +90,14 @@ def full_import(self, filepath): Log("Please provide a valid JSON\n") return - json_parts = json_object['Parts'] + json_parts = json_object[JSON_PARTS] part_file_names = [] for part in json_parts: part_file_names.append(self.create_or_update_part(part)) - # json assembly with json product object - json_product = JsonProductAssembly().parse_from_json(json_object['Products']) - - # name the freecad document after the root product - freecad_name = json_product.name - - # If there is a root document with the same name open already: - # assume that all changes of the current import are valid (CRUD) - # so clear the document - ActiveDocument(self.working_output_directory).clear_if_open_document(freecad_name) - - active_document = ActiveDocument(self.working_output_directory).open_set_and_get_document(freecad_name) - json_product.write_to_freecad(active_document) - - active_document.save_as(freecad_name) + 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") 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/products/json_product_assembly_tree_traverser.py b/VirtualSatelliteCAD/json_io/products/json_product_assembly_tree_traverser.py index 35a13f0..a79d01b 100644 --- a/VirtualSatelliteCAD/json_io/products/json_product_assembly_tree_traverser.py +++ b/VirtualSatelliteCAD/json_io/products/json_product_assembly_tree_traverser.py @@ -51,11 +51,11 @@ def traverse(self, json_object, depth=0): # 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") + 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}") + 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]: @@ -65,19 +65,23 @@ def parse_from_json(self): """ Iterate through the list created by traversing the tree in reverse and parse the found product assemblies """ - json_product = None + 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]}'") + 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_unique_name()) json_product.write_to_freecad(active_document) - active_document.save_as(json_product.get_unique_name()) + active_document.save_and_close_active_document(json_product.get_unique_name()) # + "_assembly") - return json_product + # 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_unique_name()) + + return json_product, active_document def traverse_and_parse_from_json(self, json_object): self.traverse(json_object) 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 index ba812a5..f6715db 100644 --- 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 @@ -19,11 +19,16 @@ # 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 +from test.json_io.test_json_data import TEST_JSON_PRODUCT_WITH_CHILDREN_WITH_CHILD, TEST_JSON_PRODUCT_WITHOUT_CHILDREN, \ + TEST_JSON_FULL_VISCUBE, 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 from freecad.active_document import ActiveDocument +import unittest +import glob +import os class TestJsonProductAssemblyTreeTraverser(AWorkingDirectoryTest): @@ -36,6 +41,11 @@ def setUpClass(cls): 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() @@ -55,7 +65,7 @@ def test_traverse_json(self): def test_parse_json_from_tree_without_traversing(self): traverser = JsonProductAssemblyTreeTraverser(self._WORKING_DIRECTORY) - self.assertIsNone(traverser.parse_from_json(), "Parsing no read in json object will result in 'None'") + 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 @@ -78,6 +88,73 @@ def test_traverse_and_parse_json_tree(self): active_document = ActiveDocument(self._WORKING_DIRECTORY).open_set_and_get_document("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("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("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 "BasePlateBottom2" only has a child and not a part reference + active_document = ActiveDocument(self._WORKING_DIRECTORY).open_set_and_get_document("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") + + active_document = ActiveDocument(self._WORKING_DIRECTORY).open_set_and_get_document("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") + + # TODO remove/adapt + @unittest.SkipTest + def test_traverse_and_parse_json_tree3(self): + json_data = TEST_JSON_FULL_VISCUBE + 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("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") + + active_document = ActiveDocument(self._WORKING_DIRECTORY).open_set_and_get_document("SpaceCube_a3533e02_125c_4066_bffe_d046d8d8342a") + self.assertEquals(len(active_document.app_active_document.RootObjects), 10, "Found correct amount of root objects 5 objects plus 5 sheets") + def test_traverse_and_parse_json_tree_rootassembly_without_children(self): json_data = TEST_JSON_PRODUCT_WITHOUT_CHILDREN self.create_Test_Part() @@ -87,4 +164,4 @@ def test_traverse_and_parse_json_tree_rootassembly_without_children(self): traverser = JsonProductAssemblyTreeTraverser(self._WORKING_DIRECTORY) traverser.traverse_and_parse_from_json(json_object) - self.assertIsNone(traverser.parse_from_json()) + self.assertIsNone(traverser.parse_from_json()[0], "Parsing a json object without children") diff --git a/VirtualSatelliteCAD/test/json_io/test_json_data.py b/VirtualSatelliteCAD/test/json_io/test_json_data.py index 046648d..15e9637 100644 --- a/VirtualSatelliteCAD/test/json_io/test_json_data.py +++ b/VirtualSatelliteCAD/test/json_io/test_json_data.py @@ -194,6 +194,107 @@ } """ +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", @@ -239,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 85f2afe..efeb5fa 100644 --- a/VirtualSatelliteCAD/test/json_io/test_json_importer.py +++ b/VirtualSatelliteCAD/test/json_io/test_json_importer.py @@ -36,6 +36,7 @@ from module.environment import Environment from json_io.json_definitions import JSON_ELEMENT_STL_PATH import unittest +from freecad.active_document import ActiveDocument App = FreeCAD Gui = FreeCADGui @@ -308,23 +309,10 @@ def test_full_import(self): # 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), 14, "Found correct amount of root objects 7 plus 7 sheets") - - # Check that for each child a file exists - for child in json_product.children: - product_object = active_document.app_active_document.getObjectsByLabel(child.get_unique_name()) - # An empty list in python gets asserted to true - self.assertTrue(product_object, "Found an object under the given part name") - if(child.name == "BeamStructure"): - # Check that two sub children are found - self.assertEquals(len(child.children), 2, "Correct amount of children") - for subchild in child.children: - product_object = active_document.app_active_document.getObjectsByLabel(subchild.get_unique_name()) - self.assertIsNotNone(product_object, "Found an object under the given part name") - - # Check propagation - # poz_z of -500 should be propagated from "BeamStructure" - self.assertEqual(subchild.pos_z, 500.0, "Z position got propagated correctly") + 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("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):