symcad.core.CAD.CadGeneral
Private helper module for manipulating FreeCAD models.
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 17"""Private helper module for manipulating FreeCAD models.""" 18 19from __future__ import annotations 20from typing import Any, Callable, Dict, List, Literal, Tuple, Union 21from PyFreeCAD.FreeCAD import FreeCAD, Part 22from pathlib import Path 23import zipfile 24 25PART_FEATURE_STRING = 'Part::Feature' 26CAD_BASE_PATH: Path = Path(__file__).parent.joinpath('..', '..', 'cadmodels').absolute().resolve() 27 28def is_symbolic(val: Any) -> bool: 29 """Returns whether `val` is a symbolic parameter.""" 30 try: 31 float(val) 32 return False 33 except Exception: 34 return True 35 36 37def get_free_parameters_from_model(cad_file_path: str, 38 document: Union[FreeCAD.Document, None]) -> List[str]: 39 """Returns all free parameters specified within the CAD model. 40 41 Free parameters are defined as aliased cells in a FreeCAD spreadsheet with the title 42 `Parameters` which occur before an optional row in the same spreadsheet with a cell 43 containing the word `Derived`. 44 45 Parameters 46 ---------- 47 cad_file_path : `str` 48 Absolute path of the CAD model. 49 document : `Union[FreeCAD.Document, None]` 50 An existing open FreeCAD document or `None` if no open document already exists. 51 52 Returns 53 ------- 54 `List[str]` 55 The list of free parameters in the CAD model. 56 """ 57 58 # Determine if the file corresponds to a parametric CAD model 59 parameters = [] 60 doc = FreeCAD.open(cad_file_path) if document is None else document 61 if len(doc.getObjectsByLabel('Parameters')) > 0: 62 63 # Parse all free parameters inside the model 64 params = doc.getObjectsByLabel('Parameters')[0] 65 aliases = params.cells.Content.split('Derived')[0].split('alias="') 66 for idx in range(1, len(aliases)): 67 parameters.append(aliases[idx].split('"')[0]) 68 69 # Return the list of free parameters 70 if document is None: 71 FreeCAD.closeDocument(doc.Name) 72 return parameters 73 74 75def get_free_parameters_from_method(creation_method: Callable[[Dict[str, float], bool], 76 Part.Solid]) -> List[str]: 77 """Returns all free parameters required by the specified CAD creation method. 78 79 Free parameters are defined as variables that are expected to be concretely defined within 80 the `parameters` dictionary passed to the `creation_method` in order to generate a CAD model. 81 82 Parameters 83 ---------- 84 creation_method : `Callable[[Dict[str, float], bool], Part.Solid]` 85 Callable method that can be used to create a CAD model. 86 87 Returns 88 ------- 89 `List[str]` 90 The list of free parameters required by the CAD generation method. 91 """ 92 params = {} 93 new_parameter_parsed = True 94 for displaced in [False, True]: 95 while new_parameter_parsed: 96 try: 97 new_parameter_parsed = False 98 creation_method(params, displaced) 99 except KeyError as key: 100 new_parameter_parsed = True 101 params[str(key).strip("\"'")] = 1.0 102 return list(params) 103 104 105def assign_free_parameter_values(cad_file_path: str, 106 doc: FreeCAD.Document, 107 concrete_parameters: Dict[str, float]) -> None: 108 """Assigns concrete values to all free parameters specified within the CAD model. 109 110 Free parameters are defined as aliased cells in a FreeCAD spreadsheet with the title 111 `Parameters` which occur before an optional row in the same spreadsheet with a cell 112 containing the word `Derived`. 113 114 Parameters 115 ---------- 116 cad_file_path : `str` 117 Absolute path of the CAD model. 118 document : `FreeCAD.Document` 119 An existing open FreeCAD document. 120 concrete_parameters : `Dict[str, float]` 121 Dictionary of free variables along with their desired concrete values. 122 """ 123 if len(doc.getObjectsByLabel('Parameters')) > 0: 124 125 # Ensure that all free parameters have concrete representations 126 missing_params = [key for key in get_free_parameters_from_model(cad_file_path, doc) 127 if key not in concrete_parameters] 128 if missing_params: 129 raise RuntimeError('CAD model contains symbolic free parameters without concrete ' 130 'floating-point representations: {}'.format(missing_params)) 131 132 # Assign concrete values to all symbolic free parameters 133 params = doc.getObjectsByLabel('Parameters')[0] 134 for param in get_free_parameters_from_model(cad_file_path, doc): 135 units = 'm' if ' m' in str(params.get(params.getCellFromAlias(param))) else '' 136 params.set(params.getCellFromAlias(param), str(concrete_parameters[param]) + units) 137 doc.recompute() 138 139 140def compute_placement_point(part: Part.Solid, 141 origin: Tuple[float, float, float]) -> FreeCAD.Vector: 142 """Computes the global placement point (in `mm`) of the CAD model based on the specified 143 percent-length `origin` values. 144 145 Parameters 146 ---------- 147 part : `Part.Solid` 148 The CAD part for which the placement point is being computed. 149 origin : `Tuple[float, float, float]` 150 Local coordinate (in percent length) to be used for the center of placement and rotation 151 of the CAD part. 152 153 Returns 154 ------- 155 `Tuple[float, float, float]` 156 The absolute placement point of the CAD model (in `mm`) in its FreeCAD representation format. 157 """ 158 placement_point_x = origin[0] * \ 159 float(FreeCAD.Units.Quantity(part.BoundBox.XLength, FreeCAD.Units.Length).getValueAs('mm')) 160 placement_point_y = origin[1] * \ 161 float(FreeCAD.Units.Quantity(part.BoundBox.YLength, FreeCAD.Units.Length).getValueAs('mm')) 162 placement_point_z = origin[2] * \ 163 float(FreeCAD.Units.Quantity(part.BoundBox.ZLength, FreeCAD.Units.Length).getValueAs('mm')) 164 return FreeCAD.Vector( 165 float(FreeCAD.Units.Quantity(part.BoundBox.XMin, FreeCAD.Units.Length).getValueAs('mm'))\ 166 + placement_point_x, 167 float(FreeCAD.Units.Quantity(part.BoundBox.YMin, FreeCAD.Units.Length).getValueAs('mm'))\ 168 + placement_point_y, 169 float(FreeCAD.Units.Quantity(part.BoundBox.ZMin, FreeCAD.Units.Length).getValueAs('mm'))\ 170 + placement_point_z) 171 172 173def fetch_model_physical_properties(model: Part.Feature, 174 displaced_model: Union[Part.Feature, None], 175 material_density_kg_m3: float, 176 normalize_origin: bool) -> Dict[str, float]: 177 """Returns all physical properties of the specified CAD model. 178 179 Mass properties will be computed assuming a uniform material density as specified in 180 the `material_density_kg_m3` parameter. 181 182 Parameters 183 ---------- 184 model : `Part.Feature` 185 CAD model for which to compute the geometric properties. 186 displaced_model : `Union[Part.Feature, None]` 187 CAD model for which to compute the geometric properties assuming full displacement 188 of a solid. 189 material_density_kg_m3 : `float` 190 Uniform material density to be used in mass property calculations (in `kg/m^3`). 191 normalize_origin : `bool` 192 Return physical properties with respect to the front, left, bottom corner of the model. 193 194 Returns 195 ------- 196 `Dict[str, float]` 197 A dictionary containing all physical properties of the underlying CAD model. 198 """ 199 props = { 200 'xlen': float(FreeCAD.Units.Quantity(model.BoundBox.XLength, FreeCAD.Units.Length) 201 .getValueAs('m')), 202 'ylen': float(FreeCAD.Units.Quantity(model.BoundBox.YLength, FreeCAD.Units.Length) 203 .getValueAs('m')), 204 'zlen': float(FreeCAD.Units.Quantity(model.BoundBox.ZLength, FreeCAD.Units.Length) 205 .getValueAs('m')), 206 'min_x': float(FreeCAD.Units.Quantity(model.BoundBox.XMin, FreeCAD.Units.Length) 207 .getValueAs('m')), 208 'min_y': float(FreeCAD.Units.Quantity(model.BoundBox.YMin, FreeCAD.Units.Length) 209 .getValueAs('m')), 210 'min_z': float(FreeCAD.Units.Quantity(model.BoundBox.ZMin, FreeCAD.Units.Length) 211 .getValueAs('m')), 212 'cg_x': float(FreeCAD.Units.Quantity(model.CenterOfGravity[0], FreeCAD.Units.Length) 213 .getValueAs('m')), 214 'cg_y': float(FreeCAD.Units.Quantity(model.CenterOfGravity[1], FreeCAD.Units.Length) 215 .getValueAs('m')), 216 'cg_z': float(FreeCAD.Units.Quantity(model.CenterOfGravity[2], FreeCAD.Units.Length) 217 .getValueAs('m')), 218 'cb_x': float(FreeCAD.Units.Quantity(displaced_model.CenterOfGravity[0], 219 FreeCAD.Units.Length).getValueAs('m')) 220 if displaced_model is not None else 0.0, 221 'cb_y': float(FreeCAD.Units.Quantity(displaced_model.CenterOfGravity[1], 222 FreeCAD.Units.Length).getValueAs('m')) 223 if displaced_model is not None else 0.0, 224 'cb_z': float(FreeCAD.Units.Quantity(displaced_model.CenterOfGravity[2], 225 FreeCAD.Units.Length).getValueAs('m')) 226 if displaced_model is not None else 0.0, 227 'mass': float(FreeCAD.Units.Quantity(model.Volume, FreeCAD.Units.Volume) 228 .getValueAs('m^3') * material_density_kg_m3), 229 'material_volume': float(FreeCAD.Units.Quantity(model.Volume, FreeCAD.Units.Volume) 230 .getValueAs('m^3')), 231 'displaced_volume': float(FreeCAD.Units.Quantity(displaced_model.Volume, 232 FreeCAD.Units.Volume) 233 .getValueAs('m^3')) if displaced_model is not None else 0.0, 234 'surface_area': float(FreeCAD.Units.Quantity(displaced_model.Area, FreeCAD.Units.Area) 235 .getValueAs('m^2')) if displaced_model is not None else 0.0 236 } 237 if normalize_origin: 238 props['cg_x'] -= props['min_x'] 239 props['cg_y'] -= props['min_y'] 240 props['cg_z'] -= props['min_z'] 241 props['cb_x'] -= props['min_x'] 242 props['cb_y'] -= props['min_y'] 243 props['cb_z'] -= props['min_z'] 244 props['min_x'] = props['min_y'] = props['min_z'] = 0.0 245 return props 246 247 248def fetch_assembly_physical_properties(assembly: FreeCAD.Document, 249 displaced: FreeCAD.Document, 250 material_densities: Dict[str, float]) -> Dict[str, float]: 251 """Returns all physical properties of the specified CAD assembly. 252 253 Mass properties will be computed assuming each constituent part has a uniform material density 254 as specified by its corresponding value in `kg/m^3` in the `material_densities` parameter. 255 256 Parameters 257 ---------- 258 assembly : `FreeCAD.Document` 259 CAD assembly for which to compute the geometric properties. 260 displaced : `FreeCAD.Document` 261 CAD assembly for which to compute the geometric properties assuming full displacement 262 of a solid. 263 material_densities : `Dict[str, float]` 264 Uniform material densities to be used in mass property calculations (in `kg/m^3`). 265 266 Returns 267 ------- 268 `Dict[str, float]` 269 A dictionary containing all physical properties of the underlying CAD assembly. 270 """ 271 xlen_min = ylen_min = zlen_min = 100000000.0 272 xlen_max = ylen_max = zlen_max = -100000000.0 273 props = { 'xlen': 0.0, 'ylen': 0.0, 'zlen': 0.0, 274 'cg_x': 0.0, 'cg_y': 0.0, 'cg_z': 0.0, 'cb_x': 0.0, 'cb_y': 0.0, 'cb_z': 0.0, 275 'mass': 0.0, 'material_volume': 0.0, 'displaced_volume': 0.0, 'surface_area': 0.0 } 276 for part in assembly.Objects: 277 displaced_part = [obj for obj in displaced.Objects if obj.Label == part.Label] 278 displaced_part = None if not displaced_part else displaced_part[0] 279 part_props = fetch_model_physical_properties(part.Shape, 280 displaced_part.Shape, 281 material_densities[part.Label], 282 False) 283 props['cg_x'] += (part_props['cg_x'] * part_props['mass']) 284 props['cg_y'] += (part_props['cg_y'] * part_props['mass']) 285 props['cg_z'] += (part_props['cg_z'] * part_props['mass']) 286 props['cb_x'] += (part_props['cb_x'] * part_props['displaced_volume']) 287 props['cb_y'] += (part_props['cb_y'] * part_props['displaced_volume']) 288 props['cb_z'] += (part_props['cb_z'] * part_props['displaced_volume']) 289 props['mass'] += part_props['mass'] 290 props['material_volume'] += part_props['material_volume'] 291 props['displaced_volume'] += part_props['displaced_volume'] 292 props['surface_area'] += part_props['surface_area'] 293 xlen_min = min(xlen_min, 294 FreeCAD.Units.Quantity(part.Shape.BoundBox.XMin, FreeCAD.Units.Length) 295 .getValueAs('m')) 296 ylen_min = min(ylen_min, 297 FreeCAD.Units.Quantity(part.Shape.BoundBox.YMin, FreeCAD.Units.Length) 298 .getValueAs('m')) 299 zlen_min = min(zlen_min, 300 FreeCAD.Units.Quantity(part.Shape.BoundBox.ZMin, FreeCAD.Units.Length) 301 .getValueAs('m')) 302 xlen_max = max(xlen_max, 303 FreeCAD.Units.Quantity(part.Shape.BoundBox.XMax, FreeCAD.Units.Length) 304 .getValueAs('m')) 305 ylen_max = max(ylen_max, 306 FreeCAD.Units.Quantity(part.Shape.BoundBox.YMax, FreeCAD.Units.Length) 307 .getValueAs('m')) 308 zlen_max = max(zlen_max, 309 FreeCAD.Units.Quantity(part.Shape.BoundBox.ZMax, FreeCAD.Units.Length) 310 .getValueAs('m')) 311 props['xlen'] = xlen_max - xlen_min 312 props['ylen'] = ylen_max - ylen_min 313 props['zlen'] = zlen_max - zlen_min 314 props['cg_x'] /= props['mass'] 315 props['cg_y'] /= props['mass'] 316 props['cg_z'] /= props['mass'] 317 props['cb_x'] /= props['displaced_volume'] 318 props['cb_y'] /= props['displaced_volume'] 319 props['cb_z'] /= props['displaced_volume'] 320 return props 321 322 323def retrieve_interferences(assembly: FreeCAD.Document) -> List[Tuple[str, str]]: 324 """Retrieves a list of components within the CAD model that interfere or overlap 325 with any other contained components. 326 327 Parameters 328 ---------- 329 assembly : `FreeCAD.Document` 330 CAD assembly in which to search for interfering components. 331 332 Returns 333 ------- 334 `List[Tuple[str, str]]` 335 A list of tuples containing CAD components that interfere with one another. 336 """ 337 interferences = [] 338 for idx1, component1 in enumerate(assembly.Objects): 339 for idx2, component2 in enumerate(assembly.Objects): 340 if idx2 > idx1: 341 overlap = FreeCAD.Units.Quantity(component1.Shape.common(component2.Shape).Volume, 342 FreeCAD.Units.Volume).getValueAs('m^3') 343 if overlap > 0.0: # TODO: Allow for some tolerance here 344 interferences.append((component1.Label, component2.Label)) 345 return interferences 346 347 348def save_model(file_path: str, 349 model_type: Literal['freecad', 'step', 'stl'], 350 model: Part.Feature) -> None: 351 """Saves a CAD model in the specified format. 352 353 Parameters 354 ---------- 355 file_path : `str` 356 Output file path at which to store the CAD model. 357 model_type : {'freecad', 'step', 'stl'} 358 Format of the CAD model to store. 359 model : `Part.Feature` 360 The actual CAD model being stored. 361 """ 362 if model_type == 'freecad': 363 file_path = str(file_path) if file_path.suffix.casefold() == '.fcstd' else \ 364 str(file_path) + '.FCStd' 365 model.Document.saveAs(file_path) 366 write_freecad_gui_doc(file_path, model, False) 367 elif model_type == 'step': 368 file_path = str(file_path) if file_path.suffix.casefold() == '.step' or \ 369 file_path.suffix.casefold() == '.stp' else \ 370 str(file_path) + '.stp' 371 model.Shape.exportStep(file_path) 372 elif model_type == 'stl': 373 from PyFreeCAD.FreeCAD import Mesh 374 file_path = str(file_path) if file_path.suffix.casefold() == '.stl' else \ 375 str(file_path) + '.stl' 376 Mesh.export([model], file_path) 377 else: 378 raise TypeError('Exporting to the "{}" CAD format is not currently supported' 379 .format(model_type)) 380 381 382def save_assembly(file_path: str, 383 cad_type: Literal['freecad', 'step', 'stl'], 384 assembly: FreeCAD.Document) -> None: 385 """Saves a CAD assembly in the specified format. 386 387 Parameters 388 ---------- 389 file_path : `str` 390 Output file path at which to store the CAD assembly. 391 cad_type : {'freecad', 'step', 'stl'} 392 Format of the CAD model to store. 393 assembly : `FreeCAD.Document` 394 The actual CAD assembly being stored. 395 """ 396 if cad_type == 'freecad': 397 file_path = str(file_path) if file_path.suffix.casefold() == '.fcstd' else \ 398 str(file_path) + '.FCStd' 399 assembly.saveAs(file_path) 400 write_freecad_gui_doc(file_path, assembly, True) 401 elif cad_type == 'step': 402 from PyFreeCAD.FreeCAD import Import 403 file_path = str(file_path) if file_path.suffix.casefold() == '.step' or \ 404 file_path.suffix.casefold() == '.stp' else \ 405 str(file_path) + '.stp' 406 Import.export(assembly.Objects, file_path) 407 elif cad_type == 'stl': 408 from PyFreeCAD.FreeCAD import Mesh 409 file_path = str(file_path) if file_path.suffix.casefold() == '.stl' else \ 410 str(file_path) + '.stl' 411 Mesh.export(assembly.Objects, file_path) 412 else: 413 raise TypeError('Exporting to the "{}" CAD format is not currently supported' 414 .format(cad_type)) 415 416 417def write_freecad_gui_doc(file_path: str, 418 model: Union[FreeCAD.Document, Part.Feature], 419 is_assembly: bool) -> None: 420 """Adds a GuiDocument.xml file to the specified FreeCAD model document. 421 422 This document allows the FreeCAD software to know how to set the initial orientation, 423 visibility, and placement of the model in its viewport. 424 425 Parameters 426 ---------- 427 file_path : `str` 428 Path to an existing FreeCAD model file. 429 model : `Union[FreeCAD.Document, Part.Feature]` 430 The current model or assembly that resides within the FreeCAD file. 431 is_assembly : `bool` 432 Whether the model is a full assembly or a standalone part. 433 """ 434 435 # Parse the relevant model details 436 num_models = 1 if not is_assembly else len(model.Objects) 437 438 # Create the GuiDocument.xml file contents 439 contents = '<?xml version=\'1.0\' encoding=\'utf-8\'?>\n<Document SchemaVersion="1" HasExpansion="1">\n' 440 contents += ' <Expand />\n <ViewProviderData Count="' + str(num_models) + '">\n' 441 for i in range(num_models): 442 current_model = model if not is_assembly else model.Objects[i] 443 contents += ' <ViewProvider name="' + current_model.Label + '" expanded="0">\n <Properties Count="4" TransientCount="0">\n' 444 contents += ' <Property name="DisplayMode" type="App::PropertyEnumeration" status="1">\n <Integer value="0"/>\n </Property>\n' 445 contents += ' <Property name="ShowInTree" type="App::PropertyBool" status="1">\n <Bool value="true"/>\n </Property>\n' 446 contents += ' <Property name="Transparency" type="App::PropertyPercent" status="1">\n <Integer value="0"/>\n </Property>\n' 447 contents += ' <Property name="Visibility" type="App::PropertyBool" status="1">\n <Bool value="true"/>\n </Property>\n' 448 contents += ' </Properties>\n </ViewProvider>\n' 449 contents += ' </ViewProviderData>\n' 450 contents += ' <Camera settings="OrthographicCamera { viewportMapping ADJUST_CAMERA orientation 1 0 0 1.5707965 aspectRatio 1 } "/>\n</Document>\n' 451 452 # Add the GuiDocument.xml contents to the FreeCAD file 453 with zipfile.ZipFile(file_path, 'a', zipfile.ZIP_DEFLATED) as file: 454 file.writestr('GuiDocument.xml', contents)
29def is_symbolic(val: Any) -> bool: 30 """Returns whether `val` is a symbolic parameter.""" 31 try: 32 float(val) 33 return False 34 except Exception: 35 return True
Returns whether val is a symbolic parameter.
38def get_free_parameters_from_model(cad_file_path: str, 39 document: Union[FreeCAD.Document, None]) -> List[str]: 40 """Returns all free parameters specified within the CAD model. 41 42 Free parameters are defined as aliased cells in a FreeCAD spreadsheet with the title 43 `Parameters` which occur before an optional row in the same spreadsheet with a cell 44 containing the word `Derived`. 45 46 Parameters 47 ---------- 48 cad_file_path : `str` 49 Absolute path of the CAD model. 50 document : `Union[FreeCAD.Document, None]` 51 An existing open FreeCAD document or `None` if no open document already exists. 52 53 Returns 54 ------- 55 `List[str]` 56 The list of free parameters in the CAD model. 57 """ 58 59 # Determine if the file corresponds to a parametric CAD model 60 parameters = [] 61 doc = FreeCAD.open(cad_file_path) if document is None else document 62 if len(doc.getObjectsByLabel('Parameters')) > 0: 63 64 # Parse all free parameters inside the model 65 params = doc.getObjectsByLabel('Parameters')[0] 66 aliases = params.cells.Content.split('Derived')[0].split('alias="') 67 for idx in range(1, len(aliases)): 68 parameters.append(aliases[idx].split('"')[0]) 69 70 # Return the list of free parameters 71 if document is None: 72 FreeCAD.closeDocument(doc.Name) 73 return parameters
Returns all free parameters specified within the CAD model.
Free parameters are defined as aliased cells in a FreeCAD spreadsheet with the title
Parameters which occur before an optional row in the same spreadsheet with a cell
containing the word Derived.
Parameters
- cad_file_path (
str): Absolute path of the CAD model. - document (
Union[FreeCAD.Document, None]): An existing open FreeCAD document orNoneif no open document already exists.
Returns
List[str]: The list of free parameters in the CAD model.
76def get_free_parameters_from_method(creation_method: Callable[[Dict[str, float], bool], 77 Part.Solid]) -> List[str]: 78 """Returns all free parameters required by the specified CAD creation method. 79 80 Free parameters are defined as variables that are expected to be concretely defined within 81 the `parameters` dictionary passed to the `creation_method` in order to generate a CAD model. 82 83 Parameters 84 ---------- 85 creation_method : `Callable[[Dict[str, float], bool], Part.Solid]` 86 Callable method that can be used to create a CAD model. 87 88 Returns 89 ------- 90 `List[str]` 91 The list of free parameters required by the CAD generation method. 92 """ 93 params = {} 94 new_parameter_parsed = True 95 for displaced in [False, True]: 96 while new_parameter_parsed: 97 try: 98 new_parameter_parsed = False 99 creation_method(params, displaced) 100 except KeyError as key: 101 new_parameter_parsed = True 102 params[str(key).strip("\"'")] = 1.0 103 return list(params)
Returns all free parameters required by the specified CAD creation method.
Free parameters are defined as variables that are expected to be concretely defined within
the parameters dictionary passed to the creation_method in order to generate a CAD model.
Parameters
- creation_method (
Callable[[Dict[str, float], bool], Part.Solid]): Callable method that can be used to create a CAD model.
Returns
List[str]: The list of free parameters required by the CAD generation method.
106def assign_free_parameter_values(cad_file_path: str, 107 doc: FreeCAD.Document, 108 concrete_parameters: Dict[str, float]) -> None: 109 """Assigns concrete values to all free parameters specified within the CAD model. 110 111 Free parameters are defined as aliased cells in a FreeCAD spreadsheet with the title 112 `Parameters` which occur before an optional row in the same spreadsheet with a cell 113 containing the word `Derived`. 114 115 Parameters 116 ---------- 117 cad_file_path : `str` 118 Absolute path of the CAD model. 119 document : `FreeCAD.Document` 120 An existing open FreeCAD document. 121 concrete_parameters : `Dict[str, float]` 122 Dictionary of free variables along with their desired concrete values. 123 """ 124 if len(doc.getObjectsByLabel('Parameters')) > 0: 125 126 # Ensure that all free parameters have concrete representations 127 missing_params = [key for key in get_free_parameters_from_model(cad_file_path, doc) 128 if key not in concrete_parameters] 129 if missing_params: 130 raise RuntimeError('CAD model contains symbolic free parameters without concrete ' 131 'floating-point representations: {}'.format(missing_params)) 132 133 # Assign concrete values to all symbolic free parameters 134 params = doc.getObjectsByLabel('Parameters')[0] 135 for param in get_free_parameters_from_model(cad_file_path, doc): 136 units = 'm' if ' m' in str(params.get(params.getCellFromAlias(param))) else '' 137 params.set(params.getCellFromAlias(param), str(concrete_parameters[param]) + units) 138 doc.recompute()
Assigns concrete values to all free parameters specified within the CAD model.
Free parameters are defined as aliased cells in a FreeCAD spreadsheet with the title
Parameters which occur before an optional row in the same spreadsheet with a cell
containing the word Derived.
Parameters
- cad_file_path (
str): Absolute path of the CAD model. - document (
FreeCAD.Document): An existing open FreeCAD document. - concrete_parameters (
Dict[str, float]): Dictionary of free variables along with their desired concrete values.
141def compute_placement_point(part: Part.Solid, 142 origin: Tuple[float, float, float]) -> FreeCAD.Vector: 143 """Computes the global placement point (in `mm`) of the CAD model based on the specified 144 percent-length `origin` values. 145 146 Parameters 147 ---------- 148 part : `Part.Solid` 149 The CAD part for which the placement point is being computed. 150 origin : `Tuple[float, float, float]` 151 Local coordinate (in percent length) to be used for the center of placement and rotation 152 of the CAD part. 153 154 Returns 155 ------- 156 `Tuple[float, float, float]` 157 The absolute placement point of the CAD model (in `mm`) in its FreeCAD representation format. 158 """ 159 placement_point_x = origin[0] * \ 160 float(FreeCAD.Units.Quantity(part.BoundBox.XLength, FreeCAD.Units.Length).getValueAs('mm')) 161 placement_point_y = origin[1] * \ 162 float(FreeCAD.Units.Quantity(part.BoundBox.YLength, FreeCAD.Units.Length).getValueAs('mm')) 163 placement_point_z = origin[2] * \ 164 float(FreeCAD.Units.Quantity(part.BoundBox.ZLength, FreeCAD.Units.Length).getValueAs('mm')) 165 return FreeCAD.Vector( 166 float(FreeCAD.Units.Quantity(part.BoundBox.XMin, FreeCAD.Units.Length).getValueAs('mm'))\ 167 + placement_point_x, 168 float(FreeCAD.Units.Quantity(part.BoundBox.YMin, FreeCAD.Units.Length).getValueAs('mm'))\ 169 + placement_point_y, 170 float(FreeCAD.Units.Quantity(part.BoundBox.ZMin, FreeCAD.Units.Length).getValueAs('mm'))\ 171 + placement_point_z)
Computes the global placement point (in mm) of the CAD model based on the specified
percent-length origin values.
Parameters
- part (
Part.Solid): The CAD part for which the placement point is being computed. - origin (
Tuple[float, float, float]): Local coordinate (in percent length) to be used for the center of placement and rotation of the CAD part.
Returns
Tuple[float, float, float]: The absolute placement point of the CAD model (inmm) in its FreeCAD representation format.
174def fetch_model_physical_properties(model: Part.Feature, 175 displaced_model: Union[Part.Feature, None], 176 material_density_kg_m3: float, 177 normalize_origin: bool) -> Dict[str, float]: 178 """Returns all physical properties of the specified CAD model. 179 180 Mass properties will be computed assuming a uniform material density as specified in 181 the `material_density_kg_m3` parameter. 182 183 Parameters 184 ---------- 185 model : `Part.Feature` 186 CAD model for which to compute the geometric properties. 187 displaced_model : `Union[Part.Feature, None]` 188 CAD model for which to compute the geometric properties assuming full displacement 189 of a solid. 190 material_density_kg_m3 : `float` 191 Uniform material density to be used in mass property calculations (in `kg/m^3`). 192 normalize_origin : `bool` 193 Return physical properties with respect to the front, left, bottom corner of the model. 194 195 Returns 196 ------- 197 `Dict[str, float]` 198 A dictionary containing all physical properties of the underlying CAD model. 199 """ 200 props = { 201 'xlen': float(FreeCAD.Units.Quantity(model.BoundBox.XLength, FreeCAD.Units.Length) 202 .getValueAs('m')), 203 'ylen': float(FreeCAD.Units.Quantity(model.BoundBox.YLength, FreeCAD.Units.Length) 204 .getValueAs('m')), 205 'zlen': float(FreeCAD.Units.Quantity(model.BoundBox.ZLength, FreeCAD.Units.Length) 206 .getValueAs('m')), 207 'min_x': float(FreeCAD.Units.Quantity(model.BoundBox.XMin, FreeCAD.Units.Length) 208 .getValueAs('m')), 209 'min_y': float(FreeCAD.Units.Quantity(model.BoundBox.YMin, FreeCAD.Units.Length) 210 .getValueAs('m')), 211 'min_z': float(FreeCAD.Units.Quantity(model.BoundBox.ZMin, FreeCAD.Units.Length) 212 .getValueAs('m')), 213 'cg_x': float(FreeCAD.Units.Quantity(model.CenterOfGravity[0], FreeCAD.Units.Length) 214 .getValueAs('m')), 215 'cg_y': float(FreeCAD.Units.Quantity(model.CenterOfGravity[1], FreeCAD.Units.Length) 216 .getValueAs('m')), 217 'cg_z': float(FreeCAD.Units.Quantity(model.CenterOfGravity[2], FreeCAD.Units.Length) 218 .getValueAs('m')), 219 'cb_x': float(FreeCAD.Units.Quantity(displaced_model.CenterOfGravity[0], 220 FreeCAD.Units.Length).getValueAs('m')) 221 if displaced_model is not None else 0.0, 222 'cb_y': float(FreeCAD.Units.Quantity(displaced_model.CenterOfGravity[1], 223 FreeCAD.Units.Length).getValueAs('m')) 224 if displaced_model is not None else 0.0, 225 'cb_z': float(FreeCAD.Units.Quantity(displaced_model.CenterOfGravity[2], 226 FreeCAD.Units.Length).getValueAs('m')) 227 if displaced_model is not None else 0.0, 228 'mass': float(FreeCAD.Units.Quantity(model.Volume, FreeCAD.Units.Volume) 229 .getValueAs('m^3') * material_density_kg_m3), 230 'material_volume': float(FreeCAD.Units.Quantity(model.Volume, FreeCAD.Units.Volume) 231 .getValueAs('m^3')), 232 'displaced_volume': float(FreeCAD.Units.Quantity(displaced_model.Volume, 233 FreeCAD.Units.Volume) 234 .getValueAs('m^3')) if displaced_model is not None else 0.0, 235 'surface_area': float(FreeCAD.Units.Quantity(displaced_model.Area, FreeCAD.Units.Area) 236 .getValueAs('m^2')) if displaced_model is not None else 0.0 237 } 238 if normalize_origin: 239 props['cg_x'] -= props['min_x'] 240 props['cg_y'] -= props['min_y'] 241 props['cg_z'] -= props['min_z'] 242 props['cb_x'] -= props['min_x'] 243 props['cb_y'] -= props['min_y'] 244 props['cb_z'] -= props['min_z'] 245 props['min_x'] = props['min_y'] = props['min_z'] = 0.0 246 return props
Returns all physical properties of the specified CAD model.
Mass properties will be computed assuming a uniform material density as specified in
the material_density_kg_m3 parameter.
Parameters
- model (
Part.Feature): CAD model for which to compute the geometric properties. - displaced_model (
Union[Part.Feature, None]): CAD model for which to compute the geometric properties assuming full displacement of a solid. - 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 model.
Returns
Dict[str, float]: A dictionary containing all physical properties of the underlying CAD model.
249def fetch_assembly_physical_properties(assembly: FreeCAD.Document, 250 displaced: FreeCAD.Document, 251 material_densities: Dict[str, float]) -> Dict[str, float]: 252 """Returns all physical properties of the specified CAD assembly. 253 254 Mass properties will be computed assuming each constituent part has a uniform material density 255 as specified by its corresponding value in `kg/m^3` in the `material_densities` parameter. 256 257 Parameters 258 ---------- 259 assembly : `FreeCAD.Document` 260 CAD assembly for which to compute the geometric properties. 261 displaced : `FreeCAD.Document` 262 CAD assembly for which to compute the geometric properties assuming full displacement 263 of a solid. 264 material_densities : `Dict[str, float]` 265 Uniform material densities to be used in mass property calculations (in `kg/m^3`). 266 267 Returns 268 ------- 269 `Dict[str, float]` 270 A dictionary containing all physical properties of the underlying CAD assembly. 271 """ 272 xlen_min = ylen_min = zlen_min = 100000000.0 273 xlen_max = ylen_max = zlen_max = -100000000.0 274 props = { 'xlen': 0.0, 'ylen': 0.0, 'zlen': 0.0, 275 'cg_x': 0.0, 'cg_y': 0.0, 'cg_z': 0.0, 'cb_x': 0.0, 'cb_y': 0.0, 'cb_z': 0.0, 276 'mass': 0.0, 'material_volume': 0.0, 'displaced_volume': 0.0, 'surface_area': 0.0 } 277 for part in assembly.Objects: 278 displaced_part = [obj for obj in displaced.Objects if obj.Label == part.Label] 279 displaced_part = None if not displaced_part else displaced_part[0] 280 part_props = fetch_model_physical_properties(part.Shape, 281 displaced_part.Shape, 282 material_densities[part.Label], 283 False) 284 props['cg_x'] += (part_props['cg_x'] * part_props['mass']) 285 props['cg_y'] += (part_props['cg_y'] * part_props['mass']) 286 props['cg_z'] += (part_props['cg_z'] * part_props['mass']) 287 props['cb_x'] += (part_props['cb_x'] * part_props['displaced_volume']) 288 props['cb_y'] += (part_props['cb_y'] * part_props['displaced_volume']) 289 props['cb_z'] += (part_props['cb_z'] * part_props['displaced_volume']) 290 props['mass'] += part_props['mass'] 291 props['material_volume'] += part_props['material_volume'] 292 props['displaced_volume'] += part_props['displaced_volume'] 293 props['surface_area'] += part_props['surface_area'] 294 xlen_min = min(xlen_min, 295 FreeCAD.Units.Quantity(part.Shape.BoundBox.XMin, FreeCAD.Units.Length) 296 .getValueAs('m')) 297 ylen_min = min(ylen_min, 298 FreeCAD.Units.Quantity(part.Shape.BoundBox.YMin, FreeCAD.Units.Length) 299 .getValueAs('m')) 300 zlen_min = min(zlen_min, 301 FreeCAD.Units.Quantity(part.Shape.BoundBox.ZMin, FreeCAD.Units.Length) 302 .getValueAs('m')) 303 xlen_max = max(xlen_max, 304 FreeCAD.Units.Quantity(part.Shape.BoundBox.XMax, FreeCAD.Units.Length) 305 .getValueAs('m')) 306 ylen_max = max(ylen_max, 307 FreeCAD.Units.Quantity(part.Shape.BoundBox.YMax, FreeCAD.Units.Length) 308 .getValueAs('m')) 309 zlen_max = max(zlen_max, 310 FreeCAD.Units.Quantity(part.Shape.BoundBox.ZMax, FreeCAD.Units.Length) 311 .getValueAs('m')) 312 props['xlen'] = xlen_max - xlen_min 313 props['ylen'] = ylen_max - ylen_min 314 props['zlen'] = zlen_max - zlen_min 315 props['cg_x'] /= props['mass'] 316 props['cg_y'] /= props['mass'] 317 props['cg_z'] /= props['mass'] 318 props['cb_x'] /= props['displaced_volume'] 319 props['cb_y'] /= props['displaced_volume'] 320 props['cb_z'] /= props['displaced_volume'] 321 return props
Returns all physical properties of the specified CAD assembly.
Mass properties will be computed assuming each constituent part has a uniform material density
as specified by its corresponding value in kg/m^3 in the material_densities parameter.
Parameters
- assembly (
FreeCAD.Document): CAD assembly for which to compute the geometric properties. - displaced (
FreeCAD.Document): CAD assembly for which to compute the geometric properties assuming full displacement of a solid. - material_densities (
Dict[str, float]): Uniform material densities to be used in mass property calculations (inkg/m^3).
Returns
Dict[str, float]: A dictionary containing all physical properties of the underlying CAD assembly.
324def retrieve_interferences(assembly: FreeCAD.Document) -> List[Tuple[str, str]]: 325 """Retrieves a list of components within the CAD model that interfere or overlap 326 with any other contained components. 327 328 Parameters 329 ---------- 330 assembly : `FreeCAD.Document` 331 CAD assembly in which to search for interfering components. 332 333 Returns 334 ------- 335 `List[Tuple[str, str]]` 336 A list of tuples containing CAD components that interfere with one another. 337 """ 338 interferences = [] 339 for idx1, component1 in enumerate(assembly.Objects): 340 for idx2, component2 in enumerate(assembly.Objects): 341 if idx2 > idx1: 342 overlap = FreeCAD.Units.Quantity(component1.Shape.common(component2.Shape).Volume, 343 FreeCAD.Units.Volume).getValueAs('m^3') 344 if overlap > 0.0: # TODO: Allow for some tolerance here 345 interferences.append((component1.Label, component2.Label)) 346 return interferences
Retrieves a list of components within the CAD model that interfere or overlap with any other contained components.
Parameters
- assembly (
FreeCAD.Document): CAD assembly in which to search for interfering components.
Returns
List[Tuple[str, str]]: A list of tuples containing CAD components that interfere with one another.
349def save_model(file_path: str, 350 model_type: Literal['freecad', 'step', 'stl'], 351 model: Part.Feature) -> None: 352 """Saves a CAD model in the specified format. 353 354 Parameters 355 ---------- 356 file_path : `str` 357 Output file path at which to store the CAD model. 358 model_type : {'freecad', 'step', 'stl'} 359 Format of the CAD model to store. 360 model : `Part.Feature` 361 The actual CAD model being stored. 362 """ 363 if model_type == 'freecad': 364 file_path = str(file_path) if file_path.suffix.casefold() == '.fcstd' else \ 365 str(file_path) + '.FCStd' 366 model.Document.saveAs(file_path) 367 write_freecad_gui_doc(file_path, model, False) 368 elif model_type == 'step': 369 file_path = str(file_path) if file_path.suffix.casefold() == '.step' or \ 370 file_path.suffix.casefold() == '.stp' else \ 371 str(file_path) + '.stp' 372 model.Shape.exportStep(file_path) 373 elif model_type == 'stl': 374 from PyFreeCAD.FreeCAD import Mesh 375 file_path = str(file_path) if file_path.suffix.casefold() == '.stl' else \ 376 str(file_path) + '.stl' 377 Mesh.export([model], file_path) 378 else: 379 raise TypeError('Exporting to the "{}" CAD format is not currently supported' 380 .format(model_type))
Saves a CAD model in the specified format.
Parameters
- file_path (
str): Output file path at which to store the CAD model. - model_type ({'freecad', 'step', 'stl'}): Format of the CAD model to store.
- model (
Part.Feature): The actual CAD model being stored.
383def save_assembly(file_path: str, 384 cad_type: Literal['freecad', 'step', 'stl'], 385 assembly: FreeCAD.Document) -> None: 386 """Saves a CAD assembly in the specified format. 387 388 Parameters 389 ---------- 390 file_path : `str` 391 Output file path at which to store the CAD assembly. 392 cad_type : {'freecad', 'step', 'stl'} 393 Format of the CAD model to store. 394 assembly : `FreeCAD.Document` 395 The actual CAD assembly being stored. 396 """ 397 if cad_type == 'freecad': 398 file_path = str(file_path) if file_path.suffix.casefold() == '.fcstd' else \ 399 str(file_path) + '.FCStd' 400 assembly.saveAs(file_path) 401 write_freecad_gui_doc(file_path, assembly, True) 402 elif cad_type == 'step': 403 from PyFreeCAD.FreeCAD import Import 404 file_path = str(file_path) if file_path.suffix.casefold() == '.step' or \ 405 file_path.suffix.casefold() == '.stp' else \ 406 str(file_path) + '.stp' 407 Import.export(assembly.Objects, file_path) 408 elif cad_type == 'stl': 409 from PyFreeCAD.FreeCAD import Mesh 410 file_path = str(file_path) if file_path.suffix.casefold() == '.stl' else \ 411 str(file_path) + '.stl' 412 Mesh.export(assembly.Objects, file_path) 413 else: 414 raise TypeError('Exporting to the "{}" CAD format is not currently supported' 415 .format(cad_type))
Saves a CAD assembly in the specified format.
Parameters
- file_path (
str): Output file path at which to store the CAD assembly. - cad_type ({'freecad', 'step', 'stl'}): Format of the CAD model to store.
- assembly (
FreeCAD.Document): The actual CAD assembly being stored.
418def write_freecad_gui_doc(file_path: str, 419 model: Union[FreeCAD.Document, Part.Feature], 420 is_assembly: bool) -> None: 421 """Adds a GuiDocument.xml file to the specified FreeCAD model document. 422 423 This document allows the FreeCAD software to know how to set the initial orientation, 424 visibility, and placement of the model in its viewport. 425 426 Parameters 427 ---------- 428 file_path : `str` 429 Path to an existing FreeCAD model file. 430 model : `Union[FreeCAD.Document, Part.Feature]` 431 The current model or assembly that resides within the FreeCAD file. 432 is_assembly : `bool` 433 Whether the model is a full assembly or a standalone part. 434 """ 435 436 # Parse the relevant model details 437 num_models = 1 if not is_assembly else len(model.Objects) 438 439 # Create the GuiDocument.xml file contents 440 contents = '<?xml version=\'1.0\' encoding=\'utf-8\'?>\n<Document SchemaVersion="1" HasExpansion="1">\n' 441 contents += ' <Expand />\n <ViewProviderData Count="' + str(num_models) + '">\n' 442 for i in range(num_models): 443 current_model = model if not is_assembly else model.Objects[i] 444 contents += ' <ViewProvider name="' + current_model.Label + '" expanded="0">\n <Properties Count="4" TransientCount="0">\n' 445 contents += ' <Property name="DisplayMode" type="App::PropertyEnumeration" status="1">\n <Integer value="0"/>\n </Property>\n' 446 contents += ' <Property name="ShowInTree" type="App::PropertyBool" status="1">\n <Bool value="true"/>\n </Property>\n' 447 contents += ' <Property name="Transparency" type="App::PropertyPercent" status="1">\n <Integer value="0"/>\n </Property>\n' 448 contents += ' <Property name="Visibility" type="App::PropertyBool" status="1">\n <Bool value="true"/>\n </Property>\n' 449 contents += ' </Properties>\n </ViewProvider>\n' 450 contents += ' </ViewProviderData>\n' 451 contents += ' <Camera settings="OrthographicCamera { viewportMapping ADJUST_CAMERA orientation 1 0 0 1.5707965 aspectRatio 1 } "/>\n</Document>\n' 452 453 # Add the GuiDocument.xml contents to the FreeCAD file 454 with zipfile.ZipFile(file_path, 'a', zipfile.ZIP_DEFLATED) as file: 455 file.writestr('GuiDocument.xml', contents)
Adds a GuiDocument.xml file to the specified FreeCAD model document.
This document allows the FreeCAD software to know how to set the initial orientation, visibility, and placement of the model in its viewport.
Parameters
- file_path (
str): Path to an existing FreeCAD model file. - model (
Union[FreeCAD.Document, Part.Feature]): The current model or assembly that resides within the FreeCAD file. - is_assembly (
bool): Whether the model is a full assembly or a standalone part.