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)
TESSELATION_VALUE = 1.0
class ModeledCad:
 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.

ModeledCad(cad_file_name: str)
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.

cad_file_path: str

Absolute path of the representative CAD model for a given SymPart.

def add_to_assembly( self, model_name: str, assembly: App.Document, concrete_parameters: Dict[str, float], placement_point: Tuple[float, float, float], placement_m: Tuple[float, float, float], yaw_pitch_roll_deg: Tuple[float, float, float], fully_displace: Optional[bool] = False) -> None:
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 (in m) of the placement_point of 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.
def get_physical_properties( self, concrete_parameters: Dict[str, float], placement_point: Tuple[float, float, float], yaw_pitch_roll_deg: Tuple[float, float, float], material_density_kg_m3: float, normalize_origin: bool) -> Dict[str, float]:
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 (in kg/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 physical SymPart properties as computed using the underlying CAD model.
def export_model( self, file_save_path: str, model_type: Literal['freecad', 'step', 'stl'], concrete_parameters: Dict[str, float], placement_point: Tuple[float, float, float], yaw_pitch_roll_deg: Tuple[float, float, float]) -> None:
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.