symcad.core.CAD.ModeledCad
1#!/usr/bin/env python3 2# Copyright (C) 2022, Will Hedgecock 3# 4# This program is free software: you can redistribute it and/or modify 5# it under the terms of the GNU General Public License as published by 6# the Free Software Foundation, either version 3 of the License, or 7# (at your option) any later version. 8# 9# This program is distributed in the hope that it will be useful, 10# but WITHOUT ANY WARRANTY; without even the implied warranty of 11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12# GNU General Public License for more details. 13# 14# You should have received a copy of the GNU General Public License 15# along with this program. If not, see <http://www.gnu.org/licenses/>. 16 17from __future__ import annotations 18from typing import Dict, Literal, Optional, Tuple 19from PyFreeCAD.FreeCAD import FreeCAD, Mesh, Part 20from .CadGeneral import is_symbolic 21from . import CadGeneral 22from pathlib import Path 23 24TESSELATION_VALUE = 1.0 25 26class ModeledCad(object): 27 """Private helper class to connect a `SymPart` to an existing CAD representation.""" 28 29 # Public attributes ---------------------------------------------------------------------------- 30 31 cad_file_path: str 32 """Absolute path of the representative CAD model for a given `SymPart`.""" 33 34 35 # Constructor ---------------------------------------------------------------------------------- 36 37 def __init__(self, cad_file_name: str) -> None: 38 """Initializes a `ModeledCad` object, where `cad_file_name` indicates a representative 39 CAD model for a `SymPart` relative to the `src/cad` directory. 40 """ 41 self._store_absolute_cad_file_path(cad_file_name) 42 43 44 # Built-in method implementations -------------------------------------------------------------- 45 46 def __eq__(self, other: ModeledCad) -> bool: 47 return self.cad_file_path == other.cad_file_path 48 49 50 # Private helper methods ----------------------------------------------------------------------- 51 52 def _store_absolute_cad_file_path(self, cad_file_name: str) -> None: 53 """Determines and stores the full absolute path to the specified `cad_file_name`, converting 54 a non-native model into the FreeCAD format if necessary. 55 56 Parameters 57 ---------- 58 cad_file_name : `str` 59 Name of the representative CAD file model relative to this project's `src/cad` directory. 60 """ 61 62 # Create an internal FreeCAD file if importing from another format 63 file_extension = Path(cad_file_name).suffix.casefold() 64 if file_extension != '.fcstd': 65 66 # Update the target CAD file paths 67 file_path = Path(cad_file_name).absolute().resolve() 68 cad_file_name = Path('converted').joinpath(Path(cad_file_name).stem + '.FCStd') 69 cad_file_path = CadGeneral.CAD_BASE_PATH.joinpath(cad_file_name) 70 if not cad_file_path.exists(): 71 72 # Determine the type of file to import 73 if (file_extension == '.stp') or (file_extension == '.step'): 74 doc = FreeCAD.newDocument(cad_file_path.stem) 75 model = doc.addObject(CadGeneral.PART_FEATURE_STRING, 'Model') 76 shape = Part.Shape() 77 shape.read(str(file_path)) 78 model.Shape = shape 79 doc.saveAs(str(cad_file_path)) 80 FreeCAD.closeDocument(doc.Name) 81 82 elif file_extension == '.stl': 83 doc = FreeCAD.newDocument(cad_file_path.stem) 84 model = doc.addObject('Mesh::Feature', 'Model') 85 model.Mesh = Mesh.Mesh(str(file_path)) 86 doc.saveAs(str(cad_file_path)) 87 FreeCAD.closeDocument(doc.Name) 88 89 else: 90 raise NotImplementedError('CAD files of type {} are not yet supported!' 91 .format(file_extension)) 92 93 # Store the absolute CAD file path 94 self.cad_file_path = \ 95 str(CadGeneral.CAD_BASE_PATH.joinpath(cad_file_name).absolute().resolve()) \ 96 if not Path(cad_file_name).is_absolute() else \ 97 cad_file_name 98 99 100 # Public methods ------------------------------------------------------------------------------- 101 102 def add_to_assembly(self, model_name: str, 103 assembly: FreeCAD.Document, 104 concrete_parameters: Dict[str, float], 105 placement_point: Tuple[float, float, float], 106 placement_m: Tuple[float, float, float], 107 yaw_pitch_roll_deg: Tuple[float, float, float], 108 fully_displace: Optional[bool] = False) -> None: 109 """Adds a CAD model representation of the `SymPart` to the specified assembly. 110 111 Parameters 112 ---------- 113 model_name : `str` 114 Desired name for the CAD model. 115 assembly : `FreeCAD.Document` 116 FreeCAD assembly document to which the CAD model should be added. 117 concrete_parameters : `Dict[str, float]` 118 Dictionary of free variables along with their desired concrete values. 119 placement_point : `Tuple[float, float, float]` 120 Local coordinate (in percent length) to be used for the center of rotation and placement 121 of the CAD model. 122 placement_m : `Tuple[float, float, float]` 123 Global xyz-placement (in `m`) of the `placement_point` of the CAD object in the assembly. 124 yaw_pitch_roll_deg : `Tuple[float, float, float]` 125 Global yaw-, pitch-, and roll-orientation in degrees of the CAD object. 126 fully_displace : `bool` 127 Whether to create a fully solid CAD model for displacement purposes. 128 """ 129 130 # Verify that all parameters have concrete representations 131 if is_symbolic(yaw_pitch_roll_deg[0]) or is_symbolic(yaw_pitch_roll_deg[1]) or \ 132 is_symbolic(yaw_pitch_roll_deg[2]): 133 raise RuntimeError('The orientation of the part ("{}") must not be symbolic to add it ' 134 'to a CAD assembly'.format(yaw_pitch_roll_deg)) 135 for key, val in concrete_parameters.items(): 136 if key != 'name' and is_symbolic(val): 137 raise RuntimeError('The geometric parameter "{}" of the part must not be symbolic to ' 138 'add it to a CAD assembly'.format(key)) 139 140 # Concretize the CAD model if it is parametric 141 doc = FreeCAD.open(self.cad_file_path) 142 CadGeneral.assign_free_parameter_values(self.cad_file_path, doc, concrete_parameters) 143 144 # Determine if the SymPart contains a separate displacement model 145 if fully_displace and len(doc.getObjectsByLabel('DisplacedModel')) > 0: 146 model = doc.getObjectsByLabel('DisplacedModel')[0] 147 else: 148 model = doc.getObjectsByLabel('Model')[0] 149 150 # Parse the CAD model as a solid if it is currently a mesh 151 if hasattr(model, 'Mesh'): 152 shape = Part.Shape() 153 shape.makeShapeFromMesh(model.Mesh.Topology, 0.1) 154 model = doc.addObject(CadGeneral.PART_FEATURE_STRING, 'Model') 155 model.Shape = Part.Solid(shape) 156 157 # Create and add a new CAD model to the assembly 158 cad_object = assembly.addObject(CadGeneral.PART_FEATURE_STRING, model_name) 159 cad_object.Shape = Part.getShape(model, '', needSubElement=False, refine=False) 160 cad_object.Shape.tessellate(TESSELATION_VALUE) 161 assembly.recompute() 162 163 # Properly place and orient the CAD model in the assembly 164 rotation_point = CadGeneral.compute_placement_point(model.Shape, placement_point) 165 placement = FreeCAD.Vector((1000.0 * placement_m[0]) - rotation_point.x, 166 (1000.0 * placement_m[1]) - rotation_point.y, 167 (1000.0 * placement_m[2]) - rotation_point.z) 168 rotation = FreeCAD.Rotation(*yaw_pitch_roll_deg) 169 cad_object.Placement = FreeCAD.Placement(placement, rotation, rotation_point) 170 cad_object.Shape.tessellate(TESSELATION_VALUE) 171 assembly.recompute() 172 FreeCAD.closeDocument(doc.Name) 173 174 175 def get_physical_properties(self, concrete_parameters: Dict[str, float], 176 placement_point: Tuple[float, float, float], 177 yaw_pitch_roll_deg: Tuple[float, float, float], 178 material_density_kg_m3: float, 179 normalize_origin: bool) -> Dict[str, float]: 180 """Returns all physical properties of the CAD model. 181 182 Mass properties will be computed assuming a uniform material density as specified in 183 the `material_density_kg_m3` parameter. 184 185 All free parameters within the CAD model must be concretized by passing in an appropriate 186 `concrete_parameters` dictionary containing the names of the free parameters as its keys, 187 along with their corresponding concrete floating-point values. 188 189 Parameters 190 ---------- 191 concrete_parameters : `Dict[str, float]` 192 Dictionary of free variables along with their desired concrete values. 193 placement_point : `Tuple[float, float, float]` 194 Local coordinate (in percent length) to be used for the center of placement 195 of the CAD model. 196 yaw_pitch_roll_deg : `Tuple[float, float, float]` 197 Global yaw-, pitch-, and roll-orientation in degrees of the CAD object. 198 material_density_kg_m3 : `float` 199 Uniform material density to be used in mass property calculations (in `kg/m^3`). 200 normalize_origin : `bool` 201 Return physical properties with respect to the front, left, bottom corner of the 202 underlying CAD model. 203 204 Returns 205 ------- 206 `Dict[str, float]` 207 A dictionary containing all physical `SymPart` properties as computed using the 208 underlying CAD model. 209 """ 210 211 # Verify that all parameters have concrete representations 212 if is_symbolic(yaw_pitch_roll_deg[0]) or is_symbolic(yaw_pitch_roll_deg[1]) or \ 213 is_symbolic(yaw_pitch_roll_deg[2]): 214 raise RuntimeError('The orientation of the part ("{}") must not be symbolic to calculate ' 215 'its physical properties from CAD'.format(yaw_pitch_roll_deg)) 216 for key, val in concrete_parameters.items(): 217 if key != 'name' and is_symbolic(val): 218 raise RuntimeError('The geometric parameter "{}" of the part must not be symbolic to ' 219 'calculate its physical properties from CAD'.format(key)) 220 placement_point = [0.0 if is_symbolic(p) else float(p) for p in placement_point] 221 222 # Concretize the CAD model if it is parametric 223 doc = FreeCAD.open(self.cad_file_path) 224 CadGeneral.assign_free_parameter_values(self.cad_file_path, doc, concrete_parameters) 225 226 # Recompute and parse the CAD model as a solid 227 if hasattr(doc.getObjectsByLabel('Model')[0], 'Mesh'): 228 shape = Part.Shape() 229 shape.makeShapeFromMesh(doc.getObjectsByLabel('Model')[0].Mesh.Topology, 0.1) 230 model = doc.addObject(CadGeneral.PART_FEATURE_STRING, 'Model') 231 model.Shape = Part.Solid(shape) 232 else: 233 model = doc.getObjectsByLabel('Model')[0] 234 235 # Orient and tessellate the model 236 rotation_point = CadGeneral.compute_placement_point(model.Shape, placement_point) 237 placement = FreeCAD.Vector(-rotation_point.x, -rotation_point.y, -rotation_point.z) 238 rotation = FreeCAD.Rotation(*yaw_pitch_roll_deg) 239 model.Placement = FreeCAD.Placement(placement, rotation, rotation_point) 240 model.Shape.tessellate(TESSELATION_VALUE) 241 doc.recompute() 242 243 # Determine if the SymPart contains a separate displacement model 244 if len(doc.getObjectsByLabel('DisplacedModel')) > 0: 245 displaced_model = doc.getObjectsByLabel('DisplacedModel')[0] 246 displaced_model.Placement = FreeCAD.Placement(placement, rotation, rotation_point) 247 displaced_model.Shape.tessellate(TESSELATION_VALUE) 248 doc.recompute() 249 displaced_model = displaced_model.Shape 250 else: 251 displaced_model = model.Shape 252 model = model.Shape 253 254 # Retrieve all physical model properties 255 properties = CadGeneral.fetch_model_physical_properties(model, 256 displaced_model, 257 material_density_kg_m3, 258 normalize_origin) 259 FreeCAD.closeDocument(doc.Name) 260 return properties 261 262 263 def export_model(self, file_save_path: str, 264 model_type: Literal['freecad', 'step', 'stl'], 265 concrete_parameters: Dict[str, float], 266 placement_point: Tuple[float, float, float], 267 yaw_pitch_roll_deg: Tuple[float, float, float]) -> None: 268 """Creates a CAD model of the `SymPart` in the specified format. 269 270 Parameters 271 ---------- 272 file_save_path : `str` 273 Output file path at which to store the generated CAD model. 274 model_type : {'freecad', 'step', 'stl'} 275 Format of the CAD model to export. 276 concrete_parameters : `Dict[str, float]` 277 Dictionary of free variables along with their desired concrete values. 278 placement_point : `Tuple[float, float, float]` 279 Local coordinate (in percent length) to be used for the center of placement 280 of the CAD model. 281 yaw_pitch_roll_deg : `Tuple[float, float, float]` 282 Global yaw-, pitch-, and roll-orientation in degrees of the CAD object. 283 """ 284 285 # Verify that all parameters have concrete representations 286 if is_symbolic(yaw_pitch_roll_deg[0]) or is_symbolic(yaw_pitch_roll_deg[1]) or \ 287 is_symbolic(yaw_pitch_roll_deg[2]): 288 raise RuntimeError('The orientation of the part ("{}") must not be symbolic to export ' 289 'it as a CAD model'.format(yaw_pitch_roll_deg)) 290 for key, val in concrete_parameters.items(): 291 if key != 'name' and is_symbolic(val): 292 raise RuntimeError('The geometric parameter "{}" of the part must not be symbolic to ' 293 'export it as a CAD model'.format(key)) 294 placement_point = [0.0 if is_symbolic(p) else float(p) for p in placement_point] 295 296 # Create any necessary path directories 297 file_path = Path(file_save_path).absolute().resolve() 298 if not file_path.parent.exists(): 299 file_path.parent.mkdir() 300 301 # Concretize the CAD model if it is parametric 302 doc = FreeCAD.open(self.cad_file_path) 303 CadGeneral.assign_free_parameter_values(self.cad_file_path, doc, concrete_parameters) 304 305 # Orient and tessellate the model 306 model = doc.getObjectsByLabel('Model')[0] 307 model.Shape.tessellate(TESSELATION_VALUE) 308 rotation_point = CadGeneral.compute_placement_point(model.Shape, placement_point) 309 placement = FreeCAD.Vector(-rotation_point.x, -rotation_point.y, -rotation_point.z) 310 rotation = FreeCAD.Rotation(*yaw_pitch_roll_deg) 311 model.Placement = FreeCAD.Placement(placement, rotation, rotation_point) 312 model.Shape.tessellate(TESSELATION_VALUE) 313 doc.recompute() 314 315 # Create the requested CAD format of the model 316 doc_clean = FreeCAD.newDocument('Clean') 317 clean_model = doc_clean.addObject('Part::Feature', 'Model') 318 clean_model.Shape = model.Shape.removeSplitter() 319 doc_clean.recompute() 320 CadGeneral.save_model(file_path, model_type, clean_model) 321 FreeCAD.closeDocument(doc_clean.Name) 322 FreeCAD.closeDocument(doc.Name)
27class ModeledCad(object): 28 """Private helper class to connect a `SymPart` to an existing CAD representation.""" 29 30 # Public attributes ---------------------------------------------------------------------------- 31 32 cad_file_path: str 33 """Absolute path of the representative CAD model for a given `SymPart`.""" 34 35 36 # Constructor ---------------------------------------------------------------------------------- 37 38 def __init__(self, cad_file_name: str) -> None: 39 """Initializes a `ModeledCad` object, where `cad_file_name` indicates a representative 40 CAD model for a `SymPart` relative to the `src/cad` directory. 41 """ 42 self._store_absolute_cad_file_path(cad_file_name) 43 44 45 # Built-in method implementations -------------------------------------------------------------- 46 47 def __eq__(self, other: ModeledCad) -> bool: 48 return self.cad_file_path == other.cad_file_path 49 50 51 # Private helper methods ----------------------------------------------------------------------- 52 53 def _store_absolute_cad_file_path(self, cad_file_name: str) -> None: 54 """Determines and stores the full absolute path to the specified `cad_file_name`, converting 55 a non-native model into the FreeCAD format if necessary. 56 57 Parameters 58 ---------- 59 cad_file_name : `str` 60 Name of the representative CAD file model relative to this project's `src/cad` directory. 61 """ 62 63 # Create an internal FreeCAD file if importing from another format 64 file_extension = Path(cad_file_name).suffix.casefold() 65 if file_extension != '.fcstd': 66 67 # Update the target CAD file paths 68 file_path = Path(cad_file_name).absolute().resolve() 69 cad_file_name = Path('converted').joinpath(Path(cad_file_name).stem + '.FCStd') 70 cad_file_path = CadGeneral.CAD_BASE_PATH.joinpath(cad_file_name) 71 if not cad_file_path.exists(): 72 73 # Determine the type of file to import 74 if (file_extension == '.stp') or (file_extension == '.step'): 75 doc = FreeCAD.newDocument(cad_file_path.stem) 76 model = doc.addObject(CadGeneral.PART_FEATURE_STRING, 'Model') 77 shape = Part.Shape() 78 shape.read(str(file_path)) 79 model.Shape = shape 80 doc.saveAs(str(cad_file_path)) 81 FreeCAD.closeDocument(doc.Name) 82 83 elif file_extension == '.stl': 84 doc = FreeCAD.newDocument(cad_file_path.stem) 85 model = doc.addObject('Mesh::Feature', 'Model') 86 model.Mesh = Mesh.Mesh(str(file_path)) 87 doc.saveAs(str(cad_file_path)) 88 FreeCAD.closeDocument(doc.Name) 89 90 else: 91 raise NotImplementedError('CAD files of type {} are not yet supported!' 92 .format(file_extension)) 93 94 # Store the absolute CAD file path 95 self.cad_file_path = \ 96 str(CadGeneral.CAD_BASE_PATH.joinpath(cad_file_name).absolute().resolve()) \ 97 if not Path(cad_file_name).is_absolute() else \ 98 cad_file_name 99 100 101 # Public methods ------------------------------------------------------------------------------- 102 103 def add_to_assembly(self, model_name: str, 104 assembly: FreeCAD.Document, 105 concrete_parameters: Dict[str, float], 106 placement_point: Tuple[float, float, float], 107 placement_m: Tuple[float, float, float], 108 yaw_pitch_roll_deg: Tuple[float, float, float], 109 fully_displace: Optional[bool] = False) -> None: 110 """Adds a CAD model representation of the `SymPart` to the specified assembly. 111 112 Parameters 113 ---------- 114 model_name : `str` 115 Desired name for the CAD model. 116 assembly : `FreeCAD.Document` 117 FreeCAD assembly document to which the CAD model should be added. 118 concrete_parameters : `Dict[str, float]` 119 Dictionary of free variables along with their desired concrete values. 120 placement_point : `Tuple[float, float, float]` 121 Local coordinate (in percent length) to be used for the center of rotation and placement 122 of the CAD model. 123 placement_m : `Tuple[float, float, float]` 124 Global xyz-placement (in `m`) of the `placement_point` of the CAD object in the assembly. 125 yaw_pitch_roll_deg : `Tuple[float, float, float]` 126 Global yaw-, pitch-, and roll-orientation in degrees of the CAD object. 127 fully_displace : `bool` 128 Whether to create a fully solid CAD model for displacement purposes. 129 """ 130 131 # Verify that all parameters have concrete representations 132 if is_symbolic(yaw_pitch_roll_deg[0]) or is_symbolic(yaw_pitch_roll_deg[1]) or \ 133 is_symbolic(yaw_pitch_roll_deg[2]): 134 raise RuntimeError('The orientation of the part ("{}") must not be symbolic to add it ' 135 'to a CAD assembly'.format(yaw_pitch_roll_deg)) 136 for key, val in concrete_parameters.items(): 137 if key != 'name' and is_symbolic(val): 138 raise RuntimeError('The geometric parameter "{}" of the part must not be symbolic to ' 139 'add it to a CAD assembly'.format(key)) 140 141 # Concretize the CAD model if it is parametric 142 doc = FreeCAD.open(self.cad_file_path) 143 CadGeneral.assign_free_parameter_values(self.cad_file_path, doc, concrete_parameters) 144 145 # Determine if the SymPart contains a separate displacement model 146 if fully_displace and len(doc.getObjectsByLabel('DisplacedModel')) > 0: 147 model = doc.getObjectsByLabel('DisplacedModel')[0] 148 else: 149 model = doc.getObjectsByLabel('Model')[0] 150 151 # Parse the CAD model as a solid if it is currently a mesh 152 if hasattr(model, 'Mesh'): 153 shape = Part.Shape() 154 shape.makeShapeFromMesh(model.Mesh.Topology, 0.1) 155 model = doc.addObject(CadGeneral.PART_FEATURE_STRING, 'Model') 156 model.Shape = Part.Solid(shape) 157 158 # Create and add a new CAD model to the assembly 159 cad_object = assembly.addObject(CadGeneral.PART_FEATURE_STRING, model_name) 160 cad_object.Shape = Part.getShape(model, '', needSubElement=False, refine=False) 161 cad_object.Shape.tessellate(TESSELATION_VALUE) 162 assembly.recompute() 163 164 # Properly place and orient the CAD model in the assembly 165 rotation_point = CadGeneral.compute_placement_point(model.Shape, placement_point) 166 placement = FreeCAD.Vector((1000.0 * placement_m[0]) - rotation_point.x, 167 (1000.0 * placement_m[1]) - rotation_point.y, 168 (1000.0 * placement_m[2]) - rotation_point.z) 169 rotation = FreeCAD.Rotation(*yaw_pitch_roll_deg) 170 cad_object.Placement = FreeCAD.Placement(placement, rotation, rotation_point) 171 cad_object.Shape.tessellate(TESSELATION_VALUE) 172 assembly.recompute() 173 FreeCAD.closeDocument(doc.Name) 174 175 176 def get_physical_properties(self, concrete_parameters: Dict[str, float], 177 placement_point: Tuple[float, float, float], 178 yaw_pitch_roll_deg: Tuple[float, float, float], 179 material_density_kg_m3: float, 180 normalize_origin: bool) -> Dict[str, float]: 181 """Returns all physical properties of the CAD model. 182 183 Mass properties will be computed assuming a uniform material density as specified in 184 the `material_density_kg_m3` parameter. 185 186 All free parameters within the CAD model must be concretized by passing in an appropriate 187 `concrete_parameters` dictionary containing the names of the free parameters as its keys, 188 along with their corresponding concrete floating-point values. 189 190 Parameters 191 ---------- 192 concrete_parameters : `Dict[str, float]` 193 Dictionary of free variables along with their desired concrete values. 194 placement_point : `Tuple[float, float, float]` 195 Local coordinate (in percent length) to be used for the center of placement 196 of the CAD model. 197 yaw_pitch_roll_deg : `Tuple[float, float, float]` 198 Global yaw-, pitch-, and roll-orientation in degrees of the CAD object. 199 material_density_kg_m3 : `float` 200 Uniform material density to be used in mass property calculations (in `kg/m^3`). 201 normalize_origin : `bool` 202 Return physical properties with respect to the front, left, bottom corner of the 203 underlying CAD model. 204 205 Returns 206 ------- 207 `Dict[str, float]` 208 A dictionary containing all physical `SymPart` properties as computed using the 209 underlying CAD model. 210 """ 211 212 # Verify that all parameters have concrete representations 213 if is_symbolic(yaw_pitch_roll_deg[0]) or is_symbolic(yaw_pitch_roll_deg[1]) or \ 214 is_symbolic(yaw_pitch_roll_deg[2]): 215 raise RuntimeError('The orientation of the part ("{}") must not be symbolic to calculate ' 216 'its physical properties from CAD'.format(yaw_pitch_roll_deg)) 217 for key, val in concrete_parameters.items(): 218 if key != 'name' and is_symbolic(val): 219 raise RuntimeError('The geometric parameter "{}" of the part must not be symbolic to ' 220 'calculate its physical properties from CAD'.format(key)) 221 placement_point = [0.0 if is_symbolic(p) else float(p) for p in placement_point] 222 223 # Concretize the CAD model if it is parametric 224 doc = FreeCAD.open(self.cad_file_path) 225 CadGeneral.assign_free_parameter_values(self.cad_file_path, doc, concrete_parameters) 226 227 # Recompute and parse the CAD model as a solid 228 if hasattr(doc.getObjectsByLabel('Model')[0], 'Mesh'): 229 shape = Part.Shape() 230 shape.makeShapeFromMesh(doc.getObjectsByLabel('Model')[0].Mesh.Topology, 0.1) 231 model = doc.addObject(CadGeneral.PART_FEATURE_STRING, 'Model') 232 model.Shape = Part.Solid(shape) 233 else: 234 model = doc.getObjectsByLabel('Model')[0] 235 236 # Orient and tessellate the model 237 rotation_point = CadGeneral.compute_placement_point(model.Shape, placement_point) 238 placement = FreeCAD.Vector(-rotation_point.x, -rotation_point.y, -rotation_point.z) 239 rotation = FreeCAD.Rotation(*yaw_pitch_roll_deg) 240 model.Placement = FreeCAD.Placement(placement, rotation, rotation_point) 241 model.Shape.tessellate(TESSELATION_VALUE) 242 doc.recompute() 243 244 # Determine if the SymPart contains a separate displacement model 245 if len(doc.getObjectsByLabel('DisplacedModel')) > 0: 246 displaced_model = doc.getObjectsByLabel('DisplacedModel')[0] 247 displaced_model.Placement = FreeCAD.Placement(placement, rotation, rotation_point) 248 displaced_model.Shape.tessellate(TESSELATION_VALUE) 249 doc.recompute() 250 displaced_model = displaced_model.Shape 251 else: 252 displaced_model = model.Shape 253 model = model.Shape 254 255 # Retrieve all physical model properties 256 properties = CadGeneral.fetch_model_physical_properties(model, 257 displaced_model, 258 material_density_kg_m3, 259 normalize_origin) 260 FreeCAD.closeDocument(doc.Name) 261 return properties 262 263 264 def export_model(self, file_save_path: str, 265 model_type: Literal['freecad', 'step', 'stl'], 266 concrete_parameters: Dict[str, float], 267 placement_point: Tuple[float, float, float], 268 yaw_pitch_roll_deg: Tuple[float, float, float]) -> None: 269 """Creates a CAD model of the `SymPart` in the specified format. 270 271 Parameters 272 ---------- 273 file_save_path : `str` 274 Output file path at which to store the generated CAD model. 275 model_type : {'freecad', 'step', 'stl'} 276 Format of the CAD model to export. 277 concrete_parameters : `Dict[str, float]` 278 Dictionary of free variables along with their desired concrete values. 279 placement_point : `Tuple[float, float, float]` 280 Local coordinate (in percent length) to be used for the center of placement 281 of the CAD model. 282 yaw_pitch_roll_deg : `Tuple[float, float, float]` 283 Global yaw-, pitch-, and roll-orientation in degrees of the CAD object. 284 """ 285 286 # Verify that all parameters have concrete representations 287 if is_symbolic(yaw_pitch_roll_deg[0]) or is_symbolic(yaw_pitch_roll_deg[1]) or \ 288 is_symbolic(yaw_pitch_roll_deg[2]): 289 raise RuntimeError('The orientation of the part ("{}") must not be symbolic to export ' 290 'it as a CAD model'.format(yaw_pitch_roll_deg)) 291 for key, val in concrete_parameters.items(): 292 if key != 'name' and is_symbolic(val): 293 raise RuntimeError('The geometric parameter "{}" of the part must not be symbolic to ' 294 'export it as a CAD model'.format(key)) 295 placement_point = [0.0 if is_symbolic(p) else float(p) for p in placement_point] 296 297 # Create any necessary path directories 298 file_path = Path(file_save_path).absolute().resolve() 299 if not file_path.parent.exists(): 300 file_path.parent.mkdir() 301 302 # Concretize the CAD model if it is parametric 303 doc = FreeCAD.open(self.cad_file_path) 304 CadGeneral.assign_free_parameter_values(self.cad_file_path, doc, concrete_parameters) 305 306 # Orient and tessellate the model 307 model = doc.getObjectsByLabel('Model')[0] 308 model.Shape.tessellate(TESSELATION_VALUE) 309 rotation_point = CadGeneral.compute_placement_point(model.Shape, placement_point) 310 placement = FreeCAD.Vector(-rotation_point.x, -rotation_point.y, -rotation_point.z) 311 rotation = FreeCAD.Rotation(*yaw_pitch_roll_deg) 312 model.Placement = FreeCAD.Placement(placement, rotation, rotation_point) 313 model.Shape.tessellate(TESSELATION_VALUE) 314 doc.recompute() 315 316 # Create the requested CAD format of the model 317 doc_clean = FreeCAD.newDocument('Clean') 318 clean_model = doc_clean.addObject('Part::Feature', 'Model') 319 clean_model.Shape = model.Shape.removeSplitter() 320 doc_clean.recompute() 321 CadGeneral.save_model(file_path, model_type, clean_model) 322 FreeCAD.closeDocument(doc_clean.Name) 323 FreeCAD.closeDocument(doc.Name)
Private helper class to connect a SymPart to an existing CAD representation.
38 def __init__(self, cad_file_name: str) -> None: 39 """Initializes a `ModeledCad` object, where `cad_file_name` indicates a representative 40 CAD model for a `SymPart` relative to the `src/cad` directory. 41 """ 42 self._store_absolute_cad_file_path(cad_file_name)
Initializes a ModeledCad object, where cad_file_name indicates a representative
CAD model for a SymPart relative to the src/cad directory.
103 def add_to_assembly(self, model_name: str, 104 assembly: FreeCAD.Document, 105 concrete_parameters: Dict[str, float], 106 placement_point: Tuple[float, float, float], 107 placement_m: Tuple[float, float, float], 108 yaw_pitch_roll_deg: Tuple[float, float, float], 109 fully_displace: Optional[bool] = False) -> None: 110 """Adds a CAD model representation of the `SymPart` to the specified assembly. 111 112 Parameters 113 ---------- 114 model_name : `str` 115 Desired name for the CAD model. 116 assembly : `FreeCAD.Document` 117 FreeCAD assembly document to which the CAD model should be added. 118 concrete_parameters : `Dict[str, float]` 119 Dictionary of free variables along with their desired concrete values. 120 placement_point : `Tuple[float, float, float]` 121 Local coordinate (in percent length) to be used for the center of rotation and placement 122 of the CAD model. 123 placement_m : `Tuple[float, float, float]` 124 Global xyz-placement (in `m`) of the `placement_point` of the CAD object in the assembly. 125 yaw_pitch_roll_deg : `Tuple[float, float, float]` 126 Global yaw-, pitch-, and roll-orientation in degrees of the CAD object. 127 fully_displace : `bool` 128 Whether to create a fully solid CAD model for displacement purposes. 129 """ 130 131 # Verify that all parameters have concrete representations 132 if is_symbolic(yaw_pitch_roll_deg[0]) or is_symbolic(yaw_pitch_roll_deg[1]) or \ 133 is_symbolic(yaw_pitch_roll_deg[2]): 134 raise RuntimeError('The orientation of the part ("{}") must not be symbolic to add it ' 135 'to a CAD assembly'.format(yaw_pitch_roll_deg)) 136 for key, val in concrete_parameters.items(): 137 if key != 'name' and is_symbolic(val): 138 raise RuntimeError('The geometric parameter "{}" of the part must not be symbolic to ' 139 'add it to a CAD assembly'.format(key)) 140 141 # Concretize the CAD model if it is parametric 142 doc = FreeCAD.open(self.cad_file_path) 143 CadGeneral.assign_free_parameter_values(self.cad_file_path, doc, concrete_parameters) 144 145 # Determine if the SymPart contains a separate displacement model 146 if fully_displace and len(doc.getObjectsByLabel('DisplacedModel')) > 0: 147 model = doc.getObjectsByLabel('DisplacedModel')[0] 148 else: 149 model = doc.getObjectsByLabel('Model')[0] 150 151 # Parse the CAD model as a solid if it is currently a mesh 152 if hasattr(model, 'Mesh'): 153 shape = Part.Shape() 154 shape.makeShapeFromMesh(model.Mesh.Topology, 0.1) 155 model = doc.addObject(CadGeneral.PART_FEATURE_STRING, 'Model') 156 model.Shape = Part.Solid(shape) 157 158 # Create and add a new CAD model to the assembly 159 cad_object = assembly.addObject(CadGeneral.PART_FEATURE_STRING, model_name) 160 cad_object.Shape = Part.getShape(model, '', needSubElement=False, refine=False) 161 cad_object.Shape.tessellate(TESSELATION_VALUE) 162 assembly.recompute() 163 164 # Properly place and orient the CAD model in the assembly 165 rotation_point = CadGeneral.compute_placement_point(model.Shape, placement_point) 166 placement = FreeCAD.Vector((1000.0 * placement_m[0]) - rotation_point.x, 167 (1000.0 * placement_m[1]) - rotation_point.y, 168 (1000.0 * placement_m[2]) - rotation_point.z) 169 rotation = FreeCAD.Rotation(*yaw_pitch_roll_deg) 170 cad_object.Placement = FreeCAD.Placement(placement, rotation, rotation_point) 171 cad_object.Shape.tessellate(TESSELATION_VALUE) 172 assembly.recompute() 173 FreeCAD.closeDocument(doc.Name)
Adds a CAD model representation of the SymPart to the specified assembly.
Parameters
- model_name (
str): Desired name for the CAD model. - assembly (
FreeCAD.Document): FreeCAD assembly document to which the CAD model should be added. - concrete_parameters (
Dict[str, float]): Dictionary of free variables along with their desired concrete values. - placement_point (
Tuple[float, float, float]): Local coordinate (in percent length) to be used for the center of rotation and placement of the CAD model. - placement_m (
Tuple[float, float, float]): Global xyz-placement (inm) of theplacement_pointof the CAD object in the assembly. - yaw_pitch_roll_deg (
Tuple[float, float, float]): Global yaw-, pitch-, and roll-orientation in degrees of the CAD object. - fully_displace (
bool): Whether to create a fully solid CAD model for displacement purposes.
176 def get_physical_properties(self, concrete_parameters: Dict[str, float], 177 placement_point: Tuple[float, float, float], 178 yaw_pitch_roll_deg: Tuple[float, float, float], 179 material_density_kg_m3: float, 180 normalize_origin: bool) -> Dict[str, float]: 181 """Returns all physical properties of the CAD model. 182 183 Mass properties will be computed assuming a uniform material density as specified in 184 the `material_density_kg_m3` parameter. 185 186 All free parameters within the CAD model must be concretized by passing in an appropriate 187 `concrete_parameters` dictionary containing the names of the free parameters as its keys, 188 along with their corresponding concrete floating-point values. 189 190 Parameters 191 ---------- 192 concrete_parameters : `Dict[str, float]` 193 Dictionary of free variables along with their desired concrete values. 194 placement_point : `Tuple[float, float, float]` 195 Local coordinate (in percent length) to be used for the center of placement 196 of the CAD model. 197 yaw_pitch_roll_deg : `Tuple[float, float, float]` 198 Global yaw-, pitch-, and roll-orientation in degrees of the CAD object. 199 material_density_kg_m3 : `float` 200 Uniform material density to be used in mass property calculations (in `kg/m^3`). 201 normalize_origin : `bool` 202 Return physical properties with respect to the front, left, bottom corner of the 203 underlying CAD model. 204 205 Returns 206 ------- 207 `Dict[str, float]` 208 A dictionary containing all physical `SymPart` properties as computed using the 209 underlying CAD model. 210 """ 211 212 # Verify that all parameters have concrete representations 213 if is_symbolic(yaw_pitch_roll_deg[0]) or is_symbolic(yaw_pitch_roll_deg[1]) or \ 214 is_symbolic(yaw_pitch_roll_deg[2]): 215 raise RuntimeError('The orientation of the part ("{}") must not be symbolic to calculate ' 216 'its physical properties from CAD'.format(yaw_pitch_roll_deg)) 217 for key, val in concrete_parameters.items(): 218 if key != 'name' and is_symbolic(val): 219 raise RuntimeError('The geometric parameter "{}" of the part must not be symbolic to ' 220 'calculate its physical properties from CAD'.format(key)) 221 placement_point = [0.0 if is_symbolic(p) else float(p) for p in placement_point] 222 223 # Concretize the CAD model if it is parametric 224 doc = FreeCAD.open(self.cad_file_path) 225 CadGeneral.assign_free_parameter_values(self.cad_file_path, doc, concrete_parameters) 226 227 # Recompute and parse the CAD model as a solid 228 if hasattr(doc.getObjectsByLabel('Model')[0], 'Mesh'): 229 shape = Part.Shape() 230 shape.makeShapeFromMesh(doc.getObjectsByLabel('Model')[0].Mesh.Topology, 0.1) 231 model = doc.addObject(CadGeneral.PART_FEATURE_STRING, 'Model') 232 model.Shape = Part.Solid(shape) 233 else: 234 model = doc.getObjectsByLabel('Model')[0] 235 236 # Orient and tessellate the model 237 rotation_point = CadGeneral.compute_placement_point(model.Shape, placement_point) 238 placement = FreeCAD.Vector(-rotation_point.x, -rotation_point.y, -rotation_point.z) 239 rotation = FreeCAD.Rotation(*yaw_pitch_roll_deg) 240 model.Placement = FreeCAD.Placement(placement, rotation, rotation_point) 241 model.Shape.tessellate(TESSELATION_VALUE) 242 doc.recompute() 243 244 # Determine if the SymPart contains a separate displacement model 245 if len(doc.getObjectsByLabel('DisplacedModel')) > 0: 246 displaced_model = doc.getObjectsByLabel('DisplacedModel')[0] 247 displaced_model.Placement = FreeCAD.Placement(placement, rotation, rotation_point) 248 displaced_model.Shape.tessellate(TESSELATION_VALUE) 249 doc.recompute() 250 displaced_model = displaced_model.Shape 251 else: 252 displaced_model = model.Shape 253 model = model.Shape 254 255 # Retrieve all physical model properties 256 properties = CadGeneral.fetch_model_physical_properties(model, 257 displaced_model, 258 material_density_kg_m3, 259 normalize_origin) 260 FreeCAD.closeDocument(doc.Name) 261 return properties
Returns all physical properties of the CAD model.
Mass properties will be computed assuming a uniform material density as specified in
the material_density_kg_m3 parameter.
All free parameters within the CAD model must be concretized by passing in an appropriate
concrete_parameters dictionary containing the names of the free parameters as its keys,
along with their corresponding concrete floating-point values.
Parameters
- concrete_parameters (
Dict[str, float]): Dictionary of free variables along with their desired concrete values. - placement_point (
Tuple[float, float, float]): Local coordinate (in percent length) to be used for the center of placement of the CAD model. - yaw_pitch_roll_deg (
Tuple[float, float, float]): Global yaw-, pitch-, and roll-orientation in degrees of the CAD object. - material_density_kg_m3 (
float): Uniform material density to be used in mass property calculations (inkg/m^3). - normalize_origin (
bool): Return physical properties with respect to the front, left, bottom corner of the underlying CAD model.
Returns
Dict[str, float]: A dictionary containing all physicalSymPartproperties as computed using the underlying CAD model.
264 def export_model(self, file_save_path: str, 265 model_type: Literal['freecad', 'step', 'stl'], 266 concrete_parameters: Dict[str, float], 267 placement_point: Tuple[float, float, float], 268 yaw_pitch_roll_deg: Tuple[float, float, float]) -> None: 269 """Creates a CAD model of the `SymPart` in the specified format. 270 271 Parameters 272 ---------- 273 file_save_path : `str` 274 Output file path at which to store the generated CAD model. 275 model_type : {'freecad', 'step', 'stl'} 276 Format of the CAD model to export. 277 concrete_parameters : `Dict[str, float]` 278 Dictionary of free variables along with their desired concrete values. 279 placement_point : `Tuple[float, float, float]` 280 Local coordinate (in percent length) to be used for the center of placement 281 of the CAD model. 282 yaw_pitch_roll_deg : `Tuple[float, float, float]` 283 Global yaw-, pitch-, and roll-orientation in degrees of the CAD object. 284 """ 285 286 # Verify that all parameters have concrete representations 287 if is_symbolic(yaw_pitch_roll_deg[0]) or is_symbolic(yaw_pitch_roll_deg[1]) or \ 288 is_symbolic(yaw_pitch_roll_deg[2]): 289 raise RuntimeError('The orientation of the part ("{}") must not be symbolic to export ' 290 'it as a CAD model'.format(yaw_pitch_roll_deg)) 291 for key, val in concrete_parameters.items(): 292 if key != 'name' and is_symbolic(val): 293 raise RuntimeError('The geometric parameter "{}" of the part must not be symbolic to ' 294 'export it as a CAD model'.format(key)) 295 placement_point = [0.0 if is_symbolic(p) else float(p) for p in placement_point] 296 297 # Create any necessary path directories 298 file_path = Path(file_save_path).absolute().resolve() 299 if not file_path.parent.exists(): 300 file_path.parent.mkdir() 301 302 # Concretize the CAD model if it is parametric 303 doc = FreeCAD.open(self.cad_file_path) 304 CadGeneral.assign_free_parameter_values(self.cad_file_path, doc, concrete_parameters) 305 306 # Orient and tessellate the model 307 model = doc.getObjectsByLabel('Model')[0] 308 model.Shape.tessellate(TESSELATION_VALUE) 309 rotation_point = CadGeneral.compute_placement_point(model.Shape, placement_point) 310 placement = FreeCAD.Vector(-rotation_point.x, -rotation_point.y, -rotation_point.z) 311 rotation = FreeCAD.Rotation(*yaw_pitch_roll_deg) 312 model.Placement = FreeCAD.Placement(placement, rotation, rotation_point) 313 model.Shape.tessellate(TESSELATION_VALUE) 314 doc.recompute() 315 316 # Create the requested CAD format of the model 317 doc_clean = FreeCAD.newDocument('Clean') 318 clean_model = doc_clean.addObject('Part::Feature', 'Model') 319 clean_model.Shape = model.Shape.removeSplitter() 320 doc_clean.recompute() 321 CadGeneral.save_model(file_path, model_type, clean_model) 322 FreeCAD.closeDocument(doc_clean.Name) 323 FreeCAD.closeDocument(doc.Name)
Creates a CAD model of the SymPart in the specified format.
Parameters
- file_save_path (
str): Output file path at which to store the generated CAD model. - model_type ({'freecad', 'step', 'stl'}): Format of the CAD model to export.
- concrete_parameters (
Dict[str, float]): Dictionary of free variables along with their desired concrete values. - placement_point (
Tuple[float, float, float]): Local coordinate (in percent length) to be used for the center of placement of the CAD model. - yaw_pitch_roll_deg (
Tuple[float, float, float]): Global yaw-, pitch-, and roll-orientation in degrees of the CAD object.