├── .gitignore ├── Gui └── Resources │ ├── compile_resources_pack.py │ ├── icons │ ├── Draft_Move.svg │ ├── EditUndo.svg │ ├── angleConstraint.svg │ ├── assembly2SolveConstraints.svg │ ├── axialConstraint.svg │ ├── boltMultipleCircularEdges.svg │ ├── checkAssembly.svg │ ├── circularEdgeConstraint.svg │ ├── degreesOfFreedomAnimation.svg │ ├── flipConstraint.svg │ ├── help.svg │ ├── importPart.svg │ ├── importPart_update.svg │ ├── lockRotation.svg │ ├── muxAssembly.svg │ ├── muxAssemblyRefresh.svg │ ├── partsList.svg │ ├── pause.svg │ ├── planeConstraint.svg │ ├── play.svg │ ├── preferences-assembly2.svg │ ├── sphericalSurfaceConstraint.svg │ ├── stop.svg │ └── workBenchIcon.svg │ ├── resources.rcc │ ├── ui │ ├── assembly2_prefs.ui │ ├── degreesOfFreedomAnimation.ui │ └── partsList.ui │ └── wiki │ ├── Assembly2_AngleConstraint.png │ ├── Assembly2_AxialConstraint.png │ ├── Assembly2_BoltMultipleCircularEdges.png │ ├── Assembly2_CheckAssembly.png │ ├── Assembly2_CircularEdgeConstraint.png │ ├── Assembly2_DegreesOfFreedomAnimation.png │ ├── Assembly2_DraftMove.png │ ├── Assembly2_FlipConstraint.png │ ├── Assembly2_Help.png │ ├── Assembly2_ImportPart.png │ ├── Assembly2_ImportPartUpdate.png │ ├── Assembly2_LockRotation.png │ ├── Assembly2_MuxAssembly.png │ ├── Assembly2_MuxAssemblyRefresh.png │ ├── Assembly2_PartsList.png │ ├── Assembly2_Pause.png │ ├── Assembly2_PlaneConstraint.png │ ├── Assembly2_Play.png │ ├── Assembly2_PreferencesAssembly2.png │ ├── Assembly2_SolveConstraints.png │ ├── Assembly2_SphericalSurfaceConstraint.png │ ├── Assembly2_Stop.png │ ├── Assembly2_WorkBenchIcon.png │ └── Thumbs.db ├── InitGui.py ├── LICENSE ├── README.md ├── assembly2 ├── __init__.py ├── constraints │ ├── __init__.py │ ├── angleConstraint.py │ ├── axialConstraint.py │ ├── circularEdgeConstraint.py │ ├── common.py │ ├── objectProxy.py │ ├── planeConstraint.py │ ├── sphericalSurfaceConstraint.py │ └── viewProviderProxy.py ├── core.py ├── importPart │ ├── __init__.py │ ├── fcstd_parser.py │ ├── importPath.py │ ├── path_lib.py │ ├── selectionMigration.py │ ├── tests.py │ └── viewProviderProxy.py ├── lib3D │ ├── __init__.py │ └── tests.py ├── selection.py ├── solvers │ ├── __init__.py │ ├── common.py │ ├── dof_reduction_solver │ │ ├── __init__.py │ │ ├── cache.py │ │ ├── constraintSystems.py │ │ ├── degreesOfFreedom.py │ │ ├── docs │ │ │ ├── assembly2_docs.aux │ │ │ ├── assembly2_docs.pdf │ │ │ ├── assembly2_docs.tex │ │ │ └── assembly2_docs.tex.backup │ │ ├── lineSearches.py │ │ ├── solverLib.py │ │ ├── tests.py │ │ └── variableManager.py │ ├── newton_solver │ │ ├── __init__.py │ │ ├── constraints.py │ │ ├── tests.py │ │ └── variableManager.py │ └── test_assemblies │ │ ├── testAssembly_01.fcstd │ │ ├── testAssembly_02.fcstd │ │ ├── testAssembly_03.fcstd │ │ ├── testAssembly_04.fcstd │ │ ├── testAssembly_05.fcstd │ │ ├── testAssembly_06.fcstd │ │ ├── testAssembly_07.fcstd │ │ ├── testAssembly_08.fcstd │ │ ├── testAssembly_09.fcstd │ │ ├── testAssembly_10-block_iregular_constraint_order.fcstd │ │ ├── testAssembly_11-pipe_assembly.fcstd │ │ ├── testAssembly_11b-pipe_assembly.fcstd │ │ ├── testAssembly_12-angles_clock_face.fcstd │ │ ├── testAssembly_13-spherical_surfaces_cube_vertices.fcstd │ │ ├── testAssembly_13-spherical_surfaces_hip.fcstd │ │ ├── testAssembly_14-lock_relative_axial_rotation.fcstd │ │ ├── testAssembly_15-triangular_link_assembly.fcstd │ │ ├── testAssembly_16-revolved_surface_objs.fcstd │ │ ├── testAssembly_17-bspline_objects.fcstd │ │ └── testAssembly_18-add_free_objects.fcstd └── utils │ ├── __init__.py │ ├── animate_constraint.py │ ├── boltMultipleCircularEdges.py │ ├── checkAssembly.py │ ├── degreesOfFreedomAnimation.py │ ├── muxAssembly.py │ ├── partsList.py │ ├── randomClrs.py │ ├── tests.py │ └── undo.py ├── assembly2lib.py ├── importPart.py ├── test.py └── viewProviderProxies.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | lib/ 17 | lib64/ 18 | parts/ 19 | sdist/ 20 | var/ 21 | *.egg-info/ 22 | .installed.cfg 23 | *.egg 24 | 25 | # PyInstaller 26 | # Usually these files are written by a python script from a template 27 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 28 | *.manifest 29 | *.spec 30 | 31 | # Installer logs 32 | pip-log.txt 33 | pip-delete-this-directory.txt 34 | 35 | # Unit test / coverage reports 36 | htmlcov/ 37 | .tox/ 38 | .coverage 39 | .cache 40 | nosetests.xml 41 | coverage.xml 42 | 43 | # Translations 44 | *.mo 45 | *.pot 46 | 47 | # Django stuff: 48 | *.log 49 | 50 | # Sphinx documentation 51 | docs/_build/ 52 | 53 | # PyBuilder 54 | target/ 55 | 56 | *~ 57 | 58 | spea2.py 59 | .syncthing.* 60 | *.fcstd1 61 | 62 | docs/assembly2_docs.aux 63 | docs/assembly2_docs.bbl 64 | docs/assembly2_docs.blg 65 | docs/assembly2_docs.log 66 | docs/assembly2_docs.tex.backup 67 | docs/assembly2_docs.toc 68 | 69 | Gui/constraintFile.txt 70 | -------------------------------------------------------------------------------- /Gui/Resources/compile_resources_pack.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | import os, glob 3 | 4 | qrc_filename = 'resources.qrc' 5 | assert not os.path.exists(qrc_filename) 6 | 7 | qrc = ''' 8 | ''' 9 | for fn in glob.glob('icons/*.svg') + glob.glob('icons/welding/*.svg') + glob.glob('ui/*.ui'): 10 | qrc = qrc + '\n\t\t%s' % fn 11 | qrc = qrc + '''\n\t 12 | ''' 13 | 14 | print(qrc) 15 | 16 | f = open(qrc_filename,'w') 17 | f.write(qrc) 18 | f.close() 19 | 20 | os.system('rcc -binary %s -o resources.rcc' % qrc_filename) 21 | os.remove(qrc_filename) 22 | -------------------------------------------------------------------------------- /Gui/Resources/icons/checkAssembly.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 24 | 28 | 32 | 33 | 43 | 45 | 49 | 53 | 54 | 63 | 72 | 73 | 92 | 99 | 100 | 102 | 103 | 105 | image/svg+xml 106 | 108 | 109 | 110 | 111 | 112 | 117 | 120 | 126 | 132 | 133 | 134 | 135 | -------------------------------------------------------------------------------- /Gui/Resources/resources.rcc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hamish2014/FreeCAD_assembly2/2da784f18b8af16facf3c0e28a69f0430dc7bb60/Gui/Resources/resources.rcc -------------------------------------------------------------------------------- /Gui/Resources/ui/degreesOfFreedomAnimation.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | Form 4 | 5 | 6 | 7 | 0 8 | 0 9 | 301 10 | 558 11 | 12 | 13 | 14 | DOF animation 15 | 16 | 17 | 18 | 19 | 20 | 21 | 0 22 | 0 23 | 24 | 25 | 26 | %i Degrees-of-freedom: 27 | 28 | 29 | 30 | 31 | 32 | QAbstractItemView::MultiSelection 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | animate all 42 | 43 | 44 | 45 | 46 | 47 | 48 | Qt::Horizontal 49 | 50 | 51 | 52 | 40 53 | 20 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | animate selected 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 0 75 | 0 76 | 77 | 78 | 79 | Animation parameters: 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | frames per DOF 88 | 89 | 90 | 91 | 92 | 93 | 94 | Qt::Horizontal 95 | 96 | 97 | 98 | 40 99 | 20 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 5 108 | 109 | 110 | 1000 111 | 112 | 113 | 20 114 | 115 | 116 | 40 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | time per frame 128 | 129 | 130 | 131 | 132 | 133 | 134 | Qt::Horizontal 135 | 136 | 137 | 138 | 40 139 | 20 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 1 148 | 149 | 150 | 10000 151 | 152 | 153 | 50 154 | 155 | 156 | 157 | 158 | 159 | 160 | ms 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | rotation amplification 172 | 173 | 174 | 175 | 176 | 177 | 178 | Qt::Horizontal 179 | 180 | 181 | 182 | 40 183 | 20 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 1 192 | 193 | 194 | 0.100000000000000 195 | 196 | 197 | 1.000000000000000 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | linear disp. amplification 209 | 210 | 211 | 212 | 213 | 214 | 215 | Qt::Horizontal 216 | 217 | 218 | 219 | 40 220 | 20 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 1 229 | 230 | 231 | 0.100000000000000 232 | 233 | 234 | 1.000000000000000 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | set as default 244 | 245 | 246 | 247 | 248 | 249 | 250 | <html><head/><body><p>Usage note:</p><p>The DOF animation tool is intended to aid users in fully constraining their assembly.</p><p>To animate a mechanisms motion use the animate constaint command. The animate constraint command is acessible via a constraint's popup/context menu.</p></body></html> 251 | 252 | 253 | true 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | -------------------------------------------------------------------------------- /Gui/Resources/wiki/Assembly2_AngleConstraint.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hamish2014/FreeCAD_assembly2/2da784f18b8af16facf3c0e28a69f0430dc7bb60/Gui/Resources/wiki/Assembly2_AngleConstraint.png -------------------------------------------------------------------------------- /Gui/Resources/wiki/Assembly2_AxialConstraint.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hamish2014/FreeCAD_assembly2/2da784f18b8af16facf3c0e28a69f0430dc7bb60/Gui/Resources/wiki/Assembly2_AxialConstraint.png -------------------------------------------------------------------------------- /Gui/Resources/wiki/Assembly2_BoltMultipleCircularEdges.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hamish2014/FreeCAD_assembly2/2da784f18b8af16facf3c0e28a69f0430dc7bb60/Gui/Resources/wiki/Assembly2_BoltMultipleCircularEdges.png -------------------------------------------------------------------------------- /Gui/Resources/wiki/Assembly2_CheckAssembly.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hamish2014/FreeCAD_assembly2/2da784f18b8af16facf3c0e28a69f0430dc7bb60/Gui/Resources/wiki/Assembly2_CheckAssembly.png -------------------------------------------------------------------------------- /Gui/Resources/wiki/Assembly2_CircularEdgeConstraint.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hamish2014/FreeCAD_assembly2/2da784f18b8af16facf3c0e28a69f0430dc7bb60/Gui/Resources/wiki/Assembly2_CircularEdgeConstraint.png -------------------------------------------------------------------------------- /Gui/Resources/wiki/Assembly2_DegreesOfFreedomAnimation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hamish2014/FreeCAD_assembly2/2da784f18b8af16facf3c0e28a69f0430dc7bb60/Gui/Resources/wiki/Assembly2_DegreesOfFreedomAnimation.png -------------------------------------------------------------------------------- /Gui/Resources/wiki/Assembly2_DraftMove.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hamish2014/FreeCAD_assembly2/2da784f18b8af16facf3c0e28a69f0430dc7bb60/Gui/Resources/wiki/Assembly2_DraftMove.png -------------------------------------------------------------------------------- /Gui/Resources/wiki/Assembly2_FlipConstraint.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hamish2014/FreeCAD_assembly2/2da784f18b8af16facf3c0e28a69f0430dc7bb60/Gui/Resources/wiki/Assembly2_FlipConstraint.png -------------------------------------------------------------------------------- /Gui/Resources/wiki/Assembly2_Help.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hamish2014/FreeCAD_assembly2/2da784f18b8af16facf3c0e28a69f0430dc7bb60/Gui/Resources/wiki/Assembly2_Help.png -------------------------------------------------------------------------------- /Gui/Resources/wiki/Assembly2_ImportPart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hamish2014/FreeCAD_assembly2/2da784f18b8af16facf3c0e28a69f0430dc7bb60/Gui/Resources/wiki/Assembly2_ImportPart.png -------------------------------------------------------------------------------- /Gui/Resources/wiki/Assembly2_ImportPartUpdate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hamish2014/FreeCAD_assembly2/2da784f18b8af16facf3c0e28a69f0430dc7bb60/Gui/Resources/wiki/Assembly2_ImportPartUpdate.png -------------------------------------------------------------------------------- /Gui/Resources/wiki/Assembly2_LockRotation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hamish2014/FreeCAD_assembly2/2da784f18b8af16facf3c0e28a69f0430dc7bb60/Gui/Resources/wiki/Assembly2_LockRotation.png -------------------------------------------------------------------------------- /Gui/Resources/wiki/Assembly2_MuxAssembly.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hamish2014/FreeCAD_assembly2/2da784f18b8af16facf3c0e28a69f0430dc7bb60/Gui/Resources/wiki/Assembly2_MuxAssembly.png -------------------------------------------------------------------------------- /Gui/Resources/wiki/Assembly2_MuxAssemblyRefresh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hamish2014/FreeCAD_assembly2/2da784f18b8af16facf3c0e28a69f0430dc7bb60/Gui/Resources/wiki/Assembly2_MuxAssemblyRefresh.png -------------------------------------------------------------------------------- /Gui/Resources/wiki/Assembly2_PartsList.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hamish2014/FreeCAD_assembly2/2da784f18b8af16facf3c0e28a69f0430dc7bb60/Gui/Resources/wiki/Assembly2_PartsList.png -------------------------------------------------------------------------------- /Gui/Resources/wiki/Assembly2_Pause.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hamish2014/FreeCAD_assembly2/2da784f18b8af16facf3c0e28a69f0430dc7bb60/Gui/Resources/wiki/Assembly2_Pause.png -------------------------------------------------------------------------------- /Gui/Resources/wiki/Assembly2_PlaneConstraint.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hamish2014/FreeCAD_assembly2/2da784f18b8af16facf3c0e28a69f0430dc7bb60/Gui/Resources/wiki/Assembly2_PlaneConstraint.png -------------------------------------------------------------------------------- /Gui/Resources/wiki/Assembly2_Play.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hamish2014/FreeCAD_assembly2/2da784f18b8af16facf3c0e28a69f0430dc7bb60/Gui/Resources/wiki/Assembly2_Play.png -------------------------------------------------------------------------------- /Gui/Resources/wiki/Assembly2_PreferencesAssembly2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hamish2014/FreeCAD_assembly2/2da784f18b8af16facf3c0e28a69f0430dc7bb60/Gui/Resources/wiki/Assembly2_PreferencesAssembly2.png -------------------------------------------------------------------------------- /Gui/Resources/wiki/Assembly2_SolveConstraints.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hamish2014/FreeCAD_assembly2/2da784f18b8af16facf3c0e28a69f0430dc7bb60/Gui/Resources/wiki/Assembly2_SolveConstraints.png -------------------------------------------------------------------------------- /Gui/Resources/wiki/Assembly2_SphericalSurfaceConstraint.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hamish2014/FreeCAD_assembly2/2da784f18b8af16facf3c0e28a69f0430dc7bb60/Gui/Resources/wiki/Assembly2_SphericalSurfaceConstraint.png -------------------------------------------------------------------------------- /Gui/Resources/wiki/Assembly2_Stop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hamish2014/FreeCAD_assembly2/2da784f18b8af16facf3c0e28a69f0430dc7bb60/Gui/Resources/wiki/Assembly2_Stop.png -------------------------------------------------------------------------------- /Gui/Resources/wiki/Assembly2_WorkBenchIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hamish2014/FreeCAD_assembly2/2da784f18b8af16facf3c0e28a69f0430dc7bb60/Gui/Resources/wiki/Assembly2_WorkBenchIcon.png -------------------------------------------------------------------------------- /Gui/Resources/wiki/Thumbs.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hamish2014/FreeCAD_assembly2/2da784f18b8af16facf3c0e28a69f0430dc7bb60/Gui/Resources/wiki/Thumbs.db -------------------------------------------------------------------------------- /InitGui.py: -------------------------------------------------------------------------------- 1 | import assembly2 #QtCore.QResource.registerResource happens here 2 | import FreeCAD 3 | 4 | class Assembly2Workbench (Workbench): 5 | MenuText = 'Assembly 2' 6 | def Initialize(self): 7 | commandslist = [ 8 | 'assembly2_importPart', 9 | 'assembly2_updateImportedPartsCommand', 10 | 'assembly2_movePart', 11 | 'assembly2_addCircularEdgeConstraint', 12 | 'assembly2_addPlaneConstraint', 13 | 'assembly2_addAxialConstraint', 14 | 'assembly2_addAngleConstraint', 15 | 'assembly2_addSphericalSurfaceConstraint', 16 | #'assembly2_undoConstraint', not ready yet 17 | 'assembly2_degreesOfFreedomAnimation', 18 | 'assembly2_solveConstraints', 19 | 'assembly2_muxAssembly', 20 | 'assembly2_muxAssemblyRefresh', 21 | 'assembly2_addPartsList', 22 | 'assembly2_checkAssembly' 23 | ] 24 | self.appendToolbar('Assembly 2', commandslist) 25 | shortcut_commandslist = [ 26 | 'assembly2_flipLastConstraintsDirection', 27 | 'assembly2_lockLastConstraintsRotation', 28 | 'assembly2_boltMultipleCircularEdges', 29 | ] 30 | self.appendToolbar('Assembly 2 shortcuts', shortcut_commandslist ) 31 | self.treecmdList = [ 32 | 'assembly2_importPart', 33 | 'assembly2_updateImportedPartsCommand' 34 | ] 35 | FreeCADGui.addIconPath( ':/assembly2/icons' ) 36 | FreeCADGui.addPreferencePage( ':/assembly2/ui/assembly2_prefs.ui','Assembly2' ) 37 | self.appendMenu('Assembly 2', commandslist) 38 | 39 | def Activated(self): 40 | from assembly2.constraints import updateOldStyleConstraintProperties 41 | import os 42 | doc = FreeCAD.activeDocument() 43 | if hasattr(doc, 'Objects'): 44 | updateOldStyleConstraintProperties(doc) 45 | GuiPath = os.path.expanduser ("~") 46 | constraintFile = os.path.join( GuiPath , 'constraintFile.txt') 47 | if os.path.exists(constraintFile): 48 | os.remove(constraintFile) 49 | 50 | def ContextMenu(self, recipient): 51 | selection = [s for s in FreeCADGui.Selection.getSelection() if s.Document == FreeCAD.ActiveDocument ] 52 | if len(selection) == 1: 53 | obj = selection[0] 54 | if hasattr(obj,'Content'): 55 | if 'ConstraintInfo' in obj.Content or 'ConstraintNfo' in obj.Content: 56 | redefineCmd = { 57 | 'plane':'assembly2_redefinePlaneConstraint', 58 | 'angle_between_planes':'assembly2_redefineAngleConstraint', 59 | 'axial': 'assembly2_redefineAxialConstraint', 60 | 'circularEdge' : 'assembly2_redefineCircularEdgeConstraint', 61 | 'sphericalSurface' : 'assembly2_redefineSphericalSurfaceConstraint' 62 | }[ obj.Type ] 63 | self.appendContextMenu( "Assembly2", [ 64 | 'assembly2_animate_constraint', 65 | redefineCmd, 66 | 'assembly2_selectConstraintObjects', 67 | 'assembly2_selectConstraintElements' 68 | ]) 69 | if 'sourceFile' in obj.Content: 70 | self.appendContextMenu( 71 | "Assembly2", 72 | [ 'assembly2_movePart', 73 | 'assembly2_duplicatePart', 74 | 'assembly2_editImportedPart', 75 | 'assembly2_forkImportedPart', 76 | 'assembly2_deletePartsConstraints', 77 | 'assembly2_randomColorAll'] 78 | ) 79 | 80 | Icon = ':/assembly2/icons/workBenchIcon.svg' 81 | 82 | Gui.addWorkbench( Assembly2Workbench() ) 83 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | FreeCAD_assembly2 2 | ================= 3 | 4 | Assembly workbench for FreeCAD v0.15, 0.16 and 0.17 with support for importing parts from external files. 5 | This workbench in not maintained. 6 | 7 | 8 | Linux Installation Instructions 9 | ------------------------------- 10 | 11 | For Ubuntu (Linux Mint) we recommend to add the community ppa to your systems software 12 | resources and install via the sysnaptic package manager the addon of your liking. 13 | Refer to here for more information: 14 | https://launchpad.net/~freecad-community/+archive/ubuntu/ppa 15 | 16 | On other Linux distros you may try to install manually via BASH and git: 17 | 18 | ```bash 19 | $ sudo apt-get install git python-numpy python-pyside 20 | $ mkdir ~/.FreeCAD/Mod 21 | $ cd ~/.FreeCAD/Mod 22 | $ git clone https://github.com/hamish2014/FreeCAD_assembly2.git 23 | ``` 24 | 25 | Once installed, use git to easily update to the latest version: 26 | 27 | ```bash 28 | $ cd ~/.FreeCAD/Mod/FreeCAD_assembly2 29 | $ git pull 30 | $ rm *.pyc 31 | ``` 32 | 33 | Windows Installation Instructions 34 | --------------------------------- 35 | 36 | Please use the FreeCAD-Addons-Installer provided here: 37 | https://github.com/FreeCAD/FreeCAD-addons 38 | 39 | For more in-depth information refer to the corresponding tutorial on the FreeCAD-Homepage: 40 | http://www.freecadweb.org/wiki/index.php?title=How_to_install_additional_workbenches 41 | 42 | Mac Installation Instructions 43 | ----------------------------- 44 | 45 | * download the git repository as ZIP 46 | * assuming FreeCAD is installed in "/Applications/FreeCAD/v 0.15", go to "/Applications/FreeCAD/v 0.15" in the Browser, and select FreeCAD.app 47 | * right-click and select "Show Package Contents", a new window will appear with a folder named "Contents" 48 | * single-click on the folder "Contents" and select the folder "Mod" 49 | * in the folder "Mod" create a new folder named "assembly2" 50 | * unzip downloaded repository in the folder "Contents/Mod/assembly2" 51 | (Thanks piffpoof) 52 | 53 | 54 | For more in-depth information refer to the corresponding tutorial on the FreeCAD-Homepage: 55 | http://www.freecadweb.org/wiki/index.php?title=How_to_install_additional_workbenches 56 | 57 | Testing 58 | ------- 59 | ```bash 60 | $ cd ~/.FreeCAD/Mod/FreeCAD_assembly2 61 | $ python test.py 62 | ``` 63 | 64 | Acknowledgements 65 | ---------------- 66 | 67 | My thanks to BRLRFE, easyw-fc (Maurice), kbwbe and the various others who contributed to this workbench. 68 | 69 | Wiki 70 | ---- 71 | 72 | For instructions on usage of the workbench refer to the wiki 73 | [link on top of the page](https://github.com/hamish2014/FreeCAD_assembly2/wiki). 74 | -------------------------------------------------------------------------------- /assembly2/__init__.py: -------------------------------------------------------------------------------- 1 | from .core import __dir__ 2 | from . import importPart 3 | from . import constraints 4 | from .constraints import angleConstraint 5 | from . import utils 6 | from .utils import boltMultipleCircularEdges #imported here to avoid import loop 7 | from . import lib3D 8 | -------------------------------------------------------------------------------- /assembly2/constraints/__init__.py: -------------------------------------------------------------------------------- 1 | ''' 2 | assembly2 constraints are stored under App::FeaturePython object (constraintObj) 3 | 4 | cName = findUnusedObjectName('axialConstraint') 5 | c = FreeCAD.ActiveDocument.addObject("App::FeaturePython", cName) 6 | c.addProperty("App::PropertyString","Type","ConstraintInfo","Object 1").Type = '...' 7 | 8 | see http://www.freecadweb.org/wiki/index.php?title=Scripted_objects#Available_properties for more information 9 | ''' 10 | 11 | import FreeCAD 12 | from assembly2.constraints import angleConstraint 13 | from assembly2.constraints import axialConstraint 14 | from assembly2.constraints import circularEdgeConstraint 15 | from assembly2.constraints import planeConstraint 16 | from assembly2.constraints import sphericalSurfaceConstraint 17 | from assembly2.constraints.viewProviderProxy import ConstraintViewProviderProxy 18 | from assembly2.core import debugPrint 19 | from assembly2.constraints.common import updateObjectProperties 20 | 21 | def updateOldStyleConstraintProperties( doc ): 22 | 'used to update old constraint attributes, [object, faceInd] -> [object, subElement]...' 23 | for obj in doc.Objects: 24 | if 'ConstraintInfo' in obj.Content: 25 | updateObjectProperties( obj ) 26 | 27 | def removeConstraint( constraint ): 28 | 'required as constraint.Proxy.onDelete only called when deleted through GUI' 29 | doc = constraint.Document 30 | debugPrint(2, "removing constraint %s" % constraint.Name ) 31 | if constraint.ViewObject != None: #do not this check is actually nessary ... 32 | constraint.ViewObject.Proxy.onDelete( constraint.ViewObject, None ) 33 | doc.removeObject( constraint.Name ) 34 | -------------------------------------------------------------------------------- /assembly2/constraints/angleConstraint.py: -------------------------------------------------------------------------------- 1 | from .common import * 2 | 3 | class PlaneSelectionGate: 4 | def allow(self, doc, obj, sub): 5 | s = SelectionExObject(doc, obj, sub) 6 | return planeSelected(s) or LinearEdgeSelected(s) or AxisOfPlaneSelected(s) 7 | 8 | def parseSelection(selection, objectToUpdate=None): 9 | validSelection = False 10 | if len(selection) == 2: 11 | s1, s2 = selection 12 | if s1.ObjectName != s2.ObjectName: 13 | if ( planeSelected(s1) or LinearEdgeSelected(s1) or AxisOfPlaneSelected(s1)) \ 14 | and ( planeSelected(s2) or LinearEdgeSelected(s2) or AxisOfPlaneSelected(s2)): 15 | validSelection = True 16 | cParms = [ [s1.ObjectName, s1.SubElementNames[0], s1.Object.Label ], 17 | [s2.ObjectName, s2.SubElementNames[0], s2.Object.Label ] ] 18 | if not validSelection: 19 | msg = '''Angle constraint requires a selection of 2 planes or two straight lines/axis, each from different objects.Selection made: 20 | %s''' % printSelection(selection) 21 | QtGui.QMessageBox.information( QtGui.QApplication.activeWindow(), "Incorrect Usage", msg) 22 | return 23 | 24 | if objectToUpdate is None: 25 | cName = findUnusedObjectName('angleConstraint') 26 | debugPrint(2, "creating %s" % cName ) 27 | c = FreeCAD.ActiveDocument.addObject("App::FeaturePython", cName) 28 | c.addProperty("App::PropertyString","Type","ConstraintInfo").Type = 'angle_between_planes' 29 | c.addProperty("App::PropertyString","Object1","ConstraintInfo").Object1 = cParms[0][0] 30 | c.addProperty("App::PropertyString","SubElement1","ConstraintInfo").SubElement1 = cParms[0][1] 31 | c.addProperty("App::PropertyString","Object2","ConstraintInfo").Object2 = cParms[1][0] 32 | c.addProperty("App::PropertyString","SubElement2","ConstraintInfo").SubElement2 = cParms[1][1] 33 | c.addProperty("App::PropertyAngle","angle","ConstraintInfo") 34 | c.Object1 = cParms[0][0] 35 | c.SubElement1 = cParms[0][1] 36 | c.Object2 = cParms[1][0] 37 | c.SubElement2 = cParms[1][1] 38 | for prop in ["Object1","Object2","SubElement1","SubElement2","Type"]: 39 | c.setEditorMode(prop, 1) 40 | c.Proxy = ConstraintObjectProxy() 41 | c.ViewObject.Proxy = ConstraintViewProviderProxy( c, ':/assembly2/icons/angleConstraint.svg', True, cParms[1][2], cParms[0][2]) 42 | else: 43 | debugPrint(2, "redefining %s" % objectToUpdate.Name ) 44 | c = objectToUpdate 45 | c.Object1 = cParms[0][0] 46 | c.SubElement1 = cParms[0][1] 47 | c.Object2 = cParms[1][0] 48 | c.SubElement2 = cParms[1][1] 49 | updateObjectProperties(c) 50 | constraintFile = os.path.join( GuiPath , 'constraintFile.txt') 51 | with open(constraintFile, 'w') as outfile: 52 | outfile.write(make_string(s1.ObjectName)+'\n'+str(s1.Object.Placement.Base)+'\n'+str(s1.Object.Placement.Rotation)+'\n') 53 | outfile.write(make_string(s2.ObjectName)+'\n'+str(s2.Object.Placement.Base)+'\n'+str(s2.Object.Placement.Rotation)+'\n') 54 | constraints = [ obj for obj in FreeCAD.ActiveDocument.Objects if 'ConstraintInfo' in obj.Content ] 55 | #print constraints 56 | if len(constraints) > 0: 57 | constraintFile = os.path.join( GuiPath , 'constraintFile.txt') 58 | if os.path.exists(constraintFile): 59 | with open(constraintFile, 'a') as outfile: 60 | lastConstraintAdded = constraints[-1] 61 | outfile.write(make_string(lastConstraintAdded.Name)+'\n') 62 | 63 | c.purgeTouched() 64 | c.Proxy.callSolveConstraints() 65 | repair_tree_view() 66 | 67 | 68 | selection_text = '''Selection options: 69 | - plane surface 70 | - edge 71 | - axis of plane selected''' 72 | 73 | class AngleConstraintCommand: 74 | def Activated(self): 75 | selection = FreeCADGui.Selection.getSelectionEx() 76 | sel = FreeCADGui.Selection.getSelection() 77 | if len(selection) == 2: 78 | parseSelection( selection ) 79 | else: 80 | FreeCADGui.Selection.clearSelection() 81 | ConstraintSelectionObserver( 82 | PlaneSelectionGate(), 83 | parseSelection, 84 | taskDialog_title ='add angular constraint', 85 | taskDialog_iconPath = self.GetResources()['Pixmap'], 86 | taskDialog_text = selection_text ) 87 | 88 | def GetResources(self): 89 | msg = 'Create an angular constraint between two planes' 90 | return { 91 | 'Pixmap' : ':/assembly2/icons/angleConstraint.svg', 92 | 'MenuText': msg, 93 | 'ToolTip': msg, 94 | } 95 | 96 | FreeCADGui.addCommand('assembly2_addAngleConstraint', AngleConstraintCommand()) 97 | 98 | 99 | class RedefineConstraintCommand: 100 | def Activated(self): 101 | self.constObject = FreeCADGui.Selection.getSelectionEx()[0].Object 102 | debugPrint(3,'redefining %s' % self.constObject.Name) 103 | FreeCADGui.Selection.clearSelection() 104 | ConstraintSelectionObserver( 105 | PlaneSelectionGate(), 106 | self.UpdateConstraint, 107 | taskDialog_title ='redefine angular constraint', 108 | taskDialog_iconPath = ':/assembly2/icons/angleConstraint.svg', 109 | taskDialog_text = selection_text ) 110 | 111 | def UpdateConstraint(self, selection): 112 | parseSelection( selection, self.constObject) 113 | 114 | def GetResources(self): 115 | return { 'MenuText': 'Redefine' } 116 | FreeCADGui.addCommand('assembly2_redefineAngleConstraint', RedefineConstraintCommand()) 117 | -------------------------------------------------------------------------------- /assembly2/constraints/axialConstraint.py: -------------------------------------------------------------------------------- 1 | from .common import * 2 | 3 | class AxialSelectionGate: 4 | def allow(self, doc, obj, sub): 5 | return ValidSelection(SelectionExObject(doc, obj, sub)) 6 | 7 | def ValidSelection(selectionExObj): 8 | return cylindricalPlaneSelected(selectionExObj)\ 9 | or LinearEdgeSelected(selectionExObj)\ 10 | or AxisOfPlaneSelected(selectionExObj) 11 | 12 | def parseSelection(selection, objectToUpdate=None): 13 | validSelection = False 14 | if len(selection) == 2: 15 | s1, s2 = selection 16 | if s1.ObjectName != s2.ObjectName: 17 | if ValidSelection(s1) and ValidSelection(s2): 18 | validSelection = True 19 | cParms = [ [s1.ObjectName, s1.SubElementNames[0], s1.Object.Label ], 20 | [s2.ObjectName, s2.SubElementNames[0], s2.Object.Label ] ] 21 | debugPrint(4,'cParms = %s' % (cParms)) 22 | if not validSelection: 23 | msg = '''To add an axial constraint select two cylindrical surfaces or two straight lines, each from a different part. Selection made: 24 | %s''' % printSelection(selection) 25 | QtGui.QMessageBox.information( QtGui.QApplication.activeWindow(), "Incorrect Usage", msg) 26 | return 27 | 28 | if objectToUpdate == None: 29 | cName = findUnusedObjectName('axialConstraint') 30 | debugPrint(2, "creating %s" % cName ) 31 | c = FreeCAD.ActiveDocument.addObject("App::FeaturePython", cName) 32 | c.addProperty("App::PropertyString","Type","ConstraintInfo","Object 1").Type = 'axial' 33 | c.addProperty("App::PropertyString","Object1","ConstraintInfo").Object1 = cParms[0][0] 34 | c.addProperty("App::PropertyString","SubElement1","ConstraintInfo").SubElement1 = cParms[0][1] 35 | c.addProperty("App::PropertyString","Object2","ConstraintInfo").Object2 = cParms[1][0] 36 | c.addProperty("App::PropertyString","SubElement2","ConstraintInfo").SubElement2 = cParms[1][1] 37 | 38 | c.addProperty("App::PropertyEnumeration","directionConstraint", "ConstraintInfo") 39 | c.directionConstraint = ["none","aligned","opposed"] 40 | c.addProperty("App::PropertyBool","lockRotation","ConstraintInfo") 41 | 42 | c.setEditorMode('Type',1) 43 | for prop in ["Object1","Object2","SubElement1","SubElement2"]: 44 | c.setEditorMode(prop, 1) 45 | 46 | c.Proxy = ConstraintObjectProxy() 47 | c.ViewObject.Proxy = ConstraintViewProviderProxy( c, ':/assembly2/icons/axialConstraint.svg', True, cParms[1][2], cParms[0][2]) 48 | else: 49 | debugPrint(2, "redefining %s" % objectToUpdate.Name ) 50 | c = objectToUpdate 51 | c.Object1 = cParms[0][0] 52 | c.SubElement1 = cParms[0][1] 53 | c.Object2 = cParms[1][0] 54 | c.SubElement2 = cParms[1][1] 55 | updateObjectProperties(c) 56 | constraintFile = os.path.join( GuiPath , 'constraintFile.txt') 57 | with open(constraintFile, 'w') as outfile: 58 | outfile.write(make_string(s1.ObjectName)+'\n'+str(s1.Object.Placement.Base)+'\n'+str(s1.Object.Placement.Rotation)+'\n') 59 | outfile.write(make_string(s2.ObjectName)+'\n'+str(s2.Object.Placement.Base)+'\n'+str(s2.Object.Placement.Rotation)+'\n') 60 | constraints = [ obj for obj in FreeCAD.ActiveDocument.Objects if 'ConstraintInfo' in obj.Content ] 61 | #print constraints 62 | if len(constraints) > 0: 63 | constraintFile = os.path.join( GuiPath , 'constraintFile.txt') 64 | if os.path.exists(constraintFile): 65 | with open(constraintFile, 'a') as outfile: 66 | lastConstraintAdded = constraints[-1] 67 | outfile.write(make_string(lastConstraintAdded.Name)+'\n') 68 | 69 | c.purgeTouched() 70 | c.Proxy.callSolveConstraints() 71 | repair_tree_view() 72 | 73 | selection_text = '''Selection options: 74 | - cylindrical surface 75 | - edge 76 | - face ''' 77 | 78 | class AxialConstraintCommand: 79 | def Activated(self): 80 | selection = FreeCADGui.Selection.getSelectionEx() 81 | sel = FreeCADGui.Selection.getSelection() 82 | if len(selection) == 2: 83 | parseSelection( selection ) 84 | else: 85 | FreeCADGui.Selection.clearSelection() 86 | ConstraintSelectionObserver( 87 | AxialSelectionGate(), 88 | parseSelection, 89 | taskDialog_title ='add axial constraint', 90 | taskDialog_iconPath = self.GetResources()['Pixmap'], 91 | taskDialog_text = selection_text 92 | ) 93 | def GetResources(self): 94 | return { 95 | 'Pixmap' : ':/assembly2/icons/axialConstraint.svg', 96 | 'MenuText': 'Add axial constraint', 97 | 'ToolTip': 'Add an axial constraint between two objects' 98 | } 99 | 100 | FreeCADGui.addCommand('assembly2_addAxialConstraint', AxialConstraintCommand()) 101 | 102 | class RedefineConstraintCommand: 103 | def Activated(self): 104 | self.constObject = FreeCADGui.Selection.getSelectionEx()[0].Object 105 | debugPrint(3,'redefining %s' % self.constObject.Name) 106 | FreeCADGui.Selection.clearSelection() 107 | ConstraintSelectionObserver( 108 | AxialSelectionGate(), 109 | self.UpdateConstraint, 110 | taskDialog_title ='redefine axial constraint', 111 | taskDialog_iconPath = ':/assembly2/icons/axialConstraint.svg', 112 | taskDialog_text = selection_text 113 | ) 114 | # 115 | #if wb_globals.has_key('selectionObserver'): 116 | # wb_globals['selectionObserver'].stopSelectionObservation() 117 | #wb_globals['selectionObserver'] = ConstraintSelectionObserver( AxialSelectionGate(), self.UpdateConstraint ) 118 | def UpdateConstraint(self, selection): 119 | parseSelection( selection, self.constObject) 120 | def GetResources(self): 121 | return { 'MenuText': 'Redefine' } 122 | FreeCADGui.addCommand('assembly2_redefineAxialConstraint', RedefineConstraintCommand()) 123 | -------------------------------------------------------------------------------- /assembly2/constraints/circularEdgeConstraint.py: -------------------------------------------------------------------------------- 1 | from .common import * 2 | 3 | class CircularEdgeSelectionGate: 4 | def allow(self, doc, obj, sub): 5 | return ValidSelectionFunct(SelectionExObject(doc, obj, sub),doc, obj, sub) 6 | 7 | def ValidSelectionFunct(selectionExObj, doc, obj, sub): 8 | return CircularEdgeSelected( SelectionExObject(doc, obj, sub) )\ 9 | or AxisOfPlaneSelected(selectionExObj) 10 | 11 | def parseSelection(selection, objectToUpdate=None, callSolveConstraints=True, lockRotation = False): 12 | validSelection = False 13 | if len(selection) == 2: 14 | s1, s2 = selection 15 | if s1.ObjectName != s2.ObjectName: 16 | validSelection = True 17 | cParms = [ 18 | [s1.ObjectName, s1.SubElementNames[0], s1.Object.Label ], 19 | [s2.ObjectName, s2.SubElementNames[0], s2.Object.Label ] 20 | ] 21 | debugPrint(4,'cParms = %s' % (cParms)) 22 | if not validSelection: 23 | msg = '''To add a circular edge constraint select two circular edges, each from a different part. Selection made: 24 | %s''' % printSelection(selection) 25 | QtGui.QMessageBox.information( QtGui.QApplication.activeWindow(), "Incorrect Usage", msg) 26 | return 27 | 28 | if objectToUpdate == None: 29 | cName = findUnusedObjectName('circularEdgeConstraint') 30 | debugPrint(2, "creating %s" % cName ) 31 | c = FreeCAD.ActiveDocument.addObject("App::FeaturePython", cName) 32 | 33 | c.addProperty("App::PropertyString","Type","ConstraintInfo").Type = 'circularEdge' 34 | c.addProperty("App::PropertyString","Object1","ConstraintInfo").Object1 = cParms[0][0] 35 | c.addProperty("App::PropertyString","SubElement1","ConstraintInfo").SubElement1 = cParms[0][1] 36 | c.addProperty("App::PropertyString","Object2","ConstraintInfo").Object2 = cParms[1][0] 37 | c.addProperty("App::PropertyString","SubElement2","ConstraintInfo").SubElement2 = cParms[1][1] 38 | 39 | c.addProperty("App::PropertyEnumeration","directionConstraint", "ConstraintInfo") 40 | c.directionConstraint = ["none","aligned","opposed"] 41 | c.addProperty("App::PropertyDistance","offset","ConstraintInfo") 42 | c.addProperty("App::PropertyBool","lockRotation","ConstraintInfo").lockRotation = lockRotation 43 | 44 | c.setEditorMode('Type',1) 45 | for prop in ["Object1","Object2","SubElement1","SubElement2"]: 46 | c.setEditorMode(prop, 1) 47 | 48 | c.Proxy = ConstraintObjectProxy() 49 | c.ViewObject.Proxy = ConstraintViewProviderProxy( c, ':/assembly2/icons/circularEdgeConstraint.svg', True, cParms[1][2], cParms[0][2]) 50 | else: 51 | debugPrint(2, "redefining %s" % objectToUpdate.Name ) 52 | c = objectToUpdate 53 | c.Object1 = cParms[0][0] 54 | c.SubElement1 = cParms[0][1] 55 | c.Object2 = cParms[1][0] 56 | c.SubElement2 = cParms[1][1] 57 | updateObjectProperties(c) 58 | recordConstraints( FreeCAD.ActiveDocument, s1, s2 ) 59 | c.purgeTouched() 60 | if callSolveConstraints: 61 | c.Proxy.callSolveConstraints() 62 | repair_tree_view() 63 | #FreeCADGui.Selection.clearSelection() 64 | #FreeCADGui.Selection.addSelection(c) 65 | return c 66 | 67 | selection_text = '''Select circular edges or faces''' 68 | 69 | class CircularEdgeConstraintCommand: 70 | def Activated(self): 71 | selection = FreeCADGui.Selection.getSelectionEx() 72 | if len(selection) == 2: 73 | parseSelection( selection ) 74 | else: 75 | FreeCADGui.Selection.clearSelection() 76 | ConstraintSelectionObserver( 77 | CircularEdgeSelectionGate(), 78 | parseSelection, 79 | taskDialog_title ='add circular edge constraint', 80 | taskDialog_iconPath = self.GetResources()['Pixmap'], 81 | taskDialog_text = selection_text 82 | ) 83 | 84 | def GetResources(self): 85 | return { 86 | 'Pixmap' : ':/assembly2/icons/circularEdgeConstraint.svg' , 87 | 'MenuText': 'Add circular edge constraint', 88 | 'ToolTip': 'Add a circular edge constraint between two objects' 89 | } 90 | 91 | FreeCADGui.addCommand('assembly2_addCircularEdgeConstraint', CircularEdgeConstraintCommand()) 92 | 93 | 94 | class RedefineCircularEdgeConstraintCommand: 95 | def Activated(self): 96 | self.constObject = FreeCADGui.Selection.getSelectionEx()[0].Object 97 | debugPrint(3,'redefining %s' % self.constObject.Name) 98 | FreeCADGui.Selection.clearSelection() 99 | ConstraintSelectionObserver( 100 | CircularEdgeSelectionGate(), 101 | self.UpdateConstraint, 102 | taskDialog_title ='redefine circular edge constraint', 103 | taskDialog_iconPath = ':/assembly2/icons/circularEdgeConstraint.svg', 104 | taskDialog_text = selection_text 105 | ) 106 | 107 | def UpdateConstraint(self, selection): 108 | parseSelection( selection, self.constObject) 109 | 110 | def GetResources(self): 111 | return { 'MenuText': 'Redefine' } 112 | FreeCADGui.addCommand('assembly2_redefineCircularEdgeConstraint', RedefineCircularEdgeConstraintCommand()) 113 | 114 | 115 | class FlipLastConstraintsDirectionCommand: 116 | def Activated(self): 117 | constraints = [ obj for obj in FreeCAD.ActiveDocument.Objects 118 | if 'ConstraintInfo' in obj.Content ] 119 | if len(constraints) == 0: 120 | QtGui.QMessageBox.information( QtGui.QApplication.activeWindow(), "Command Aborted", 'Flip aborted since no assembly2 constraints in active document.') 121 | return 122 | lastConstraintAdded = constraints[-1] 123 | if hasattr( lastConstraintAdded, 'directionConstraint' ): 124 | if lastConstraintAdded.directionConstraint == "none": 125 | QtGui.QMessageBox.information( QtGui.QApplication.activeWindow(), "Command Aborted", 'Flip aborted since direction of the last constraint is unset') 126 | return 127 | if lastConstraintAdded.directionConstraint == "aligned": 128 | lastConstraintAdded.directionConstraint = "opposed" 129 | else: 130 | lastConstraintAdded.directionConstraint = "aligned" 131 | elif hasattr( lastConstraintAdded, 'angle' ): 132 | if lastConstraintAdded.angle.Value <= 0: 133 | lastConstraintAdded.angle = lastConstraintAdded.angle.Value + 180.0 134 | else: 135 | lastConstraintAdded.angle = lastConstraintAdded.angle.Value - 180.0 136 | else: 137 | QtGui.QMessageBox.information( QtGui.QApplication.activeWindow(), "Command Aborted", 'Flip aborted since the last constraint added does not have a direction or an angle attribute.') 138 | return 139 | FreeCAD.ActiveDocument.recompute() 140 | 141 | def GetResources(self): 142 | return { 143 | 'Pixmap' : ':/assembly2/icons/flipConstraint.svg' , 144 | 'MenuText': "Flip last constraint's direction", 145 | 'ToolTip': 'Flip the direction of the last constraint added' 146 | } 147 | 148 | FreeCADGui.addCommand('assembly2_flipLastConstraintsDirection', FlipLastConstraintsDirectionCommand()) 149 | 150 | 151 | 152 | class LockRotationOfLastConstraintAddedCommand: 153 | def Activated(self): 154 | constraints = [ obj for obj in FreeCAD.ActiveDocument.Objects 155 | if 'ConstraintInfo' in obj.Content ] 156 | if len(constraints) == 0: 157 | QtGui.QMessageBox.information( QtGui.QApplication.activeWindow(), "Command Aborted", 'Set LockRotation=True aborted since no assembly2 constraints in active document.') 158 | return 159 | lastConstraintAdded = constraints[-1] 160 | if hasattr( lastConstraintAdded, 'lockRotation' ): 161 | if not lastConstraintAdded.lockRotation: 162 | lastConstraintAdded.lockRotation = True 163 | FreeCAD.ActiveDocument.recompute() 164 | else: 165 | QtGui.QMessageBox.information( QtGui.QApplication.activeWindow(), "Information", 'Last constraints LockRotation attribute already set to True.') 166 | else: 167 | QtGui.QMessageBox.information( QtGui.QApplication.activeWindow(), "Command Aborted", 'Set LockRotation=True aborted since the last constraint added does not the LockRotation attribute.') 168 | return 169 | 170 | def GetResources(self): 171 | return { 172 | 'Pixmap' : ':/assembly2/icons/lockRotation.svg' , 173 | 'MenuText': "Set lockRotation->True for the last constraint added", 174 | 'ToolTip': 'Set lockRotation->True for the last constraint added' 175 | } 176 | 177 | FreeCADGui.addCommand('assembly2_lockLastConstraintsRotation', LockRotationOfLastConstraintAddedCommand()) 178 | -------------------------------------------------------------------------------- /assembly2/constraints/common.py: -------------------------------------------------------------------------------- 1 | from assembly2.core import * 2 | from assembly2.lib3D import * 3 | from assembly2.selection import * 4 | from .objectProxy import ConstraintObjectProxy 5 | from .viewProviderProxy import ConstraintViewProviderProxy, repair_tree_view 6 | from pivy import coin 7 | from PySide import QtGui 8 | 9 | __dir2__ = os.path.dirname(__file__) 10 | GuiPath = os.path.expanduser ("~") #GuiPath = os.path.join( __dir2__, 'Gui' ) 11 | 12 | 13 | def recordConstraints( doc, s1, s2 ): 14 | constraintFile = os.path.join( GuiPath , 'constraintFile.txt') 15 | with open(constraintFile, 'w') as outfile: 16 | outfile.write(make_string(s1.ObjectName)+'\n'+str(s1.Object.Placement.Base)+'\n'+str(s1.Object.Placement.Rotation)+'\n') 17 | outfile.write(make_string(s2.ObjectName)+'\n'+str(s2.Object.Placement.Base)+'\n'+str(s2.Object.Placement.Rotation)+'\n') 18 | constraints = [ obj for obj in FreeCAD.ActiveDocument.Objects if 'ConstraintInfo' in obj.Content ] 19 | #print constraints 20 | if len(constraints) > 0: 21 | constraintFile = os.path.join( GuiPath , 'constraintFile.txt') 22 | if os.path.exists(constraintFile): 23 | with open(constraintFile, 'a') as outfile: 24 | lastConstraintAdded = constraints[-1] 25 | outfile.write(make_string(lastConstraintAdded.Name)+'\n') 26 | 27 | def updateObjectProperties( c ): 28 | if hasattr(c,'FaceInd1'): 29 | debugPrint(3,'updating properties of %s' % c.Name ) 30 | for i in [1,2]: 31 | c.addProperty('App::PropertyString','SubElement%i'%i,'ConstraintInfo') 32 | setattr(c,'SubElement%i'%i,'Face%i'%(getattr(c,'FaceInd%i'%i)+1)) 33 | c.setEditorMode('SubElement%i'%i, 1) 34 | c.removeProperty('FaceInd%i'%i) 35 | if hasattr(c,'planeOffset'): 36 | v = c.planeOffset 37 | c.removeProperty('planeOffset') 38 | c.addProperty('App::PropertyDistance','offset',"ConstraintInfo") 39 | c.offset = '%f mm' % v 40 | if hasattr(c,'degrees'): 41 | v = c.degrees 42 | c.removeProperty('degrees') 43 | c.addProperty("App::PropertyAngle","angle","ConstraintInfo") 44 | c.angle = v 45 | elif hasattr(c,'EdgeInd1'): 46 | debugPrint(3,'updating properties of %s' % c.Name ) 47 | for i in [1,2]: 48 | c.addProperty('App::PropertyString','SubElement%i'%i,'ConstraintInfo') 49 | setattr(c,'SubElement%i'%i,'Edge%i'%(getattr(c,'EdgeInd%i'%i)+1)) 50 | c.setEditorMode('SubElement%i'%i, 1) 51 | c.removeProperty('EdgeInd%i'%i) 52 | v = c.offset 53 | c.removeProperty('offset') 54 | c.addProperty('App::PropertyDistance','offset',"ConstraintInfo") 55 | c.offset = '%f mm' % v 56 | if c.Type == 'axial' or c.Type == 'circularEdge': 57 | if not hasattr(c, 'lockRotation'): 58 | debugPrint(3,'updating properties of %s, to add lockRotation (default=false)' % c.Name ) 59 | c.addProperty("App::PropertyBool","lockRotation","ConstraintInfo") 60 | if FreeCAD.GuiUp: 61 | if not isinstance( c.ViewObject.Proxy , ConstraintViewProviderProxy): 62 | iconPaths = { 63 | 'angle_between_planes':':/assembly2/icons/angleConstraint.svg', 64 | 'axial':':/assembly2/icons/axialConstraint.svg', 65 | 'circularEdge':':/assembly2/icons/circularEdgeConstraint.svg', 66 | 'plane':':/assembly2/icons/planeConstraint.svg', 67 | 'sphericalSurface': ':/assembly2/icons/sphericalSurfaceConstraint.svg' 68 | } 69 | c.ViewObject.Proxy = ConstraintViewProviderProxy( c, iconPaths[c.Type] ) 70 | -------------------------------------------------------------------------------- /assembly2/constraints/objectProxy.py: -------------------------------------------------------------------------------- 1 | from assembly2.core import * 2 | 3 | class ConstraintObjectProxy: 4 | 5 | def execute(self, obj): 6 | preferences = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/Assembly2") 7 | if preferences.GetBool('autoSolveConstraintAttributesChanged', True): 8 | self.callSolveConstraints() 9 | #obj.touch() 10 | 11 | def onChanged(self, obj, prop): 12 | if hasattr(self, 'mirror_name'): 13 | cMirror = obj.Document.getObject( self.mirror_name ) 14 | if cMirror.Proxy == None: 15 | return #this occurs during document loading ... 16 | if obj.getGroupOfProperty( prop ) == 'ConstraintInfo': 17 | cMirror.Proxy.disable_onChanged = True 18 | setattr( cMirror, prop, getattr( obj, prop) ) 19 | cMirror.Proxy.disable_onChanged = False 20 | 21 | def reduceDirectionChoices( self, obj, value): 22 | if hasattr(self, 'mirror_name'): 23 | cMirror = obj.Document.getObject( self.mirror_name ) 24 | cMirror.directionConstraint = ["aligned","opposed"] #value should be updated in onChanged call due to assignment in 2 lines 25 | obj.directionConstraint = ["aligned","opposed"] 26 | obj.directionConstraint = value 27 | 28 | def callSolveConstraints(self): 29 | from assembly2.solvers import solveConstraints 30 | solveConstraints( FreeCAD.ActiveDocument ) 31 | 32 | class ConstraintMirrorObjectProxy: 33 | def __init__(self, obj, constraintObj ): 34 | self.constraintObj_name = constraintObj.Name 35 | constraintObj.Proxy.mirror_name = obj.Name 36 | self.disable_onChanged = False 37 | obj.Proxy = self 38 | 39 | def execute(self, obj): 40 | return #no work required in onChanged causes touched in original constraint ... 41 | 42 | def onChanged(self, obj, prop): 43 | ''' 44 | is triggered by Python code! 45 | And on document loading... 46 | ''' 47 | #FreeCAD.Console.PrintMessage("%s.%s property changed\n" % (obj.Name, prop)) 48 | if getattr( self, 'disable_onChanged', True): 49 | return 50 | if obj.getGroupOfProperty( prop ) == 'ConstraintNfo': 51 | if hasattr( self, 'constraintObj_name' ): 52 | constraintObj = obj.Document.getObject( self.constraintObj_name ) 53 | try: 54 | if getattr(constraintObj, prop) != getattr( obj, prop): 55 | setattr( constraintObj, prop, getattr( obj, prop) ) 56 | except AttributeError as e: 57 | pass #loading issues... 58 | -------------------------------------------------------------------------------- /assembly2/constraints/planeConstraint.py: -------------------------------------------------------------------------------- 1 | from .common import * 2 | 3 | class PlaneSelectionGate: 4 | def allow(self, doc, obj, sub): 5 | return planeSelected( SelectionExObject(doc, obj, sub) ) 6 | 7 | class PlaneSelectionGate2: 8 | def allow(self, doc, obj, sub): 9 | s2 = SelectionExObject(doc, obj, sub) 10 | return planeSelected(s2) or vertexSelected(s2) 11 | 12 | def promt_user_for_axis_for_constraint_label(): 13 | preferences = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/Assembly2") 14 | return preferences.GetBool('promtUserForAxisConstraintLabel', False) 15 | 16 | 17 | def parseSelection(selection, objectToUpdate=None): 18 | validSelection = False 19 | if len(selection) == 2: 20 | s1, s2 = selection 21 | if s1.ObjectName != s2.ObjectName: 22 | if not planeSelected(s1): 23 | s2, s1 = s1, s2 24 | if planeSelected(s1) and (planeSelected(s2) or vertexSelected(s2)): 25 | validSelection = True 26 | cParms = [ [s1.ObjectName, s1.SubElementNames[0], s1.Object.Label ], 27 | [s2.ObjectName, s2.SubElementNames[0], s2.Object.Label ] ] 28 | if not validSelection: 29 | msg = '''Plane constraint requires a selection of either 30 | - 2 planes, or 31 | - 1 plane and 1 vertex 32 | 33 | Selection made: 34 | %s''' % printSelection(selection) 35 | QtGui.QMessageBox.information( QtGui.QApplication.activeWindow(), "Incorrect Usage", msg) 36 | return 37 | 38 | if objectToUpdate is None: 39 | if promt_user_for_axis_for_constraint_label(): 40 | extraText, extraOk = QtGui.QInputDialog.getText(QtGui.QApplication.activeWindow(), "Axis", "Axis for constraint Label", QtGui.QLineEdit.Normal, "0") 41 | if not extraOk: 42 | return 43 | else: 44 | extraText = '' 45 | cName = findUnusedObjectName('planeConstraint') 46 | debugPrint(2, "creating %s" % cName ) 47 | c = FreeCAD.ActiveDocument.addObject("App::FeaturePython", cName) 48 | c.addProperty("App::PropertyString","Type","ConstraintInfo").Type = 'plane' 49 | c.addProperty("App::PropertyString","Object1","ConstraintInfo").Object1 = cParms[0][0] 50 | c.addProperty("App::PropertyString","SubElement1","ConstraintInfo").SubElement1 = cParms[0][1] 51 | c.addProperty("App::PropertyString","Object2","ConstraintInfo").Object2 = cParms[1][0] 52 | c.addProperty("App::PropertyString","SubElement2","ConstraintInfo").SubElement2 = cParms[1][1] 53 | c.addProperty('App::PropertyDistance','offset',"ConstraintInfo") 54 | 55 | c.addProperty("App::PropertyEnumeration","directionConstraint", "ConstraintInfo") 56 | c.directionConstraint = ["none","aligned","opposed"] 57 | 58 | c.setEditorMode('Type',1) 59 | for prop in ["Object1","Object2","SubElement1","SubElement2"]: 60 | c.setEditorMode(prop, 1) 61 | c.Proxy = ConstraintObjectProxy() 62 | c.ViewObject.Proxy = ConstraintViewProviderProxy( c, ':/assembly2/icons/planeConstraint.svg', True, cParms[1][2], cParms[0][2], extraText) 63 | else: 64 | debugPrint(2, "redefining %s" % objectToUpdate.Name ) 65 | c = objectToUpdate 66 | c.Object1 = cParms[0][0] 67 | c.SubElement1 = cParms[0][1] 68 | c.Object2 = cParms[1][0] 69 | c.SubElement2 = cParms[1][1] 70 | updateObjectProperties(c) 71 | recordConstraints( FreeCAD.ActiveDocument, s1, s2 ) 72 | c.purgeTouched() 73 | c.Proxy.callSolveConstraints() 74 | repair_tree_view() 75 | 76 | 77 | selection_text = '''Selection 1 options: 78 | - plane 79 | Selection 2 options: 80 | - plane 81 | - vertex ''' 82 | 83 | class PlaneConstraintCommand: 84 | def Activated(self): 85 | selection = FreeCADGui.Selection.getSelectionEx() 86 | sel = FreeCADGui.Selection.getSelection() 87 | if len(selection) == 2: 88 | parseSelection( selection ) 89 | else: 90 | FreeCADGui.Selection.clearSelection() 91 | ConstraintSelectionObserver( 92 | PlaneSelectionGate(), 93 | parseSelection, 94 | taskDialog_title ='add plane constraint', 95 | taskDialog_iconPath = self.GetResources()['Pixmap'], 96 | taskDialog_text = selection_text, 97 | secondSelectionGate = PlaneSelectionGate2() 98 | ) 99 | 100 | def GetResources(self): 101 | return { 102 | 'Pixmap' : ':/assembly2/icons/planeConstraint.svg', 103 | 'MenuText': 'Add plane constraint', 104 | 'ToolTip': 'Add a plane constraint between two objects' 105 | } 106 | 107 | FreeCADGui.addCommand('assembly2_addPlaneConstraint', PlaneConstraintCommand()) 108 | 109 | 110 | class RedefineConstraintCommand: 111 | def Activated(self): 112 | self.constObject = FreeCADGui.Selection.getSelectionEx()[0].Object 113 | debugPrint(3,'redefining %s' % self.constObject.Name) 114 | FreeCADGui.Selection.clearSelection() 115 | ConstraintSelectionObserver( 116 | PlaneSelectionGate(), 117 | self.UpdateConstraint, 118 | taskDialog_title ='add plane constraint', 119 | taskDialog_iconPath = ':/assembly2/icons/planeConstraint.svg', 120 | taskDialog_text = selection_text, 121 | secondSelectionGate = PlaneSelectionGate2() 122 | ) 123 | 124 | def UpdateConstraint(self, selection): 125 | parseSelection( selection, self.constObject) 126 | 127 | def GetResources(self): 128 | return { 'MenuText': 'Redefine' } 129 | FreeCADGui.addCommand('assembly2_redefinePlaneConstraint', RedefineConstraintCommand()) 130 | -------------------------------------------------------------------------------- /assembly2/constraints/sphericalSurfaceConstraint.py: -------------------------------------------------------------------------------- 1 | from .common import * 2 | 3 | class SphericalSurfaceSelectionGate: 4 | def allow(self, doc, obj, sub): 5 | if sub.startswith('Face'): 6 | face = getObjectFaceFromName( obj, sub) 7 | return str( face.Surface ).startswith('Sphere ') 8 | elif sub.startswith('Vertex'): 9 | return True 10 | else: 11 | return False 12 | 13 | 14 | def parseSelection(selection, objectToUpdate=None): 15 | validSelection = False 16 | if len(selection) == 2: 17 | s1, s2 = selection 18 | if s1.ObjectName != s2.ObjectName: 19 | if ( vertexSelected(s1) or sphericalSurfaceSelected(s1)) \ 20 | and ( vertexSelected(s2) or sphericalSurfaceSelected(s2)): 21 | validSelection = True 22 | cParms = [ [s1.ObjectName, s1.SubElementNames[0], s1.Object.Label ], 23 | [s2.ObjectName, s2.SubElementNames[0], s2.Object.Label ] ] 24 | 25 | if not validSelection: 26 | msg = '''To add a spherical surface constraint select two spherical surfaces (or vertexs), each from a different part. Selection made: 27 | %s''' % printSelection(selection) 28 | QtGui.QMessageBox.information( QtGui.QApplication.activeWindow(), "Incorrect Usage", msg) 29 | return 30 | 31 | if objectToUpdate == None: 32 | cName = findUnusedObjectName('sphericalSurfaceConstraint') 33 | debugPrint(2, "creating %s" % cName ) 34 | c = FreeCAD.ActiveDocument.addObject("App::FeaturePython", cName) 35 | 36 | c.addProperty("App::PropertyString","Type","ConstraintInfo").Type = 'sphericalSurface' 37 | c.addProperty("App::PropertyString","Object1","ConstraintInfo").Object1 = cParms[0][0] 38 | c.addProperty("App::PropertyString","SubElement1","ConstraintInfo").SubElement1 = cParms[0][1] 39 | c.addProperty("App::PropertyString","Object2","ConstraintInfo").Object2 = cParms[1][0] 40 | c.addProperty("App::PropertyString","SubElement2","ConstraintInfo").SubElement2 = cParms[1][1] 41 | 42 | c.setEditorMode('Type',1) 43 | for prop in ["Object1","Object2","SubElement1","SubElement2"]: 44 | c.setEditorMode(prop, 1) 45 | 46 | c.Proxy = ConstraintObjectProxy() 47 | c.ViewObject.Proxy = ConstraintViewProviderProxy( c, ':/assembly2/icons/sphericalSurfaceConstraint.svg', True, cParms[1][2], cParms[0][2]) 48 | else: 49 | debugPrint(2, "redefining %s" % objectToUpdate.Name ) 50 | c = objectToUpdate 51 | c.Object1 = cParms[0][0] 52 | c.SubElement1 = cParms[0][1] 53 | c.Object2 = cParms[1][0] 54 | c.SubElement2 = cParms[1][1] 55 | updateObjectProperties(c) 56 | recordConstraints( FreeCAD.ActiveDocument, s1, s2 ) 57 | c.purgeTouched() 58 | c.Proxy.callSolveConstraints() 59 | repair_tree_view() 60 | 61 | selection_text = '''Selection options: 62 | - spherical surface 63 | - vertex''' 64 | 65 | 66 | class SphericalSurfaceConstraintCommand: 67 | def Activated(self): 68 | selection = FreeCADGui.Selection.getSelectionEx() 69 | if len(selection) == 2: 70 | parseSelection( selection ) 71 | else: 72 | FreeCADGui.Selection.clearSelection() 73 | ConstraintSelectionObserver( 74 | SphericalSurfaceSelectionGate(), 75 | parseSelection, 76 | taskDialog_title ='add spherical surface constraint', 77 | taskDialog_iconPath = self.GetResources()['Pixmap'], 78 | taskDialog_text = selection_text 79 | ) 80 | 81 | def GetResources(self): 82 | return { 83 | 'Pixmap' : ':/assembly2/icons/sphericalSurfaceConstraint.svg', 84 | 'MenuText': 'Add a spherical surface constraint', 85 | 'ToolTip': 'Add a spherical surface constraint between two objects' 86 | } 87 | 88 | FreeCADGui.addCommand('assembly2_addSphericalSurfaceConstraint', SphericalSurfaceConstraintCommand()) 89 | 90 | 91 | class RedefineSphericalSurfaceConstraintCommand: 92 | def Activated(self): 93 | self.constObject = FreeCADGui.Selection.getSelectionEx()[0].Object 94 | debugPrint(3,'redefining %s' % self.constObject.Name) 95 | FreeCADGui.Selection.clearSelection() 96 | ConstraintSelectionObserver( 97 | SphericalSurfaceSelectionGate(), 98 | self.UpdateConstraint, 99 | taskDialog_title ='redefine spherical surface constraint', 100 | taskDialog_iconPath = ':/assembly2/icons/sphericalSurfaceConstraint.svg', 101 | taskDialog_text = selection_text 102 | ) 103 | def UpdateConstraint(self, selection): 104 | parseSelection( selection, self.constObject) 105 | 106 | def GetResources(self): 107 | return { 'MenuText': 'Redefine' } 108 | FreeCADGui.addCommand('assembly2_redefineSphericalSurfaceConstraint', RedefineSphericalSurfaceConstraintCommand()) 109 | -------------------------------------------------------------------------------- /assembly2/constraints/viewProviderProxy.py: -------------------------------------------------------------------------------- 1 | import FreeCAD, FreeCADGui 2 | from pivy import coin 3 | import traceback 4 | 5 | def group_constraints_under_parts(): 6 | preferences = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/Assembly2") 7 | return preferences.GetBool('groupConstraintsUnderParts', True) 8 | 9 | def allow_deletetion_when_activice_doc_ne_object_doc(): 10 | parms = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/Assembly2") 11 | return parms.GetBool('allowDeletetionFromExternalDocuments', False) 12 | 13 | 14 | class ConstraintViewProviderProxy: 15 | def __init__( self, constraintObj, iconPath, createMirror=True, origLabel = '', mirrorLabel = '', extraLabel = '' ): 16 | self.iconPath = iconPath 17 | self.constraintObj_name = constraintObj.Name 18 | constraintObj.purgeTouched() 19 | if createMirror and group_constraints_under_parts(): 20 | part1 = constraintObj.Document.getObject( constraintObj.Object1 ) 21 | part2 = constraintObj.Document.getObject( constraintObj.Object2 ) 22 | if hasattr( getattr(part1.ViewObject,'Proxy',None),'claimChildren') \ 23 | or hasattr( getattr(part2.ViewObject,'Proxy',None),'claimChildren'): 24 | self.mirror_name = create_constraint_mirror( constraintObj, iconPath, origLabel, mirrorLabel, extraLabel ) 25 | 26 | def getIcon(self): 27 | return self.iconPath 28 | 29 | def attach(self, vobj): #attach to what document? 30 | vobj.addDisplayMode( coin.SoGroup(),"Standard" ) 31 | 32 | def getDisplayModes(self,obj): 33 | "'''Return a list of display modes.'''" 34 | return ["Standard"] 35 | 36 | def getDefaultDisplayMode(self): 37 | "'''Return the name of the default display mode. It must be defined in getDisplayModes.'''" 38 | return "Standard" 39 | 40 | def onDelete(self, viewObject, subelements): # subelements is a tuple of strings 41 | 'does not seem to be called when an object is deleted pythonatically' 42 | from objectProxy import ConstraintMirrorObjectProxy 43 | if not allow_deletetion_when_activice_doc_ne_object_doc() and FreeCAD.activeDocument() != viewObject.Object.Document: 44 | FreeCAD.Console.PrintMessage("preventing deletetion of %s since active document != %s. Disable behavior in assembly2 preferences.\n" % (viewObject.Object.Label, viewObject.Object.Document.Name) ) 45 | return False 46 | #add code to delete constraint mirrors, or original 47 | obj = viewObject.Object 48 | doc = obj.Document 49 | if isinstance( obj.Proxy, ConstraintMirrorObjectProxy ): 50 | doc.removeObject( obj.Proxy.constraintObj_name ) # also delete the original constraint which obj mirrors 51 | elif hasattr( obj.Proxy, 'mirror_name'): # the original constraint, #isinstance( obj.Proxy, ConstraintObjectProxy ) not done since ConstraintObjectProxy not defined in namespace 52 | doc.removeObject( obj.Proxy.mirror_name ) # also delete mirror 53 | return True 54 | 55 | 56 | class ConstraintMirrorViewProviderProxy( ConstraintViewProviderProxy ): 57 | def __init__( self, constraintObj, iconPath ): 58 | self.iconPath = iconPath 59 | self.constraintObj_name = constraintObj.Name 60 | def attach(self, vobj): 61 | vobj.addDisplayMode( coin.SoGroup(),"Standard" ) 62 | 63 | 64 | def create_constraint_mirror( constraintObj, iconPath, origLabel= '', mirrorLabel='', extraLabel = '' ): 65 | from objectProxy import ConstraintMirrorObjectProxy 66 | #FreeCAD.Console.PrintMessage("creating constraint mirror\n") 67 | cName = constraintObj.Name + '_mirror' 68 | cMirror = constraintObj.Document.addObject("App::FeaturePython", cName) 69 | if origLabel == '': 70 | cMirror.Label = constraintObj.Label + '_' 71 | else: 72 | cMirror.Label = constraintObj.Label + '__' + mirrorLabel 73 | constraintObj.Label = constraintObj.Label + '__' + origLabel 74 | if extraLabel != '': 75 | cMirror.Label += '__' + extraLabel 76 | constraintObj.Label += '__' + extraLabel 77 | for pName in constraintObj.PropertiesList: 78 | if constraintObj.getGroupOfProperty( pName ) == 'ConstraintInfo': 79 | #if constraintObj.getTypeIdOfProperty( pName ) == 'App::PropertyEnumeration': 80 | # continue #App::Enumeration::contains(const char*) const: Assertion `_EnumArray' failed. 81 | cMirror.addProperty( 82 | constraintObj.getTypeIdOfProperty( pName ), 83 | pName, 84 | "ConstraintNfo" #instead of ConstraintInfo, as to not confuse the assembly2sovler 85 | ) 86 | if pName == 'directionConstraint': 87 | v = constraintObj.directionConstraint 88 | if v != "none": #then updating a document with mirrors 89 | cMirror.directionConstraint = ["aligned","opposed"] 90 | cMirror.directionConstraint = v 91 | else: 92 | cMirror.directionConstraint = ["none","aligned","opposed"] 93 | else: 94 | setattr( cMirror, pName, getattr( constraintObj, pName) ) 95 | if constraintObj.getEditorMode(pName) == ['ReadOnly']: 96 | cMirror.setEditorMode( pName, 1 ) 97 | ConstraintMirrorObjectProxy( cMirror, constraintObj ) 98 | cMirror.ViewObject.Proxy = ConstraintMirrorViewProviderProxy( constraintObj, iconPath ) 99 | #cMirror.purgeTouched() 100 | return cMirror.Name 101 | 102 | 103 | def repair_tree_view(): 104 | from PySide import QtGui 105 | doc = FreeCAD.ActiveDocument 106 | matches = [] 107 | def search_children_recursively( node ): 108 | for c in node.children(): 109 | if isinstance(c,QtGui.QTreeView) and isinstance(c, QtGui.QTreeWidget): 110 | matches.append(c) 111 | search_children_recursively( c) 112 | search_children_recursively(QtGui.QApplication.activeWindow()) 113 | for m in matches: 114 | tree_nodes = get_treeview_nodes(m) 115 | def get_node_by_label( label ): 116 | if label in tree_nodes and len( tree_nodes[label] ) == 1: 117 | return tree_nodes[label][0] 118 | elif not obj.Label in tree_nodes: 119 | FreeCAD.Console.PrintWarning( " repair_tree_view: skipping %s since no node with text(0) == %s\n" % ( label, label) ) 120 | else: 121 | FreeCAD.Console.PrintWarning( " repair_tree_view: skipping %s since multiple nodes matching label\n" % ( label, label) ) 122 | if doc.Label in tree_nodes: #all the code up until now has geen to find the QtGui.QTreeView widget (except for the get_node_by_label function) 123 | #FreeCAD.Console.PrintMessage( tree_nodes ) 124 | for imported_obj in doc.Objects: 125 | try: #allow use of assembly2 contraints also on non imported objects 126 | if isinstance( imported_obj.ViewObject.Proxy, ImportedPartViewProviderProxy ): 127 | #FreeCAD.Console.PrintMessage( 'checking claim children for %s\n' % imported_obj.Label ) 128 | if get_node_by_label( imported_obj.Label ): 129 | node_imported_obj = get_node_by_label( imported_obj.Label ) 130 | if not hasattr( imported_obj.ViewObject.Proxy, 'Object'): 131 | imported_obj.ViewObject.Proxy.Object = imported_obj # proxy.attach not called properly 132 | FreeCAD.Console.PrintMessage('repair_tree_view: %s.ViewObject.Proxy.Object = %s' % (imported_obj.Name, imported_obj.Name) ) 133 | for constraint_obj in imported_obj.ViewObject.Proxy.claimChildren(): 134 | #FreeCAD.Console.PrintMessage(' - %s\n' % constraint_obj.Label ) 135 | if get_node_by_label( constraint_obj.Label ): 136 | #FreeCAD.Console.PrintMessage(' (found treeview node)\n') 137 | node_constraint_obj = get_node_by_label( constraint_obj.Label ) 138 | if id( node_constraint_obj.parent()) != id(node_imported_obj): 139 | FreeCAD.Console.PrintMessage("repair_tree_view: %s under %s and not %s, repairing\n" % (constraint_obj.Label, node_constraint_obj.parent().text(0), imported_obj.Label )) 140 | wrong_parent = node_constraint_obj.parent() 141 | wrong_parent.removeChild( node_constraint_obj ) 142 | node_imported_obj.addChild( node_constraint_obj ) 143 | except: 144 | # FreeCAD.Console.PrintWarning( "not repaired %s \n" % ( imported_obj.Label ) ) 145 | pass 146 | #break 147 | 148 | def get_treeview_nodes( treeWidget ): 149 | from PySide import QtGui 150 | tree_nodes = {} 151 | def walk( node ): 152 | key = node.text(0) 153 | #print(key) 154 | if not key in tree_nodes: 155 | tree_nodes[ key ] = [] 156 | tree_nodes[key].append( node ) 157 | for i in range( node.childCount() ): 158 | walk( node.child( i ) ) 159 | walk( treeWidget.itemAt(0,0) ) 160 | return tree_nodes 161 | -------------------------------------------------------------------------------- /assembly2/core.py: -------------------------------------------------------------------------------- 1 | import numpy, os, sys 2 | import FreeCAD 3 | import FreeCADGui 4 | import Part 5 | from PySide import QtGui, QtCore 6 | 7 | path_assembly2 = os.path.dirname( os.path.dirname(__file__) ) 8 | #path_assembly2_icons = os.path.join( path_assembly2, 'Resources', 'icons') 9 | #path_assembly2_ui = os.path.join( path_assembly2, 'Resources', 'ui') 10 | path_assembly2_resources = os.path.join( path_assembly2, 'Gui', 'Resources', 'resources.rcc') 11 | resourcesLoaded = QtCore.QResource.registerResource(path_assembly2_resources) 12 | assert resourcesLoaded 13 | #update resources file using 14 | # $rcc -binary Gui/Resources/resources.qrc -o Gui/Resources/resources.rcc 15 | 16 | __dir__ = path_assembly2 17 | wb_globals = {} 18 | __dir2__ = os.path.dirname(__file__) 19 | GuiPath = os.path.expanduser ("~") #GuiPath = os.path.join( __dir2__, 'Gui' ) 20 | 21 | def make_string(input): 22 | if (sys.version_info > (3, 0)): #py3 23 | if isinstance(input, str): 24 | return input 25 | else: 26 | input = input.encode('utf-8') 27 | return input 28 | else: #py2 29 | if type(input) == unicode: 30 | input = input.encode('utf-8') 31 | return input 32 | else: 33 | return input 34 | 35 | def debugPrint( level, msg ): 36 | if level <= debugPrint.level: 37 | FreeCAD.Console.PrintMessage(msg + '\n') 38 | debugPrint.level = 4 if hasattr(os,'uname') and os.uname()[1].startswith('antoine') else 2 39 | #debugPrint.level = 4 #maui to debug 40 | 41 | def formatDictionary( d, indent): 42 | return '%s{' % indent + '\n'.join(['%s%s:%s' % (indent,k,d[k]) for k in sorted(d.keys())]) + '}' 43 | 44 | def findUnusedObjectName(base, counterStart=1, fmt='%02i', document=None): 45 | i = counterStart 46 | objName = '%s%s' % (base, fmt%i) 47 | if document == None: 48 | document = FreeCAD.ActiveDocument 49 | usedNames = [ obj.Name for obj in document.Objects ] 50 | while objName in usedNames: 51 | i = i + 1 52 | objName = '%s%s' % (base, fmt%i) 53 | return objName 54 | 55 | def findUnusedLabel(base, counterStart=1, fmt='%02i', document=None): 56 | i = counterStart 57 | label = '%s%s' % (base, fmt%i) 58 | if document == None: 59 | document = FreeCAD.ActiveDocument 60 | usedLabels = [ obj.Label for obj in document.Objects ] 61 | while label in usedLabels: 62 | i = i + 1 63 | label = '%s%s' % (base, fmt%i) 64 | return label 65 | 66 | 67 | -------------------------------------------------------------------------------- /assembly2/importPart/fcstd_parser.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Used instead of openning document via FreeCAD.openDocument 3 | 4 | Info on the FreeCAD file format: 5 | https://www.freecadweb.org/wiki/index.php?title=File_Format_FCStd 6 | ''' 7 | 8 | import FreeCAD 9 | import Part #from FreeCAD 10 | import os 11 | import numpy 12 | from zipfile import ZipFile 13 | import xml.etree.ElementTree as XML_Tree 14 | 15 | def xml_prettify( xml_str ): 16 | import xml.dom.minidom as minidom 17 | xml = minidom.parseString( xml_str ) 18 | S = xml.toprettyxml(indent=' ') 19 | return '\n'.join( s for s in S.split('\n') if s.strip() != '' ) 20 | 21 | class Fcstd_File_Parser: 22 | ''' 23 | https://www.freecadweb.org/wiki/index.php?title=File_Format_FCStd 24 | Each object, even if it is parametric, has its shape stored as an individual .brep file, so it can be accessed by components without the need to recalculate the shape. 25 | ''' 26 | def __init__( 27 | self, 28 | fn, 29 | only_load_visible_shapes = True, 30 | visible_if_ViewObject_missing = True, 31 | printLevel=0 32 | ): 33 | z = ZipFile( fn ) 34 | if printLevel > 0: 35 | print( z.namelist() ) 36 | print( xml_prettify( z.open('Document.xml').read() ) ) 37 | if 'GuiDocument.xml' in z.namelist(): 38 | print( xml_prettify( z.open('GuiDocument.xml').read() ) ) 39 | tree_doc = XML_Tree.fromstring( z.open('Document.xml').read() ) 40 | if 'GuiDocument.xml' in z.namelist(): 41 | tree_gui = XML_Tree.fromstring( z.open('GuiDocument.xml').read() ) 42 | else: 43 | tree_gui = None 44 | #tree_shapes = ElementTree.fromstring( z.open('PartShape.brp').read() ) 45 | doc = Fcstd_Property_List( tree_doc.find('Properties') ) 46 | self.__dict__.update( doc.__dict__ ) 47 | self.Name = os.path.split( fn )[1][:-6] 48 | self.Objects = [] 49 | self.Objects_dict = {} 50 | #objectData 51 | for o in tree_doc.find('ObjectData').findall('Object'): 52 | k = o.attrib['name'] 53 | assert not k in self.Objects 54 | obj = Fcstd_Property_List( o.find('Properties') ) 55 | obj.Name = k 56 | obj.Content = XML_Tree.tostring( o ) 57 | self.Objects_dict[k] = obj 58 | self.Objects.append( self.Objects_dict[k] ) 59 | #viewObjects 60 | if tree_gui != None: 61 | for o in tree_gui.find('ViewProviderData').findall('ViewProvider'): 62 | k = o.attrib['name'] 63 | if k in self.Objects_dict: 64 | ViewObject = Fcstd_Property_List( o.find('Properties') ) 65 | ViewObject.isVisible = isVisible_Bound_Method( ViewObject ) 66 | self.Objects_dict[k].ViewObject = ViewObject 67 | else: 68 | for obj in self.Objects: 69 | xml = ' ' % ( 'true' if visible_if_ViewObject_missing else 'false' ) 70 | obj.ViewObject = Fcstd_Property_List( XML_Tree.fromstring(xml) ) 71 | obj.ViewObject.isVisible = isVisible_Bound_Method( obj.ViewObject ) 72 | #shapes 73 | for obj in self.Objects: 74 | if hasattr( obj, 'Shape'): 75 | shape_zip_name = obj.Shape 76 | delattr( obj, 'Shape' ) 77 | if not only_load_visible_shapes or obj.ViewObject.Visibility: 78 | obj.Shape = Part.Shape() 79 | obj.Shape.importBrepFromString( z.open( shape_zip_name ).read() ) 80 | #colour lists 81 | for obj in self.Objects: 82 | if hasattr( obj, 'ViewObject' ): 83 | v = obj.ViewObject 84 | if not only_load_visible_shapes or obj.ViewObject.Visibility: 85 | for p_name, p_type in zip( v.PropertiesList, v.PropertiesTypes ): 86 | if p_type == 'App::PropertyColorList': 87 | #print( p_name, getattr(v,p_name) ) 88 | fn = getattr(v,p_name) 89 | C = parse_Clr_Array( z.open( fn ).read() ) 90 | setattr( v, p_name, C ) 91 | 92 | 93 | class isVisible_Bound_Method: 94 | def __init__( self, ViewObject_Property_List ): 95 | self.Properties = ViewObject_Property_List 96 | def __call__( self ): 97 | return self.Properties.Visibility 98 | 99 | 100 | class Fcstd_Property_List: 101 | def __init__( self, Properties_XML_Tree ): 102 | self.PropertiesList = [] 103 | self.PropertiesTypes = [] 104 | for p in Properties_XML_Tree.findall('Property'): 105 | #print( XML_Tree.tostring( p ).strip() ) 106 | name = p.attrib['name'] 107 | p_type = p.attrib['type'] 108 | if p_type == 'App::PropertyMaterial': 109 | continue # not implemented yet 110 | #print( XML_Tree.tostring( p ).strip() ) 111 | #print( len(p) ) 112 | if len( p ) == 1: 113 | #print(p[0].tag) 114 | if p[0].tag == 'Bool': 115 | v = p[0].attrib['value'] == 'true' 116 | elif p[0].tag == 'Float': 117 | v = float(p[0].attrib['value']) 118 | elif p[0].tag == 'Integer' or p_type in ("App::PropertyPercent"): 119 | v = int(p[0].attrib['value']) 120 | elif p_type == "App::PropertyColor": 121 | v = parse_App_PropertyColor(p[0].attrib['value']) 122 | elif p_type == "App::PropertyPlacement": 123 | v = App_PropertyPlacement( p[0] ) 124 | else: 125 | v = p[0].attrib[ p[0].keys()[0]] 126 | self.addProperty( name, p_type, v ) 127 | #print(name,v) 128 | elif p_type == "App::PropertyEnumeration": 129 | #print( XML_Tree.tostring( p ) ) 130 | ind = int(p[0].attrib['value']) 131 | self.addProperty( name, p_type, p[1][ind].attrib['value'] ) 132 | else: 133 | FreeCAD.Console.PrintWarning( 'unable to parse\n %s \nsince more than 1 childern\n' % (XML_Tree.tostring( p )) ) 134 | def addProperty( self, name, p_type, value ): 135 | assert not hasattr(self, name) 136 | setattr(self, name, value) 137 | self.PropertiesList.append( name ) 138 | self.PropertiesTypes.append( p_type ) 139 | 140 | class App_PropertyPlacement: 141 | def __init__( self, property_xml ): 142 | #print( XML_Tree.tostring( property_xml ) ) 143 | self.Base = App_PropertyPlacement_Base( property_xml ) 144 | self.Rotation = App_PropertyPlacement_Rotation( property_xml ) 145 | class App_PropertyPlacement_Base: 146 | def __init__( self, p ): 147 | self.x = float( p.attrib['Px'] ) 148 | self.y = float( p.attrib['Py'] ) 149 | self.z = float( p.attrib['Pz'] ) 150 | class App_PropertyPlacement_Rotation: 151 | def __init__( self, p ): 152 | self.Q = [ float(p.attrib[k]) for k in ('Q0','Q1','Q2','Q3') ] 153 | 154 | 155 | def parse_App_PropertyColor( t ): 156 | ''' 157 | written as a workaround for the 158 | 'type must be int or tuple of float, not tuple' 159 | error, which does not accept ints 160 | ''' 161 | c = int( t ) 162 | V = [ 163 | (c / 16777216 ) % 256, 164 | (c / 65536 ) % 256, 165 | (c / 256 ) % 256, 166 | c % 256, 167 | ] 168 | return tuple( numpy.array( V, dtype='float64' ) / 255 ) 169 | 170 | def parse_Clr_Array( fileContent ): 171 | C = numpy.fromstring( fileContent, dtype=numpy.uint32 ) 172 | n = C[0] 173 | assert len(C) == n + 1 174 | return [ parse_App_PropertyColor(c) for c in C[1:] ] 175 | 176 | 177 | 178 | #testing done in importPart/tests.py 179 | -------------------------------------------------------------------------------- /assembly2/importPart/importPath.py: -------------------------------------------------------------------------------- 1 | import os, posixpath, ntpath 2 | import FreeCAD 3 | 4 | DEBUG = False 5 | 6 | def path_split( pathLib, path): 7 | parentPath, childPath = pathLib.split( path ) 8 | parts = [childPath] 9 | while childPath != '': 10 | parentPath, childPath = pathLib.split( parentPath ) 11 | parts.insert(0, childPath) 12 | parts[0] = parentPath 13 | if pathLib == ntpath and parts[0].endswith(':/'): #ntpath ... 14 | parts[0] = parts[0][:-2] + ':\\' 15 | return parts 16 | 17 | def path_join( pathLib, parts): 18 | if pathLib == posixpath and parts[0].endswith(':\\'): 19 | path = parts[0][:-2]+ ':/' 20 | else: 21 | path = parts[0] 22 | for part in parts[1:]: 23 | path = pathLib.join( path, part) 24 | return path 25 | 26 | def path_convert( path, pathLibFrom, pathLibTo): 27 | parts = path_split( pathLibFrom, path) 28 | return path_join(pathLibTo, parts ) 29 | 30 | def path_rel_to_abs(path): 31 | j = FreeCAD.ActiveDocument.FileName.rfind('/') 32 | k = path.find('/') 33 | absPath = FreeCAD.ActiveDocument.FileName[:j] + path[k:] 34 | if DEBUG: 35 | FreeCAD.Console.PrintMessage("First %s\n" % FreeCAD.ActiveDocument.FileName[:j]) 36 | FreeCAD.Console.PrintMessage("Next %s\n" % path[k:]) 37 | FreeCAD.Console.PrintMessage("absolutePath is %s\n" % absPath) 38 | if path.startswith('.') and os.path.exists( absPath ): 39 | return absPath 40 | else: 41 | return None 42 | 43 | -------------------------------------------------------------------------------- /assembly2/importPart/path_lib.py: -------------------------------------------------------------------------------- 1 | import os, posixpath, ntpath 2 | 3 | def path_split( pathLib, path): 4 | parentPath, childPath = pathLib.split( path ) 5 | parts = [childPath] 6 | while childPath != '': 7 | parentPath, childPath = pathLib.split( parentPath ) 8 | parts.insert(0, childPath) 9 | parts[0] = parentPath 10 | if pathLib == ntpath and parts[0].endswith(':/'): #ntpath ... 11 | parts[0] = parts[0][:-2] + ':\\' 12 | return parts 13 | 14 | def path_join( pathLib, parts): 15 | if pathLib == posixpath and parts[0].endswith(':\\'): 16 | path = parts[0][:-2]+ ':/' 17 | else: 18 | path = parts[0] 19 | for part in parts[1:]: 20 | path = pathLib.join( path, part) 21 | return path 22 | 23 | def path_convert( path, pathLibFrom, pathLibTo): 24 | parts = path_split( pathLibFrom, path) 25 | return path_join(pathLibTo, parts ) 26 | 27 | def path_rel_to_abs(path): 28 | j = FreeCAD.ActiveDocument.FileName.rfind('/') 29 | k = path.find('/') 30 | absPath = FreeCAD.ActiveDocument.FileName[:j] + path[k:] 31 | FreeCAD.Console.PrintMessage("First %s\n" % FreeCAD.ActiveDocument.FileName[:j]) 32 | FreeCAD.Console.PrintMessage("Next %s\n" % path[k:]) 33 | FreeCAD.Console.PrintMessage("absolutePath is %s\n" % absPath) 34 | if path.startswith('.') and os.path.exists( absPath ): 35 | return absPath 36 | else: 37 | return None 38 | 39 | -------------------------------------------------------------------------------- /assembly2/importPart/selectionMigration.py: -------------------------------------------------------------------------------- 1 | ''' 2 | When parts are updated, the shape elements naming often changes. 3 | i.e. Edge4 -> Edge10 4 | The constraint reference are therefore get mangled. 5 | Below follows code to help migrate the shape references during a shape update. 6 | ''' 7 | 8 | 9 | 10 | from assembly2.selection import * 11 | from assembly2.lib3D import * 12 | from assembly2.solvers.dof_reduction_solver.variableManager import ReversePlacementTransformWithBoundsNormalization 13 | 14 | class _SelectionWrapper: 15 | 'as to interface with assembly2lib classification functions' 16 | def __init__(self, obj, subElementName): 17 | self.Object = obj 18 | self.SubElementNames = [subElementName] 19 | 20 | 21 | def classifySubElement( obj, subElementName ): 22 | selection = _SelectionWrapper( obj, subElementName ) 23 | if planeSelected( selection ): 24 | return 'plane' 25 | elif cylindricalPlaneSelected( selection ): 26 | return 'cylindricalSurface' 27 | elif CircularEdgeSelected( selection ): 28 | return 'circularEdge' 29 | elif LinearEdgeSelected( selection ): 30 | return 'linearEdge' 31 | elif vertexSelected( selection ): 32 | return 'vertex' #all vertex belong to Vertex classification 33 | elif sphericalSurfaceSelected( selection ): 34 | return 'sphericalSurface' 35 | else: 36 | return 'other' 37 | 38 | def classifySubElements( obj ): 39 | C = { 40 | 'plane': [], 41 | 'cylindricalSurface': [], 42 | 'circularEdge':[], 43 | 'linearEdge':[], 44 | 'vertex':[], 45 | 'sphericalSurface':[], 46 | 'other':[] 47 | } 48 | prefixDict = {'Vertexes':'Vertex','Edges':'Edge','Faces':'Face'} 49 | for listName in ['Vertexes','Edges','Faces']: 50 | for j, subelement in enumerate( getattr( obj.Shape, listName) ): 51 | subElementName = '%s%i' % (prefixDict[listName], j+1 ) 52 | catergory = classifySubElement( obj, subElementName ) 53 | C[catergory].append(subElementName) 54 | return C 55 | 56 | class SubElementDifference: 57 | def __init__(self, obj1, SE1, T1, obj2, SE2, T2): 58 | self.obj1 = obj1 59 | self.SE1 = SE1 60 | self.T1 = T1 61 | self.obj2 = obj2 62 | self.SE2 = SE2 63 | self.T2 = T2 64 | self.catergory = classifySubElement( obj1, SE1 ) 65 | #assert self.catergory == classifySubElement( obj2, SE2 ) 66 | self.error1 = 0 #not used for 'vertex','sphericalSurface','other' 67 | if self.catergory in ['cylindricalSurface','circularEdge','plane','linearEdge']: 68 | v1 = getSubElementAxis( obj1, SE1 ) 69 | v2 = getSubElementAxis( obj2, SE2 ) 70 | self.error1 = 1 - dot( T1.unRotate(v1), T2.unRotate(v2) ) 71 | if self.catergory != 'other': 72 | p1 = getSubElementPos( obj1, SE1 ) 73 | p2 = getSubElementPos( obj2, SE2 ) 74 | self.error2 = norm( T1(p1) - T2(p2) ) 75 | else: 76 | self.error2 = 1 - (SE1 == SE2) #subelements have the same name 77 | def __lt__(self, b): 78 | if self.error1 != b.error1: 79 | return self.error1 < b.error1 80 | else: 81 | return self.error2 < b.error2 82 | def __str__(self): 83 | return '' % ( self.catergory, self.SE1, self.SE2, self.error1, self.error2 ) 84 | 85 | def subElements_equal(obj1, SE1, T1, obj2, SE2, T2): 86 | try: 87 | if classifySubElement( obj1, SE1 ) == classifySubElement( obj2, SE2 ): 88 | diff = SubElementDifference(obj1, SE1, T1, obj2, SE2, T2) 89 | return diff.error1 == 0 and diff.error2 == 0 90 | else: 91 | return False 92 | except (IndexError, AttributeError) as e: 93 | return False 94 | 95 | 96 | def importUpdateConstraintSubobjects( doc, oldObject, newObject ): 97 | ''' 98 | TO DO (if time allows): add a task dialog (using FreeCADGui.Control.addDialog) as to allow the user to specify which scheme to use to update the constraint subelement names. 99 | ''' 100 | #classify subelements 101 | if len([c for c in doc.Objects if 'ConstraintInfo' in c.Content and oldObject.Name in [c.Object1, c.Object2] ]) == 0: 102 | debugPrint(3,'Aborint Import Updating Constraint SubElements Names since no matching constraints') 103 | return 104 | partName = oldObject.Name 105 | debugPrint(2,'Import: Updating Constraint SubElements Names: "%s"' % partName) 106 | newObjSubElements = classifySubElements( newObject ) 107 | debugPrint(3,'newObjSubElements: %s' % newObjSubElements) 108 | # generating transforms 109 | T_old = ReversePlacementTransformWithBoundsNormalization( oldObject ) 110 | T_new = ReversePlacementTransformWithBoundsNormalization( newObject ) 111 | for c in doc.Objects: 112 | if 'ConstraintInfo' in c.Content: 113 | if partName == c.Object1: 114 | SubElement = "SubElement1" 115 | elif partName == c.Object2: 116 | SubElement = "SubElement2" 117 | else: 118 | SubElement = None 119 | if SubElement: #same as subElement != None 120 | subElementName = getattr(c, SubElement) 121 | debugPrint(3,' updating %s.%s' % (c.Name, SubElement)) 122 | if not subElements_equal( oldObject, subElementName, T_old, newObject, subElementName, T_new): 123 | catergory = classifySubElement( oldObject, subElementName ) 124 | D = [ SubElementDifference( oldObject, subElementName, T_old, newObject, SE2, T_new) 125 | for SE2 in newObjSubElements[catergory] ] 126 | assert len(D) > 0, "%s no longer has any %ss." % ( partName, catergory) 127 | #for d in D: 128 | # debugPrint(2,' %s' % d) 129 | d_min = min(D) 130 | debugPrint(3,' closest match %s' % d_min) 131 | newSE = d_min.SE2 132 | debugPrint(2,' updating %s.%s %s->%s' % (c.Name, SubElement, subElementName, newSE)) 133 | setattr(c, SubElement, newSE) 134 | c.purgeTouched() #prevent constraint Proxy.execute being called when document recomputed. 135 | else: 136 | debugPrint(3,' leaving %s.%s as is, since subElement in old and new shape are equal' % (c.Name, SubElement)) 137 | 138 | 139 | 140 | 141 | -------------------------------------------------------------------------------- /assembly2/importPart/tests.py: -------------------------------------------------------------------------------- 1 | if __name__ == '__main__': 2 | print('''run tests via 3 | FreeCAD_assembly2$ python2 test.py assembly2.importPart.tests''') 4 | exit() 5 | 6 | import unittest, numpy 7 | import FreeCAD 8 | from FreeCAD import Base 9 | import Part 10 | import os 11 | from fcstd_parser import Fcstd_File_Parser 12 | 13 | class Test_Fcstd_Parser(unittest.TestCase): 14 | 15 | def test_clr_parser( self ): 16 | from fcstd_parser import parse_App_PropertyColor 17 | v = parse_App_PropertyColor( 0b11001000110010001100100000000 ) 18 | error = numpy.linalg.norm( numpy.array(v) - numpy.array( [0.09803921729326248, 0.09803921729326248, 0.09803921729326248, 0.0] ) ) 19 | self.assertTrue( 20 | error < 10**-8, "%s != %s" % (v, (0.09803921729326248, 0.09803921729326248, 0.09803921729326248, 0.0)) 21 | ) 22 | 23 | def _test_parsing( self, Doc, fn ): 24 | from fcstd_parser import Fcstd_File_Parser 25 | assert not os.path.exists( fn ), "%s exists" % fn 26 | Doc.saveAs( fn ) 27 | try: 28 | d = Fcstd_File_Parser( fn ) 29 | finally: 30 | os.remove(fn) 31 | return d 32 | 33 | 34 | def test_simple_box( self ): 35 | Doc = FreeCAD.newDocument('doc1') 36 | Doc.addObject("Part::Box","Box") 37 | Doc.Box.Placement.Base = Base.Vector( 2, 3, 4 ) 38 | Doc.recompute() 39 | d = self._test_parsing( Doc, '/tmp/test_a2import_block.fcstd' ) 40 | self.assertTrue( d.Name == 'test_a2import_block', d.Name ) 41 | 42 | def est_all( self ): 43 | for root, dirs, files in os.walk('/home/'): 44 | for f in files: 45 | if f.endswith('.fcstd'): 46 | fn = os.path.join( root, f ) 47 | print(fn) 48 | Fcstd_File_Parser( fn ) 49 | 50 | #def test_more_complicated_part( self ): 51 | # f2 = Fcstd_File_Parser( '/tmp/part2.fcstd' ) 52 | # self.assertTrue( f2.Name == 'part2', f2.Name ) 53 | 54 | def est_load_assembly( self ): 55 | Fcstd_File_Parser( '/tmp/assem1.fcstd', printLevel=0 ) 56 | 57 | 58 | from importPath import ntpath, posixpath, path_split, path_join 59 | 60 | class Test_Paths(unittest.TestCase): 61 | 62 | 63 | def _test_splitting_and_rejoining( self, pathLib, path ): 64 | parts = path_split( pathLib, path ) 65 | path2 = path_join(pathLib, parts ) 66 | self.assertEqual( path, path2 ) 67 | 68 | @unittest.expectedFailure 69 | def test_splitting_and_rejoining_ntpath1( self ): 70 | self._test_splitting_and_rejoining( ntpath, 'C:/Users/gyb/Desktop/Circular Saw Jig\Side support V1.00.FCStd') 71 | 72 | @unittest.expectedFailure 73 | def test_splitting_and_rejoining_ntpath2( self ): 74 | self._test_splitting_and_rejoining( ntpath, 'C:/Users/gyb/Desktop/Circular Saw Jig/Side support V1.00.FCStd') 75 | 76 | def test_splitting_and_rejoining_posix( self ): 77 | self._test_splitting_and_rejoining( posixpath, '/temp/hello1/foo.FCStd') 78 | 79 | def test_conversion( self ): 80 | from importPath import ntpath, posixpath, path_convert 81 | path = r'C:\Users\gyb\Desktop\Circular Saw Jig\Side support V1.00.FCStd' 82 | converted = path_convert( path, ntpath, posixpath) 83 | correct = 'C:/Users/gyb/Desktop/Circular Saw Jig/Side support V1.00.FCStd' 84 | self.assertEqual( converted, correct ) 85 | -------------------------------------------------------------------------------- /assembly2/importPart/viewProviderProxy.py: -------------------------------------------------------------------------------- 1 | import FreeCAD, FreeCADGui 2 | from pivy import coin 3 | import traceback 4 | 5 | def group_constraints_under_parts(): 6 | preferences = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/Assembly2") 7 | return preferences.GetBool('groupConstraintsUnderParts', True) 8 | 9 | def allow_deletetion_when_activice_doc_ne_object_doc(): 10 | parms = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/Assembly2") 11 | return parms.GetBool('allowDeletetionFromExternalDocuments', False) 12 | 13 | 14 | class ImportedPartViewProviderProxy: 15 | def onDelete(self, viewObject, subelements): # subelements is a tuple of strings 16 | if not allow_deletetion_when_activice_doc_ne_object_doc() and FreeCAD.activeDocument() != viewObject.Object.Document: 17 | FreeCAD.Console.PrintMessage("preventing deletetion of %s since active document != %s. Disable behavior in assembly2 preferences.\n" % (viewObject.Object.Label, viewObject.Object.Document.Name) ) 18 | return False 19 | obj = viewObject.Object 20 | doc = obj.Document 21 | #FreeCAD.Console.PrintMessage('ConstraintObjectViewProviderProxy.onDelete: removing constraints refering to %s (label:%s)\n' % (obj.Name, obj.Label)) 22 | deleteList = [] 23 | for c in doc.Objects: 24 | if 'ConstraintInfo' in c.Content: 25 | if obj.Name in [ c.Object1, c.Object2 ]: 26 | deleteList.append(c) 27 | if len(deleteList) > 0: 28 | #FreeCAD.Console.PrintMessage(" delete list %s\n" % str(deleteList) ) 29 | for c in deleteList: 30 | #FreeCAD.Console.PrintMessage(" - removing constraint %s\n" % c.Name ) 31 | if hasattr( c.Proxy, 'mirrorName'): # then also deleter constraints mirror 32 | doc.removeObject( c.Proxy.mirrorName ) 33 | doc.removeObject(c.Name) 34 | return True # If False is returned the object won't be deleted 35 | 36 | def __getstate__(self): 37 | return None 38 | 39 | def __setstate__(self, state): 40 | return None 41 | 42 | def attach(self, vobj): 43 | self.object_Name = vobj.Object.Name 44 | #self.ViewObject = vobj 45 | self.Object = vobj.Object 46 | 47 | def claimChildren(self): 48 | ''' 49 | loading notes: 50 | if isinstance( getattr(obj.ViewObject, 'Proxy'): 51 | ... 52 | elif elif isinstance( getattr(obj.ViewObject, 'Proxy'), ConstraintViewProviderProxy): 53 | ... 54 | check did not work. 55 | 56 | theory, FreeCAD loading in the follow order 57 | -> load stripped objects 58 | -> set object properties 59 | -> loads stripped proxies (and calls proxies methods, such as claim children) 60 | -> set proxies properties 61 | 62 | or something like that ... 63 | ''' 64 | 65 | 66 | children = [] 67 | if hasattr(self, 'Object'): 68 | importedPart = self.Object 69 | else: 70 | return [] 71 | if not group_constraints_under_parts(): 72 | return [] 73 | 74 | #if hasattr(self, 'object_Name'): 75 | # importedPart = FreeCAD.ActiveDocument.getObject( self.object_Name ) 76 | # if importedPart == None: 77 | # return [] 78 | #else: 79 | # return [] 80 | for obj in importedPart.Document.Objects: 81 | if hasattr( obj, 'ViewObject'): 82 | if 'ConstraintNfo' in obj.Content: #constraint mirror 83 | if obj.Object2 == importedPart.Name: 84 | children.append( obj ) 85 | elif 'ConstraintInfo' in obj.Content: #constraint original 86 | #if hasattr(obj.ViewObject.Proxy, 'mirrorName'): #wont work as obj.ViewObject.Proxy still being loaded 87 | if obj.Object1 == importedPart.Name: 88 | children.append( obj ) 89 | return children 90 | 91 | def setupContextMenu(self, ViewObject, popup_menu): 92 | ''' for playing around in an iPythonConsole: 93 | from PySide import * 94 | app = QtGui.QApplication([]) 95 | menu = QtGui.QMenu() 96 | ''' 97 | #self.pop_up_menu_items = [] #worried about the garbage collector ... 98 | #popup_menu.addSeparator() 99 | #menu = popup_menu.addMenu('Assembly 2') 100 | #PopUpMenuItem( self, menu, 'edit', 'assembly2_editImportedPart' ) 101 | #if self.Object.Document == FreeCAD.ActiveDocument: 102 | # for label, cmd in [ 103 | # [ 'move', 'assembly2_movePart'], 104 | # [ 'duplicate', 'assembly2_duplicatePart'], 105 | # [ 'fork', 'assembly2_forkImportedPart'], 106 | # [ 'delete constraints', 'assembly2_deletePartsConstraints'] 107 | # ]: 108 | # PopUpMenuItem( self, menu, label, cmd ) 109 | # abandoned since context menu not shown when contextMenu activated in viewer 110 | 111 | class PopUpMenuItem: 112 | def __init__( self, proxy, menu, label, Freecad_cmd ): 113 | self.Object = proxy.Object 114 | self.Freecad_cmd = Freecad_cmd 115 | action = menu.addAction(label) 116 | action.triggered.connect( self.execute ) 117 | proxy.pop_up_menu_items.append( self ) 118 | def execute( self ): 119 | try: 120 | FreeCADGui.runCommand( self.Freecad_cmd ) 121 | except: 122 | FreeCAD.Console.PrintError( traceback.format_exc() ) 123 | -------------------------------------------------------------------------------- /assembly2/solvers/__init__.py: -------------------------------------------------------------------------------- 1 | ''' 2 | The solvers for assembly2 constraint systems are accessed here. 3 | ''' 4 | 5 | from assembly2.core import * 6 | from assembly2.constraints import updateOldStyleConstraintProperties 7 | from assembly2.constraints import common 8 | from .common import constraintsObjectsAllExist 9 | from assembly2.solvers.dof_reduction_solver import solveConstraints as solveConstraints_dof_reduction_solver 10 | from assembly2.solvers.newton_solver import solveConstraints as solveConstraints_newton_solver 11 | 12 | _default = "_assembly2_preference_" 13 | 14 | def solveConstraints( 15 | doc, 16 | solver_name = _default, 17 | showFailureErrorDialog = True, 18 | printErrors = True, 19 | use_cache = _default, 20 | ): 21 | if solver_name == _default or use_cache == _default: 22 | preferences = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/Assembly2") 23 | if solver_name == _default: 24 | #solver_name = preferences.GetString('solver_to_use', 'dof_reduction_solver') 25 | solver_name = { 26 | 0:'dof_reduction_solver', 27 | 1:'newton_solver_slsqp' 28 | }[ preferences.GetInt('solver_to_use',0) ] 29 | if use_cache == _default: 30 | use_cache = preferences.GetBool('useCache', False) 31 | if not constraintsObjectsAllExist(doc): 32 | return 33 | updateOldStyleConstraintProperties(doc) 34 | if solver_name == 'dof_reduction_solver': 35 | return solveConstraints_dof_reduction_solver( doc, showFailureErrorDialog, printErrors, use_cache ) 36 | elif solver_name == 'newton_solver_slsqp': 37 | return solveConstraints_newton_solver( doc, showFailureErrorDialog, printErrors, use_cache ) 38 | else: 39 | raise NotImplementedError( '%s solver interface not added yet' % solver_name ) 40 | 41 | 42 | 43 | class Assembly2SolveConstraintsCommand: 44 | def Activated(self): 45 | solveConstraints( FreeCAD.ActiveDocument ) 46 | def GetResources(self): 47 | return { 48 | 'Pixmap' : ':/assembly2/icons/assembly2SolveConstraints.svg', 49 | 'MenuText': 'Solve Assembly 2 constraints', 50 | 'ToolTip': 'Solve Assembly 2 constraints' 51 | } 52 | 53 | FreeCADGui.addCommand('assembly2_solveConstraints', Assembly2SolveConstraintsCommand()) 54 | 55 | 56 | -------------------------------------------------------------------------------- /assembly2/solvers/common.py: -------------------------------------------------------------------------------- 1 | from assembly2.core import debugPrint 2 | 3 | 4 | def findBaseObject( doc, objectNames ): 5 | debugPrint( 4,'solveConstraints: searching for fixed object to begin solving constraints from.' ) 6 | fixed = [ getattr( doc.getObject( name ), 'fixedPosition', False ) for name in objectNames ] 7 | if sum(fixed) > 0: 8 | return objectNames[ fixed.index(True) ] 9 | if sum(fixed) == 0: 10 | debugPrint( 1, 'It is recommended that the assembly 2 module is used with parts imported using the assembly 2 module.') 11 | debugPrint( 1, 'This allows for part updating, parts list support, object copying (shift + assembly2 move) and also tells the solver which objects to treat as fixed.') 12 | debugPrint( 1, 'since no objects have the fixedPosition attribute, fixing the postion of the first object in the first constraint') 13 | debugPrint( 1, 'assembly 2 solver: assigning %s a fixed position' % objectNames[0]) 14 | debugPrint( 1, 'assembly 2 solver: assigning %s, %s a fixed position' % (objectNames[0], doc.getObject(objectNames[0]).Label)) 15 | return objectNames[0] 16 | 17 | def constraintsObjectsAllExist( doc ): 18 | objectNames = [ obj.Name for obj in doc.Objects if not 'ConstraintInfo' in obj.Content ] 19 | for obj in doc.Objects: 20 | if 'ConstraintInfo' in obj.Content: 21 | if not (obj.Object1 in objectNames and obj.Object2 in objectNames): 22 | flags = QtGui.QMessageBox.StandardButton.Yes | QtGui.QMessageBox.StandardButton.Abort 23 | message = "%s is refering to an object no longer in the assembly. Delete constraint? otherwise abort solving." % obj.Name 24 | response = QtGui.QMessageBox.critical(QtGui.QApplication.activeWindow(), "Broken Constraint", message, flags ) 25 | if response == QtGui.QMessageBox.Yes: 26 | FreeCAD.Console.PrintError("removing constraint %s" % obj.Name) 27 | doc.removeObject(obj.Name) 28 | else: 29 | missingObject = obj.Object2 if obj.Object1 in objectNames else obj.Object1 30 | FreeCAD.Console.PrintError("aborted solving constraints due to %s refering the non-existent object %s" % (obj.Name, missingObject)) 31 | return False 32 | return True 33 | -------------------------------------------------------------------------------- /assembly2/solvers/dof_reduction_solver/__init__.py: -------------------------------------------------------------------------------- 1 | ''' 2 | degree-of-freedom reduction solver 3 | ''' 4 | from assembly2.lib3D import * 5 | from assembly2.solvers.common import * 6 | from assembly2.core import QtGui 7 | import time, numpy 8 | from numpy import pi, inf 9 | from numpy.linalg import norm 10 | from .solverLib import * 11 | from .variableManager import VariableManager 12 | from .constraintSystems import * 13 | 14 | import traceback 15 | from . import cache as cacheLib 16 | 17 | cache = cacheLib.defaultCache 18 | 19 | def solveConstraints( 20 | doc, 21 | showFailureErrorDialog=True, 22 | printErrors=True, 23 | use_cache=False 24 | ): 25 | T_start = time.time() 26 | constraintObjectQue = [ obj for obj in doc.Objects if 'ConstraintInfo' in obj.Content ] 27 | #doc.Objects already in tree order so no additional sorting / order checking required for constraints. 28 | objectNames = [] 29 | for c in constraintObjectQue: 30 | for attr in ['Object1','Object2']: 31 | objectName = getattr(c, attr, None) 32 | if objectName != None and not objectName in objectNames: 33 | objectNames.append( objectName ) 34 | variableManager = VariableManager( doc, objectNames ) 35 | debugPrint(3,' variableManager.X0 %s' % variableManager.X0 ) 36 | constraintSystem = FixedObjectSystem( variableManager, findBaseObject(doc, objectNames) ) 37 | debugPrint(4, 'solveConstraints base system: %s' % constraintSystem.str() ) 38 | 39 | solved = True 40 | 41 | if use_cache: 42 | t_cache_start = time.time() 43 | constraintSystem, que_start = cache.retrieve( constraintSystem, constraintObjectQue) 44 | debugPrint(3,"~cached solution available for first %i out-off %i constraints (retrieved in %3.2fs)" % (que_start, len(constraintObjectQue), time.time() - t_cache_start ) ) 45 | cache.prepare() 46 | else: 47 | que_start = 0 48 | 49 | for constraintObj in constraintObjectQue[que_start:]: 50 | debugPrint( 3, ' parsing %s, type:%s' % (constraintObj.Name, constraintObj.Type )) 51 | try: 52 | cArgs = [variableManager, constraintObj] 53 | if not constraintSystem.containtsObject( constraintObj.Object1) and not constraintSystem.containtsObject( constraintObj.Object2): 54 | constraintSystem = AddFreeObjectsUnion(constraintSystem, *cArgs) 55 | if constraintObj.Type == 'plane': 56 | if constraintObj.SubElement2.startswith('Face'): #otherwise vertex 57 | constraintSystem = AxisAlignmentUnion(constraintSystem, *cArgs, constraintValue = constraintObj.directionConstraint ) 58 | constraintSystem = PlaneOffsetUnion(constraintSystem, *cArgs, constraintValue = constraintObj.offset.Value) 59 | elif constraintObj.Type == 'angle_between_planes': 60 | constraintSystem = AngleUnion(constraintSystem, *cArgs, constraintValue = constraintObj.angle.Value*pi/180 ) 61 | elif constraintObj.Type == 'axial': 62 | constraintSystem = AxisAlignmentUnion(constraintSystem, *cArgs, constraintValue = constraintObj.directionConstraint) 63 | constraintSystem = AxisDistanceUnion(constraintSystem, *cArgs, constraintValue = 0) 64 | if constraintObj.lockRotation: constraintSystem = LockRelativeAxialRotationUnion(constraintSystem, *cArgs, constraintValue = 0) 65 | elif constraintObj.Type == 'circularEdge': 66 | constraintSystem = AxisAlignmentUnion(constraintSystem, *cArgs, constraintValue=constraintObj.directionConstraint) 67 | constraintSystem = AxisDistanceUnion(constraintSystem, *cArgs, constraintValue=0) 68 | constraintSystem = PlaneOffsetUnion(constraintSystem, *cArgs, constraintValue=constraintObj.offset.Value) 69 | if constraintObj.lockRotation: constraintSystem = LockRelativeAxialRotationUnion(constraintSystem, *cArgs, constraintValue = 0) 70 | elif constraintObj.Type == 'sphericalSurface': 71 | constraintSystem = VertexUnion(constraintSystem, *cArgs, constraintValue=0) 72 | else: 73 | raise NotImplementedError('constraintType %s not supported yet' % constraintObj.Type) 74 | if use_cache: 75 | cache.record( constraintSystem ) 76 | 77 | 78 | except Assembly2SolverError as e: 79 | if printErrors: 80 | FreeCAD.Console.PrintError('UNABLE TO SOLVE CONSTRAINTS! info:') 81 | FreeCAD.Console.PrintError(e) 82 | solved = False 83 | break 84 | except: 85 | if printErrors: 86 | FreeCAD.Console.PrintError('UNABLE TO SOLVE CONSTRAINTS! info:') 87 | FreeCAD.Console.PrintError( traceback.format_exc()) 88 | solved = False 89 | break 90 | if solved: 91 | debugPrint(4,'placement X %s' % constraintSystem.variableManager.X ) 92 | 93 | if use_cache: 94 | t_cache_record_start = time.time() 95 | cache.commit( constraintSystem, constraintObjectQue, que_start) 96 | debugPrint( 4,' time cache.record %3.2fs' % (time.time()-t_cache_record_start) ) 97 | 98 | t_update_freecad_start = time.time() 99 | variableManager.updateFreeCADValues( constraintSystem.variableManager.X ) 100 | debugPrint( 4,' time to update FreeCAD placement variables %3.2fs' % (time.time()-t_update_freecad_start) ) 101 | 102 | debugPrint(2,'Constraint system solved in %2.2fs; resulting system has %i degrees-of-freedom' % (time.time()-T_start, len( constraintSystem.degreesOfFreedom))) 103 | elif showFailureErrorDialog and QtGui.qApp != None: #i.e. GUI active 104 | # http://www.blog.pythonlibrary.org/2013/04/16/pyside-standard-dialogs-and-message-boxes/ 105 | flags = QtGui.QMessageBox.StandardButton.Yes 106 | flags |= QtGui.QMessageBox.StandardButton.No 107 | #flags |= QtGui.QMessageBox.Ignore 108 | message = """The assembly2 solver failed to satisfy the constraint "%s". 109 | 110 | possible causes 111 | - impossible/contridictorary constraints have be specified, or 112 | - the contraint problem is too difficult for the solver, or 113 | - a bug in the assembly 2 workbench 114 | 115 | potential solutions 116 | - redefine the constraint (popup menu item in the treeView) 117 | - delete constraint, and try again using a different constraint scheme. 118 | 119 | Delete constraint "%s"? 120 | """ % (constraintObj.Name, constraintObj.Name) 121 | response = QtGui.QMessageBox.critical(QtGui.QApplication.activeWindow(), "Solver Failure!", message, flags) 122 | if response == QtGui.QMessageBox.Yes: 123 | from assembly2.constraints import removeConstraint 124 | removeConstraint( constraintObj ) 125 | #elif response == QtGui.QMessageBox.Ignore: 126 | # variableManager.updateFreeCADValues( constraintSystem.variableManager.X ) 127 | return constraintSystem if solved else None 128 | -------------------------------------------------------------------------------- /assembly2/solvers/dof_reduction_solver/degreesOfFreedom.py: -------------------------------------------------------------------------------- 1 | from assembly2.lib3D import * 2 | import numpy 3 | 4 | 5 | maxStep_linearDisplacement = 10.0 6 | class PlacementDegreeOfFreedom: 7 | def __init__(self, parentSystem, objName, object_dof): 8 | self.system = parentSystem 9 | self.objName = objName 10 | self.object_dof = object_dof 11 | self.vM = parentSystem.variableManager 12 | self.ind = parentSystem.variableManager.index[objName] + object_dof 13 | if self.ind % 6 < 3: 14 | self.directionVector = numpy.zeros(3) 15 | self.directionVector[ self.ind % 6 ] = 1 16 | def getValue( self): 17 | return self.vM.X[self.ind] 18 | def setValue( self, value): 19 | self.vM.X[self.ind] = value 20 | def maxStep(self): 21 | if self.ind % 6 < 3: 22 | return maxStep_linearDisplacement 23 | else: 24 | return pi/5 25 | def rotational(self): 26 | return self.ind % 6 > 2 27 | def migrate_to_new_variableManager( self, new_vM): 28 | self.vM = new_vM 29 | self.ind = new_vM.index[self.objName] + self.object_dof 30 | def str(self, indent=''): 31 | return '%s' % (indent, self.objName, ['x','y','z','azimuth','elavation','rotation'][self.ind % 6], self.getValue()) 32 | def __repr__(self): 33 | return self.str() 34 | 35 | 36 | class LinearMotionDegreeOfFreedom: 37 | def __init__(self, parentSystem, objName): 38 | self.system = parentSystem 39 | self.objName = objName 40 | self.vM = parentSystem.variableManager 41 | self.objInd = parentSystem.variableManager.index[objName] 42 | def setDirection(self, directionVector): 43 | self.directionVector = directionVector 44 | def getValue( self ): 45 | i = self.objInd 46 | return dotProduct( self.directionVector, self.vM.X[i:i+3]) 47 | def setValue( self, value): 48 | currentValue = self.getValue() 49 | correction = (value -currentValue)*self.directionVector 50 | i = self.objInd 51 | self.vM.X[i:i+3] = self.vM.X[i:i+3] + correction 52 | def maxStep(self): 53 | return maxStep_linearDisplacement #inf 54 | def rotational(self): 55 | return False 56 | def migrate_to_new_variableManager( self, new_vM): 57 | self.vM = new_vM 58 | self.objInd = new_vM.index[self.objName] 59 | def str(self, indent=''): 60 | return '%s' % (indent, self.objName, self.directionVector, self.getValue()) 61 | def __repr__(self): 62 | return self.str() 63 | 64 | def prettyPrintArray( A, indent=' ', fmt='%1.1e' ): 65 | def pad(t): 66 | return t if t[0] == '-' else ' ' + t 67 | for r in A: 68 | txt = ' '.join( pad(fmt % v) for v in r) 69 | print(indent + '[ %s ]' % txt) 70 | 71 | class AxisRotationDegreeOfFreedom: 72 | ''' 73 | calculate the rotation variables ( azi, ela, angle )so that 74 | R_effective = R_about_axis * R_to_align_axis 75 | where 76 | R = azimuth_elevation_rotation_matrix(azi, ela, theta ) 77 | 78 | ''' 79 | def __init__(self, parentSystem, objName): 80 | self.system = parentSystem 81 | self.vM = parentSystem.variableManager 82 | self.objName = objName 83 | self.objInd = self.vM.index[objName] 84 | def setAxis(self, axis, axis_r, check_R_to_align_axis=False): 85 | if not ( hasattr(self, 'axis') and numpy.array_equal( self.axis, axis )): #if to avoid unnessary updates. 86 | self.axis = axis 87 | axis2, angle2 = rotation_required_to_rotate_a_vector_to_be_aligned_to_another_vector( axis_r, axis ) 88 | self.R_to_align_axis = axis_rotation_matrix( angle2, *axis2 ) 89 | if check_R_to_align_axis: 90 | print('NOTE: checking AxisRotationDegreeOfFreedom self.R_to_align_axis') 91 | if norm( dotProduct(self.R_to_align_axis, axis_r) - axis ) > 10**-12: 92 | raise ValueError(" dotProduct(self.R_to_align_axis, axis_r) - axis ) [%e] > 10**-12" % norm( dotProduct(self.R_to_align_axis, axis_r) - axis )) 93 | 94 | if not hasattr(self, 'x_ref_r'): 95 | self.x_ref_r, self.y_ref_r = plane_degrees_of_freedom( axis_r ) 96 | else: #use gram_schmidt_orthonormalization ; import for case where axis close to z-axis, where numerical noise effects the azimuth angle used to generate plane DOF... 97 | notUsed, self.x_ref_r, self.y_ref_r = gram_schmidt_orthonormalization( axis_r, self.x_ref_r, self.y_ref_r) #still getting wonky rotations :( 98 | self.x_ref = dotProduct(self.R_to_align_axis, self.x_ref_r) 99 | self.y_ref = dotProduct(self.R_to_align_axis, self.y_ref_r) 100 | 101 | def determine_R_about_axis(self, R_effective, checkAnswer=True, tol=10**-12): #not used anymore 102 | 'determine R_about_axis so that R_effective = R_about_axis * R_to_align_axis' 103 | A = self.R_to_align_axis.transpose() 104 | X = numpy.array([ 105 | numpy.linalg.solve(A, R_effective[row,:]) for row in range(3) 106 | ]) 107 | #prettyPrintArray(X) 108 | if checkAnswer: 109 | print(' determine_R_about_axis: diff between R_effective and R_about_axis * R_to_align_axis (should be all close to zero):') 110 | error = R_effective - dotProduct(X, self.R_to_align_axis) 111 | assert norm(error) <= tol 112 | return X 113 | 114 | def vectorsAngleInDofsCoordinateSystem(self,v): 115 | return numpy.arctan2( 116 | dotProduct(self.y_ref, v), 117 | dotProduct(self.x_ref, v), 118 | ) 119 | 120 | def getValue( self, refApproach=True, tol=10**-7 ): 121 | i = self.objInd 122 | R_effective = azimuth_elevation_rotation_matrix( *self.vM.X[i+3:i+6] ) 123 | if refApproach: 124 | v = dotProduct( R_effective, self.x_ref_r) 125 | if tol != None and abs( dotProduct(v, self.axis) ) > tol: 126 | raise ValueError("abs( dotProduct(v, self.axis) ) > %e [error %e]" % (tol, abs( dotProduct(v, self.axis) ))) 127 | angle = self.vectorsAngleInDofsCoordinateSystem(v) 128 | else: 129 | raise NotImplementedError("does not work yet") 130 | R_effective = azimuth_elevation_rotation_matrix( *self.vM.X[i+3:i+6] ) 131 | R_about_axis = self.determine_R_about_axis(R_effective) 132 | axis, angle = rotation_matrix_axis_and_angle( R_about_axis ) 133 | print( axis ) 134 | print( self.axis ) 135 | # does not work because axis(R_about_axis) != self.axis #which is damm weird if you ask me 136 | return angle 137 | 138 | def setValue( self, angle): 139 | R_about_axis = axis_rotation_matrix( angle, *self.axis ) 140 | R = dotProduct(R_about_axis, self.R_to_align_axis) 141 | axis, angle = rotation_matrix_axis_and_angle( R ) 142 | #todo, change to quaternions 143 | #Q2 = quaternion2( self.value, *self.axis ) 144 | #q0,q1,q2,q3 = quaternion_multiply( Q2, self.Q1 ) 145 | #axis, angle = quaternion_to_axis_and_angle( q1, q2, q3, q0 ) 146 | azi, ela = axis_to_azimuth_and_elevation_angles(*axis) 147 | i = self.objInd 148 | self.vM.X[i+3:i+6] = azi, ela, angle 149 | 150 | def maxStep(self): 151 | return pi/5 152 | def rotational(self): 153 | return True 154 | def migrate_to_new_variableManager( self, new_vM): 155 | self.vM = new_vM 156 | self.objInd = new_vM.index[self.objName] 157 | def str(self, indent=''): 158 | return '%s' % (indent, self.objName, self.axis, self.getValue()) 159 | def __repr__(self): 160 | return self.str() 161 | 162 | 163 | -------------------------------------------------------------------------------- /assembly2/solvers/dof_reduction_solver/docs/assembly2_docs.aux: -------------------------------------------------------------------------------- 1 | \relax 2 | \@writefile{toc}{\contentsline {section}{\numberline {1}Solver approach}{1}} 3 | \@writefile{toc}{\contentsline {section}{\numberline {2}Plane mating constraint}{1}} 4 | \@writefile{toc}{\contentsline {section}{\numberline {3}plane offset union - analytical solution}{1}} 5 | \@writefile{toc}{\contentsline {section}{\numberline {4}Numerically Reducing the System's Degrees-of-Freedom}{2}} 6 | \@writefile{toc}{\contentsline {subsection}{\numberline {4.1}assuming $h$ is a linear system}{2}} 7 | \newlabel{eq:reduction_dof_linear1}{{10}{2}} 8 | \newlabel{eq:reduction_dof_linear2}{{11}{2}} 9 | \@writefile{toc}{\contentsline {subsection}{\numberline {4.2}assuming $h$ is a quatdratic system}{2}} 10 | \newlabel{eq:reduction_dof_quad1}{{13}{2}} 11 | \newlabel{eq:reduction_dof_quad2}{{14}{2}} 12 | \@writefile{toc}{\contentsline {subsection}{\numberline {4.3}trail and error approach}{3}} 13 | \@writefile{toc}{\contentsline {section}{\numberline {5}generating repairing/orthogonal basis functions}{3}} 14 | -------------------------------------------------------------------------------- /assembly2/solvers/dof_reduction_solver/docs/assembly2_docs.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hamish2014/FreeCAD_assembly2/2da784f18b8af16facf3c0e28a69f0430dc7bb60/assembly2/solvers/dof_reduction_solver/docs/assembly2_docs.pdf -------------------------------------------------------------------------------- /assembly2/solvers/dof_reduction_solver/docs/assembly2_docs.tex: -------------------------------------------------------------------------------- 1 | \documentclass[a4paper,10pt]{article} 2 | \usepackage[utf8]{inputenc} 3 | \usepackage[cm]{fullpage} 4 | \usepackage{amsmath} 5 | \usepackage{amssymb} 6 | 7 | %opening 8 | \title{Assembly 2 Docs } 9 | \author{Hamish} 10 | 11 | \begin{document} 12 | 13 | \maketitle 14 | 15 | \begin{abstract} 16 | 17 | Documents for personal use to help track of the maths behind the Assembly 2 workbench add-on for FreeCAD~v0.15+. 18 | 19 | \end{abstract} 20 | 21 | \tableofcontents 22 | 23 | % \vfil 24 | 25 | % \pagebreak 26 | 27 | 28 | \section{Solver approach} 29 | 30 | Adjust the placement variables (position and rotation variables, 6 for each object) as to satisfy all constraints. 31 | Approach entails reducing the systems degrees-of-freedom, one constraint at a time until all constraints are processed. 32 | Ideally, this degrees-of-freedom can be identified, so that they can be adjusted with having to check/reprocess previous constraints. 33 | For non-simple systems this is not practical however as there are to many combinations to hard-code. 34 | 35 | Therefore a hierarchical constraint system is used. 36 | When attempting the solve the current constraint, the placement variables are also adjusted/refreshed according to the previous constraints to allow for non-perfect degrees-of-freedom. 37 | This is done in a hierarchical way, with parent constraints minimally adjusting the placement variables as to satisfy the assembly constraints. 38 | 39 | \section{Plane mating constraint} 40 | 41 | 2 parts 42 | \begin{enumerate} 43 | \item rotating objects as to align selected faces (not done if face and vertex are selected), \textbf{axis alignment union} 44 | \item moving the objects as to specified offset is satisfied, \textbf{plane offset union} 45 | \end{enumerate} 46 | 47 | 48 | \section{plane offset union - analytical solution} 49 | 50 | inputs 51 | \begin{description} 52 | \item[~~~~$\mathbf{a}$] - normal vector of reference face. 53 | \item[~~~~$\mathbf{p}$] - point on object 1. 54 | \item[~~~~$\mathbf{q}$] - point on object 2. 55 | \item[~~~~$\alpha$] - specified offset 56 | \item[~~~~$\mathbf{d}_1,\mathbf{d}_2...$] - linear motion degree-of-freedom for object 1/2 (max of 3, min of 1) 57 | \end{description} 58 | required displacement in the direction of $\mathbf{a}$: 59 | \begin{equation} 60 | r = \mathbf{a} \cdot (\mathbf{p} - \mathbf{q}) - \alpha 61 | \end{equation} 62 | require components for each $\mathbf{d}$, $v_1,v_2,...$ therefore equal to 63 | \begin{equation} 64 | \mathbf{a} \cdot ( v_1 \mathbf{d}_1 + v_2 \mathbf{d}_2 + v_3 \mathbf{d}_3 ) = r 65 | \end{equation} 66 | which has infinite solutions if more than one degree-of-freedom with $\mathbf{d} \cdot \mathbf{a} \ne 0 $ 67 | 68 | Therefore looking for least norm solution of: 69 | \begin{equation} 70 | r = a_x ( v_1 d_{1,x} + v_2 d_{2,x } + \dots) + a_y ( v_1 d_{1,y} + v_2 d_{2,y } + \dots) + a_z ( v_1 d_{1,z} + v_2 d_{2,z } + \dots) 71 | \end{equation} 72 | which gives 73 | \begin{align} 74 | \begin{bmatrix} 75 | (a_x d_{1,x} + a_y d_{1,y} + a_z d_{1,z}) & 76 | (a_x d_{2,x} + a_y d_{2,y} + a_z d_{2,z}) & 77 | (a_x d_{3,x} + a_y d_{3,y} + a_z d_{3,z}) 78 | \end{bmatrix} 79 | \begin{bmatrix} 80 | v_1 \\ v_2 \\ v_3 81 | \end{bmatrix} 82 | = [ r ] \\ 83 | \begin{bmatrix} 84 | \mathbf{a} \cdot \mathbf{d}_1 & \mathbf{a} \cdot \mathbf{d}_2 & \mathbf{a} \cdot \mathbf{d}_3 85 | \end{bmatrix} 86 | \begin{bmatrix} 87 | v_1 \\ v_2 \\ v_3 88 | \end{bmatrix} = r \\ 89 | A \mathbf{v} = [r] 90 | \end{align} 91 | then solve for least norm using numpy.linalg.lstsq 92 | 93 | 94 | 95 | \section{Numerically Reducing the System's Degrees-of-Freedom} 96 | 97 | New degrees-of-freedom need to be determined which allow for adjustment without the constraint's equality function, $h$, being violated. 98 | Therefore $h$ as a function of the current DOF $\mathbf{x}$ needs be satisfied so that 99 | \begin{equation} 100 | h(\mathbf{x}) = 0 101 | \end{equation} 102 | 103 | The DOF of the new system will therefore result in a change of $\mathbf{x}$ ( $\Delta \mathbf{x}$ ) so that 104 | \begin{equation} 105 | h(\mathbf{x} + \Delta \mathbf{x}) = 0 106 | \end{equation} 107 | 108 | \subsection{assuming $h$ is a linear system} 109 | 110 | \begin{align} 111 | \begin{bmatrix} dh/dx1 & df/dx2 &\dots \end{bmatrix} \mathbf{x} + c &= 0 \\ 112 | \mathbf{b} \cdot \mathbf{x} + c &= 0 \label{eq:reduction_dof_linear1} 113 | \end{align} 114 | 115 | DOF therefore need to satisfy 116 | \begin{align} 117 | \mathbf{b} \cdot (\mathbf{x} + \Delta \mathbf{x}) + c &= 0 \label{eq:reduction_dof_linear2} 118 | \end{align} 119 | leading to (eq \ref{eq:reduction_dof_linear2}- eq \ref{eq:reduction_dof_linear1}) 120 | \begin{align} 121 | \mathbf{b} \cdot \Delta \mathbf{x} &= 0 122 | \end{align} 123 | Therefore any vector orgonal to $\mathbf{b}$ is a degree of freedom. 124 | 125 | \subsection{assuming $h$ is a quatdratic system} 126 | 127 | \begin{align} 128 | \frac{1}{2} \mathbf{x}^T \mathbf{A} \mathbf{x} + \mathbf{b} \cdot \mathbf{x} + c &= 0 \label{eq:reduction_dof_quad1} 129 | \end{align} 130 | The changes in $\mathbf{x}$ are allowed which satisfy 131 | \begin{align} 132 | \frac{1}{2} (\mathbf{x}+\Delta \mathbf{x})^T \mathbf{A} (\mathbf{x}+\Delta \mathbf{x}) + \mathbf{b} \cdot (\mathbf{x}+\Delta \mathbf{x}) + c &= 0 \label{eq:reduction_dof_quad2} \\ 133 | \frac{1}{2} (\mathbf{x}+\Delta \mathbf{x})^T ( \mathbf{A} \mathbf{x}+ \mathbf{A} \Delta \mathbf{x}) + \mathbf{b} \cdot (\mathbf{x}+\Delta \mathbf{x}) + c &= 0 \\ 134 | \frac{1}{2} (\mathbf{x}^T \mathbf{A} \mathbf{x} + \mathbf{x}^T \mathbf{A} \Delta \mathbf{x} + \Delta\mathbf{x}^T\mathbf{A}\mathbf{x}+ \Delta\mathbf{x}^T\mathbf{A} \Delta \mathbf{x}) + \mathbf{b} \cdot (\mathbf{x}+\Delta \mathbf{x}) + c &= 0 135 | \end{align} 136 | since $\frac{1}{2} \mathbf{x}^T \mathbf{A} \mathbf{x} + \mathbf{b} \cdot \mathbf{x} + c = 0$, 137 | \begin{align} 138 | \frac{1}{2} (\mathbf{x}^T \mathbf{A} \Delta \mathbf{x} + \Delta\mathbf{x}^T\mathbf{A}\mathbf{x}+ \Delta\mathbf{x}^T\mathbf{A} \Delta \mathbf{x}) + \mathbf{b} \cdot \Delta \mathbf{x} &= 0 139 | \end{align} 140 | 141 | furthmore assuming that $\mathbf{x}^T \mathbf{A} \Delta \mathbf{x} = \Delta\mathbf{x}^T\mathbf{A}\mathbf{x}$ ($\mathbf{A} = \mathbf{A} ^T)$ gives 142 | 143 | \begin{align} 144 | \frac{1}{2} \Delta\mathbf{x}^T\mathbf{A} \Delta \mathbf{x} + (\mathbf{b}+\mathbf{x}^T \mathbf{A}) \cdot \Delta \mathbf{x} &= 0 145 | \end{align} 146 | 147 | 148 | Find solutions for $\Delta \mathbf{x}$ allows for the degrees-of-freedom of the resulting system to be determined. 149 | By definition these DOFs can be alterted without violating $h(\mathbf{x})$(eq. \ref{eq:reduction_dof_quad1}), hence not requiring the previous constraints to be resatisfied/resolved. 150 | 151 | Scenarios do occur however, where $h(\mathbf{x}$ is a complicated functional which is not quatdratic. 152 | 153 | 154 | \subsection{trail and error approach} 155 | 156 | Here the approach is to pass dimensions of $\mathbf{x}$ directly through, to create non-perfect or false degrees of freedom. 157 | The passed through dimensions of $\mathbf{y}$ when altered require that the previous constraints in the system be resolved. 158 | The approach inside assembly 2 is determined the $\mathbf{y}$ with largest number of dimensions, which can passed through and changed, while still be able to satisfy previous constraints. 159 | 160 | Order probably maters, but anyway, the going to be assumed that order does not matter. 161 | 162 | \section{generating repairing/orthogonal basis functions} 163 | 164 | Use the Gram–Schmidt process https://en.wikipedia.org/wiki/Gram\%E2\%80\%93Schmidt\_process : 165 | the Gram–Schmidt process is a method for orthonormalising a set of vectors in an inner product space, most commonly the Euclidean space $\Re^n$. 166 | 167 | 168 | 169 | 170 | 171 | 172 | \end{document} 173 | -------------------------------------------------------------------------------- /assembly2/solvers/dof_reduction_solver/docs/assembly2_docs.tex.backup: -------------------------------------------------------------------------------- 1 | \documentclass[a4paper,10pt]{article} 2 | \usepackage[utf8]{inputenc} 3 | \usepackage[cm]{fullpage} 4 | \usepackage{amsmath} 5 | \usepackage{amssymb} 6 | 7 | %opening 8 | \title{Assembly 2 Docs } 9 | \author{Hamish} 10 | 11 | \begin{document} 12 | 13 | \maketitle 14 | 15 | \begin{abstract} 16 | 17 | Documents for personal use to help track of the maths behind the Assembly 2 workbench add-on for FreeCAD~v0.15+. 18 | 19 | \end{abstract} 20 | 21 | \tableofcontents 22 | 23 | % \vfil 24 | 25 | % \pagebreak 26 | 27 | 28 | \section{Solver approach} 29 | 30 | Adjust the placement variables (position and rotation variables, 6 for each object) as to satisfy all constraints. 31 | Approach entails reducing the systems degrees-of-freedom, one constraint at a time until all constraints are processed. 32 | Ideally, this degrees-of-freedom can be identified, so that they can be adjusted with having to check/reprocess previous constraints. 33 | For non-simple systems this is not practical however as there are to many combinations to hard-code. 34 | 35 | Therefore a hierarchical constraint system is used. 36 | When attempting the solve the current constraint, the placement variables are also adjusted/refreshed according to the previous constraints to allow for non-perfect degrees-of-freedom. 37 | This is done in a hierarchical way, with parent constraints minimally adjusting the placement variables as to satisfy the assembly constraints. 38 | 39 | \section{Plane mating constraint} 40 | 41 | 2 parts 42 | \begin{enumerate} 43 | \item rotating objects as to align selected faces (not done if face and vertex are selected), \textbf{axis alignment union} 44 | \item moving the objects as to specified offset is satisfied, \textbf{plane offset union} 45 | \end{enumerate} 46 | 47 | 48 | \section{plane offset union - analytical solution} 49 | 50 | inputs 51 | \begin{description} 52 | \item[~~~~$\mathbf{a}$] - normal vector of reference face. 53 | \item[~~~~$\mathbf{p}$] - point on object 1. 54 | \item[~~~~$\mathbf{q}$] - point on object 2. 55 | \item[~~~~$\alpha$] - specified offset 56 | \item[~~~~$\mathbf{d}_1,\mathbf{d}_2...$] - linear motion degree-of-freedom for object 1/2 (max of 3, min of 1) 57 | \end{description} 58 | required displacement in the direction of $\mathbf{a}$: 59 | \begin{equation} 60 | r = \mathbf{a} \cdot (\mathbf{p} - \mathbf{q}) - \alpha 61 | \end{equation} 62 | require components for each $\mathbf{d}$, $v_1,v_2,...$ therefore equal to 63 | \begin{equation} 64 | \mathbf{a} \cdot ( v_1 \mathbf{d}_1 + v_2 \mathbf{d}_2 + v_3 \mathbf{d}_3 ) = r 65 | \end{equation} 66 | which has infinite solutions if more than one degree-of-freedom with $\mathbf{d} \cdot \mathbf{a} \ne 0 $ 67 | 68 | Therefore looking for least norm solution of: 69 | \begin{equation} 70 | r = a_x ( v_1 d_{1,x} + v_2 d_{2,x } + \dots) + a_y ( v_1 d_{1,y} + v_2 d_{2,y } + \dots) + a_z ( v_1 d_{1,z} + v_2 d_{2,z } + \dots) 71 | \end{equation} 72 | which gives 73 | \begin{align} 74 | \begin{bmatrix} 75 | (a_x d_{1,x} + a_y d_{1,y} + a_z d_{1,z}) & 76 | (a_x d_{2,x} + a_y d_{2,y} + a_z d_{2,z}) & 77 | (a_x d_{3,x} + a_y d_{3,y} + a_z d_{3,z}) 78 | \end{bmatrix} 79 | \begin{bmatrix} 80 | v_1 \\ v_2 \\ v_3 81 | \end{bmatrix} 82 | = [ r ] \\ 83 | \begin{bmatrix} 84 | \mathbf{a} \cdot \mathbf{d}_1 & \mathbf{a} \cdot \mathbf{d}_2 & \mathbf{a} \cdot \mathbf{d}_3 85 | \end{bmatrix} 86 | \begin{bmatrix} 87 | v_1 \\ v_2 \\ v_3 88 | \end{bmatrix} = r \\ 89 | A \mathbf{v} = [r] 90 | \end{align} 91 | then solve for least norm using numpy.linalg.lstsq 92 | 93 | 94 | 95 | \section{Numerically Reducing the System's Degrees-of-Freedom} 96 | 97 | New degrees-of-freedom need to be determined which allow for adjustment without the constraint's equality function, $h$, being violated. 98 | Therefore $h$ as a function of the current DOF $\mathbf{x}$ needs be satisfied so that 99 | \begin{equation} 100 | h(\mathbf{x}) = 0 101 | \end{equation} 102 | 103 | The DOF of the new system will therefore result in a change of $\mathbf{x}$ ( $\Delta \mathbf{x}$ ) so that 104 | \begin{equation} 105 | h(\mathbf{x} + \Delta \mathbf{x}) = 0 106 | \end{equation} 107 | 108 | \subsection{assuming $h$ is a linear system} 109 | 110 | \begin{align} 111 | \begin{bmatrix} dh/dx1 & df/dx2 &\dots \end{bmatrix} \mathbf{x} + c &= 0 \\ 112 | \mathbf{b} \cdot \mathbf{x} + c &= 0 \label{eq:reduction_dof_linear1} 113 | \end{align} 114 | 115 | DOF therefore need to satisfy 116 | \begin{align} 117 | \mathbf{b} \cdot (\mathbf{x} + \Delta \mathbf{x}) + c &= 0 \label{eq:reduction_dof_linear2} 118 | \end{align} 119 | leading to (eq \ref{eq:reduction_dof_linear2}- eq \ref{eq:reduction_dof_linear1}) 120 | \begin{align} 121 | \mathbf{b} \cdot \Delta \mathbf{x} &= 0 122 | \end{align} 123 | Therefore any vector orgonal to $\mathbf{b}$ is a degree of freedom. 124 | 125 | \subsection{assuming $h$ is a quatdratic system} 126 | 127 | \begin{align} 128 | \frac{1}{2} \mathbf{x}^T \mathbf{A} \mathbf{x} + \mathbf{b} \cdot \mathbf{x} + c &= 0 \label{eq:reduction_dof_quad1} 129 | \end{align} 130 | The changes in $\mathbf{x}$ are allowed which satisfy 131 | \begin{align} 132 | \frac{1}{2} (\mathbf{x}+\Delta \mathbf{x})^T \mathbf{A} (\mathbf{x}+\Delta \mathbf{x}) + \mathbf{b} \cdot (\mathbf{x}+\Delta \mathbf{x}) + c &= 0 \label{eq:reduction_dof_quad2} \\ 133 | \frac{1}{2} (\mathbf{x}+\Delta \mathbf{x})^T ( \mathbf{A} \mathbf{x}+ \mathbf{A} \Delta \mathbf{x}) + \mathbf{b} \cdot (\mathbf{x}+\Delta \mathbf{x}) + c &= 0 \\ 134 | \frac{1}{2} (\mathbf{x}^T \mathbf{A} \mathbf{x} + \mathbf{x}^T \mathbf{A} \Delta \mathbf{x} + \Delta\mathbf{x}^T\mathbf{A}\mathbf{x}+ \Delta\mathbf{x}^T\mathbf{A} \Delta \mathbf{x}) + \mathbf{b} \cdot (\mathbf{x}+\Delta \mathbf{x}) + c &= 0 135 | \end{align} 136 | since $\frac{1}{2} \mathbf{x}^T \mathbf{A} \mathbf{x} + \mathbf{b} \cdot \mathbf{x} + c = 0$, 137 | \begin{align} 138 | \frac{1}{2} (\mathbf{x}^T \mathbf{A} \Delta \mathbf{x} + \Delta\mathbf{x}^T\mathbf{A}\mathbf{x}+ \Delta\mathbf{x}^T\mathbf{A} \Delta \mathbf{x}) + \mathbf{b} \cdot \Delta \mathbf{x} &= 0 139 | \end{align} 140 | 141 | furthmore assuming that $\mathbf{x}^T \mathbf{A} \Delta \mathbf{x} = \Delta\mathbf{x}^T\mathbf{A}\mathbf{x}$ ($\mathbf{A} = \mathbf{A} ^T)$ gives 142 | 143 | \begin{align} 144 | \frac{1}{2} \Delta\mathbf{x}^T\mathbf{A} \Delta \mathbf{x} + (\mathbf{b}+\mathbf{x}^T \mathbf{A}) \cdot \Delta \mathbf{x} &= 0 145 | \end{align} 146 | 147 | 148 | 149 | 150 | 151 | \end{document} 152 | -------------------------------------------------------------------------------- /assembly2/solvers/dof_reduction_solver/lineSearches.py: -------------------------------------------------------------------------------- 1 | 2 | import numpy, math 3 | from numpy.linalg import norm 4 | 5 | class LineSearchEvaluation: 6 | def __init__(self, f, x, searchDirection, lam, fv=None): 7 | self.lam = lam 8 | self.xv = x + lam*searchDirection 9 | self.fv = f(x + lam*searchDirection) if not fv else fv 10 | def __lt__(self, b): 11 | return self.fv < b.fv 12 | def __eq__(self, b): 13 | return self.lam == b.lam 14 | def str(self, prefix='LSEval', lamFmt='%1.6f', fvFmt='%1.2e'): 15 | return '%s %s,%s' % (prefix, lamFmt % self.lam, fvFmt % self.fv ) 16 | 17 | 18 | 19 | phi = (5.0**0.5 - 1)/2 20 | def goldenSectionSearch( f, x1, f1, intialStep, it, debugPrintLevel, printF, it_min_at_x1=12): #f1 added to save resources... 21 | def LSEval(lam, fv=None): 22 | return LineSearchEvaluation( f, x1, intialStep, lam, fv ) 23 | y1 = LSEval( 0.0, f1) 24 | y2 = LSEval( phi**2 ) 25 | y3 = LSEval( phi ) 26 | y4 = LSEval( 1.0 ) 27 | if debugPrintLevel > 0: 28 | printF(' goldenSection search it 0: lam %1.3f %1.3f %1.3f %1.3f f(lam) %1.2e %1.2e %1.2e %1.2e' % ( y1.lam, y2.lam, y3.lam, y4.lam, y1.fv, y2.fv, y3.fv, y4.fv)) 29 | for k in range(it_min_at_x1): 30 | y_min = min([ y1, y2, y3, y4 ]) 31 | if y_min == y2 or y_min == y1 : 32 | y4 = y3 33 | y3 = y2 34 | y2 = LSEval( y1.lam + phi**2 * (y4.lam - y1.lam) ) 35 | elif y_min == y3 : 36 | y1 = y2 37 | y2 = y3 38 | y3 = LSEval( y1.lam + phi*(y4.lam - y1.lam) ) 39 | elif y_min == y4 : 40 | y2 = y3 41 | y3 = y4 42 | y4 = LSEval( y1.lam + (phi**-1) * (y4.lam - y1.lam) ) 43 | if debugPrintLevel > 0: 44 | printF(' goldenSection search it %i: lam %1.3f %1.3f %1.3f %1.3f f(lam) %1.2e %1.2e %1.2e %1.2e' % ( it,y1.lam,y2.lam,y3.lam,y4.lam,y1.fv,y2.fv,y3.fv,y4.fv)) 45 | if y1.lam > 0 and k+1 >= it: 46 | break 47 | return min([ y1, y2, y3, y4 ]).xv 48 | 49 | def quadraticLineSearch( f, x1, f1, intialStep, it, debugPrintLevel, printF, tol_stag=3, tol_x=10**-6): 50 | if norm(intialStep) == 0 : 51 | if debugPrintLevel > 0: 52 | printF(' quadraticLineSearch: norm search direction is 0, aborting!') 53 | return x1 54 | def LSEval(lam, fv=None): 55 | return LineSearchEvaluation( f, x1, intialStep, lam, fv ) 56 | Y = [ LSEval( 0.0, f1), LSEval( 1 ), LSEval( 2 )] 57 | y_min_prev = min(Y) 58 | count_stagnation = 0 59 | tol_lambda = tol_x / norm(intialStep) 60 | for k in range(it): 61 | Y.sort() 62 | if debugPrintLevel > 0: 63 | printF(' quadratic line search it %i, fmin %1.2e, lam %1.6f %1.6f %1.6f, f(lam) %1.2e %1.2e %1.2e'%( k+1, Y[0].fv, Y[0].lam,Y[1].lam,Y[2].lam,Y[0].fv,Y[1].fv,Y[2].fv )) 64 | #``p[0]*x**(N-1) + p[1]*x**(N-2) + ... + p[N-2]*x + p[N-1]`` 65 | quadraticCoefs, residuals, rank, singular_values, rcond = numpy.polyfit( [y.lam for y in Y], [y.fv for y in Y], 2, full=True) 66 | if quadraticCoefs[0] > 0 and rank == 3: 67 | lam_c = -quadraticCoefs[1] / (2*quadraticCoefs[0]) #diff poly a*x**2 + b*x + c -> grad_poly = 2*a*x + b 68 | lam_c = min( max( [y.lam for y in Y])*4, lam_c) 69 | if lam_c < 0: 70 | if debugPrintLevel > 1: printF(' quadratic line search lam_c < 0') 71 | lam_c = 1.0 / (k + 1) ** 2 72 | else: 73 | if debugPrintLevel > 1: printF(' quadratic fit invalid, using interval halving instead') 74 | lam_c = ( Y[0].lam + Y[1].lam )/2 75 | del Y[2] # Y sorted at start of each iteration 76 | Y.append( LSEval( lam_c )) 77 | y_min = min(Y) 78 | if y_min == y_min_prev: 79 | count_stagnation = count_stagnation + 1 80 | if count_stagnation > tol_stag: 81 | if debugPrintLevel > 0: printF(' terminating quadratic line search as count_stagnation > tol_stag') 82 | break 83 | else: 84 | y_min_prev = y_min 85 | count_stagnation = 0 86 | Lam = [y.lam for y in Y] 87 | if max(Lam) - min(Lam) < tol_lambda: 88 | if debugPrintLevel > 0: printF(' terminating quadratic max(Lam)-min(Lam) < tol_lambda (%e < %e)' % (max(Lam) - min(Lam), tol_lambda)) 89 | break 90 | 91 | return min(Y).xv 92 | 93 | 94 | if __name__ == '__main__': 95 | print('Testing linesearches') 96 | from matplotlib import pyplot 97 | from numpy import sin 98 | 99 | def f1(x): 100 | '(1+sin(x))*(x-0.6)**2' 101 | return float( (1+sin(x))*(x-0.6)**2 ) 102 | def f2(x): 103 | '(1+sin(x))*(x-0.0001)**2' 104 | return float( (1+sin(x))*(x-0.0001)**2 ) 105 | class recordingWrapper: 106 | def __init__(self, f): 107 | self.f = f 108 | self.f_hist = [] 109 | self.x_hist = [] 110 | def __call__(self, x): 111 | self.x_hist.append(x) 112 | self.f_hist.append(self.f(x)) 113 | return self.f_hist[-1] 114 | def printF(text): 115 | print(text) 116 | 117 | lineSearchesToTest = [ goldenSectionSearch, quadraticLineSearch ] 118 | names = ['golden','quadratic'] 119 | 120 | for testFunction in [f1,f2]: 121 | print(testFunction.func_doc) 122 | T =[] 123 | for L,name in zip(lineSearchesToTest,names): 124 | t = recordingWrapper(testFunction) 125 | T.append(t) 126 | xOpt = L(t, numpy.array([0.0]), t( numpy.array([0.0]) ), numpy.array([0.5]), 10, debugPrintLevel=1, printF=printF) 127 | print(' %s line search, xOpt %f, f(xOpt) %e' % (name, xOpt, t(xOpt) ) ) 128 | x_max = max( max(T[0].x_hist), max(T[1].x_hist ) ) 129 | pyplot.figure() 130 | x_plot = numpy.linspace(0, x_max, 100) 131 | y_plot = [ testFunction(x) for x in x_plot ] 132 | pyplot.plot(x_plot, y_plot) 133 | pyplot.title(testFunction.func_doc) 134 | for t,s,label in zip(T, ['r^','go'], names): 135 | pyplot.plot( t.x_hist, t.f_hist, s ,label=label) 136 | pyplot.legend() 137 | 138 | #pyplot.show() 139 | -------------------------------------------------------------------------------- /assembly2/solvers/dof_reduction_solver/solverLib.py: -------------------------------------------------------------------------------- 1 | import numpy 2 | from numpy.linalg import norm 3 | from numpy.random import rand 4 | from .lineSearches import * 5 | 6 | def toStdOut(txt): 7 | print(txt) 8 | 9 | def prettyPrintArray( A, printF, indent=' ', fmt='%1.1e' ): 10 | def pad(t): 11 | return t if t[0] == '-' else ' ' + t 12 | for r in A: 13 | txt = ' '.join( pad(fmt % v) for v in r) 14 | printF(indent + '[ %s ]' % txt) 15 | 16 | def solve_via_slsqp( constraintEqs, x0, bounds=None , iterations=160, fprime=None , f_tol=10.0**-3): 17 | import scipy.optimize 18 | algName = 'scipy.optimize.fmin_slsqp (Sequential Least SQuares Programming)' 19 | errorNorm = lambda x: numpy.linalg.norm(constraintEqs(x)) 20 | R = scipy.optimize.fmin_slsqp( errorNorm, x0, bounds=bounds, disp=False, full_output=True, iter=iterations, fprime=fprime, acc=f_tol**2) 21 | optResults = dict( zip(['xOpt', 'fOpt' , 'iter', 'imode', 'smode'], R ) ) # see scipy.optimize.fmin_bfgs docs for info 22 | if optResults['imode'] == 0: 23 | warningMsg = '' 24 | else: 25 | warningMsg = optResults['smode'] 26 | return algName, warningMsg, optResults 27 | 28 | class GradientApproximatorRandomPoints: 29 | def __init__(self, f): 30 | '''samples random points around a given X. as to approximate the gradient. 31 | Random sample should help to aviod saddle points. 32 | Testing showed that noise on gradient causes to scipy.optimize.fmin_slsqp to bomb-out so does not really help... 33 | ''' 34 | self.f = f 35 | def __call__(self, X, eps=10**-7): 36 | n = len(X) 37 | samplePoints = eps*( rand(n+1,n) - 0.5 ) 38 | #print(samplePoints) 39 | #samplePoints[:,n] = 1 40 | values = [ numpy.array(self.f( X + sp )) for sp in samplePoints ] 41 | #print(values[0].shape) 42 | A = numpy.ones([n+1,n+1]) 43 | A[:,:n] = samplePoints 44 | x_c, residuals, rank, s = numpy.linalg.lstsq( A, values ) 45 | return x_c[:-1].transpose() 46 | 47 | def addEps( x, dim, eps): 48 | y = x.copy() 49 | y[dim] = y[dim] + eps 50 | return y 51 | 52 | class GradientApproximatorForwardDifference: 53 | def __init__(self, f): 54 | self.f = f 55 | def __call__(self, x, eps=10**-7, f0=None): 56 | if hasattr(self.f,'addNote'): self.f.addNote('starting gradient approximation') 57 | n = len(x) 58 | if f0 == None: 59 | f0 = self.f(x) 60 | f0 = numpy.array(f0) 61 | if f0.shape == () or f0.shape == (1,): 62 | grad_f = numpy.zeros(n) 63 | else: 64 | grad_f = numpy.zeros([n,len(f0)]) 65 | for i in range(n): 66 | f_c = self.f( addEps(x,i,eps) ) 67 | grad_f[i] = (f_c - f0)/eps 68 | if hasattr(self.f,'addNote'): self.f.addNote('finished gradient approximation') 69 | return grad_f.transpose() 70 | 71 | class GradientApproximatorCentralDifference: 72 | def __init__(self, f): 73 | self.f = f 74 | def __call__(self, x, eps=10**-6): 75 | n = len(x) 76 | if hasattr(self.f,'addNote'): self.f.addNote('starting gradient approximation') 77 | grad_f = None 78 | for i in range(n): 79 | f_a = self.f( addEps(x,i, eps) ) 80 | f_b = self.f( addEps(x,i,-eps) ) 81 | if grad_f is None: 82 | if f_a.shape == () or f_a.shape == (1,): 83 | grad_f = numpy.zeros(n) 84 | else: 85 | grad_f = numpy.zeros([n,len(f_a)]) 86 | grad_f[i] = (f_a - f_b)/(2*eps) 87 | if hasattr(self.f,'addNote'): self.f.addNote('finished gradient approximation') 88 | return grad_f.transpose() 89 | 90 | def solve_via_Newtons_method( f_org, x0, maxStep, grad_f=None, x_tol=10**-6, f_tol=None, maxIt=100, randomPertubationCount=2, 91 | debugPrintLevel=0, printF=toStdOut, lineSearchIt=5, record=False): 92 | ''' 93 | determine the routes of a non-linear equation using netwons method. 94 | ''' 95 | f = SearchAnalyticsWrapper(f_org) if record else f_org 96 | n = len(x0) 97 | x = numpy.array(x0) 98 | x_c = numpy.zeros(n) * numpy.nan 99 | x_prev = numpy.zeros( [ maxIt+1, n ] ) #used to check against cyclic behaviour, for randomPertubationCount 100 | x_prev[0,:] = x 101 | if grad_f == None: 102 | #grad_f = GradientApproximatorForwardDifference(f) 103 | grad_f = GradientApproximatorCentralDifference(f) 104 | if lineSearchIt > 0: 105 | f_ls = lambda x: norm(f(x)) 106 | for i in range(maxIt): 107 | b = numpy.array(-f(x)) 108 | singleEq = b.shape == () or b.shape == (1,) 109 | if debugPrintLevel > 0: 110 | printF('it %02i: norm(prev. step) %1.1e norm(f(x)) %1.1e' % (i, norm(x_c), norm(-b))) 111 | if debugPrintLevel > 1: 112 | printF(' x %s' % x) 113 | printF(' f(x) %s' % (-b)) 114 | if norm(x_c) <= x_tol: 115 | break 116 | if f_tol != None: 117 | if singleEq and abs(b) < f_tol: 118 | break 119 | elif singleEq==False and all( abs(b) < f_tol ): 120 | break 121 | if not isinstance( grad_f, GradientApproximatorForwardDifference): 122 | A = grad_f(x) 123 | else: 124 | A = grad_f(x, f0=-b) 125 | if len(A.shape) == 1: #singleEq 126 | A = numpy.array([A]) 127 | b = numpy.array([b]) 128 | try: 129 | x_c, residuals, rank, s = numpy.linalg.lstsq( A, b) 130 | except ValueError as e: 131 | printF(' solve_via_Newtons_method numpy.linalg.lstsq failed: %s. Setting x_c = x' % str(e)) 132 | x_c = x 133 | if debugPrintLevel > 1: 134 | if singleEq: 135 | printF(' grad_f : %s' % A) 136 | else: 137 | printF(' grad_f :') 138 | prettyPrintArray(A, printF, ' ') 139 | printF(' x_c %s' % x_c) 140 | r = abs(x_c / maxStep) 141 | if r.max() > 1: 142 | x_c = x_c / r.max() 143 | if lineSearchIt > 0: 144 | #x_next = goldenSectionSearch( f_ls, x, norm(b), x_c, lineSearchIt, lineSearchIt_x0, debugPrintLevel, printF ) 145 | x_next = quadraticLineSearch( f_ls, x, norm(b), x_c, lineSearchIt, debugPrintLevel-2, printF, tol_x=x_tol ) 146 | x_c = x_next - x 147 | x = x + x_c 148 | if randomPertubationCount > 0 : #then peturb as to avoid lock-up [i.e jam which occurs when trying to solve axis direction constraint] 149 | distances = ((x_prev[:i+1,:] -x)**2).sum(axis=1) 150 | #print(distances) 151 | if any(distances <= x_tol) : 152 | if debugPrintLevel > 0: 153 | printF(' any(distances < x_tol) therefore randomPertubation...') 154 | x_p = (0.5 - rand(n)) * numpy.array(maxStep)* (1 - i*1.0/maxIt) 155 | x = x + x_p 156 | x_c = x_c + x_p 157 | randomPertubationCount = randomPertubationCount - 1 158 | x_prev[i,:] = x 159 | return x 160 | 161 | analytics = {} 162 | class SearchAnalyticsWrapper: 163 | def __init__(self, f): 164 | self.f = f 165 | self.x = [] 166 | self.f_x = [] 167 | self.notes = {} 168 | analytics['lastSearch'] = self 169 | def __call__(self, x): 170 | self.x.append(x) 171 | self.f_x.append( self.f(x) ) 172 | return self.f_x[-1] 173 | def addNote(self, note): 174 | key = len(self.x) 175 | assert key not in self.notes 176 | self.notes[key] = note 177 | def __repr__(self): 178 | return '' % len(self.x) 179 | def plot(self): 180 | from matplotlib import pyplot 181 | pyplot.figure() 182 | it_ls = [] #ls = lineseach 183 | y_ls = [] 184 | it_ga = [] #gradient approximation 185 | y_ga = [] 186 | gradApprox = False 187 | for i in range(len(self.x)): 188 | y = norm( self.f_x[i] ) + 10**-9 189 | if i in self.notes: 190 | if self.notes[i] == 'starting gradient approximation': 191 | gradApprox = True 192 | if self.notes[i] == 'finished gradient approximation': 193 | gradApprox = False 194 | if gradApprox: 195 | it_ga.append( i ) 196 | y_ga.append( y ) 197 | else: 198 | it_ls.append( i ) 199 | y_ls.append( y ) 200 | pyplot.semilogy( it_ls, y_ls, 'go') 201 | pyplot.semilogy( it_ga, y_ga, 'bx') 202 | pyplot.xlabel('function evaluation') 203 | pyplot.ylabel('norm(f(x)) + 10**-9') 204 | pyplot.legend(['line searches', 'gradient approx' ]) 205 | 206 | pyplot.show() 207 | 208 | 209 | 210 | -------------------------------------------------------------------------------- /assembly2/solvers/dof_reduction_solver/variableManager.py: -------------------------------------------------------------------------------- 1 | ''' 2 | In Freecad properties of surfaces and edges are expressed in absolute co-ordinates when acess object.shape . 3 | Therefore when altering a objects placement variables as to get two surface aligned (or the like), 4 | its recommended to the feature variables are converted into a relative co-ordinate system. 5 | This relative co-ordinate system can then be transformed by object placements variables, when trying to determine the placement variables to solve the system. 6 | 7 | 6 variables per object 8 | - x, y, z 9 | first tried ZYX euler angles ( theta, phi, psi ) for rotational degrees of freedom, which did not work for all scenarios, now trying 10 | - aximuth angle, elevation angle, rotation angle # where aximuth angle and elevation angle define the axis of rotation. 11 | 12 | 13 | >>> d.part2.Placement.Base.x 14 | 1.999668037735 15 | >>> d.part2.Shape.Faces[1].Surface.Position 16 | Vector (1.999612710940575, 1.0000000000004354, 1.000000001530527) 17 | >>> d.part2.Shape.Faces[1].Surface.Axis 18 | Vector (-5.53267944549685e-05, 4.35523981361563e-13, -0.999999998469473) 19 | >>> d.part2.Placement.Base.x = 6 20 | >>> d.part2.Shape.Faces[1].Surface.Position 21 | Vector (5.999944673204917, 1.0, 1.0000000015305273) 22 | >>> d.part2.Shape.Faces[1].Surface.Axis 23 | Vector (-5.532679508357674e-05, 0.0, -0.9999999984694729) 24 | >>> d.part2.Placement.Rotation.Angle 25 | 1.5708516535900086 26 | >>> d.part2.Placement.Rotation.Angle = 3.1 27 | >>> d.part2.Shape.Faces[1].Surface.Position 28 | Vector (5.000864849726721, 1.0, 1.9584193375667096) 29 | >>> d.part2.Shape.Faces[1].Surface.Axis 30 | Vector (-0.9991351502732795, 0.0, -0.04158066243329049) 31 | 32 | 33 | ''' 34 | 35 | 36 | from assembly2.lib3D import * 37 | import numpy 38 | from assembly2.core import debugPrint 39 | 40 | 41 | class VariableManager: 42 | def __init__(self, doc, objectNames=None): 43 | self.doc = doc 44 | self.index = {} 45 | X = [] 46 | if objectNames == None: 47 | objectNames = [obj.Name for obj in doc.Objects if hasattr(obj,'Placement')] 48 | for objectName in objectNames: 49 | self.index[objectName] = len(X) 50 | obj = doc.getObject(objectName) 51 | x, y, z = obj.Placement.Base.x, obj.Placement.Base.y, obj.Placement.Base.z 52 | axis, theta = quaternion_to_axis_and_angle( *obj.Placement.Rotation.Q ) 53 | if theta != 0: 54 | azi, ela = axis_to_azimuth_and_elevation_angles(*axis) 55 | else: 56 | azi, ela = 0, 0 57 | X = X + [ x, y, z, azi, ela, theta] 58 | self.X0 = numpy.array(X) 59 | self.X = self.X0.copy() 60 | 61 | def updateFreeCADValues(self, X, tol_base = 10.0**-8, tol_rotation = 10**-6): 62 | for objectName in self.index.keys(): 63 | i = self.index[objectName] 64 | obj = self.doc.getObject(objectName) 65 | #obj.Placement.Base.x = X[i] 66 | #obj.Placement.Base.y = X[i+1] 67 | #obj.Placement.Base.z = X[i+2] 68 | if norm( numpy.array(obj.Placement.Base) - X[i:i+3] ) > tol_base: #for speed considerations only update placement variables if change in values occurs 69 | obj.Placement.Base = tuple( X[i:i+3] ) 70 | azi, ela, theta = X[i+3:i+6] 71 | axis = azimuth_and_elevation_angles_to_axis( azi, ela ) 72 | new_Q = quaternion( theta, *axis ) #tuple type 73 | if norm( numpy.array(obj.Placement.Rotation.Q) - numpy.array(new_Q)) > tol_rotation: 74 | obj.Placement.Rotation.Q = new_Q 75 | 76 | def bounds(self): 77 | return [ [ -inf, inf], [ -inf, inf], [ -inf, inf], [-pi,pi], [-pi,pi], [-pi,pi] ] * len(self.index) 78 | 79 | def rotate(self, objectName, p, X): 80 | 'rotate a vector p by objectNames placement variables defined in X' 81 | i = self.index[objectName] 82 | return azimuth_elevation_rotation( p, *X[i+3:i+6]) 83 | 84 | def rotateUndo( self, objectName, p, X): 85 | i = self.index[objectName] 86 | R = azimuth_elevation_rotation_matrix(*X[i+3:i+6]) 87 | return numpy.linalg.solve(R,p) 88 | 89 | def rotateAndMove( self, objectName, p, X): 90 | 'rotate the vector p by objectNames placement rotation and then move using objectNames placement' 91 | i = self.index[objectName] 92 | return azimuth_elevation_rotation( p, *X[i+3:i+6]) + X[i:i+3] 93 | 94 | def rotateAndMoveUndo( self, objectName, p, X): # or un(rotate_and_then_move) #synomyn to get co-ordinates relative to objects placement variables. 95 | i = self.index[objectName] 96 | v = numpy.array(p) - X[i:i+3] 97 | R = azimuth_elevation_rotation_matrix(*X[i+3:i+6]) 98 | return numpy.linalg.solve(R,v) 99 | 100 | 101 | class ReversePlacementTransformWithBoundsNormalization: 102 | def __init__(self, obj): 103 | x, y, z = obj.Placement.Base.x, obj.Placement.Base.y, obj.Placement.Base.z 104 | self.offset = numpy.array([x, y, z]) #placement offset 105 | axis, theta = quaternion_to_axis_and_angle( *obj.Placement.Rotation.Q ) 106 | if theta != 0: 107 | azi, ela = axis_to_azimuth_and_elevation_angles(*axis) 108 | else: 109 | azi, ela = 0, 0 110 | self.R = azimuth_elevation_rotation_matrix( azi, ela, theta ) #placement rotation 111 | #now for bounds normalization 112 | #V = [ self.undoPlacement(v.Point) for v in obj.Shape.Vertexes] #no nessary in BoundBox is now used. 113 | V = [] 114 | BB = obj.Shape.BoundBox 115 | extraPoints = [] 116 | for z in [ BB.ZMin, BB.ZMax ]: 117 | for y in [ BB.YMin, BB.YMax ]: 118 | for x in [ BB.XMin, BB.XMax ] : 119 | V.append( self.undoPlacement([x,y,z]) ) 120 | V = numpy.array(V) 121 | self.Bmin = V.min(axis=0) 122 | self.Bmax = V.max(axis=0) 123 | self.dB = self.Bmax - self.Bmin 124 | 125 | def undoPlacement(self, p): 126 | # p = R*q + offset 127 | return numpy.linalg.solve( self.R, numpy.array(p) - self.offset ) 128 | 129 | def unRotate(self, p): 130 | return numpy.linalg.solve( self.R, p) 131 | 132 | def __call__( self, p): 133 | q = self.undoPlacement(p) 134 | # q = self.Bmin + r* self.dB (where r is in normilezed coordinates) 135 | return (q - self.Bmin) / self.dB 136 | 137 | 138 | -------------------------------------------------------------------------------- /assembly2/solvers/newton_solver/tests.py: -------------------------------------------------------------------------------- 1 | if __name__ == '__main__': 2 | print('''run tests via 3 | FreeCAD_assembly2$ python2 test.py assembly2.solvers.newton_solver.tests.Test_Newton_Slsqp_Solver''') 4 | exit() 5 | 6 | import unittest 7 | import FreeCAD 8 | import assembly2 9 | import os, time, numpy 10 | test_assembly_path = os.path.join( assembly2.__dir__ , 'assembly2', 'solvers', 'test_assemblies' ) 11 | from assembly2.solvers import solveConstraints 12 | from assembly2.core import debugPrint 13 | 14 | 15 | class Stats: 16 | pass 17 | stats = Stats() 18 | 19 | # To do 20 | # from assembly2.solvers.dof_reduction_solver.tests import Test_Dof_Reduction_Solver 21 | # class Test_Newton_Slsqp_Solver(Test_Dof_Reduction_Solver): 22 | # 23 | # proper solution checking to be implemented 24 | # 25 | class Test_Newton_Slsqp_Solver(unittest.TestCase): 26 | 27 | use_cache = False 28 | 29 | @classmethod 30 | def setUpClass(cls): 31 | stats.t_solver = 0 32 | stats.t_cache = 0 33 | stats.t_start = time.time() 34 | stats.n_attempted = 0 35 | stats.n_solved = 0 36 | 37 | 38 | @classmethod 39 | def tearDownClass(cls): 40 | debugPrint(0,'\n------------------------------------------') 41 | debugPrint(0,' Newton_slsqp_solver passed %i/%i tests' % ( stats.n_solved, stats.n_attempted ) ) 42 | debugPrint(0,' time solver: %3.2f s' % stats.t_solver ) 43 | debugPrint(0,' time cached solutions: %3.2f s' % stats.t_cache ) 44 | debugPrint(0,' total running time: %3.2f s' % (time.time() - stats.t_start) ) 45 | debugPrint(0,'------------------------------------------') 46 | 47 | 48 | def _test_file( self, testFile_basename, solution = None ): 49 | testFile = os.path.join( test_assembly_path, testFile_basename + '.fcstd' ) 50 | debugPrint(1, testFile_basename ) 51 | stats.n_attempted += 1 52 | #if testFile == 'tests/testAssembly11-Pipe_assembly.fcstd': 53 | # print('Skipping known fail') 54 | # continue 55 | doc = FreeCAD.open(testFile) 56 | t_start_solver = time.time() 57 | xOpt = solveConstraints( doc, solver_name = 'newton_solver_slsqp', use_cache = self.use_cache, showFailureErrorDialog=False ) 58 | if solution: 59 | self.check_solution( xOpt, solution ) 60 | stats.t_solver += time.time() - t_start_solver 61 | assert not self.use_cache 62 | if not xOpt is None: 63 | stats.n_solved += 1 64 | FreeCAD.closeDocument( doc.Name ) 65 | debugPrint(1,'\n\n\n') 66 | return xOpt 67 | 68 | def testAssembly_01_2_cubes( self ): 69 | X = self._test_file( 'testAssembly_01' ) 70 | 71 | def testAssembly_02_3_cubes( self ): 72 | self._test_file( 'testAssembly_02' ) 73 | 74 | def testAssembly_03_2_cubes( self ): 75 | self._test_file( 'testAssembly_03' ) 76 | 77 | def testAssembly_04_angle_constraint( self ): 78 | x_c = self._test_file( 'testAssembly_04') 79 | self.assertFalse( x_c is None ) 80 | a = x_c[0:3] 81 | b = numpy.array( [-14.7637558, -1.81650472, 16.39465332] ) 82 | self.assertTrue( 83 | len(a) == len(b) and numpy.allclose( a, b ), 84 | 'Solver solution incorrect: %s != %s' % ( a, b ) 85 | ) 86 | 87 | @unittest.skip("takes along time to run") 88 | def testAssembly_05( self ): 89 | self._test_file( 'testAssembly_05') 90 | 91 | @unittest.skip("takes along time to run") 92 | def testAssembly_06( self ): 93 | self._test_file( 'testAssembly_06') 94 | 95 | @unittest.skip("takes along time to run") 96 | def testAssembly_07( self ): 97 | self._test_file( 'testAssembly_07') 98 | 99 | @unittest.skip("known fail which takes along time to run") 100 | def testAssembly_08( self ): 101 | self._test_file( 'testAssembly_08') 102 | 103 | @unittest.skip("takes along time to run") 104 | def testAssembly_09( self ): 105 | self._test_file( 'testAssembly_09') 106 | 107 | @unittest.skip("error: failed in converting 4th argument `xl' of _slsqp.slsqp to C/Fortran array") 108 | def testAssembly_10_block_iregular_constraint_order( self ): 109 | self._test_file( 'testAssembly_10-block_iregular_constraint_order') 110 | 111 | @unittest.skip("known failuire with lots of output") 112 | def testAssembly_11_pipe_assembly( self ): 113 | self._test_file( 'testAssembly_11-pipe_assembly') 114 | 115 | @unittest.skip("takes along time to run") 116 | def testAssembly_11b_pipe_assembly( self ): 117 | self._test_file( 'testAssembly_11b-pipe_assembly') 118 | 119 | #@unittest.skip("takes along time to run") 120 | def testAssembly_12_angles_clock_face( self ): 121 | self._test_file( 'testAssembly_12-angles_clock_face') 122 | #@unittest.skip("takes along time to run") 123 | def testAssembly_13_spherical_surfaces_hip( self ): 124 | self._test_file( 'testAssembly_13-spherical_surfaces_hip') 125 | #@unittest.skip 126 | def testAssembly_13_spherical_surfaces_cube_vertices( self ): 127 | self._test_file( 'testAssembly_13-spherical_surfaces_cube_vertices') 128 | 129 | @unittest.skip("error: failed in converting 4th argument `xl' of _slsqp.slsqp to C/Fortran array") 130 | def testAssembly_14_lock_relative_axial_rotation( self ): 131 | self._test_file( 'testAssembly_14-lock_relative_axial_rotation') 132 | 133 | #@unittest.skip("takes along time to run") 134 | def testAssembly_15_triangular_link_assembly( self ): 135 | self._test_file( 'testAssembly_15-triangular_link_assembly') 136 | 137 | @unittest.skip("takes along time to run") 138 | def testAssembly_16_revolved_surface_objs( self ): 139 | self._test_file( 'testAssembly_16-revolved_surface_objs') 140 | 141 | #@unittest.expectedFailure 142 | @unittest.skip("to do") 143 | def testAssembly_17_bspline_objects( self ): 144 | self._test_file( 'testAssembly_17-bspline_objects') 145 | 146 | @unittest.skip("takes along time to run") 147 | def testAssembly_18_add_free_objects( self ): 148 | self._test_file( 'testAssembly_18-add_free_objects') 149 | -------------------------------------------------------------------------------- /assembly2/solvers/newton_solver/variableManager.py: -------------------------------------------------------------------------------- 1 | 2 | from assembly2.lib3D import * 3 | import numpy 4 | from numpy import inf 5 | 6 | class VariableManager: 7 | def __init__(self, doc): 8 | self.doc = doc 9 | self.placementVariables = {} 10 | def getPlacementValues(self, objectName): 11 | if not self.placementVariables.has_key(objectName): 12 | self.placementVariables[objectName] = PlacementVariables( self.doc, objectName ) 13 | return self.placementVariables[objectName] 14 | def getValues(self): 15 | return sum([ pV.getValues() for key,pV in self.placementVariables.iteritems() if not pV.fixed ], []) 16 | def setValues(self, values): 17 | i = 0 18 | for key,pV in self.placementVariables.iteritems(): 19 | if not pV.fixed: 20 | pV.setValues( values[ i*6: (i+1)*6 ] ) 21 | i = i + 1 22 | def updateFreeCADValues(self): 23 | [ pV.updateFreeCADValues() for pV in self.placementVariables.values() if not pV.fixed ] 24 | def bounds(self): 25 | bounds = [] 26 | for key,pV in self.placementVariables.iteritems(): 27 | if not pV.fixed: 28 | bounds = bounds + pV.bounds() 29 | return bounds 30 | def peturbValues(self, objectsToPeturb): 31 | X = [] 32 | for key,pV in self.placementVariables.iteritems(): 33 | if not pV.fixed: 34 | y = numpy.array( pV.getValues() ) 35 | if key in objectsToPeturb: 36 | y[0:3] = y[0:3] + 42*( numpy.random.rand(3) - 0.5 ) 37 | y[3:6] = 2*pi *( numpy.random.rand(3) - 0.5 ) 38 | X = X + y.tolist() 39 | return X 40 | def fixObj( self, objectName ): 41 | self.placementVariables[objectName].fixed = True 42 | def fixEveryObjectExcept(self, objectName): 43 | for key,pV in self.placementVariables.iteritems(): 44 | if key != objectName: 45 | pV.fixed = True 46 | def objFixed( self, objectName ): 47 | return self.placementVariables[objectName].fixed 48 | 49 | class PlacementVariables: 50 | def __init__(self, doc, objName): 51 | ''' 52 | variables 53 | - x, y, z 54 | - theta, phi, psi #using euler angles instead of quaternions because i think it will make the constraint problem easier to solver... 55 | 56 | NB - object,shapes,faces placement properties given in abosolute co-ordinates 57 | >>> App.ActiveDocument.Pocket.Placement 58 | Placement [Pos=(0,0,0), Yaw-Pitch-Roll=(0,0,0)] 59 | >>> Pocket.Shape.Faces[9].Surface.Center 60 | Vector (25.0, 15.0, 100.0) 61 | >>> Pocket.Placement.Base.x = 10 62 | >>> Pocket.Shape.Faces[9].Surface.Center 63 | Vector (35.0, 15.0, 100.0) 64 | >>> Pocket.Shape.Faces[9].Surface.Axis 65 | Vector (0.0, 0.0, 1.0) 66 | >>> Pocket.Placement.Rotation.Q = ( 1, 0, 0, 0) #rotate 180 about the x-axis 67 | >>> Pocket.Shape.Faces[9].Surface.Axis 68 | Vector (0.0, 0.0, -1.0) 69 | >>> Pocket.Shape.Faces[9].Surface.Center 70 | Vector (35.0, -15.0, -100.0) 71 | 72 | ''' 73 | self.doc = doc 74 | self.objName = objName 75 | obj = doc.getObject(self.objName) 76 | self.x = obj.Placement.Base.x 77 | self.y = obj.Placement.Base.y 78 | self.z = obj.Placement.Base.z 79 | self.theta, self.phi, self.psi = quaternion_to_euler( *obj.Placement.Rotation.Q ) 80 | self.fixed = obj.fixedPosition 81 | 82 | def getValues(self): 83 | assert not self.fixed 84 | return [self.x, self.y, self.z, self.theta, self.phi, self.psi] 85 | 86 | def bounds(self): 87 | return [ [ -inf, inf], [ -inf, inf], [ -inf, inf], [-pi,pi], [-pi,pi], [-pi,pi] ] 88 | 89 | def setValues(self, values): 90 | assert not self.fixed 91 | self.x, self.y, self.z, self.theta, self.phi, self.psi = values 92 | 93 | def updateFreeCADValues(self): 94 | '''http://en.wikipedia.org/wiki/Rotation_formalisms_in_three_dimensions ''' 95 | assert not self.fixed 96 | obj = self.doc.getObject( self.objName ) 97 | obj.Placement.Base = ( self.x, self.y, self.z ) 98 | obj.Placement.Rotation.Q = euler_to_quaternion( self.theta, self.phi, self.psi ) 99 | #self.doc.getObject(self.objName).touch() 100 | 101 | def rotate( self, p): 102 | #debugPrint( 3, "p %s" % p) 103 | #debugPrint( 3, "theta %2.1f, phi %2.1f, psi %2.1f" % ( self.theta/pi*180, self.phi/pi*180, self.psi/pi*180 )) 104 | #debugPrint( 3, 'result %s' % euler_ZYX_rotation( p, self.theta, self.phi, self.psi )) 105 | return euler_ZYX_rotation( p, self.theta, self.phi, self.psi ) 106 | 107 | def rotate_undo( self, p ): # or unrotate 108 | R = euler_ZYX_rotation_matrix( self.theta, self.phi, self.psi ) 109 | return numpy.linalg.solve(R,p) 110 | 111 | def rotate_and_then_move( self, p): 112 | return self.rotate(p) + numpy.array([ self.x, self.y, self.z ]) 113 | 114 | def rotate_and_then_move_undo( self, p): # or un(rotate_and_then_move) 115 | return self.rotate_undo( numpy.array(p) - numpy.array([ self.x, self.y, self.z ]) ) 116 | 117 | -------------------------------------------------------------------------------- /assembly2/solvers/test_assemblies/testAssembly_01.fcstd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hamish2014/FreeCAD_assembly2/2da784f18b8af16facf3c0e28a69f0430dc7bb60/assembly2/solvers/test_assemblies/testAssembly_01.fcstd -------------------------------------------------------------------------------- /assembly2/solvers/test_assemblies/testAssembly_02.fcstd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hamish2014/FreeCAD_assembly2/2da784f18b8af16facf3c0e28a69f0430dc7bb60/assembly2/solvers/test_assemblies/testAssembly_02.fcstd -------------------------------------------------------------------------------- /assembly2/solvers/test_assemblies/testAssembly_03.fcstd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hamish2014/FreeCAD_assembly2/2da784f18b8af16facf3c0e28a69f0430dc7bb60/assembly2/solvers/test_assemblies/testAssembly_03.fcstd -------------------------------------------------------------------------------- /assembly2/solvers/test_assemblies/testAssembly_04.fcstd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hamish2014/FreeCAD_assembly2/2da784f18b8af16facf3c0e28a69f0430dc7bb60/assembly2/solvers/test_assemblies/testAssembly_04.fcstd -------------------------------------------------------------------------------- /assembly2/solvers/test_assemblies/testAssembly_05.fcstd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hamish2014/FreeCAD_assembly2/2da784f18b8af16facf3c0e28a69f0430dc7bb60/assembly2/solvers/test_assemblies/testAssembly_05.fcstd -------------------------------------------------------------------------------- /assembly2/solvers/test_assemblies/testAssembly_06.fcstd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hamish2014/FreeCAD_assembly2/2da784f18b8af16facf3c0e28a69f0430dc7bb60/assembly2/solvers/test_assemblies/testAssembly_06.fcstd -------------------------------------------------------------------------------- /assembly2/solvers/test_assemblies/testAssembly_07.fcstd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hamish2014/FreeCAD_assembly2/2da784f18b8af16facf3c0e28a69f0430dc7bb60/assembly2/solvers/test_assemblies/testAssembly_07.fcstd -------------------------------------------------------------------------------- /assembly2/solvers/test_assemblies/testAssembly_08.fcstd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hamish2014/FreeCAD_assembly2/2da784f18b8af16facf3c0e28a69f0430dc7bb60/assembly2/solvers/test_assemblies/testAssembly_08.fcstd -------------------------------------------------------------------------------- /assembly2/solvers/test_assemblies/testAssembly_09.fcstd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hamish2014/FreeCAD_assembly2/2da784f18b8af16facf3c0e28a69f0430dc7bb60/assembly2/solvers/test_assemblies/testAssembly_09.fcstd -------------------------------------------------------------------------------- /assembly2/solvers/test_assemblies/testAssembly_10-block_iregular_constraint_order.fcstd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hamish2014/FreeCAD_assembly2/2da784f18b8af16facf3c0e28a69f0430dc7bb60/assembly2/solvers/test_assemblies/testAssembly_10-block_iregular_constraint_order.fcstd -------------------------------------------------------------------------------- /assembly2/solvers/test_assemblies/testAssembly_11-pipe_assembly.fcstd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hamish2014/FreeCAD_assembly2/2da784f18b8af16facf3c0e28a69f0430dc7bb60/assembly2/solvers/test_assemblies/testAssembly_11-pipe_assembly.fcstd -------------------------------------------------------------------------------- /assembly2/solvers/test_assemblies/testAssembly_11b-pipe_assembly.fcstd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hamish2014/FreeCAD_assembly2/2da784f18b8af16facf3c0e28a69f0430dc7bb60/assembly2/solvers/test_assemblies/testAssembly_11b-pipe_assembly.fcstd -------------------------------------------------------------------------------- /assembly2/solvers/test_assemblies/testAssembly_12-angles_clock_face.fcstd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hamish2014/FreeCAD_assembly2/2da784f18b8af16facf3c0e28a69f0430dc7bb60/assembly2/solvers/test_assemblies/testAssembly_12-angles_clock_face.fcstd -------------------------------------------------------------------------------- /assembly2/solvers/test_assemblies/testAssembly_13-spherical_surfaces_cube_vertices.fcstd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hamish2014/FreeCAD_assembly2/2da784f18b8af16facf3c0e28a69f0430dc7bb60/assembly2/solvers/test_assemblies/testAssembly_13-spherical_surfaces_cube_vertices.fcstd -------------------------------------------------------------------------------- /assembly2/solvers/test_assemblies/testAssembly_13-spherical_surfaces_hip.fcstd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hamish2014/FreeCAD_assembly2/2da784f18b8af16facf3c0e28a69f0430dc7bb60/assembly2/solvers/test_assemblies/testAssembly_13-spherical_surfaces_hip.fcstd -------------------------------------------------------------------------------- /assembly2/solvers/test_assemblies/testAssembly_14-lock_relative_axial_rotation.fcstd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hamish2014/FreeCAD_assembly2/2da784f18b8af16facf3c0e28a69f0430dc7bb60/assembly2/solvers/test_assemblies/testAssembly_14-lock_relative_axial_rotation.fcstd -------------------------------------------------------------------------------- /assembly2/solvers/test_assemblies/testAssembly_15-triangular_link_assembly.fcstd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hamish2014/FreeCAD_assembly2/2da784f18b8af16facf3c0e28a69f0430dc7bb60/assembly2/solvers/test_assemblies/testAssembly_15-triangular_link_assembly.fcstd -------------------------------------------------------------------------------- /assembly2/solvers/test_assemblies/testAssembly_16-revolved_surface_objs.fcstd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hamish2014/FreeCAD_assembly2/2da784f18b8af16facf3c0e28a69f0430dc7bb60/assembly2/solvers/test_assemblies/testAssembly_16-revolved_surface_objs.fcstd -------------------------------------------------------------------------------- /assembly2/solvers/test_assemblies/testAssembly_17-bspline_objects.fcstd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hamish2014/FreeCAD_assembly2/2da784f18b8af16facf3c0e28a69f0430dc7bb60/assembly2/solvers/test_assemblies/testAssembly_17-bspline_objects.fcstd -------------------------------------------------------------------------------- /assembly2/solvers/test_assemblies/testAssembly_18-add_free_objects.fcstd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hamish2014/FreeCAD_assembly2/2da784f18b8af16facf3c0e28a69f0430dc7bb60/assembly2/solvers/test_assemblies/testAssembly_18-add_free_objects.fcstd -------------------------------------------------------------------------------- /assembly2/utils/__init__.py: -------------------------------------------------------------------------------- 1 | from assembly2.utils import animate_constraint 2 | #import boltMultipleCircularEdges, import in assembly2 to avoid import loop 3 | from assembly2.utils import checkAssembly 4 | from assembly2.utils import degreesOfFreedomAnimation 5 | #import muxAssembly 6 | from assembly2.utils import randomClrs 7 | import FreeCAD 8 | from assembly2.utils import partsList 9 | from assembly2.utils import undo 10 | -------------------------------------------------------------------------------- /assembly2/utils/boltMultipleCircularEdges.py: -------------------------------------------------------------------------------- 1 | from assembly2.constraints.common import * 2 | from assembly2.constraints.circularEdgeConstraint import parseSelection 3 | from assembly2.importPart import duplicateImportedPart 4 | 5 | 6 | class CircularEdgeSelectionGate: 7 | def allow(self, doc, obj, sub): 8 | return CircularEdgeSelected( SelectionExObject(doc, obj, sub) ) 9 | 10 | class boltMultipleCircularEdgesCommand: 11 | def Activated(self): 12 | selection = FreeCADGui.Selection.getSelectionEx() 13 | if len(selection) < 2: 14 | FreeCADGui.Selection.clearSelection() 15 | self.taskDialog = RapidBoltingTaskDialog() 16 | FreeCADGui.Control.showDialog( self.taskDialog ) 17 | FreeCADGui.Selection.removeSelectionGate() 18 | FreeCADGui.Selection.addSelectionGate( CircularEdgeSelectionGate() ) 19 | else: 20 | valid = True 21 | for s in selection: 22 | for se_name in s.SubElementNames: 23 | if not CircularEdgeSelected( SelectionExObject(FreeCAD.ActiveDocument, s.Object, se_name) ): 24 | valid = False 25 | break 26 | if valid: 27 | boltSelection() 28 | else: 29 | QtGui.QMessageBox.information( QtGui.QApplication.activeWindow(), "Incorrect Usage", 'Select only circular edges') 30 | def GetResources(self): 31 | msg = 'Bolt multiple circular edges' 32 | return { 33 | 'Pixmap' : ':/assembly2/icons/boltMultipleCircularEdges.svg', 34 | 'MenuText': msg, 35 | 'ToolTip': msg 36 | } 37 | 38 | FreeCADGui.addCommand('assembly2_boltMultipleCircularEdges', boltMultipleCircularEdgesCommand()) 39 | 40 | class RapidBoltingTaskDialog: 41 | def __init__(self): 42 | self.form = RapidBoltingForm('''Instructions: 43 | 44 | 1) select mating edge on Bolt 45 | 2) add to the Selection the edges 46 | to which the bolt is to be mated 47 | 3) press Ok ''' ) 48 | self.form.setWindowTitle( 'Bolt multiple circular edges' ) 49 | self.form.setWindowIcon( QtGui.QIcon( ':/assembly2/icons/boltMultipleCircularEdges.svg' ) ) 50 | def reject(self): 51 | FreeCADGui.Selection.removeSelectionGate() 52 | FreeCADGui.Control.closeDialog() 53 | def accept(self): 54 | FreeCADGui.Selection.removeSelectionGate() 55 | FreeCADGui.Control.closeDialog() 56 | boltSelection() 57 | class RapidBoltingForm(QtGui.QWidget): 58 | def __init__(self, textLines ): 59 | super(RapidBoltingForm, self).__init__() 60 | self.textLines = textLines 61 | self.initUI() 62 | def initUI(self): 63 | vbox = QtGui.QVBoxLayout() 64 | for line in self.textLines.split('\n'): 65 | vbox.addWidget( QtGui.QLabel(line) ) 66 | self.setLayout(vbox) 67 | 68 | 69 | 70 | 71 | def boltSelection(): 72 | from assembly2.solvers import solveConstraints 73 | doc = FreeCAD.ActiveDocument 74 | doc.openTransaction('Bolt Multiple Circular Edges') 75 | selection = FreeCADGui.Selection.getSelectionEx() 76 | bolt = selection[0].Object 77 | bolt_se_name = selection[0].SubElementNames[0] 78 | S = [] #edgesToConstrainTo 79 | for s in selection[1:]: 80 | for se_name in s.SubElementNames: 81 | S.append( SelectionExObject(doc, s.Object, se_name) ) 82 | for s2 in S: 83 | newBolt = duplicateImportedPart(bolt) 84 | s1 = SelectionExObject(doc, newBolt, bolt_se_name) 85 | debugPrint(3,'s1 %s' % [s1, s2]) 86 | parseSelection( 87 | [s1, s2 ], 88 | callSolveConstraints= False, lockRotation = True 89 | ) 90 | solveConstraints( doc ) 91 | FreeCAD.ActiveDocument.commitTransaction() 92 | repair_tree_view() 93 | -------------------------------------------------------------------------------- /assembly2/utils/checkAssembly.py: -------------------------------------------------------------------------------- 1 | from assembly2.core import * 2 | import time, traceback 3 | from FreeCAD import Base 4 | 5 | moduleVars = {} 6 | 7 | class CheckAssemblyCommand: 8 | def Activated(self): 9 | debugPrint(2, 'Conducting Assembly Part Overlap Check for: %s' % FreeCAD.ActiveDocument.Label) 10 | objects = [obj for obj in FreeCAD.ActiveDocument.Objects if hasattr(obj, 'Shape') and obj.Name != 'muxedAssembly'] 11 | n = len(objects) 12 | no_of_checks = 0.5*(n-1)*(n) 13 | moduleVars['progressDialog'] = QtGui.QProgressDialog("Checking assembly", "Cancel", 0, no_of_checks)#, QtGui.QApplication.activeWindow()) with parent cancel does not work for some reason 14 | p = moduleVars['progressDialog'] 15 | p.setWindowModality(QtCore.Qt.WindowModal) 16 | p.forceShow() 17 | count = 0 18 | t_start = time.time() 19 | overlapMsgs = [] 20 | errorMsgs = [] 21 | for i in range(0, len(objects)-1): 22 | for j in range(i+1, len(objects)): 23 | if not p.wasCanceled(): 24 | msg = 'overlap check between: "%s" & "%s"' % (objects[i].Label, objects[j].Label) 25 | debugPrint(3, ' ' + msg) 26 | p.setLabelText(msg) 27 | p.setValue(count) 28 | if boundBoxesOverlap(objects[i].Shape, objects[j].Shape, tol = 10**-5 ): #first do a rough check, to speed up checks, on the test case used, time reduce from 11s ->10s ... 29 | try: 30 | overlap = objects[i].Shape.common( objects[j].Shape ) 31 | overlap_ratio = overlap.Volume / min( objects[j].Shape.Volume, objects[i].Shape.Volume ) 32 | if overlap.Volume > 0: 33 | overlapMsgs.append('%s & %s : %3.3f%%*' % (objects[i].Label, objects[j].Label, 100*overlap_ratio )) 34 | except: #BRep_API: command not done 35 | FreeCAD.Console.PrintError('Unable to perform %s:\n' % msg) 36 | errorMsgs.append('Unable to perform %s:\n' % msg) 37 | FreeCAD.Console.PrintError(traceback.format_exc()) 38 | else: 39 | debugPrint(3, ' skipping check based on boundBoxesOverlap check') 40 | count = count + 1 41 | else: 42 | break 43 | debugPrint(3, 'ProgressDialog canceled %s' % p.wasCanceled()) 44 | if not p.wasCanceled(): 45 | p.setValue(count) 46 | debugPrint(2, 'Assembly overlap check duration: %3.1fs' % (time.time() - t_start) ) 47 | errorMsg = '\n\nWARNING: %i Check(s) could not be conducted:\n - %s' % (len(errorMsgs), " \n - ".join(errorMsgs)) if len(errorMsgs) > 0 else '' 48 | #p.setValue(no_of_checks) 49 | if len(overlapMsgs) > 0: 50 | #flags |= QtGui.QMessageBox.Ignore 51 | message = "Overlap detected between:\n - %s" % " \n - ".join(overlapMsgs) 52 | message = message + '\n\n*overlap.Volume / min( shape1.Volume, shape2.Volume )' 53 | FreeCAD.Console.PrintError( message + '\n' ) 54 | response = QtGui.QMessageBox.critical(QtGui.QApplication.activeWindow(), "Assembly Check", message + errorMsg)#, flags) 55 | else: 56 | QtGui.QMessageBox.information( QtGui.QApplication.activeWindow(), "Assembly Check", "Passed:\n - No overlap/interferance dectected." + errorMsg) 57 | def GetResources(self): 58 | msg = 'Check assembly for part overlap/interferance' 59 | return { 60 | 'Pixmap' : ':/assembly2/icons/checkAssembly.svg', 61 | 'MenuText': msg, 62 | 'ToolTip': msg 63 | } 64 | FreeCADGui.addCommand('assembly2_checkAssembly', CheckAssemblyCommand()) 65 | 66 | 67 | def boundBoxesOverlap( shape1, shape2, tol ): 68 | try: 69 | bb1 = shape1.BoundBox 70 | box1 = Part.makeBox( bb1.XLength, bb1.YLength, bb1.ZLength, Base.Vector( bb1.XMin, bb1.YMin, bb1.ZMin )) 71 | bb2 = shape2.BoundBox 72 | box2 = Part.makeBox( bb2.XLength, bb2.YLength, bb2.ZLength, Base.Vector( bb2.XMin, bb2.YMin, bb2.ZMin )) 73 | overlap = box1.common(box2) 74 | except: 75 | return True 76 | overlap_ratio = overlap.Volume / min( box1.Volume, box2.Volume ) 77 | debugPrint(3, ' boundBoxesOverlap:overlap_ratio %e' % overlap_ratio) 78 | return overlap_ratio > tol 79 | -------------------------------------------------------------------------------- /assembly2/utils/degreesOfFreedomAnimation.py: -------------------------------------------------------------------------------- 1 | from assembly2.core import * 2 | from PySide import QtGui, QtCore 3 | import traceback 4 | from assembly2.solvers.dof_reduction_solver import degreesOfFreedom 5 | 6 | moduleVars = {} 7 | 8 | class AnimateCommand: 9 | def Activated(self): 10 | from assembly2.solvers import solveConstraints 11 | constraintSystem = solveConstraints(FreeCAD.ActiveDocument) 12 | self.taskPanel = AnimateDegreesOfFreedomTaskPanel( constraintSystem ) 13 | FreeCADGui.Control.showDialog( self.taskPanel ) 14 | def GetResources(self): 15 | msg = 'Animate degrees of freedom' 16 | return { 17 | 'Pixmap' : ':/assembly2/icons/degreesOfFreedomAnimation.svg', 18 | 'MenuText': msg, 19 | 'ToolTip': msg 20 | } 21 | animateCommand = AnimateCommand() 22 | FreeCADGui.addCommand('assembly2_degreesOfFreedomAnimation', animateCommand) 23 | 24 | 25 | class AnimateDegreesOfFreedomTaskPanel: 26 | def __init__(self, constraintSystem): 27 | self.constraintSystem = constraintSystem 28 | self.form = FreeCADGui.PySideUic.loadUi( ':/assembly2/ui/degreesOfFreedomAnimation.ui' ) 29 | self.form.setWindowIcon(QtGui.QIcon( ':/assembly2/icons/degreesOfFreedomAnimation.svg' ) ) 30 | self.form.groupBox_DOF.setTitle('%i Degrees-of-freedom:' % len(constraintSystem.degreesOfFreedom)) 31 | for i, d in enumerate(constraintSystem.degreesOfFreedom): 32 | item = QtGui.QListWidgetItem('%i. %s' % (i+1, str(d)[1:-1].replace('DegreeOfFreedom ','')), self.form.listWidget_DOF) 33 | if i == 0: item.setSelected(True) 34 | self.form.pushButton_animateSelected.clicked.connect(self.animateSelected) 35 | self.form.pushButton_animateAll.clicked.connect(self.animateAll) 36 | self.form.pushButton_set_as_default.clicked.connect( self.setDefaults ) 37 | self.setIntialValues() 38 | 39 | 40 | def _startAnimation(self, degreesOfFreedomToAnimate): 41 | frames_per_DOF = self.form.spinBox_frames_per_DOF.value() 42 | ms_per_frame = self.form.spinBox_ms_per_frame.value() 43 | rotationAmplification = self.form.doubleSpinBox_rotMag.value() 44 | linearDispAmplification = self.form.doubleSpinBox_linMag.value() 45 | if len(self.constraintSystem.degreesOfFreedom) > 0: 46 | moduleVars['animation'] = AnimateDOF(self.constraintSystem, degreesOfFreedomToAnimate, ms_per_frame, frames_per_DOF, rotationAmplification, linearDispAmplification) 47 | #moduleVars['animation'] assignment required to protect the QTimer from the garbage collector 48 | else: 49 | FreeCAD.Console.PrintError('Aborting Animation! Constraint system has no degrees of freedom.') 50 | FreeCADGui.Control.closeDialog() 51 | 52 | def setIntialValues(self): 53 | parms = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/Assembly2/degreeOfFreedomAnimation") 54 | form = self.form 55 | form.spinBox_frames_per_DOF.setValue( parms.GetFloat('frames_per_DOF', 42) ) 56 | form.spinBox_ms_per_frame.setValue( parms.GetFloat('ms_per_frame', 50) ) 57 | form.doubleSpinBox_rotMag.setValue( parms.GetFloat('rotation_magnitude', 1.0) ) 58 | form.doubleSpinBox_linMag.setValue( parms.GetFloat('linear_magnitude', 1.0) ) 59 | def setDefaults(self): 60 | parms = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/Assembly2/degreeOfFreedomAnimation") 61 | form = self.form 62 | parms.SetFloat('frames_per_DOF', form.spinBox_frames_per_DOF.value() ) 63 | parms.SetFloat('ms_per_frame', form.spinBox_ms_per_frame.value() ) 64 | parms.SetFloat('rotation_magnitude', form.doubleSpinBox_rotMag.value() ) 65 | parms.SetFloat('linear_magnitude', form.doubleSpinBox_linMag.value() ) 66 | 67 | def animateSelected(self): 68 | debugPrint(4,'pushButton_animateSelected has been clicked') 69 | D_to_animate = [] 70 | for index, d in enumerate( self.constraintSystem.degreesOfFreedom ): 71 | #debugPrint(4,'getting item at index %i' % index) 72 | item = self.form.listWidget_DOF.item(index) 73 | #debugPrint(4,'checking if %s is selected' % d.str()) 74 | if item.isSelected(): 75 | D_to_animate.append( d ) 76 | if len(D_to_animate) > 0: 77 | self._startAnimation( D_to_animate ) 78 | def animateAll(self): 79 | self._startAnimation( [d for d in self.constraintSystem.degreesOfFreedom] ) 80 | 81 | def reject(self): #or more correctly close, given the button settings 82 | if 'animation' in moduleVars: 83 | moduleVars['animation'].timer.stop() 84 | self.constraintSystem.variableManager.updateFreeCADValues(moduleVars['animation'].X_before_animation) 85 | del moduleVars['animation'] 86 | FreeCADGui.Control.closeDialog() 87 | 88 | def getStandardButtons(self): #http://forum.freecadweb.org/viewtopic.php?f=10&t=11801 89 | return 0x00200000 90 | 91 | 92 | 93 | 94 | class AnimateDOF(object): 95 | 'based on http://freecad-tutorial.blogspot.com/2014/06/piston-conrod-animation.html' 96 | def __init__(self, constraintSystem, degreesOfFreedomToAnimate, tick=50, framesPerDOF=40, rotationAmplification=1.0, linearDispAmplification=1.0): 97 | self.constraintSystem = constraintSystem 98 | self.degreesOfFreedomToAnimate = degreesOfFreedomToAnimate 99 | self.Y0 = numpy.array([ d.getValue() for d in degreesOfFreedomToAnimate] ) 100 | self.X_before_animation = constraintSystem.variableManager.X.copy() 101 | self.framesPerDOF = framesPerDOF 102 | self.rotationAmplification = rotationAmplification 103 | self.linearDispAmplification = linearDispAmplification 104 | debugPrint(2,'beginning degrees of freedom animation') 105 | self.count = 0 106 | self.dof_count = 0 107 | self.updateAmplitude() 108 | self.timer = QtCore.QTimer() 109 | QtCore.QObject.connect(self.timer, QtCore.SIGNAL("timeout()"), self.renderFrame) 110 | self.timer.start( tick ) 111 | 112 | def updateAmplitude( self) : 113 | D = self.degreesOfFreedomToAnimate 114 | if D[self.dof_count].rotational(): 115 | self.amplitude = 1.0 * self.rotationAmplification 116 | else: 117 | obj = FreeCAD.ActiveDocument.getObject( D[self.dof_count].objName ) 118 | self.amplitude = obj.Shape.BoundBox.DiagonalLength / 2 * self.linearDispAmplification 119 | 120 | def renderFrame(self): 121 | try: 122 | debugPrint(5,'timer loop running') 123 | self.count = self.count + 1 124 | D = self.degreesOfFreedomToAnimate 125 | if self.count > self.framesPerDOF: #placed here so that if is error timer still exits 126 | self.count = 0 127 | self.dof_count = self.dof_count + 1 128 | if base_rotation_dof( D[self.dof_count-1] ): 129 | self.dof_count = self.dof_count + 2 130 | if self.dof_count + 1 > len( D ): 131 | self.timer.stop() 132 | self.constraintSystem.variableManager.updateFreeCADValues(self.X_before_animation) 133 | return 134 | self.updateAmplitude() 135 | 136 | if self.count == 1: debugPrint(3,'animating %s' % D[self.dof_count]) 137 | debugPrint(4,'dof %i, dof frame %i' % (self.dof_count, self.count)) 138 | Y = self.Y0.copy() 139 | r = 2*numpy.pi*( 1.0*self.count/self.framesPerDOF) 140 | Y[self.dof_count] = self.Y0[self.dof_count] + self.amplitude * numpy.sin(r) 141 | if self.dof_count + 2 < len(D): 142 | if base_rotation_dof( D[self.dof_count] ) and base_rotation_dof( D[self.dof_count+1] ) and base_rotation_dof( D[self.dof_count+2] ): #then also adjust other base rotation degrees of freedom so that rotation is always visible 143 | Y[self.dof_count+1] = self.Y0[self.dof_count+1] +self.amplitude*numpy.sin(r) 144 | Y[self.dof_count+2] = self.Y0[self.dof_count+2] +self.amplitude*numpy.sin(r) 145 | debugPrint(5,'Y frame %s, sin(r) %1.2f' % (Y,numpy.sin(r))) 146 | except: 147 | self.timer.stop() 148 | App.Console.PrintError(traceback.format_exc()) 149 | App.Console.PrintError('Aborting animation') 150 | try: 151 | for d,y in zip( D, Y): 152 | d.setValue(y) 153 | self.constraintSystem.update() 154 | self.constraintSystem.variableManager.updateFreeCADValues( self.constraintSystem.variableManager.X ) 155 | debugPrint(5,'updated assembly') 156 | except: 157 | FreeCAD.Console.PrintError('AnimateDegreeOfFreedom (dof %i, dof frame %i) unable to update constraint system\n' % (self.dof_count, self.count)) 158 | FreeCAD.Console.PrintError(traceback.format_exc()) 159 | debugPrint(5,'finished timer loop') 160 | 161 | def base_rotation_dof(d): 162 | if isinstance(d,degreesOfFreedom.PlacementDegreeOfFreedom) and hasattr(d, 'ind'): 163 | return d.ind % 6 > 2 164 | return False 165 | 166 | 167 | -------------------------------------------------------------------------------- /assembly2/utils/muxAssembly.py: -------------------------------------------------------------------------------- 1 | from assembly2.core import * 2 | import Part 3 | import os, numpy 4 | 5 | class Proxy_muxAssemblyObj: 6 | def execute(self, shape): 7 | pass 8 | 9 | def muxObjects(doc, mode=0): 10 | 'combines all the imported shape object in doc into one shape' 11 | faces = [] 12 | 13 | if mode == 1: 14 | objects = doc.getSelection() 15 | else: 16 | objects = doc.Objects 17 | 18 | for obj in objects: 19 | if 'importPart' in obj.Content: 20 | debugPrint(3, ' - parsing "%s"' % (obj.Name)) 21 | if hasattr(obj, 'Shape'): 22 | faces = faces + obj.Shape.Faces 23 | return Part.makeShell(faces) 24 | 25 | def muxMapColors(doc, muxedObj, mode=0): 26 | 'call after muxedObj.Shape = muxObjects(doc)' 27 | diffuseColors = [] 28 | faceMap = {} 29 | 30 | if mode == 1: 31 | objects = doc.getSelection() 32 | else: 33 | objects = doc.Objects 34 | 35 | for obj in objects: 36 | if 'importPart' in obj.Content and hasattr(obj, 'Shape'): 37 | for i, face in enumerate(obj.Shape.Faces): 38 | if i < len(obj.ViewObject.DiffuseColor): 39 | clr = obj.ViewObject.DiffuseColor[i] 40 | else: 41 | clr = obj.ViewObject.DiffuseColor[0] 42 | faceMap[faceMapKey(face)] = clr 43 | for f in muxedObj.Shape.Faces: 44 | try: 45 | key = faceMapKey(f) 46 | clr = faceMap[key] 47 | del faceMap[key] 48 | except KeyError: 49 | debugPrint(3, 'muxMapColors: waring no faceMap entry for %s - key %s' % (f,faceMapKey(f))) 50 | clr = muxedObj.ViewObject.ShapeColor 51 | diffuseColors.append( clr ) 52 | muxedObj.ViewObject.DiffuseColor = diffuseColors 53 | 54 | def faceMapKey(face): 55 | c = sum([ [ v.Point.x, v.Point.y, v.Point.z] for v in face.Vertexes ], []) 56 | return tuple(c) 57 | 58 | def createMuxedAssembly(name=None): 59 | partName='muxedAssembly' 60 | if name!=None: 61 | partName = name 62 | debugPrint(2, 'creating assembly mux "%s"' % (partName)) 63 | muxedObj = FreeCAD.ActiveDocument.addObject("Part::FeaturePython",partName) 64 | muxedObj.Proxy = Proxy_muxAssemblyObj() 65 | muxedObj.ViewObject.Proxy = 0 66 | muxedObj.addProperty("App::PropertyString","type") 67 | muxedObj.type = 'muxedAssembly' 68 | muxedObj.addProperty("App::PropertyBool","ReadOnly") 69 | muxedObj.ReadOnly = False 70 | FreeCADGui.ActiveDocument.getObject(muxedObj.Name).Visibility = False 71 | muxedObj.addProperty("App::PropertyStringList","muxedObjectList") 72 | tmplist=[] 73 | for objlst in FreeCADGui.Selection.getSelection(): 74 | if 'importPart' in objlst.Content: 75 | tmplist.append(objlst.Name) 76 | muxedObj.muxedObjectList=tmplist 77 | if len(tmplist)>0: 78 | #there are objects selected, mux them 79 | muxedObj.Shape = muxObjects(FreeCADGui.Selection, 1) 80 | muxMapColors(FreeCADGui.Selection, muxedObj, 1) 81 | else: 82 | #mux all objects (original behavior) 83 | for objlst in FreeCAD.ActiveDocument.Objects: 84 | if 'importPart' in objlst.Content: 85 | tmplist.append(objlst.Name) 86 | muxedObj.muxedObjectList=tmplist 87 | if len(tmplist)>0: 88 | muxedObj.Shape = muxObjects(FreeCAD.ActiveDocument, 0) 89 | muxMapColors(FreeCAD.ActiveDocument, muxedObj, 0) 90 | else: 91 | debugPrint(2, 'Nothing to Mux') 92 | 93 | 94 | 95 | class MuxAssemblyCommand: 96 | def Activated(self): 97 | #we have to handle the mux name here 98 | createMuxedAssembly() 99 | FreeCAD.ActiveDocument.recompute() 100 | 101 | 102 | def GetResources(self): 103 | msg = 'Combine all parts into a single object \n\ 104 | or combine all selected parts into a single object\n(for example to create a drawing of the whole or part of the assembly)' 105 | return { 106 | 'Pixmap' : ':/assembly2/icons/muxAssembly.svg', 107 | 'MenuText': msg, 108 | 'ToolTip': msg 109 | } 110 | 111 | class MuxAssemblyRefreshCommand: 112 | def Activated(self): 113 | 114 | #first list all muxes in active document 115 | allMuxesList=[] 116 | for objlst in FreeCAD.ActiveDocument.Objects: 117 | if hasattr(objlst,'type'): 118 | if 'muxedAssembly' in objlst.type: 119 | if objlst.ReadOnly==False: 120 | allMuxesList.append(objlst.Name) 121 | #Second, create a list of selected objects and check if there is a mux 122 | allSelMuxesList=[] 123 | for objlst in FreeCADGui.Selection.getSelection(): 124 | tmpobj = FreeCAD.ActiveDocument.getObject(objlst.Name) 125 | if 'muxedAssembly' in tmpobj.type: 126 | if tmpobj.ReadOnly==False: 127 | allSelMuxesList.append(objlst.Name) 128 | refreshMuxesList=[] 129 | if len(allSelMuxesList) > 0 : 130 | refreshMuxesList=allSelMuxesList 131 | debugPrint(2, 'there are %d muxes in selected objects' % len(allSelMuxesList)) 132 | else: 133 | if len(allMuxesList) > 0 : 134 | debugPrint(2, 'there are %d muxes in Active Document' % len(allMuxesList)) 135 | refreshMuxesList=allMuxesList 136 | #ok there are at least 1 mux to refresh, we have to retrieve the object list for each mux 137 | if len(refreshMuxesList)>0: 138 | FreeCADGui.Selection.clearSelection() 139 | for muxesobj in refreshMuxesList: 140 | for newselobjs in FreeCAD.ActiveDocument.getObject(muxesobj).muxedObjectList: 141 | FreeCADGui.Selection.addSelection(FreeCAD.ActiveDocument.getObject(newselobjs)) 142 | tmpstr=FreeCAD.ActiveDocument.getObject(muxesobj).Label 143 | FreeCAD.ActiveDocument.removeObject(muxesobj) 144 | debugPrint(2, 'Refreshing Assembly Mux '+muxesobj) 145 | createMuxedAssembly(tmpstr) 146 | 147 | else: 148 | debugPrint(2, 'there are no muxes in Active Document' ) 149 | FreeCADGui.Selection.clearSelection() 150 | FreeCAD.ActiveDocument.recompute() 151 | 152 | 153 | def GetResources(self): 154 | msg = 'Refresh all muxedAssembly\n\ 155 | or refresh all selected muxedAssembly\n\ 156 | use the ReadOnly property to avoid accidental refresh' 157 | return { 158 | 'Pixmap' : ':/assembly2/icons/muxAssemblyRefresh.svg', 159 | 'MenuText': msg, 160 | 'ToolTip': msg 161 | } 162 | 163 | 164 | 165 | FreeCADGui.addCommand('assembly2_muxAssembly', MuxAssemblyCommand()) 166 | FreeCADGui.addCommand('assembly2_muxAssemblyRefresh', MuxAssemblyRefreshCommand()) 167 | 168 | 169 | -------------------------------------------------------------------------------- /assembly2/utils/partsList.py: -------------------------------------------------------------------------------- 1 | ''' 2 | create parts list 3 | ''' 4 | from assembly2.core import * 5 | from PySide import QtCore 6 | 7 | if FreeCAD.GuiUp: 8 | try: 9 | from drawingDimensioning import table as table_dd 10 | from drawingDimensioning.table import getDrawingPageGUIVars, PlacementClick, previewDimension 11 | from drawingDimensioning.svgLib import SvgTextRenderer 12 | #dimensioningTracker = DimensioningProcessTracker() 13 | d = table_dd.d 14 | drawing_dimensioning_installed = True 15 | except ImportError: 16 | drawing_dimensioning_installed = False 17 | else: 18 | drawing_dimensioning_installed = False 19 | 20 | class PartsList: 21 | def __init__(self): 22 | self.entries = [] 23 | self.directoryMask = [] 24 | def addObject(self, obj): 25 | try: 26 | index = self.entries.index(obj) 27 | self.entries[index].count = self.entries[index].count + 1 28 | except ValueError: 29 | self.entries.append(PartListEntry( obj )) 30 | class PartListEntry: 31 | def __init__(self, obj): 32 | self.obj = obj 33 | self.count = 1 34 | self.sourceFile = obj.sourceFile 35 | self.name = os.path.basename( obj.sourceFile ) 36 | self.parentDirectory = os.path.basename( os.path.dirname( obj.sourceFile )) 37 | def __eq__(self, b): 38 | return self.sourceFile == b.sourceFile 39 | 40 | 41 | def parts_list_clickHandler( x, y ): 42 | d.selections = [ PlacementClick( x, y) ] 43 | return 'createDimension:%s' % findUnusedObjectName('partsList') 44 | 45 | 46 | class AddPartsList: 47 | def Activated(self): 48 | if not drawing_dimensioning_installed: 49 | QtGui.QMessageBox.critical( QtGui.QApplication.activeWindow(), 'drawing dimensioning wb required', 'the parts list feature requires the drawing dimensioning wb (https://github.com/hamish2014/FreeCAD_drawing_dimensioning). Release from 12 April 2016 or later required.' ) 50 | return 51 | V = getDrawingPageGUIVars() #needs to be done before dialog show, else Qt active is dialog and not freecads 52 | d.activate( V, dialogIconPath= ':/assembly2/icons/partsList.svg') 53 | P = PartsList() 54 | for obj in FreeCAD.ActiveDocument.Objects: 55 | if 'importPart' in obj.Content: 56 | debugPrint(3, 'adding %s to parts list' % obj.Name) 57 | P.addObject( PartListEntry(obj) ) 58 | d.partsList = P 59 | for pref in d.preferences: 60 | pref.dimensioningProcess = d #required to be compadible with drawing dimensioning 61 | d.taskDialog = PartsListTaskDialog() 62 | FreeCADGui.Control.showDialog( d.taskDialog ) 63 | previewDimension.initializePreview( d, table_dd.table_preview, parts_list_clickHandler ) 64 | 65 | def GetResources(self): 66 | tip = 'Create a part list from the objects imported using the assembly 2 workbench' 67 | return { 68 | 'Pixmap' : ':/assembly2/icons/partsList.svg' , 69 | 'MenuText': tip, 70 | 'ToolTip': tip 71 | } 72 | FreeCADGui.addCommand('assembly2_addPartsList', AddPartsList()) 73 | 74 | 75 | class PartsListTaskDialog: 76 | def __init__(self): 77 | from assembly2.core import __dir__ 78 | self.form = FreeCADGui.PySideUic.loadUi( ':/assembly2/ui/partsList.ui' ) 79 | self.form.setWindowIcon(QtGui.QIcon( ':/assembly2/icons/partsList.svg' ) ) 80 | self.setIntialValues() 81 | self.getValues() 82 | for groupBox in self.form.children(): 83 | for w in groupBox.children(): 84 | if hasattr(w, 'valueChanged'): 85 | w.valueChanged.connect( self.getValues ) 86 | if isinstance(w, QtGui.QLineEdit): 87 | w.textChanged.connect( self.getValues ) 88 | self.form.pushButton_set_as_default.clicked.connect( self.setDefaults ) 89 | 90 | def setIntialValues(self): 91 | parms = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/Assembly2/partsList") 92 | form = self.form 93 | form.doubleSpinBox_column_part_width.setValue( parms.GetFloat('column_part_width', 20) ) 94 | form.doubleSpinBox_column_sourceFile_width.setValue( parms.GetFloat('column_sourceFile_width', 80) ) 95 | form.doubleSpinBox_column_quantity_width.setValue( parms.GetFloat('column_quantity_width', 40) ) 96 | form.lineEdit_column_part_label.setText( parms.GetString('column_part_label', 'part #')) 97 | form.lineEdit_column_sourceFile_label.setText( parms.GetString('column_sourceFile_label', 'source file')) 98 | form.lineEdit_column_quantity_label.setText( parms.GetString('column_quantity_label', 'quantity')) 99 | form.doubleSpinBox_lineWidth.setValue( parms.GetFloat('lineWidth', 0.4) ) 100 | form.doubleSpinBox_fontSize.setValue( parms.GetFloat('fontSize', 4.0) ) 101 | form.lineEdit_fontColor.setText( parms.GetString('fontColor','rgb(0,0,0)') ) 102 | form.doubleSpinBox_padding.setValue( parms.GetFloat('padding', 1.5)) 103 | filtersAdded = [] 104 | for entry in d.partsList.entries: 105 | if not entry.parentDirectory in filtersAdded: 106 | item = QtGui.QListWidgetItem('%s' % entry.parentDirectory, form.listWidget_directoryFilter) 107 | item.setCheckState( QtCore.Qt.CheckState.Checked ) 108 | filtersAdded.append( entry.parentDirectory ) 109 | d.partsList.directoryMask = filtersAdded 110 | form.listWidget_directoryFilter.itemChanged.connect( self.update_directoryFilter ) 111 | 112 | def setDefaults(self): 113 | parms = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/Assembly2/partsList") 114 | form = self.form 115 | parms.SetFloat('column_part_width', form.doubleSpinBox_column_part_width.value() ) 116 | parms.SetFloat('column_sourceFile_width', form.doubleSpinBox_column_sourceFile_width.value() ) 117 | parms.SetFloat('column_quantity_width', form.doubleSpinBox_column_quantity_width.value() ) 118 | parms.SetString('column_part_label', form.lineEdit_column_part_label.text() ) 119 | parms.SetString('column_sourceFile_label',form.lineEdit_column_sourceFile_label.text() ) 120 | parms.SetString('column_quantity_label', form.lineEdit_column_quantity_label.text() ) 121 | parms.SetFloat('lineWidth', form.doubleSpinBox_lineWidth.value() ) 122 | parms.SetFloat('fontSize', form.doubleSpinBox_fontSize.value() ) 123 | parms.SetString('fontColor', form.lineEdit_fontColor.text() ) 124 | parms.SetFloat('padding', form.doubleSpinBox_padding.setValue() ) 125 | 126 | 127 | def getValues(self, notUsed=None): 128 | form = self.form 129 | contents = [ 130 | form.lineEdit_column_part_label.text(), 131 | form.lineEdit_column_sourceFile_label.text(), 132 | form.lineEdit_column_quantity_label.text() 133 | ] 134 | partsList = d.partsList 135 | entries = [ e for e in partsList.entries if e.parentDirectory in partsList.directoryMask ] 136 | for ind, entry in enumerate(entries): 137 | contents.extend([ 138 | str(ind+1), 139 | os.path.basename(entry.sourceFile).replace('.fcstd',''), 140 | str(entry.count) 141 | ]) 142 | d.dimensionConstructorKWs = dict( 143 | column_widths = [ 144 | form.doubleSpinBox_column_part_width.value(), 145 | form.doubleSpinBox_column_sourceFile_width.value(), 146 | form.doubleSpinBox_column_quantity_width.value() 147 | ], 148 | contents = contents, 149 | row_heights = [ 150 | form.doubleSpinBox_fontSize.value() + 2*form.doubleSpinBox_padding.value() 151 | ], 152 | border_width = form.doubleSpinBox_lineWidth.value(), 153 | border_color='rgb(0,0,0)', 154 | padding_x = form.doubleSpinBox_padding.value(), 155 | padding_y = form.doubleSpinBox_padding.value(), 156 | extra_rows= 0, 157 | textRenderer_table = SvgTextRenderer( 158 | u'inherit', 159 | u'%1.2f pt' % form.doubleSpinBox_fontSize.value(), 160 | form.lineEdit_fontColor.text() 161 | ) 162 | ) 163 | 164 | def update_directoryFilter(self, *args): 165 | try: 166 | del d.partsList.directoryMask[:] 167 | listWidget = self.form.listWidget_directoryFilter 168 | for index in range(listWidget.count()): 169 | item = listWidget.item(index) 170 | if item.checkState() == QtCore.Qt.CheckState.Checked: 171 | d.partsList.directoryMask.append( item.text() ) 172 | self.getValues() 173 | except: 174 | import traceback 175 | FreeCAD.Console.PrintError(traceback.format_exc()) 176 | 177 | def reject(self): 178 | previewDimension.removePreviewGraphicItems( recomputeActiveDocument = True ) 179 | FreeCADGui.Control.closeDialog() 180 | 181 | def getStandardButtons(self): #http://forum.freecadweb.org/viewtopic.php?f=10&t=11801 182 | return 0x00400000 183 | -------------------------------------------------------------------------------- /assembly2/utils/randomClrs.py: -------------------------------------------------------------------------------- 1 | from assembly2.core import * 2 | from random import random, choice 3 | 4 | class RandomColorAllCommand: 5 | def Activated(self): 6 | randomcolors=(0.1,0.18,0.33,0.50,0.67,0.78,0.9) 7 | for objs in FreeCAD.ActiveDocument.Objects: 8 | if 'importPart' in objs.Content: 9 | FreeCADGui.ActiveDocument.getObject(objs.Name).ShapeColor=(choice(randomcolors),choice(randomcolors),choice(randomcolors)) 10 | 11 | def GetResources(self): 12 | return { 13 | 'MenuText': 'Apply a random color to all imported objects', 14 | 'ToolTip': 'Apply a random color to all imported objects' 15 | } 16 | 17 | FreeCADGui.addCommand('assembly2_randomColorAll', RandomColorAllCommand()) 18 | -------------------------------------------------------------------------------- /assembly2/utils/tests.py: -------------------------------------------------------------------------------- 1 | if __name__ == '__main__': 2 | print('''run tests via 3 | FreeCAD_assembly2$ python2 test.py assembly2.utils.tests''') 4 | exit() 5 | 6 | import unittest 7 | 8 | class Tests_Animate_Constraint(unittest.TestCase): 9 | def test_interpolation(self): 10 | from assembly2.utils.animate_constraint import numpy, spline_interp, linear_interp 11 | P = [ 12 | [ 0, 0], 13 | [ 1, -1], 14 | [ 1, -4], 15 | [ 0, -5], 16 | [ 4, -6], 17 | [10, -4], 18 | [8, -4], 19 | [6, -5], 20 | [3, -2] 21 | ] 22 | A = numpy.arange( len(P) ) * 4.2 23 | P_spline = spline_interp( P, A, 16 ) 24 | P_linear = linear_interp( P, A, 16 ) 25 | if False: 26 | from matplotlib import pyplot 27 | pyplot.figure() 28 | pyplot.plot( P_spline[:,0], P_spline[:,1], '-bx' ) 29 | pyplot.plot( P_linear[:,0], P_linear[:,1], '--g' ) 30 | pyplot.plot( [p[0] for p in P], [p[1] for p in P], 'go' ) 31 | pyplot.show() 32 | -------------------------------------------------------------------------------- /assembly2/utils/undo.py: -------------------------------------------------------------------------------- 1 | from assembly2.core import * 2 | from assembly2.lib3D import * 3 | from pivy import coin 4 | from PySide import QtGui 5 | __dir2__ = os.path.dirname(__file__) 6 | iconPath = os.path.join( __dir2__, 'Gui','Resources', 'icons' ) 7 | GuiPath = os.path.expanduser ("~") #GuiPath = os.path.join( __dir2__, 'Gui' ) 8 | 9 | s_nm = [] 10 | s_plc = [] 11 | 12 | #todo check redefining will update the undo 13 | 14 | class UndoConstraint: 15 | def Activated(self): 16 | constraints = [ obj for obj in FreeCAD.ActiveDocument.Objects if 'ConstraintInfo' in obj.Content ] 17 | if len(constraints) == 0: 18 | QtGui.QMessageBox.information( QtGui.QApplication.activeWindow(), "Command Aborted", 'Undo aborted since no assembly2 constraints in active document.') 19 | return 20 | lastConstraintAdded = constraints[-1] 21 | #print lastConstraintAdded.Name 22 | constraintFile = os.path.join( GuiPath , 'constraintFile.txt') 23 | if os.path.exists(constraintFile): 24 | s_nm = [] 25 | s_plcB = [] 26 | s_plcR = [] 27 | undo_constraint='' 28 | lines = [line.rstrip('\n') for line in open(constraintFile)] 29 | #with open(constraintFile, 'r') as inpfile: 30 | #for line in inpfile: 31 | # print line 32 | s_nm.append(lines[0]) 33 | if len (lines) > 6: 34 | s_nm.append(lines[3]) 35 | undo_constraint=lines[6] 36 | elif len (lines) > 3: 37 | undo_constraint=lines[3] #not redefining 38 | plc0B=lines[1].strip('Vector (').strip(')').split(',') 39 | plc0R=lines[2].strip('Rotation (').strip(')').split(',') 40 | s_plcB.append([float(plc0B[0]),float(plc0B[1]),float(plc0B[2])]) 41 | #s_plcB.append(FreeCAD.Vector (plc0B[0],plc0B[1],plc0B[2])) 42 | s_plcR.append([float(plc0R[0]),float(plc0R[1]),float(plc0R[2]),float(plc0R[3])]) 43 | if len (lines) > 6: 44 | plc0B=lines[4].strip('Vector (').strip(')').split(',') 45 | plc0R=lines[5].strip('Rotation (').strip(')').split(',') 46 | s_plcB.append([float(plc0B[0]),float(plc0B[1]),float(plc0B[2])]) 47 | #s_plcB.append(FreeCAD.Vector (plc0B[0],plc0B[1],plc0B[2])) 48 | s_plcR.append([float(plc0R[0]),float(plc0R[1]),float(plc0R[2]),float(plc0R[3])]) 49 | #print s_nm,s_plcB, s_plcR 50 | FreeCAD.ActiveDocument.getObject(s_nm[0]).Placement.Base = FreeCAD.Vector (s_plcB[0][0],s_plcB[0][1],s_plcB[0][2],) #App.Vector (5.000000000000001, 5.000000000000003, 5.00) 51 | FreeCAD.ActiveDocument.getObject(s_nm[0]).Placement.Rotation = FreeCAD.Rotation (s_plcR[0][0],s_plcR[0][1],s_plcR[0][2],s_plcR[0][3]) #App.Vector (5.000000000000001, 5.000000000000003, 5.00) 52 | if len (lines) > 6: 53 | FreeCAD.ActiveDocument.getObject(s_nm[1]).Placement.Base = FreeCAD.Vector (s_plcB[1][0],s_plcB[1][1],s_plcB[1][2],) #App.Vector (5.000000000000001, 5.000000000000003, 5.00) 54 | FreeCAD.ActiveDocument.getObject(s_nm[1]).Placement.Rotation = FreeCAD.Rotation (s_plcR[1][0],s_plcR[1][1],s_plcR[1][2],s_plcR[1][3]) #App.Vector (5.000000000000001, 5.000000000000003, 5.00) 55 | constraints = [ obj for obj in FreeCAD.ActiveDocument.Objects if 'ConstraintInfo' in obj.Content ] 56 | if len(constraints) == 0: 57 | QtGui.QMessageBox.information( QtGui.QApplication.activeWindow(), "Command Aborted", 'Flip aborted since no assembly2 constraints in active document.') 58 | return 59 | lastConstraintAdded = constraints[-1] 60 | if undo_constraint == lastConstraintAdded.Name: 61 | #print lastConstraintAdded.Name 62 | FreeCAD.ActiveDocument.removeObject(lastConstraintAdded.Name) 63 | FreeCAD.ActiveDocument.recompute() 64 | FreeCAD.ActiveDocument.recompute() 65 | os.remove(constraintFile) 66 | return 67 | 68 | def IsActive(self): 69 | constraintFile = os.path.join( GuiPath , 'constraintFile.txt') 70 | if not os.path.exists(constraintFile): 71 | return False 72 | return True 73 | 74 | def GetResources(self): 75 | return { 76 | 'Pixmap' : os.path.join( iconPath , 'EditUndo.svg'), 77 | 'MenuText': 'Undo last Constrain', 78 | 'ToolTip': 'Undo last Constrain' 79 | } 80 | 81 | FreeCADGui.addCommand('assembly2_undoConstraint', UndoConstraint()) 82 | -------------------------------------------------------------------------------- /assembly2lib.py: -------------------------------------------------------------------------------- 1 | #created for backward compatibility 2 | from assembly2.constraints.objectProxy import ConstraintObjectProxy, ConstraintMirrorObjectProxy 3 | -------------------------------------------------------------------------------- /importPart.py: -------------------------------------------------------------------------------- 1 | #created for backward compatibility 2 | from assembly2.importPart import Proxy_importPart 3 | 4 | -------------------------------------------------------------------------------- /test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import sys, os 3 | sys.path.append('/usr/lib/freecad/lib/') #path to FreeCAD library on Linux 4 | try: 5 | import FreeCAD, FreeCADGui 6 | except ImportError as msg: 7 | print('Import error, is this testing script being run from Python2?') 8 | raise ImportError(msg) 9 | assert not hasattr(FreeCADGui, 'addCommand') 10 | 11 | def addCommand_check( name, command): 12 | pass 13 | #if not name.startswith('assembly2_'): 14 | # raise ValueError('%s does not begin with %s' % ( name, 'assembly2_' ) ) 15 | 16 | FreeCADGui.addCommand = addCommand_check 17 | 18 | import assembly2 19 | import argparse 20 | from assembly2.core import debugPrint 21 | 22 | parser = argparse.ArgumentParser() 23 | parser.add_argument('--failfast', action='store_true', help='Stop the test run on the first error or failure.') 24 | parser.add_argument('--buffer', action='store_true', help='The standard output and standard error streams are buffered during the test run. Output during a passing test is discarded. Output is echoed normally on test fail or error and is added to the failure messages.') 25 | parser.add_argument('-v','--verbosity', type=int, default=1 ) 26 | parser.add_argument('--no_descriptions', action='store_true' ) 27 | parser.add_argument('testSuiteName', type=str, nargs='*') 28 | args = parser.parse_args() 29 | 30 | debugPrint.level = 0 31 | 32 | testLoader = unittest.TestLoader() 33 | if args.testSuiteName == []: 34 | suite = testLoader.discover( 35 | start_dir = os.path.join( assembly2.__dir__ , 'assembly2' ), 36 | pattern='test*.py', 37 | top_level_dir=None 38 | ) 39 | else: 40 | suite = unittest.TestSuite() 41 | for name in args.testSuiteName: 42 | suite.addTest( testLoader.loadTestsFromName(name ) ) 43 | 44 | runner = unittest.TextTestRunner( 45 | failfast = args.failfast, 46 | verbosity = args.verbosity, 47 | descriptions = not args.no_descriptions, 48 | buffer = args.buffer 49 | ) 50 | runner.run( suite ) 51 | -------------------------------------------------------------------------------- /viewProviderProxies.py: -------------------------------------------------------------------------------- 1 | #created for backward compatibility 2 | from assembly2.importPart import ImportedPartViewProviderProxy 3 | from assembly2.constraints.viewProviderProxy import ConstraintViewProviderProxy, ConstraintMirrorViewProviderProxy 4 | from assembly2.constraints.objectProxy import ConstraintObjectProxy, ConstraintMirrorObjectProxy 5 | --------------------------------------------------------------------------------