diff --git a/Lattesh/README.md b/Lattesh/README.md index f28de14a92d4cf15de3e3a70db8b054d1586a010..dc6d6e568eecd9c80b4da7b35755fef91d0c1046 100644 --- a/Lattesh/README.md +++ b/Lattesh/README.md @@ -60,6 +60,8 @@ side bar of the 3D Viewport. The panel is depicted below.  +#### Rectangular lattice generation + The top part contains the parameters for regular lattice generation. The parameters are: - **Cell size**: the length defining the individual cells. @@ -70,18 +72,41 @@ parameters are: Clicking on the **Create Mesh Lattice** will create a rectangular lattice with these parameters. +#### Filling a volume with lattice + If a mesh object is selected, clicking on **Fill Mesh Lattice** will create a -regular lattice of *cell size* and *cell type* that fills the selected object. +regular lattice of *cell size* and *cell type* that fills the selected object. +The boolean intersection algorithm is experimental and can fail. In that case, +a lattice is still created and must be manually edited to remove the parts outside +the target mesh. + +#### Cylindrical struts If a lattice is selected (or any mesh object), an **Opencascade fill** menu is available. Upon clicking on **Lattice Volume (OCC)**, the lattice will be given a volume, where for each edge in the original object a cylindrical strut will -be created. The strut radius is available as an input parameter. +be created. The strut radius is available as an input parameter. This operation will take only some seconds for very regular lattices, but a large irregular lattice can take several hours to process. In this case, the add-on prints messages on the terminal relating the progress. +#### Variable strut radius + +It is possible to generate struts with varing thickness in a single lattice. To +that end, the mesh must have a float attribute called *radius*, with domain *'POINTS'*. +When such a parameter is present in the selected mesh, an extra option, +*Variable radius*, becomes usable. When checked, the radius of the generated mesh +at each lattice node will be given by this parameter. + +The *radius* parameter can be generated using [geometry nodes](https://docs.blender.org/manual/en/latest/modeling/geometry_nodes/index.html) +in Blender. An example is shown below. + + + + + + ### Installation The Add-on is installed as usual in Blender via the *Preferences* menu. However, diff --git a/Lattesh/__init__.py b/Lattesh/__init__.py index 63eef4a2671ca9f2c488b9367f3bd41a3c847d84..faeba14bf1722ef8840acb5ff869f9084fa6c0f7 100644 --- a/Lattesh/__init__.py +++ b/Lattesh/__init__.py @@ -21,7 +21,7 @@ bl_info = { - "name": "Lattice Tools", + "name": "Lattesh - Lattice Tools", "author": "Ubiratan S. Freitas", "blender": (2, 80, 0), "description": "Tools for generating lattice structures", @@ -65,7 +65,7 @@ class SceneProperties(PropertyGroup): r_ratio: FloatProperty( name="R_ratio", description="Ratio of actual radius to metaball radius", - default=0.8, + default=0.65, min=0.01, max=0.99, ) @@ -108,6 +108,13 @@ class SceneProperties(PropertyGroup): default=False, ) + variable_radius: BoolProperty( + name="Variable radius", + description="Variable strut radius using 'radius' attribute from mesh", + default=False, + ) + + charac_length: FloatProperty( name="Caracteristic length", description="Caracteristic length", diff --git a/Lattesh/doc/nodes.png b/Lattesh/doc/nodes.png new file mode 100644 index 0000000000000000000000000000000000000000..7256c1ef21f6cfdef13c25616d25898f65e619a8 Binary files /dev/null and b/Lattesh/doc/nodes.png differ diff --git a/Lattesh/doc/result.png b/Lattesh/doc/result.png new file mode 100644 index 0000000000000000000000000000000000000000..013418e164268d73a869e9504bda2a552a32e60b Binary files /dev/null and b/Lattesh/doc/result.png differ diff --git a/Lattesh/doc/side_panel.png b/Lattesh/doc/side_panel.png index 529b5349e154a92c4d2f5581b5881e12488e3a30..4cd5c06f547603152a1952b4f5fe56c935db3f22 100644 Binary files a/Lattesh/doc/side_panel.png and b/Lattesh/doc/side_panel.png differ diff --git a/Lattesh/operators.py b/Lattesh/operators.py index 3b0e8fbe63e59506c9bc70413a1d8883dd93dd0b..2ca9e04feb96101f90aa41a4037fd66da28a6c4b 100644 --- a/Lattesh/operators.py +++ b/Lattesh/operators.py @@ -799,10 +799,9 @@ def occ_compute_node(ev, r=0.5, mesh_pars=(0.1, False, math.radians(60), False), -def createMesh(target, r=0.5, lin_deflect=0.1, ang_deflect=math.radians(60.)): - """Gives a volume to each edge in target using cylinders of radius r and nsides sides""" +def createMesh(target, r=0.5, var_radius=False, lin_deflect=0.1, ang_deflect=math.radians(60.)): + """Gives a volume to each edge in target using cylinders of radius r""" min_angle = math.radians(30) - min_length = r / math.tan(min_angle / 2.0) quantization = 100 tmesh = target.data name = target.name + "Cyl" @@ -811,6 +810,22 @@ def createMesh(target, r=0.5, lin_deflect=0.1, ang_deflect=math.radians(60.)): verts = np.empty(nverts * 3, dtype=np.float32) edges = np.empty(nedges * 2, dtype=np.int32) + var_radius = var_radius and 'radius' in tmesh.attributes + if var_radius : + std_r = 0.5 + radius = np.empty(nverts, dtype=np.float32) + tmesh.attributes['radius'].data.foreach_get('value', radius) + dia = ' {:.1f}-{:.1f}'.format(radius.min() * 2, radius.max() * 2) + # prescale the radius + radius /= std_r + else: + std_r = r + radius = np.full(nverts, r, dtype=np.float32) + dia = ' {:.1f}'.format(r * 2) + name += dia + + min_length = radius / math.tan(min_angle / 2.0) + tmesh.vertices.foreach_get('co', verts) tmesh.edges.foreach_get('vertices', edges) @@ -840,7 +855,7 @@ def createMesh(target, r=0.5, lin_deflect=0.1, ang_deflect=math.radians(60.)): mesh = bpy.data.meshes.new(name+'Mesh') - print(f'{nedges} total edges, {len(dqedges)} quantized orientations, {min_length:.2f}, minimal edge length.') + print(f'{nedges} total edges, {len(dqedges)} quantized orientations, {min_length.max():.2f}, minimal edge length.') # Generate all possible edge orientations, including reverse @@ -884,7 +899,7 @@ def createMesh(target, r=0.5, lin_deflect=0.1, ang_deflect=math.radians(60.)): continue if len(nodes[k]) == 1: single_nodes.add(k) - if edge_len[nodes[k][0][0]] < min_length: + if edge_len[nodes[k][0][0]] < min_length[k]: # Discard single nodes with small edge length discarded_edges.add(nodes[k][0]) continue @@ -893,7 +908,7 @@ def createMesh(target, r=0.5, lin_deflect=0.1, ang_deflect=math.radians(60.)): for kk, eddir in nodes[k]: if kk in discarded_edges: continue - if edge_len[kk] < min_length: + if edge_len[kk] < min_length[k]: if len(nodes[edges[kk, int(eddir)]]) == 1: # Discard single nodes with small edge length discarded_edges.add(kk) @@ -935,11 +950,11 @@ def createMesh(target, r=0.5, lin_deflect=0.1, ang_deflect=math.radians(60.)): # Compute node geomery with Opencascade try: retry_count = 0 - node_verts, node_faces, node_nsides, node_edverts, shape = occ_compute_node(ev, r, mesh_pars=(lin_deflect, False, ang_deflect, False)) + node_verts, node_faces, node_nsides, node_edverts, shape = occ_compute_node(ev, std_r, mesh_pars=(lin_deflect, False, ang_deflect, False)) while len(node_edverts) != len(nt) and retry_count < 20: retry_count += 1 print(f'Wrong number of edge verts in processed node {k} with {len(nt)} edges and {len(node_edverts)} found edges. Retry nb. {retry_count}.', end='\r') - node_verts, node_faces, node_nsides, node_edverts, shape = occ_compute_node(ev, r, mesh_pars=(lin_deflect, False, ang_deflect, False), shuffle=True) + node_verts, node_faces, node_nsides, node_edverts, shape = occ_compute_node(ev, std_r, mesh_pars=(lin_deflect, False, ang_deflect, False), shuffle=True) if retry_count > 0: print() except: @@ -971,7 +986,11 @@ def createMesh(target, r=0.5, lin_deflect=0.1, ang_deflect=math.radians(60.)): # Translate the vertices of each node type to the actual position of the nodes and # insert them in newverts if verts_per_node: - newverts[vert_count:vert_count + verts_per_node, :] = node_verts + verts[n,:] + if var_radius: + temp_verts = node_verts * radius[n] + else: + temp_verts = node_verts + newverts[vert_count:vert_count + verts_per_node, :] = temp_verts + verts[n,:] # Insert the node type faces in newfaces for each of the node instances if faces_per_node: newfaces[face_count:face_count + faces_per_node, :] = node_faces + vert_count @@ -1046,7 +1065,7 @@ def createMesh(target, r=0.5, lin_deflect=0.1, ang_deflect=math.radians(60.)): # Link object to scene - bpy.context.scene.collection.objects.link(newobj) + bpy.context.collection.objects.link(newobj) @@ -1054,12 +1073,12 @@ def createMetaball(target, r=0.5, p=0.9): if target.type != 'MESH': return - name = target.name + "Rod" + name = target.name + "Rod {:.1f}".format(r * 2) print("Creating", name) metaball = bpy.data.metaballs.new(name+'Meta') obj = bpy.data.objects.new(name, metaball) - bpy.context.scene.collection.objects.link(obj) + bpy.context.collection.objects.link(obj) stiffness = 10.0 # clip p @@ -1141,7 +1160,8 @@ class ObjectOCCLattice(bpy.types.Operator): def execute(self, context): lattice_mesh = context.scene.lattice_mesh - createMesh(context.active_object, lattice_mesh.radius, lattice_mesh.lin_deflect, lattice_mesh.ang_deflect) + depsgraph = context.evaluated_depsgraph_get() + createMesh(context.active_object.evaluated_get(depsgraph), lattice_mesh.radius, lattice_mesh.variable_radius, lattice_mesh.lin_deflect, lattice_mesh.ang_deflect) return {'FINISHED'} @@ -1173,7 +1193,7 @@ class ObjectCreateMeshLattice(bpy.types.Operator): newobj = bpy.data.objects.new(name, mesh) # Link object to scene - bpy.context.scene.collection.objects.link(newobj) + bpy.context.collection.objects.link(newobj) return {'FINISHED'} @@ -1214,7 +1234,7 @@ class ObjectFillMeshLattice(bpy.types.Operator): newobj.matrix_world = obj.matrix_world.copy() # Link object to scene - bpy.context.scene.collection.objects.link(newobj) + bpy.context.collection.objects.link(newobj) return {'FINISHED'} @@ -1252,7 +1272,7 @@ class ObjectFillVoronoiLattice(bpy.types.Operator): newobj.matrix_world = obj.matrix_world.copy() # Link object to scene - bpy.context.scene.collection.objects.link(newobj) + bpy.context.collection.objects.link(newobj) return {'FINISHED'} diff --git a/Lattesh/ui.py b/Lattesh/ui.py index 967402207ad19afa35d5589cd8c3dcef82aa3775..aff1b9a49e1f86b47b33299cce5ee7276190beb6 100644 --- a/Lattesh/ui.py +++ b/Lattesh/ui.py @@ -85,11 +85,19 @@ class VIEW3D_PT_lattice_OCC(View3DLatticePanel, Panel): def draw(self, context): lattice_mesh = context.scene.lattice_mesh + depsgraph = context.evaluated_depsgraph_get() + has_radius = 'radius' in context.active_object.evaluated_get(depsgraph).data.attributes layout = self.layout - row = layout.row(align=True) - row.label(text="Strut radius") - row.prop(lattice_mesh, "radius", text="") + radius_row = layout.row(align=True) + radius_row.label(text="Strut radius") + radius_row.prop(lattice_mesh, "radius", text="") + if has_radius and lattice_mesh.variable_radius: + radius_row.enabled = False + var_row = layout.row(align=True) + var_row.label(text="Variable radius") + var_row.prop(lattice_mesh, "variable_radius", text="") + var_row.enabled = has_radius row = layout.row(align=True) row.label(text="Linear deflection") row.prop(lattice_mesh, "lin_deflect", text="")