├── .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 |
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 |
--------------------------------------------------------------------------------