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 {&#10;  viewportMapping ADJUST_CAMERA&#10;  orientation 1 0 0  1.5707965&#10;  aspectRatio 1&#10;}&#10;"/>\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)
PART_FEATURE_STRING = 'Part::Feature'
CAD_BASE_PATH: pathlib.Path = PosixPath('/home/runner/work/SymCAD/SymCAD/src/symcad/cadmodels')
def is_symbolic(val: Any) -> bool:
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.

def get_free_parameters_from_model(cad_file_path: str, document: Optional[App.Document]) -> List[str]:
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 or None if no open document already exists.
Returns
  • List[str]: The list of free parameters in the CAD model.
def get_free_parameters_from_method( creation_method: Callable[[Dict[str, float], bool], Part.Solid]) -> List[str]:
 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.
def assign_free_parameter_values( cad_file_path: str, doc: App.Document, concrete_parameters: Dict[str, float]) -> None:
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.
def compute_placement_point(part: Part.Solid, origin: Tuple[float, float, float]) -> Base.Vector:
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 (in mm) in its FreeCAD representation format.
def fetch_model_physical_properties( model: Part.Feature, displaced_model: Optional[Part.Feature], material_density_kg_m3: float, normalize_origin: bool) -> Dict[str, float]:
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 (in kg/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.
def fetch_assembly_physical_properties( assembly: App.Document, displaced: App.Document, material_densities: Dict[str, float]) -> Dict[str, float]:
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 (in kg/m^3).
Returns
  • Dict[str, float]: A dictionary containing all physical properties of the underlying CAD assembly.
def retrieve_interferences(assembly: App.Document) -> List[Tuple[str, str]]:
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.
def save_model( file_path: str, model_type: Literal['freecad', 'step', 'stl'], model: Part.Feature) -> None:
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.
def save_assembly( file_path: str, cad_type: Literal['freecad', 'step', 'stl'], assembly: App.Document) -> None:
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.
def write_freecad_gui_doc( file_path: str, model: Union[App.Document, Part.Feature], is_assembly: bool) -> None:
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 {&#10;  viewportMapping ADJUST_CAMERA&#10;  orientation 1 0 0  1.5707965&#10;  aspectRatio 1&#10;}&#10;"/>\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.