diff --git a/assembly_mesh_plugin/plugin.py b/assembly_mesh_plugin/plugin.py index 22d9d5c..8d6143a 100644 --- a/assembly_mesh_plugin/plugin.py +++ b/assembly_mesh_plugin/plugin.py @@ -182,7 +182,131 @@ def assembly_to_gmsh(self, mesh_path="tagged_mesh.msh"): gmsh.finalize() +def assembly_to_imprinted_gmsh(self, mesh_path="tagged_mesh.msh"): + """ + Exports an imprinted assembly to capture conformal meshes. + """ + + gmsh.initialize() + gmsh.option.setNumber("General.Terminal", 0) + gmsh.model.add("assembly") + + # The mesh volume and surface ids should line up with the order of solids and faces in the assembly + vol_id = 1 + surface_id = 1 + + # Tracks multi-surface physical groups + multi_material_groups = {} + surface_groups = {} + + # Tracks the solids with tagged faces + tagged_faces = {} + solids_with_tagged_faces = {} + + # Imprint the assembly + imprinted_assembly, imprinted_solids_with_orginal_ids = ( + cq.occ_impl.assembly.imprint(self) + ) + + print(imprinted_solids_with_orginal_ids) + for solid, id in imprinted_solids_with_orginal_ids.items(): + # Add the current solid to the mesh + # Work-around for a segfault with in-memory passing of OCCT objects + with tempfile.NamedTemporaryFile(suffix=".brep") as temp_file: + solid.exportBrep(temp_file.name) + ps = gmsh.model.occ.importShapes(temp_file.name) + gmsh.model.occ.synchronize() + + # Technically, importShapes could import multiple entities/dimensions, so filter those + vol_ents = [] + for p in ps: + if p[0] == 3: + vol_ents.append(p[1]) + + # Set the physical name to be the part name in the assembly for all the solids + ps2 = gmsh.model.addPhysicalGroup(3, vol_ents) + gmsh.model.setPhysicalName(3, ps2, f"{id[0].split('/')[-1]}") + + # Get the original assembly part + object_name = id[0].split("/")[-1] + assembly_part = self.objects[object_name] + + # Collect any tags from the part + for tag, wp in assembly_part.obj.ctx.tags.items(): + tagged_face = wp.faces().all()[0].val() + for face in wp.faces().all(): + tagged_faces[face.val()] = tag + + # Iterate over the faces of the assembly part + for face in assembly_part.obj.faces(): + for tagged_face, tag in tagged_faces.items(): + if TopoDS_Shape.IsEqual(face.wrapped, tagged_face.wrapped): + print(f"{vol_id}_{surface_id}", tag) + solids_with_tagged_faces[f"{vol_id}_{surface_id}"] = ( + object_name, + tag, + ) + + surface_id += 1 + vol_id += 1 + + # Reset the volume and surface IDs + vol_id = 1 + surface_id = 1 + + # Step through the imprinted assembly/shape and check for tagged faces + for solid in imprinted_assembly.solids(): + for face in solid.faces().Faces(): + # Check to see if this face has been tagged + if f"{vol_id}_{surface_id}" in solids_with_tagged_faces.keys(): + short_name = solids_with_tagged_faces[f"{vol_id}_{surface_id}"][0] + tag = solids_with_tagged_faces[f"{vol_id}_{surface_id}"][1] + + # Find out if this is a multi-material tag + if tag.startswith("~"): + # Set the surface name to be the name of the tag without the ~ + group_name = tag.replace("~", "").split("-")[0] + + # Add this face to the multi-material group + if group_name in multi_material_groups: + multi_material_groups[group_name].append(surface_id) + else: + multi_material_groups[group_name] = [surface_id] + else: + # We want to track all surfaces that might be in a tag group + cur_tag_name = f"{short_name}_{tag}" + if cur_tag_name in surface_groups: + print("Append: ", cur_tag_name, short_name, surface_id) + surface_groups[cur_tag_name].append(surface_id) + else: + print("New: ", cur_tag_name, short_name, surface_id) + surface_groups[cur_tag_name] = [surface_id] + + surface_id += 1 + vol_id += 1 + + # Handle tagged surface groups + for t_name, surf_group in surface_groups.items(): + ps = gmsh.model.addPhysicalGroup(2, surf_group) + gmsh.model.setPhysicalName(2, ps, t_name) + + # Handle multi-material tags + for group_name, mm_group in multi_material_groups.items(): + ps = gmsh.model.addPhysicalGroup(2, mm_group) + gmsh.model.setPhysicalName(2, ps, f"{group_name}") + + gmsh.model.occ.synchronize() + + gmsh.model.mesh.field.setAsBackgroundMesh(2) + + gmsh.model.mesh.generate(3) + gmsh.write(mesh_path) + + gmsh.finalize() + + # Patch the new assembly functions into CadQuery's importers package cq.Assembly.assemblyToGmsh = assembly_to_gmsh cq.Assembly.saveToGmsh = assembly_to_gmsh # Alias name that works better on cq.Assembly cq.Assembly.getTaggedGmsh = get_tagged_gmsh +cq.Assembly.assemblyToImprintedGmsh = assembly_to_imprinted_gmsh diff --git a/environment.yml b/environment.yml index b1cbc38..8127879 100644 --- a/environment.yml +++ b/environment.yml @@ -1,13 +1,14 @@ name: tagged-meshes channels: - conda-forge + - cadquery dependencies: - python>=3.9,<=3.12 - - cadquery + - cadquery=master - pytest - gmsh - python-gmsh - appdirs - pip - pip: - - --editable=. \ No newline at end of file + - --editable=. diff --git a/tests/test_meshes.py b/tests/test_meshes.py index c5e9f44..65cb484 100644 --- a/tests/test_meshes.py +++ b/tests/test_meshes.py @@ -79,3 +79,34 @@ def test_subshape_assembly(): continue assert cur_name in ["cube_1_cube_1_top_face"] + + +def test_imprinted_assembly(): + # Create the basic assembly + assy = generate_simple_nested_boxes() + + assy.assemblyToImprintedGmsh("tagged_imprinted_mesh.msh") + + gmsh.initialize() + + gmsh.open("tagged_imprinted_mesh.msh") + + # Check the solids for the correct tags + physical_groups = gmsh.model.getPhysicalGroups(3) + for group in physical_groups: + # Get the name for the current volume + cur_name = gmsh.model.getPhysicalName(3, group[1]) + + assert cur_name in ["shell", "insert"] + + # Check the surfaces for the correct tags + physical_groups = gmsh.model.getPhysicalGroups(2) + for group in physical_groups: + # Get the name for this group + cur_name = gmsh.model.getPhysicalName(2, group[1]) + + # Skip any groups that are not tagged explicitly + if "_surface_" in cur_name: + continue + + assert cur_name in ["shell_inner-right", "insert_outer-right", "in_contact"]