├── freecad └── frametools │ ├── version.py │ ├── __init__.py │ ├── init_gui.py │ ├── miter-cut.ui │ ├── plane-cut.ui │ ├── shape-cut.ui │ ├── boxtools.py │ ├── commands.py │ ├── screw_maker.py │ ├── bspline_tools.py │ ├── make_beam.ui │ ├── fem2d.py │ ├── icons │ ├── nurbs_connect.svg │ ├── beam.svg │ ├── beam_plane_cut.svg │ ├── linked_face.svg │ ├── reload.svg │ ├── beam_miter_cut.svg │ ├── screw.svg │ ├── beam_shape_cut.svg │ └── generic_solver.svg │ ├── beamobj.py │ └── interaction.py ├── MANIFEST.in ├── record_frame.gif ├── example ├── fahrrad.fcstd └── gate │ ├── gate.FCStd │ ├── beams_cut.png │ ├── drawing.png │ ├── inner_beams.png │ ├── outer_beams.png │ ├── beams_sketch.png │ ├── profile_sketch.png │ └── outer_beams_cut.png ├── notebooks └── .ipynb_checkpoints │ └── Untitled-checkpoint.ipynb ├── setup.py ├── .gitignore ├── package.xml └── README.md /freecad/frametools/version.py: -------------------------------------------------------------------------------- 1 | __version__ = "0.1.0" -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-include freecad/frametools/icons * -------------------------------------------------------------------------------- /record_frame.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/looooo/freecad.frametools/HEAD/record_frame.gif -------------------------------------------------------------------------------- /example/fahrrad.fcstd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/looooo/freecad.frametools/HEAD/example/fahrrad.fcstd -------------------------------------------------------------------------------- /example/gate/gate.FCStd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/looooo/freecad.frametools/HEAD/example/gate/gate.FCStd -------------------------------------------------------------------------------- /example/gate/beams_cut.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/looooo/freecad.frametools/HEAD/example/gate/beams_cut.png -------------------------------------------------------------------------------- /example/gate/drawing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/looooo/freecad.frametools/HEAD/example/gate/drawing.png -------------------------------------------------------------------------------- /example/gate/inner_beams.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/looooo/freecad.frametools/HEAD/example/gate/inner_beams.png -------------------------------------------------------------------------------- /example/gate/outer_beams.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/looooo/freecad.frametools/HEAD/example/gate/outer_beams.png -------------------------------------------------------------------------------- /example/gate/beams_sketch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/looooo/freecad.frametools/HEAD/example/gate/beams_sketch.png -------------------------------------------------------------------------------- /example/gate/profile_sketch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/looooo/freecad.frametools/HEAD/example/gate/profile_sketch.png -------------------------------------------------------------------------------- /example/gate/outer_beams_cut.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/looooo/freecad.frametools/HEAD/example/gate/outer_beams_cut.png -------------------------------------------------------------------------------- /notebooks/.ipynb_checkpoints/Untitled-checkpoint.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [], 3 | "metadata": {}, 4 | "nbformat": 4, 5 | "nbformat_minor": 2 6 | } 7 | -------------------------------------------------------------------------------- /freecad/frametools/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | from .version import __version__ 3 | DIR = os.path.dirname(__file__) 4 | ICON_PATH = os.path.join(DIR, "icons") 5 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | import sys, os 3 | 4 | version_path = os.path.join(os.path.abspath(os.path.dirname(__file__)), 5 | "freecad", "frametools", "version.py") 6 | with open(version_path) as fp: 7 | exec(fp.read()) 8 | 9 | setup(name='freecad.frametools', 10 | version=__version__, 11 | packages=['freecad', 12 | 'freecad.frametools'], 13 | maintainer="looooo", 14 | maintainer_email="sppedflyer@gmail.com", 15 | url="https://github.com/looooo/pip-integration", 16 | description="some frame-tools for freecad", 17 | install_requires=[], 18 | include_package_data=True) -------------------------------------------------------------------------------- /.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 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | *.egg-info/ 23 | .installed.cfg 24 | *.egg 25 | 26 | # PyInstaller 27 | # Usually these files are written by a python script from a template 28 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 29 | *.manifest 30 | *.spec 31 | 32 | # Installer logs 33 | pip-log.txt 34 | pip-delete-this-directory.txt 35 | 36 | # Unit test / coverage reports 37 | htmlcov/ 38 | .tox/ 39 | .coverage 40 | .coverage.* 41 | .cache 42 | nosetests.xml 43 | coverage.xml 44 | *,cover 45 | 46 | # Translations 47 | *.mo 48 | *.pot 49 | 50 | # Django stuff: 51 | *.log 52 | 53 | # Sphinx documentation 54 | docs/_build/ 55 | 56 | # PyBuilder 57 | target/ 58 | -------------------------------------------------------------------------------- /package.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Frame and beams workbench 5 | 6 | A workbench for beams and frames 7 | 8 | 0.1.1 9 | 10 | 2023-08-08 11 | 12 | looooo 13 | 14 | LGPL-2.1-or-later 15 | 16 | https://github.com/looooo/freecad.frametools 17 | 18 | https://github.com/looooo/freecad.frametools/issues 19 | 20 | https://github.com/looooo/freecad.frametools/blob/master/README.md 21 | 22 | https://github.com/looooo/freecad.frametools 23 | 24 | freecad/frametools/icons/beam.svg 25 | 26 | 27 | 28 | 0.19.0 29 | scipy 30 | frame 31 | beam 32 | FrameWorkbench 33 | ./ 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /freecad/frametools/init_gui.py: -------------------------------------------------------------------------------- 1 | try: 2 | import FreeCADGui as Gui 3 | import FreeCAD 4 | except ImportError: 5 | print("module not loaded with freecad") 6 | 7 | import os 8 | from freecad.frametools import ICON_PATH 9 | 10 | class FrameWorkbench(Gui.Workbench): 11 | '''frame workbench''' 12 | 13 | MenuText = 'Frame and beams' 14 | ToolTip = 'Frame and beams' 15 | Icon = os.path.join(ICON_PATH, 'beam.svg') 16 | toolbox = ['Beam', 'CutMiter', 'CutPlane', 'CutShape', 'Reload'] 17 | boxbox = ['LinkedFace', 'ExtrudedFace', 'FlatFace', 'FemSolver', 'ScrewMaker'] 18 | 19 | def GetClassName(self): 20 | return 'Gui::PythonWorkbench' 21 | 22 | def Initialize(self): 23 | 24 | from freecad.frametools import commands 25 | Gui.addCommand('Beam', commands.Beam()) 26 | Gui.addCommand('CutMiter', commands.CutMiter()) 27 | Gui.addCommand('CutPlane', commands.CutPlane()) 28 | Gui.addCommand('CutShape', commands.CutShape()) 29 | Gui.addCommand('Reload', commands.Reload()) 30 | Gui.addCommand('LinkedFace', commands.LinkedFace()) 31 | Gui.addCommand('ExtrudedFace', commands.ExtrudedFace()) 32 | Gui.addCommand('FlatFace', commands.FlatFace()) 33 | Gui.addCommand('FlatFace', commands.NurbsConnection()) 34 | Gui.addCommand('FemSolver', commands.FemSolver()) 35 | Gui.addCommand('ScrewMaker', commands.ScrewMaker()) 36 | 37 | self.appendToolbar('Frame', self.toolbox) 38 | self.appendMenu('Frame', self.toolbox) 39 | self.appendToolbar('Box', self.boxbox) 40 | self.appendMenu('Box', self.boxbox) 41 | 42 | def Activated(self): 43 | pass 44 | 45 | def Deactivated(self): 46 | pass 47 | 48 | Gui.addWorkbench(FrameWorkbench()) 49 | -------------------------------------------------------------------------------- /freecad/frametools/miter-cut.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | FrameAndBeamGui::CutBeamsParameters 4 | 5 | 6 | 7 | 0 8 | 0 9 | 271 10 | 604 11 | 12 | 13 | 14 | Cut type 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | Beam 1 23 | 24 | 25 | true 26 | 27 | 28 | 29 | 30 | 31 | 32 | true 33 | 34 | 35 | 36 | 37 | 38 | 39 | Beam 2 40 | 41 | 42 | true 43 | 44 | 45 | 46 | 47 | 48 | 49 | true 50 | 51 | 52 | 53 | 54 | 55 | 56 | Cut 57 | 58 | 59 | 60 | 61 | 62 | 63 | true 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /freecad/frametools/plane-cut.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | FrameAndBeamGui::CutBeamsParameters 4 | 5 | 6 | 7 | 0 8 | 0 9 | 271 10 | 604 11 | 12 | 13 | 14 | Cut type 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | Beam to cut 23 | 24 | 25 | true 26 | 27 | 28 | 29 | 30 | 31 | 32 | true 33 | 34 | 35 | 36 | 37 | 38 | 39 | Cut with 40 | 41 | 42 | true 43 | 44 | 45 | 46 | 47 | 48 | 49 | true 50 | 51 | 52 | 53 | 54 | 55 | 56 | Cut 57 | 58 | 59 | 60 | 61 | 62 | 63 | true 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /freecad/frametools/shape-cut.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | FrameAndBeamGui::CutBeamsParameters 4 | 5 | 6 | 7 | 0 8 | 0 9 | 271 10 | 604 11 | 12 | 13 | 14 | Cut type 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | Beam to cut 23 | 24 | 25 | true 26 | 27 | 28 | 29 | 30 | 31 | 32 | true 33 | 34 | 35 | 36 | 37 | 38 | 39 | Cut beam with 40 | 41 | 42 | true 43 | 44 | 45 | 46 | 47 | 48 | 49 | true 50 | 51 | 52 | 53 | 54 | 55 | 56 | Cut 57 | 58 | 59 | 60 | 61 | 62 | 63 | true 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /freecad/frametools/boxtools.py: -------------------------------------------------------------------------------- 1 | import FreeCAD as App 2 | import FreeCADGui as Gui 3 | import Part 4 | import numpy as np 5 | 6 | def create_extruded_face(): 7 | sel = Gui.Selection.getSelectionEx()[0] 8 | base = sel.Object 9 | name = sel.SubElementNames[0] 10 | thickness = 1 11 | feature = App.ActiveDocument.addObject("Part::FeaturePython", "Extruded_" + name) 12 | ExtrudedFace(feature, base, name, thickness) 13 | feature.ViewObject.Proxy = 0 14 | App.ActiveDocument.recompute() 15 | 16 | 17 | def create_linked_face(): 18 | sel = Gui.Selection.getSelectionEx()[0] 19 | base = sel.Object 20 | name = sel.SubElementNames[0] 21 | feature = App.ActiveDocument.addObject("Part::FeaturePython", "Linked_" + name) 22 | LinkedFace(feature, base, name) 23 | feature.ViewObject.Proxy = 0 24 | App.ActiveDocument.recompute() 25 | 26 | 27 | def create_flat_face(): 28 | base = Gui.Selection.getSelection()[0] 29 | feature = App.ActiveDocument.addObject("Part::FeaturePython", "Flat_" + base.Name) 30 | FlatFace(feature, base) 31 | feature.ViewObject.Proxy = 0 32 | App.ActiveDocument.recompute() 33 | 34 | 35 | class ExtrudedFace(object): 36 | def __init__(self, obj, base, name, thickness): 37 | self.obj = obj 38 | obj.Proxy = self 39 | obj.addProperty("App::PropertyLink", "base", "not yet", "docs").base = base 40 | obj.addProperty("App::PropertyString", "name", "not yet", "docs").name = name 41 | obj.addProperty("App::PropertyFloat", "thickness", "not yet", "docs").thickness = thickness 42 | 43 | def execute(self, fp): 44 | # check if it is a face: 45 | face = None 46 | try: 47 | face = fp.base.Shape.getElement(fp.name) 48 | except AttributeError as e: 49 | Warning(e) 50 | print(face) 51 | if face and isinstance(face, Part.Face): 52 | fp.Shape = face.extrude(face.normalAt(0, 0) * fp.thickness) 53 | 54 | def __getstate__(self): 55 | return None 56 | 57 | def __setstate__(self, state): 58 | return None 59 | 60 | 61 | class LinkedFace(object): 62 | def __init__(self, obj, base, name): 63 | self.obj = obj 64 | obj.Proxy = self 65 | obj.addProperty("App::PropertyLink", "base", "group", "docs").base = base 66 | obj.addProperty("App::PropertyString", "name", "group", "docs").name = name 67 | 68 | def execute(self, fp): 69 | # check if it is a face: 70 | face = None 71 | try: 72 | face = fp.base.Shape.getElement(fp.name) 73 | except AttributeError as e: 74 | Warning(e) 75 | print(face) 76 | if face and isinstance(face, Part.Face): 77 | fp.Shape = face 78 | 79 | def __getstate__(self): 80 | return None 81 | 82 | def __setstate__(self, state): 83 | return None 84 | 85 | class FlatFace(object): 86 | def __init__(self, obj, base): 87 | self.obj = obj 88 | obj.Proxy = self 89 | obj.addProperty("App::PropertyLink", "base", "group", "docs").base = base 90 | 91 | def execute(self, fp): 92 | # get face with biggest area 93 | area = 0 94 | for i, face in enumerate(fp.base.Shape.Faces): 95 | if area < face.Area: 96 | item_nr = i 97 | area = face.Area 98 | # assume a refined shape 99 | face = Part.Face(fp.base.Shape.Faces[item_nr]) 100 | 101 | normal = face.normalAt(0, 0) 102 | rot = App.Rotation(normal, App.Vector(0, 0, 1)) 103 | placement = App.Placement(App.Vector(), rot) 104 | face = face.transformGeometry(placement.toMatrix()) 105 | trans = App.Vector(0, 0, 0) - face.Faces[0].valueAt(0, 0) 106 | placement = App.Placement(trans, App.Rotation()) 107 | face = face.transformGeometry(placement.toMatrix()) 108 | fp.Shape = Part.Compound(face.Wires) 109 | 110 | def __getstate__(self): 111 | return None 112 | 113 | def __setstate__(self, state): 114 | return None 115 | -------------------------------------------------------------------------------- /freecad/frametools/commands.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import FreeCADGui as Gui 4 | import FreeCAD as App 5 | from freecad.frametools import ICON_PATH 6 | from . import interaction, boxtools, bspline_tools 7 | from . import fem2d 8 | from . import screw_maker 9 | 10 | 11 | __all__ = [ 12 | "Beam", 13 | "CutMiter", 14 | "CutPlane", 15 | "CutShape"] 16 | 17 | class BaseCommand(object): 18 | 19 | def __init__(self): 20 | pass 21 | 22 | def GetResources(self): 23 | return {'Pixmap': '.svg', 'MenuText': 'Text', 'ToolTip': 'Text'} 24 | 25 | def IsActive(self): 26 | if App.ActiveDocument is None: 27 | return False 28 | else: 29 | return True 30 | 31 | def Activated(self): 32 | pass 33 | 34 | @property 35 | def view(self): 36 | return Gui.ActiveDocument.ActiveView 37 | 38 | 39 | class Beam(BaseCommand): 40 | 41 | def Activated(self): 42 | interaction.make_beam(self.view) 43 | 44 | def GetResources(self): 45 | return {'Pixmap': os.path.join(ICON_PATH, 'beam.svg'), 'MenuText': 'Beam', 'ToolTip': 'Create a beam'} 46 | 47 | 48 | class CutMiter(BaseCommand): 49 | 50 | def Activated(self): 51 | interaction.make_miter_cut(self.view) 52 | 53 | def GetResources(self): 54 | return {'Pixmap': os.path.join(ICON_PATH, 'beam_miter_cut.svg'), 'MenuText': 'Miter Cut', 'ToolTip': 'Perform miter cut of 2 beams'} 55 | 56 | 57 | class CutPlane(BaseCommand): 58 | 59 | def Activated(self): 60 | interaction.make_plane_cut(self.view) 61 | 62 | def GetResources(self): 63 | return {'Pixmap': os.path.join(ICON_PATH, 'beam_plane_cut.svg'), 'MenuText': 'Plane Cut', 'ToolTip': 'Cut a beam by a face of another beam'} 64 | 65 | 66 | class CutShape(BaseCommand): 67 | 68 | def Activated(self): 69 | interaction.make_shape_cut(self.view) 70 | 71 | def GetResources(self): 72 | return {'Pixmap': os.path.join(ICON_PATH, 'beam_shape_cut.svg'), 'MenuText': 'Shape Cut', 'ToolTip': 'Cut a beam by outer surface of another beam'} 73 | 74 | 75 | class LinkedFace(BaseCommand): 76 | 77 | def Activated(self): 78 | boxtools.create_linked_face() 79 | 80 | def GetResources(self): 81 | return {'Pixmap': os.path.join(ICON_PATH, 'linked_face.svg'), 'MenuText': 'Linked Face', 'ToolTip': 'linked_face'} 82 | 83 | 84 | class ExtrudedFace(BaseCommand): 85 | 86 | def Activated(self): 87 | boxtools.create_extruded_face() 88 | 89 | def GetResources(self): 90 | return {'Pixmap': os.path.join(ICON_PATH, 'extruded_face.svg'), 'MenuText': 'Extruded Face', 'ToolTip': 'extruded_face'} 91 | 92 | 93 | class FlatFace(BaseCommand): 94 | 95 | def Activated(self): 96 | boxtools.create_flat_face() 97 | 98 | def GetResources(self): 99 | return {'Pixmap': os.path.join(ICON_PATH, 'linked_face.svg'), 'MenuText': 'Flat Face', 'ToolTip': 'flat_face'} 100 | 101 | class ScrewMaker(BaseCommand): 102 | 103 | def Activated(self): 104 | a = App.ActiveDocument.addObject("Part::FeaturePython", "screw") 105 | screw_maker.Screw(a) 106 | screw_maker.ViewproviderScrew(a.ViewObject) 107 | 108 | def GetResources(self): 109 | return {'Pixmap': os.path.join(ICON_PATH, 'screw.svg'), 'MenuText': 'create Screw', 'ToolTip': 'create Screw'} 110 | 111 | 112 | 113 | class NurbsConnection(BaseCommand): 114 | 115 | def Activated(self): 116 | bspline_tools.make_nurbs_connection() 117 | 118 | def GetResources(self): 119 | return {'Pixmap': os.path.join(ICON_PATH, 'nurbs_connect.svg'), 'MenuText': 'NURBS Connect', 'ToolTip': 'nurbs_connect'} 120 | 121 | 122 | class FemSolver(BaseCommand): 123 | 124 | def Activated(self): 125 | sel = Gui.Selection.getSelection() 126 | fem2d.make_GenericSolver(sel[0], sel[1]) 127 | App.ActiveDocument.recompute() 128 | 129 | def GetResources(self): 130 | return {'Pixmap': os.path.join(ICON_PATH, "generic_solver.svg"), 'MenuText': 'FEM Solver', 'ToolTip': 'fem_solver'} 131 | 132 | 133 | class Reload(): 134 | NOT_RELOAD = ["freecad.frametools.init_gui"] 135 | RELOAD = ["freecad.frametools"] 136 | def GetResources(self): 137 | return {'Pixmap': os.path.join(ICON_PATH, 'reload.svg'), 'MenuText': 'Refresh', 'ToolTip': 'Refresh'} 138 | 139 | def IsActive(self): 140 | return True 141 | 142 | def Activated(self): 143 | try: 144 | from importlib import reload 145 | except ImportError: 146 | pass # this is python2 147 | import sys 148 | for name, mod in sys.modules.copy().items(): 149 | for rld in self.RELOAD: 150 | if rld in name: 151 | if mod and name not in self.NOT_RELOAD: 152 | print('reload {}'.format(name)) 153 | reload(mod) 154 | from pivy import coin 155 | -------------------------------------------------------------------------------- /freecad/frametools/screw_maker.py: -------------------------------------------------------------------------------- 1 | import os 2 | import numpy as np 3 | import FreeCAD as App 4 | from Part import BSplineCurve, Wire, Face 5 | import Part 6 | import copy 7 | 8 | class Screw(object): 9 | def __init__(self, obj): 10 | '''Screw''' 11 | print("was soll das") 12 | obj.addProperty("App::PropertyInteger", "threads", "screw properties", "number of threads").threads = 1 13 | obj.addProperty("App::PropertyLink","profile","screw properties","sketch profile of section") 14 | obj.addProperty("App::PropertyLength", "height", "screw properties", "total height").height = 1 15 | obj.addProperty("App::PropertyBool", "left_hand", "screw properties", "rotation").left_hand = False 16 | obj.Proxy = self 17 | self.Object = obj 18 | 19 | def execute(self, obj): 20 | if not obj.profile: 21 | return 22 | # 1: get all edges 23 | edges = obj.profile.Shape.Edges 24 | # 2: discretize edges: 25 | edges_discretized = [] 26 | for e in edges: 27 | edges_discretized.append(np.array(e.discretize(100))) 28 | # assume that the profile is in xz-plane (rz) 29 | # so the helical projection is in the xy-plane (z=0) 30 | # the z-axis is the direction of the helix 31 | # 3: height (pitch) of profile 32 | height = 0 33 | for ed in edges_discretized: 34 | height = max([height, max(ed.T[2])]) 35 | pitch = obj.threads * height 36 | all_edges_discretized = [edges_discretized] 37 | for i in range(1, obj.threads): 38 | segment_edges = [] 39 | for ed in edges_discretized: 40 | ed_new = copy.deepcopy(ed) 41 | ed_new.T[2] += height * i 42 | segment_edges.append(ed_new) 43 | all_edges_discretized.append(segment_edges) 44 | # 4: do the helical projection 45 | segments = [] 46 | for segment in all_edges_discretized: 47 | profile_coords = [] 48 | for ed in segment: 49 | profile_coords.append(helical_projection(ed.T[0], ed.T[2], pitch, obj.left_hand)) 50 | segments.append(profile_coords) 51 | # 5: create the bspline interpolation 52 | wires = [] 53 | for s in segments: 54 | wires.append(make_bspline_wire(s)) 55 | try: 56 | wire = Wire(wires) 57 | face = Face(wire) 58 | except Part.OCCError: 59 | # we have multiple faces 60 | face = Face(wires) 61 | sign = -(int(obj.left_hand) * 2 - 1) 62 | obj.Shape = helicalextrusion(face, obj.height, sign * obj.height / pitch * 2 * np.pi) 63 | 64 | def __getstate__(self): 65 | return None 66 | 67 | def __setstate__(self, state): 68 | return None 69 | 70 | def attach(self): 71 | pass 72 | 73 | class ViewproviderScrew(object): 74 | def __init__(self, obj): 75 | ''' Set this object to the proxy object of the actual view provider ''' 76 | obj.Proxy = self 77 | self.vobj = obj 78 | 79 | def attach(self, vobj): 80 | self.vobj = vobj 81 | 82 | def claimChildren(self): 83 | return [self.vobj.Object.profile] 84 | 85 | def getIcon(self): 86 | _dir = os.path.dirname(os.path.realpath(__file__)) 87 | return(_dir + "/" + "icons/screw.svg") 88 | 89 | def __getstate__(self): 90 | return None 91 | 92 | def __setstate__(self, state): 93 | return None 94 | 95 | 96 | 97 | def helical_projection(r, z, pitch, left_hand): 98 | """ 99 | pitch: height of one helical revolution 100 | r: radius of points 101 | z: heihgt of points 102 | """ 103 | sign = int(left_hand) * 2 - 1 # 1 * 2 -1 = 1, 0 * 2 - 1 = -1 104 | phi = 2 * sign * np.pi * z / pitch 105 | x = r * np.cos(phi) 106 | y = r * np.sin(phi) 107 | z = 0 * y 108 | return np.array([x, y, 0 * y]). T 109 | 110 | def make_bspline_wire(pts): 111 | wi = [] 112 | for i in pts: 113 | out = BSplineCurve() 114 | out.interpolate(list(map(fcvec, i))) 115 | wi.append(out.toShape()) 116 | return Wire(wi) 117 | 118 | def fcvec(x): 119 | if len(x) == 2: 120 | return(App.Vector(x[0], x[1], 0)) 121 | else: 122 | return(App.Vector(x[0], x[1], x[2])) 123 | 124 | 125 | def helicalextrusion(face, height, angle): 126 | """ 127 | A helical extrusion using the BRepOffsetAPI 128 | face -- the face to extrude (may contain holes, i.e. more then one wires) 129 | height -- the height of the extrusion, normal to the face 130 | angle -- the twist angle of the extrusion in radians 131 | 132 | returns a solid 133 | """ 134 | pitch = height * 2 * np.pi / abs(angle) 135 | radius = 10.0 # as we are only interested in the "twist", we take an arbitrary constant here 136 | cone_angle = 0 137 | direction = bool(angle < 0) 138 | spine = Part.makeHelix(pitch, height, radius, cone_angle, direction) 139 | def make_pipe(path, profile): 140 | """ 141 | returns (shell, last_wire) 142 | """ 143 | mkPS = Part.BRepOffsetAPI.MakePipeShell(path) 144 | mkPS.setFrenetMode(True) # otherwise, the profile's normal would follow the path 145 | mkPS.add(profile, False, False) 146 | mkPS.build() 147 | return (mkPS.shape(), mkPS.lastShape()) 148 | shell_faces = [] 149 | top_wires = [] 150 | for wire in face.Wires: 151 | pipe_shell, top_wire = make_pipe(spine, wire) 152 | shell_faces.extend(pipe_shell.Faces) 153 | top_wires.append(top_wire) 154 | top_face = Part.Face(top_wires) 155 | shell_faces.append(top_face) 156 | shell_faces.append(face) # the bottom is what we extruded 157 | shell = Part.makeShell(shell_faces) 158 | #shell.sewShape() # fill gaps that may result from accumulated tolerances. Needed? 159 | #shell = shell.removeSplitter() # refine. Needed? 160 | return Part.makeSolid(shell) 161 | 162 | def make_face(edge1, edge2): 163 | v1, v2 = edge1.Vertexes 164 | v3, v4 = edge2.Vertexes 165 | e1 = Wire(edge1) 166 | e2 = LineSegment(v1.Point, v3.Point).toShape().Edges[0] 167 | e3 = edge2 168 | e4 = LineSegment(v4.Point, v2.Point).toShape().Edges[0] 169 | w = Wire([e3, e4, e1, e2]) 170 | return(Face(w)) 171 | -------------------------------------------------------------------------------- /freecad/frametools/bspline_tools.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import Part 3 | import FreeCADGui as gui 4 | try: 5 | from scipy.optimize import least_squares 6 | except: 7 | least_squares = None 8 | 9 | 10 | def curve_between_lines(p0, p1, v0, v1, n=None): 11 | if n is None: 12 | n = np.cross(v0, v1) 13 | n /= np.linalg.norm(n) 14 | def project_to_plane(p): 15 | return p - p.dot(n) * n 16 | tang0 = -v0 / np.linalg.norm(v0) 17 | tang1 = -v1 / np.linalg.norm(v1) 18 | knots = np.array([0., 1.]) 19 | mults = np.array([4, 4]) 20 | p0_proj, p1_proj, t0_proj, t1_proj = list(map(project_to_plane, [p0, p1, tang0, tang1])) 21 | s0, s1 = abs(np.linalg.lstsq(np.array([t0_proj, t1_proj]).T, p1_proj - p0_proj)[0]) 22 | print(s0, s1) 23 | w0 = 0.56265951 24 | t00 = 0.65863639 25 | n0 = np.cross(n, t0_proj) 26 | n1 = np.cross(n, t1_proj) 27 | sn0 , sn1 = np.linalg.lstsq(np.array([n0, n1]).T, p1_proj - p0_proj)[0] 28 | center = p0_proj + sn0 * n0 29 | def min_function(params): 30 | w0, t00 = params 31 | t0 = t00 * abs(s0) 32 | t1 = t00 * abs(s1) 33 | weights = np.array([1., w0, w0, 1.]) 34 | poles = np.array([p0_proj, p0_proj + t0 * t0_proj, 35 | p1_proj + t1 * t1_proj, p1_proj]) 36 | r = (np.linalg.norm(center - p0_proj) + np.linalg.norm(center - p1_proj)) / 2. 37 | bs = Part.BSplineCurve() 38 | bs.buildFromPolesMultsKnots(poles, mults, knots) 39 | for i, wi in enumerate(weights): 40 | bs.setWeight(i + 1, wi) 41 | dist = (np.array(bs.discretize(100)) - center) / r 42 | return sum((np.linalg.norm(dist, axis=1) - 1)**2) 43 | if not least_squares is None: 44 | w0, t00 = least_squares(min_function, [w0, t00]).x 45 | t0 = t00 * abs(s0) 46 | t1 = t00 * abs(s1) 47 | weights = np.array([1, w0, w0, 1]) 48 | poles = np.array([p0, p0 + t0 * tang0, p1 + t1 * tang1, p1]) 49 | bs = Part.BSplineCurve() 50 | bs.buildFromPolesMultsKnots(poles, mults, knots) 51 | for i, wi in enumerate(weights): 52 | bs.setWeight(i + 1, wi) 53 | return bs 54 | 55 | 56 | def make_nurbs_connection(): 57 | edge1 = gui.Selection.getSelectionEx()[0].SubObjects[0] 58 | edge2 = gui.Selection.getSelectionEx()[1].SubObjects[0] 59 | try: 60 | normal = gui.Selection.getSelectionEx()[2].SubObjects[0] 61 | except Exception: 62 | normal = None 63 | p11 = np.array(edge1.valueAt(edge1.FirstParameter)) 64 | p12 = np.array(edge1.valueAt(edge1.LastParameter)) 65 | p21 = np.array(edge2.valueAt(edge2.FirstParameter)) 66 | p22 = np.array(edge2.valueAt(edge2.LastParameter)) 67 | if normal is None: 68 | n = None 69 | else: 70 | n = np.array(normal.tangentAt(normal.FirstParameter)) 71 | l = [np.linalg.norm(i) for i in [p11 - p21, p11 - p22, p12 - p21, p12 - p22]] 72 | l_min = min(l) 73 | i = l.index(l_min) 74 | if i == 0: 75 | t1 = edge1.FirstParameter 76 | s1 = 1 77 | t2 = edge2.FirstParameter 78 | s2 = 1 79 | 80 | if i == 1: 81 | t1 = edge1.FirstParameter 82 | s1 = 1 83 | t2 = edge2.LastParameter 84 | s2 = -1 85 | 86 | if i == 2: 87 | t1 = edge1.LastParameter 88 | s1 = -1 89 | t2 = edge2.FirstParameter 90 | s2 = 1 91 | 92 | if i == 3: 93 | t1 = edge1.LastParameter 94 | s1 = -1 95 | t2 = edge2.LastParameter 96 | s2 = -1 97 | if edge1.Orientation != 'Forward': 98 | s1 *= -1 99 | if edge2.Orientation != 'Forward': 100 | s2 *= -1 101 | 102 | p1 = np.array(edge1.valueAt(t1)) 103 | p2 = np.array(edge2.valueAt(t2)) 104 | tang1 = np.array(edge1.tangentAt(t1)) 105 | tang2 = np.array(edge2.tangentAt(t2)) 106 | bs = curve_between_lines(p1, p2, s1 * tang1, s2 * tang2, n) 107 | Part.show(bs.toShape()) 108 | 109 | def least_square_normal(points): 110 | mat = np.array(p.tolist + [[1., 1., 1.]]) 111 | rhs = [0] * len(points) + [1] # add a condition that the sum of all n_i is not equal to zero 112 | normal = np.linalg.lstsq(mat, rhs) 113 | normal /= np.linalg.norm(normal) 114 | return normal 115 | 116 | import numpy as np 117 | import FreeCADGui as gui 118 | from scipy.optimize import minimize 119 | 120 | def least_square_circle(): 121 | """ 122 | finds the center and the radius (approximatly) of an edge 123 | """ 124 | edge = gui.Selection.getSelectionEx()[0].SubObjects[0] 125 | points = np.array(edge.discretize(100)) 126 | def min_function(center): 127 | """ 128 | minimizing the std deviation of the distance to the center 129 | """ 130 | dx = points - center 131 | ri = np.linalg.norm(dx, axis=1) 132 | return np.std(ri) 133 | start = np.mean(points, axis=0) 134 | center = minimize(min_function, start).x 135 | dx = points - center 136 | ri = np.linalg.norm(dx, axis=1) 137 | r_mean = np.mean(ri) 138 | r_std = np.std(ri) 139 | print("center = {} \nradius = {} \nstd = {}".format(center, r_mean, r_std)) 140 | 141 | def least_square_point(): 142 | import FreeCADGui as Gui 143 | from freecad import app 144 | lines = Gui.Selection.getSelection() 145 | one = np.eye(3) 146 | mat = [] 147 | rhs = [] 148 | for line in lines: 149 | start = np.array([[*line.Start]]).T 150 | end = np.array([[*line.End]]).T 151 | t = end - start 152 | t /= np.linalg.norm(t) 153 | A = t @ t.T 154 | t = t[0] 155 | r = start - A @ start 156 | m = one - A 157 | for i in [0, 1, 2]: 158 | mat.append(m[i]) 159 | rhs.append(r[i]) 160 | mat = np.array(mat) 161 | rhs = np.array(rhs) 162 | sol = np.linalg.lstsq(mat, rhs, rcond=None) 163 | Part.show(Part.Point(app.Vector(*sol[0])).toShape()) 164 | 165 | def proj_point2line(): 166 | import FreeCADGui as Gui 167 | from freecad import app 168 | point, line = Gui.Selection.getSelection() 169 | point = point.Shape.Vertexes[0] 170 | point = np.array([point.X, point.Y, point.Z]) 171 | start = np.array([*line.Start]) 172 | end = np.array([*line.End]) 173 | t = end - start 174 | t /= np.linalg.norm(t) 175 | diff = point - start 176 | s = diff @ t 177 | sol = start + s * t 178 | Part.show(Part.Point(app.Vector(*sol)).toShape()) 179 | 180 | 181 | 182 | def apply_transformation(): 183 | obj = gui.Selection.getSelection()[0] 184 | obj.Shape = obj.Shape.transformGeometry(obj.Placement.Matrix) 185 | obj.Placement = App.Matrix() -------------------------------------------------------------------------------- /freecad/frametools/make_beam.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | FrameAndBeamGui::CreateBeamParameters 4 | 5 | 6 | 7 | 0 8 | 0 9 | 271 10 | 604 11 | 12 | 13 | 14 | Create beam 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | Profile 23 | 24 | 25 | true 26 | 27 | 28 | 29 | 30 | 31 | 32 | true 33 | 34 | 35 | 36 | 37 | 38 | 39 | Path 40 | 41 | 42 | true 43 | 44 | 45 | 46 | 47 | 48 | 49 | true 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 0 58 | 0 59 | 60 | 61 | 62 | Extent 1: 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 0 71 | 0 72 | 73 | 74 | 75 | 76 | 0 77 | 5 78 | 79 | 80 | 81 | mm 82 | 83 | 84 | 0.000000000000000 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 0 93 | 0 94 | 95 | 96 | 97 | Extent 2: 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 0 106 | 0 107 | 108 | 109 | 110 | 111 | 0 112 | 5 113 | 114 | 115 | 116 | mm 117 | 118 | 119 | 0.000000000000000 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 0 128 | 0 129 | 130 | 131 | 132 | Profile rotation: 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 0 141 | 0 142 | 143 | 144 | 145 | 146 | 0 147 | 5 148 | 149 | 150 | 151 | deg 152 | 153 | 154 | 0.000000000000000 155 | 156 | 157 | 158 | 159 | 160 | 161 | Create 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FreeCAD Frame Workbench 2 | A frame-module for FreeCAD 3 | 4 | ## Requirements 5 | * FreeCAD >= v0.16 6 | * scipy v?? <---- 7 | 8 | ## Installation 9 | ### Automatic Installation 10 | As of v0.17 it's possible to use the built-in FreeCAD [Addon Manager](https://github.com/FreeCAD/FreeCAD-addons#1-builtin-addon-manager) 11 | to install this workbench. 12 | 13 | ### Manual Installation 14 | * `cd ~/.freecad/Mod || mkdir ~/.freecad/Mod && cd ~/.freecad/Mod` 15 | * `git clone https://github.com/looooo/freecad_frame.git` 16 | * restart FreeCAD 17 | 18 | ## Usage 19 | 20 | ### Create a Beam 21 | 22 | * Create new document 23 | * Create profile. Possible sources for profile: 24 | * Sketch 25 | * Draw a sketch. 26 | * Go to Part WB. 27 | * Select profile. 28 | * Create Face from profile. 29 | * "Profile" command in Arch WB allows to insert some predefined 30 | profiles like RHS. 31 | 32 | The profile should be in XY-plane (this is current limitation). 33 | * Create a path to make beam. The path can be: 34 | * Straight lines from any sketch. 35 | * Straight lines from Draw WB. 36 | 37 | Curves can't be used as path for beams. 38 | * Go to Frame and beams WB. 39 | * Select "Beam" command 40 | * Select profile. 41 | * Select path. 42 | * Enter extents if beams should be longer than path. 43 | * Enter rotation for profile to turn beam around its path. 44 | * Press "Create". 45 | * Select another path or profile to create more beams or press 46 | "Close" to finish command. 47 | 48 | ### Miter cut two beams 49 | 50 | * Select "Miter Cut" command from Frame and beam WB 51 | * Select first beam. 52 | * Select second beam which intesects the first beam. 53 | * Press "Cut". 54 | * To cut another beams select them and press "Cut" or press "Close" to 55 | finish command. 56 | 57 | If one or two beams are selected when "Miter Cut" command activated 58 | they will be preselected as "Beam 1" and "Beam 2". To cut them press 59 | "Cut" button. 60 | 61 | ### Plane Cut 62 | 63 | * Select "Plane Cut" command from Frame and beam WB 64 | * Select first beam. 65 | * Select face of another beam which intersects the first beam. 66 | * Press "Cut". 67 | * To cut another beams select them and press "Cut" or press "Close" to 68 | finish command. 69 | 70 | If beam and beam face (in that order) are selected when "Plane Cut" 71 | command activated they will be preselected as "Beam to cut" and "Cut 72 | with". To cut them press "Cut" button. 73 | 74 | ### Shape Cut 75 | 76 | * Select "Shape Cut" command from Frame and beam WB 77 | * Select first beam. 78 | * Select second beam which intesects the first beam. 79 | * Press "Cut". 80 | * To cut another beams select them and press "Cut" or press "Close" to 81 | finish command. 82 | 83 | If one or two beams are selected when "Shape Cut" command activated 84 | they will be preselected as "Beam 1" and "Beam 2". To cut them press 85 | "Cut" button. 86 | 87 | ## Example 88 | 89 | Gate leaf creation. 90 | 91 | 92 | 93 | Prerequisites: 94 | 95 | * Made from hollow profile with rounded corners with size 40x20mm and thickness 2mm. 96 | * Internal height between top and bottom bars is 1500mm. 97 | * Horizontal size 1700mm. 98 | * Middle bars added to have decorations inside between top and middle 99 | bar and bottom and middle bar (not shown due to lack of 100 | object). Height of decorations is 155mm. 101 | * Diagonal supports added. 102 | 103 | 1. Create profile. 104 | 105 | 106 | 107 | 1. Create a new sketch in XY plane. 108 | 2. Repeat drawing from picture above. Outer rounding radius is 3mm 109 | and inner rounding radius is 2mm. 110 | 3. Close sketch. 111 | 4. Select Part Workbench and convert sketch to face ("Make face from 112 | wires" command). 113 | 114 | 2. Create sketch for beam axes. Since this is gate leaf the sketch 115 | should be created, for example, in XZ plane. 116 | 117 | 118 | 119 | This will require a little bit more work because some measurements 120 | give outer sizes, some give inner and no one give axes. The 121 | diagonal supports are drawn according to other beams. To achieve 122 | correct lines construction geometry should be used for everything 123 | that is not beam in final design like outer edge of gate. 124 | 125 | 3. Create outer beams. Since they should fully intersect the "Extent 126 | 1" and "Extent 2" should have value 20mm at least. Please don't 127 | enter very big values because some extra parts can be left after 128 | cutting. 129 | 130 | 131 | 132 | 4. Create inner beams. They axes long enough to get full intersection 133 | with other beams so the extents can be left as is. 134 | 135 | 136 | 137 | 5. Miter cut outer beams. 138 | 139 | 140 | 141 | Please note that *every* cut operation creates a new beam so if 142 | beam needs to be cut twice it should be reselected after the 143 | first cut. Removing cut beam can break other cut beams if it is 144 | used in the other beam cut operation. 145 | 146 | 6. Plane cut inner beams by outer beams inner planes. Plane cut 147 | diagonals. 148 | 149 | 150 | 151 | 7. Select TechDraw Workbench 152 | 153 | 154 | 155 | 1. Insert page from template and select A4 Portrait template. 156 | 2. Go to the 3D view and select "Front View" to see front side. The 157 | TechDraw inserts view in current active projection. 158 | 3. Select Main Diagonal. 159 | 4. Go to the inserted page and press "Insert view". The page will 160 | contain huge page placed as viewed. 161 | 5. Select view in tree and set scale to 0.1. This makes beam 162 | completely inside drawing. 163 | 6. The TechDraw don't allow to simple rotations like "make this 164 | line vertical" so view should be rotated by -55.582° 165 | (measured as angle between vertical border and angled one). This 166 | make beam straight vertical on drawing. 167 | 7. Add dimensions to overall length and how to cut corners. 168 | 169 | Repeat steps above for every unique beam. Fill fields below as 170 | needed. This gives the set of drawing suitable to create all parts 171 | of gate leaf. 172 | 173 | To have drawing with guide to assemble it full view (with all part 174 | selected) should be inserted on a new page. Welding annotations 175 | will describe how to join parts. 176 | 177 | ## Feedback 178 | Offer feedback and discuss this workbench in the [dedicated FreeCAD forum thread]() 179 | 180 | ## Known Limitations 181 | 182 | ## Developer 183 | [@looooo](https://github.com/looooo) 184 | 185 | ## License 186 | GNU Lesser General Public License v2.1 187 | -------------------------------------------------------------------------------- /freecad/frametools/fem2d.py: -------------------------------------------------------------------------------- 1 | from PySide import QtGui, QtCore 2 | import FreeCADGui as Gui 3 | import FreeCAD as App 4 | import numpy as np 5 | from scipy import sparse 6 | from scipy.sparse import linalg 7 | from freecad.frametools import ICON_PATH 8 | import os 9 | import yaml 10 | 11 | class PropertyEditor(object): 12 | widget_name = 'properties editor' 13 | 14 | def __init__(self, obj): 15 | self.obj = obj 16 | self.form = [] 17 | self.base_widget = QtGui.QWidget() 18 | self.form.append(self.base_widget) 19 | self.base_widget.setWindowTitle(self.widget_name) 20 | self.base_widget.setLayout(QtGui.QVBoxLayout()) 21 | self.plain_text = QtGui.QPlainTextEdit() 22 | self.plain_text.setTabStopWidth(30) 23 | self.plain_text.document().setPlainText(yaml.dump(self.obj.parameter_dict)) 24 | self.addRefButton = QtGui.QPushButton("add entities") 25 | self.addRefButton.clicked.connect(self.add_entities) 26 | self.base_widget.layout().addWidget(self.addRefButton) 27 | self.base_widget.layout().addWidget(self.plain_text) 28 | 29 | def accept(self): 30 | try: 31 | self.obj.parameter_dict = yaml.load(self.plain_text.document().toPlainText()) 32 | except Exception as e: 33 | print(e) 34 | return 35 | else: 36 | Gui.Control.closeDialog() 37 | 38 | def reject(self): 39 | Gui.Control.closeDialog() 40 | 41 | def add_entities(self): 42 | sel = Gui.Selection.getSelectionEx()[0] 43 | for i, name in enumerate(sel.SubElementNames): 44 | if i != 0: 45 | self.plain_text.insertPlainText(", ") 46 | self.plain_text.insertPlainText(name) 47 | 48 | class ViewProviderGenericSolver(object): 49 | def __init__(self, obj): 50 | obj.Proxy = self 51 | self.obj = obj.Object 52 | 53 | def setupContextMenu(self, obj, menu): 54 | action = menu.addAction("edit parameters") 55 | action.triggered.connect(self.edit_dict) 56 | action = menu.addAction("solve") 57 | action.triggered.connect(self.solve) 58 | action = menu.addAction("show_result") 59 | action.triggered.connect(self.show_result) 60 | action = menu.addAction("edge_min_value") 61 | action.triggered.connect(self.edge_min_value) 62 | 63 | def edit_dict(self): 64 | Gui.Control.showDialog(PropertyEditor(self.obj)) 65 | 66 | def solve(self): 67 | self.obj.Proxy.solve() 68 | 69 | def getIcon(self): 70 | return os.path.join(ICON_PATH, "generic_solver.svg") 71 | 72 | def show_result(self): 73 | tris = [] 74 | for domain in self.obj.Proxy.lambda_domains: 75 | tris += np.array(self.obj.Proxy.n2elements[domain]).tolist() 76 | 77 | from freecad.plot import Plot 78 | from matplotlib import pyplot as plt 79 | import matplotlib.cm as cm 80 | fig = Plot.figure() 81 | fig.fig.add_axes() 82 | ax = fig.fig.axes[0] 83 | ax.tricontourf(self.obj.Proxy.nodes[:, 0], 84 | self.obj.Proxy.nodes[:, 1], 85 | tris, self.obj.Proxy.t, levels=100, cmap=cm.coolwarm) 86 | ax.grid() 87 | ax.set_aspect('equal') 88 | # fig.fig.colorbar() 89 | fig.show() 90 | 91 | def edge_min_value(self): 92 | message_box = QtGui.QMessageBox() 93 | message_box.setText(self.obj.Proxy.edge_min_value()) 94 | message_box.exec_() 95 | 96 | def attach(self, view_obj): 97 | self.view_obj = view_obj 98 | self.obj = view_obj.Object 99 | 100 | def __getstate__(self): 101 | pass 102 | 103 | def __setstate__(self, state): 104 | pass 105 | 106 | class GenericSolver(): 107 | def __init__(self, obj, geo_obj, mesh_obj): 108 | '''A generic solver class for FEM-analysis''' 109 | obj.Proxy = self 110 | obj.addProperty("App::PropertyLink","geo_obj","Link","A shape object").geo_obj = geo_obj 111 | obj.addProperty("App::PropertyLink","mesh_obj","Link","A mesh object").mesh_obj = mesh_obj 112 | obj.addProperty('App::PropertyPythonObject', 'parameter_dict', 'parameters', 'parameters as dict') 113 | self.obj = obj 114 | self.init_parameters() 115 | 116 | @property 117 | def n2e(self): 118 | return self.obj.parameter_dict["entities"] 119 | 120 | @property 121 | def n2tnv(self): 122 | return self.obj.parameter_dict["properties"] 123 | 124 | def init_parameters(self): 125 | self.obj.parameter_dict = {"entities": {"name": ["entity1", "entity2"]}, 126 | "properties": {"name": {"type1": "value1", "type2": "value2"}}} 127 | 128 | def solve(self): 129 | self.lambda_domains = [name for name, prop in self.n2tnv.items() if "lambda" in prop] 130 | dirichlet_domains = [name for name, prop in self.n2tnv.items() if ("T" in prop and not "R" in prop)] 131 | neumann_domains = [name for name, prop in self.n2tnv.items() if "q" in prop] 132 | mixed_domains = [name for name, prop in self.n2tnv.items() if ("T" in prop and "R" in prop)] 133 | self.n2elements = {} 134 | for name, entities in self.n2e.items(): 135 | elements = [] 136 | for entity in entities: 137 | if "Face" in entity: 138 | elements += (self.obj.mesh_obj.FemMesh.getFacesByFace( 139 | self.obj.geo_obj.Shape.getElement(entity))) 140 | if "Edge" in entity: 141 | elements += (self.obj.mesh_obj.FemMesh.getEdgesByEdge( 142 | self.obj.geo_obj.Shape.getElement(entity))) 143 | self.n2elements[name] = [np.array(self.obj.mesh_obj.FemMesh.getElementNodes(el_index), dtype=int) - 1 for el_index in elements] 144 | self.nodes = np.array(list(map(list, self.obj.mesh_obj.FemMesh.Nodes.values()))) 145 | self.nodes = self.nodes[:,:-1] # delete z-values 146 | 147 | ## creating the matrix of the diffusion-matrix 148 | mat_entries = [] 149 | for name in self.lambda_domains: 150 | lam = self.n2tnv[name]["lambda"] 151 | for element in self.n2elements[name]: 152 | mat_entries += self.diffuseTerm(element, lam) 153 | 154 | neumann_entries = [] 155 | for name in neumann_domains: 156 | q = self.n2tnv[name]["q"] 157 | for element in self.n2elements[name]: 158 | neumann_entries += self.neumannTerm(element, q) 159 | 160 | for name in mixed_domains: 161 | T = self.n2tnv[name]["T"] 162 | R = self.n2tnv[name]["R"] 163 | for element in self.n2elements[name]: 164 | neumann_entries += self.neumannTerm(element, T / R) 165 | mat_entries += self.unknown_neumannTerm(element, 1. / R) 166 | 167 | dirichlet_indices = [] 168 | dirichlet_data = [] 169 | for name in dirichlet_domains: 170 | T = self.n2tnv[name]["T"] 171 | nodes = [] 172 | for elements in self.n2elements[name]: 173 | nodes += elements.tolist() 174 | nodes = list(set(nodes)) # removing duplicated nodes 175 | dirichlet_indices += nodes 176 | dirichlet_data += [T] * len(nodes) 177 | 178 | # assemblying: 179 | row_indices, col_indices, values = np.array(mat_entries).T 180 | row_indices = np.int64(row_indices) 181 | col_indices = np.int64(col_indices) 182 | mat = sparse.csr_matrix((values, (row_indices, col_indices))) 183 | rhs = sparse.csr_matrix((len(self.nodes), 1)) 184 | 185 | # now we have to delete all matrix entries which are on the dirichlet boundary 186 | # a diagonal matrix to delete all the entries on bc_d data 187 | 188 | if dirichlet_data and dirichlet_indices: 189 | d_mod_indices = list(set(dirichlet_indices)) 190 | bc_d_mat_1 = sparse.csr_matrix(([1] * len(d_mod_indices), (d_mod_indices, d_mod_indices)), 191 | shape=(len(self.nodes), len(self.nodes))) 192 | bc_d_mat_2 = sparse.csr_matrix(([1] * len(dirichlet_indices), (dirichlet_indices, dirichlet_indices)), 193 | shape=(len(self.nodes), len(self.nodes))) 194 | dia_mat = sparse.dia_matrix(([1] * len(self.nodes), 0), 195 | shape=(len(self.nodes), len(self.nodes))) 196 | mat = (dia_mat - bc_d_mat_1) @ mat + bc_d_mat_2 197 | 198 | rhs += sparse.csr_matrix((dirichlet_data, (dirichlet_indices, [0] * len(dirichlet_indices))), 199 | shape=(len(self.nodes), 1)) 200 | if neumann_entries: 201 | neumann_indices, neumann_data = np.array(neumann_entries).T 202 | neumann_indices = np.int64(neumann_indices) 203 | rhs += sparse.csr_matrix((neumann_data, (neumann_indices, [0] * len(neumann_indices))), 204 | shape=(len(self.nodes), 1)) 205 | self.t = linalg.spsolve(mat, rhs) 206 | 207 | def diffuseTerm(self, element, lam): 208 | BTB = self.get_BTB(element) 209 | values = lam * BTB 210 | row_indices = self.get_row_indices(element) 211 | col_indices = row_indices.T 212 | return np.array([row_indices.flatten(), col_indices.flatten(), values.flatten()]).T.tolist() 213 | 214 | def neumannTerm(self, element, q): 215 | HTH = self.get_HTH(element) 216 | values = HTH @ np.array([[q], [q]]).flatten() 217 | return np.array([element, values]).T.tolist() 218 | 219 | def unknown_neumannTerm(self, element, q): 220 | HTH = self.get_HTH(element) * q 221 | row_indices = self.get_row_indices(element) 222 | col_indices = row_indices.T 223 | return np.array([row_indices.flatten(), col_indices.flatten(), HTH.flatten()]).T.tolist() 224 | 225 | def get_HTH(self, element): 226 | x1, x2 = self.nodes[element] 227 | l = np.linalg.norm(x2 - x1) 228 | return l * np.array([ 229 | [1. / 3., 1. / 6.], 230 | [1. / 6., 1. / 3.]], dtype=float) 231 | 232 | def get_BTB(self, element): 233 | x1, y1, x2, y2, x3, y3 = self.nodes[element].flatten() 234 | y12 = y2 - y1; y23 = y3 - y2; y31 = y1 - y3 235 | x12 = x2 - x1; x23 = x3 - x2; x31 = x1 - x3 236 | area = 0.5 * abs(x12 * y31 - x31 * y12) 237 | B = np.array([[-y23, -y31, -y12], 238 | [ x23, x31, x12]]) / (2 * area) 239 | return area * B.T @ B 240 | 241 | def get_row_indices(self, element): 242 | return np.array([element] * len(element)) 243 | 244 | def onDocumentRestored(self, obj): 245 | self.obj = obj 246 | 247 | def __getstate__(self): 248 | return None 249 | 250 | def __setstate__(self, state): 251 | return None 252 | 253 | def edge_min_value(self): 254 | output = "" 255 | for name, entities in self.n2e.items(): 256 | nodes = [] 257 | for entity in entities: 258 | if "Edge" in entity: 259 | nodes += list(self.obj.mesh_obj.FemMesh.getNodesByEdge( 260 | self.obj.geo_obj.Shape.getElement(entity))) 261 | nodes = list(set(nodes)) 262 | if nodes: 263 | output += "min value of " + name + "= {0:.3f} °C".format(min(self.t[nodes])) 264 | output += "\n" 265 | return output 266 | 267 | def make_GenericSolver(geo_obj, mesh_obj): 268 | obj = App.ActiveDocument.addObject("App::FeaturePython", "generic_solver") 269 | GenericSolver(obj, geo_obj, mesh_obj) 270 | ViewProviderGenericSolver(obj.ViewObject) 271 | App.ActiveDocument.recompute() 272 | return obj 273 | -------------------------------------------------------------------------------- /freecad/frametools/icons/nurbs_connect.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 25 | 29 | 33 | 37 | 41 | 44 | 48 | 52 | 53 | 60 | 65 | 66 | 73 | 78 | 79 | 86 | 91 | 92 | 99 | 104 | 105 | 112 | 117 | 118 | 125 | 130 | 131 | 138 | 143 | 144 | 151 | 156 | 157 | 164 | 169 | 170 | 177 | 180 | 184 | 188 | 192 | 196 | 200 | 204 | 205 | 206 | 213 | 218 | 219 | 226 | 231 | 232 | 239 | 245 | 246 | 253 | 259 | 260 | 269 | 270 | 292 | 297 | 302 | 307 | 312 | 317 | 322 | 323 | 325 | 326 | 328 | image/svg+xml 329 | 331 | 332 | 333 | 334 | 335 | 339 | 344 | 350 | 356 | 357 | 358 | -------------------------------------------------------------------------------- /freecad/frametools/icons/beam.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 18 | 38 | 40 | 46 | 51 | 52 | 58 | 63 | 64 | 70 | 75 | 76 | 82 | 85 | 89 | 93 | 97 | 101 | 105 | 109 | 110 | 111 | 117 | 122 | 123 | 129 | 134 | 135 | 136 | 138 | 139 | 141 | image/svg+xml 142 | 144 | 145 | 146 | 147 | 148 | 152 | 156 | 160 | 164 | 168 | 172 | 176 | 180 | 184 | 188 | 192 | 196 | 200 | 204 | 208 | 212 | 216 | 220 | 224 | 228 | 232 | 236 | 240 | 244 | 245 | -------------------------------------------------------------------------------- /freecad/frametools/beamobj.py: -------------------------------------------------------------------------------- 1 | from __future__ import division 2 | import copy 3 | 4 | import os 5 | import numpy as np 6 | 7 | import FreeCAD as App 8 | import Part 9 | 10 | from freecad.frametools import ICON_PATH 11 | 12 | __all__ = [ 13 | "Beam", 14 | "CBeam", 15 | "ViewProviderBeam", 16 | "ViewProviderCBeam"] 17 | 18 | 19 | class Beam(): 20 | def __init__(self, obj, profile, path, path_name): 21 | '''Beam: representing a straight extrusion of a profile''' 22 | print("was soll das") 23 | obj.addProperty("App::PropertyString", "type", "Beam", "type of the object").type = "beam" 24 | obj.addProperty("App::PropertyLink","profile","Beam","sketch profile of the beam").profile = profile 25 | obj.addProperty("App::PropertyLink","path","Beam","path of the beam").path = path 26 | obj.addProperty("App::PropertyString","path_name","Beam", "name of beam line").path_name = path_name 27 | obj.addProperty("App::PropertyDistance", "exdent_1", "Beam", "exdent side 1").exdent_1 = "0 mm" 28 | obj.addProperty("App::PropertyDistance", "exdent_2", "Beam", "exdent side 2").exdent_2 = "0 mm" 29 | obj.addProperty("App::PropertyAngle", "Rotation", "Beam", "rotation of the profile").Rotation = 0 30 | obj.setEditorMode("path_name", 1) 31 | obj.Proxy = self 32 | self.Object = obj 33 | 34 | def initialize(self): 35 | print("initialize ...") 36 | 37 | @property 38 | def profile(self): 39 | if isinstance(self.Object.profile.Shape, Part.Face): 40 | # create a copy of the face 41 | return Part.Face(self.Object.profile.Shape) 42 | wires_copy = wires = copy.copy(self.Object.profile.Shape.Wires) 43 | # check boundingbox diagonals to get outer shape 44 | if len(wires) > 1: 45 | diagonals = [wire.BoundBox.DiagonalLength for wire in wires] 46 | pos = diagonals.index(max(diagonals)) 47 | # reaordering 48 | external_wire = wires[pos] 49 | wires.pop(pos) 50 | wires.insert(0, external_wire) 51 | # check face oriendation 52 | orientation = wires[0].Orientation 53 | normal = Part.Face(wires[0]).normalAt(0, 0) 54 | wires_copy = [wires[0]] 55 | for wire in wires[1:]: 56 | fi = Part.Face(wire) 57 | if fi.normalAt(0, 0).dot(normal) > 0: 58 | fi.reverse() 59 | wires_copy.append(fi.Wires[0]) 60 | face = Part.Face(wires_copy) 61 | return face 62 | 63 | @property 64 | def outer_shape(self): 65 | path, a, b, n = self.path_a_b_n 66 | profile = Part.Face(self.profile.Wires[0]) 67 | new_profile = self.transform_to_n(profile, a - n * self.Object.exdent_1.Value, n) 68 | return new_profile.extrude(App.Vector(b - a) + n * (self.Object.exdent_1.Value + self.Object.exdent_2.Value)) 69 | 70 | def attach(self, fp): 71 | print("hi") 72 | fp.Proxy.Object = fp 73 | 74 | def execute(self, fp): 75 | '''Do something when doing a recomputation, this method is mandatory''' 76 | fp.Proxy.Object = fp 77 | path, a, b, n = self.path_a_b_n 78 | new_profile = self.transform_to_n(self.profile, a - n * fp.exdent_1.Value, n) 79 | fp.Shape = new_profile.extrude(App.Vector(b - a) + n * (fp.exdent_1.Value + fp.exdent_2.Value)) 80 | 81 | def transform_to_n(self, profile, p, n): 82 | '''transform a profile (Shape): 83 | rotating from t to n 84 | and translating from 0 to p''' 85 | 86 | 87 | n.normalize() 88 | t = sketch_normal(n, self.Object.path) 89 | t.normalize() 90 | v = n.cross(t) 91 | v.normalize() 92 | 93 | rot_mat = App.Matrix() 94 | rot_mat.A11 = t.x 95 | rot_mat.A21 = t.y 96 | rot_mat.A31 = t.z 97 | rot_mat.A12 = v.x 98 | rot_mat.A22 = v.y 99 | rot_mat.A32 = v.z 100 | rot_mat.A13 = n.x 101 | rot_mat.A23 = n.y 102 | rot_mat.A33 = n.z 103 | rot_mat.A14 = p.x 104 | rot_mat.A24 = p.y 105 | rot_mat.A34 = p.z 106 | if hasattr(self.Object.profile, "Sources"): 107 | translation = self.Object.profile.Sources[0].Placement.Base 108 | #translation += self.Object.profile.Placement.Base 109 | else: 110 | translation = self.Object.profile.Placement.Base 111 | print(self.Object.profile.Placement) 112 | print(profile.Placement) 113 | print(profile.Placement) 114 | rot_mat_2 = profile.Placement.toMatrix().inverse() 115 | profile.Placement.Base -= translation 116 | rot_mat_1 = App.Matrix() 117 | rot_mat_1.rotateZ(np.deg2rad(self.Object.Rotation.Value)) 118 | 119 | placement = rot_mat.multiply(rot_mat_1.multiply(rot_mat_2)) 120 | profile.Placement = App.Placement(placement).multiply(profile.Placement) 121 | 122 | return profile 123 | 124 | # @property 125 | # def midpoint(self): 126 | # profile_midpoint = filter(lambda x: isinstance(x, App.Base.Vector), self.Object.profile.Geometry) 127 | # if len(profile_midpoint) == 0 or profile_midpoint is None: 128 | # profile_midpoint = App.Vector(0, 0, 0) 129 | # else: 130 | # profile_midpoint = profile_midpoint[0] 131 | # return profile_midpoint 132 | 133 | @property 134 | def path_a_b_n(self): 135 | path = self.Object.path.Shape.getElement(self.Object.path_name) 136 | a, b = path.Vertexes 137 | n = b.Point - a.Point 138 | n.normalize() 139 | return (path, a.Point, b.Point, n) 140 | 141 | @property 142 | def profile_bound_box(self): 143 | fac = 0.3 144 | bb = self.Object.profile.Shape.BoundBox 145 | xmax = bb.XMax + bb.XLength * fac 146 | xmin = bb.XMin - bb.XLength * fac 147 | ymax = bb.YMax + bb.YLength * fac 148 | ymin = bb.YMin - bb.YLength * fac 149 | pol = Part.makePolygon( 150 | [App.Vector(xmax, ymax, 0), 151 | App.Vector(xmax, ymin, 0), 152 | App.Vector(xmin, ymin, 0), 153 | App.Vector(xmin, ymax, 0), 154 | App.Vector(xmax, ymax, 0)]) 155 | path, a, b, n = self.path_a_b_n 156 | return self.transform_to_n(pol, a, n) 157 | 158 | def project_profile_bound_box(self, plane_n, plane_p): 159 | bb = self.profile_bound_box.Vertexes 160 | vectors = [v.Point for v in bb] 161 | path, a, b, n = self.path_a_b_n 162 | projected = [project_to_plane(plane_p, plane_n, v, n) for v in vectors] 163 | projected.append(projected[0]) 164 | pol = Part.makePolygon(projected) 165 | return pol 166 | 167 | def __getstate__(self): 168 | return None 169 | 170 | def __setstate__(self, state): 171 | return None 172 | 173 | def attach(self): 174 | pass 175 | 176 | 177 | class ViewProviderBeam: 178 | def __init__(self, obj): 179 | ''' Set this object to the proxy object of the actual view provider ''' 180 | obj.Proxy = self 181 | 182 | def attach(self, vobj): 183 | self.vobj = vobj 184 | 185 | def getIcon(self): 186 | _dir = os.path.dirname(os.path.realpath(__file__)) 187 | return(_dir + "/" + "icons/beam.svg") 188 | 189 | def __getstate__(self): 190 | return None 191 | 192 | def __setstate__(self, state): 193 | return None 194 | 195 | 196 | class CBeam(object): 197 | def __init__(self, obj, beam): 198 | "'''Beam: representing a straight extrusion of a profile'''" 199 | obj.addProperty("App::PropertyString", "type", "Beam", "type of the object").type = "c_beam" 200 | obj.addProperty("App::PropertyLink","this_beam","Beam","sketch profile of the beam").this_beam = beam 201 | obj.addProperty("App::PropertyLink","cut_obj","Cut","link to cut obj") 202 | obj.addProperty("App::PropertyString","cut_type","Cut","type of cut obj") 203 | obj.addProperty("App::PropertyString","cut_obj_name","Cut","type of cut obj") 204 | obj.addProperty("App::PropertyBool", "cut", "Beam", "show the cut").cut = True 205 | self.Object = obj 206 | obj.Proxy = self 207 | 208 | def attach(self, fp): 209 | self.Object = fp 210 | 211 | def execute(self, fp): 212 | fp.Proxy.Object = fp 213 | if self.Object.cut: 214 | if fp.cut_type == "miter": 215 | fp.Shape = self.miter_cut(self.Object.cut_obj) 216 | elif fp.cut_type == "cut": 217 | face = self.Object.cut_obj.Shape.getElement(self.Object.cut_obj_name) 218 | fp.Shape = self.face_cut(self.Object.cut_obj, face) 219 | elif fp.cut_type == "shape_cut": 220 | fp.Shape = self.shape_cut(self.Object.cut_obj) 221 | else: 222 | fp.Shape = fp.this_beam.Shape 223 | 224 | def find_top_beam(self, beam): 225 | if "path_a_b_n" in dir(beam.Proxy): 226 | beam.Proxy.Object = beam 227 | return beam 228 | else: 229 | return self.find_top_beam(beam.this_beam) 230 | 231 | # def find_path_a_b_n(self, beam): 232 | # if not hasattr(beam.Proxy, "path_a_b_n"): 233 | # return beam.this_beam.Proxy.path_a_b_n 234 | # else: 235 | # return beam.Proxy.path_a_b_n 236 | 237 | # def find_project_profile_bound_box(self, beam): 238 | # if not hasattr(beam.Proxy, "project_profile_bound_box"): 239 | # return beam.this_beam.Proxy.project_profile_bound_box 240 | # else: 241 | # return beam.Proxy.project_profile_bound_box 242 | 243 | def miter_cut(self, beam): 244 | # get nearest point of the two lines 245 | top_beam_1 = self.find_top_beam(self.Object.this_beam) 246 | top_beam_2 = self.find_top_beam(beam) 247 | 248 | path, p_1_a, p_1_b, n1 = top_beam_1.Proxy.path_a_b_n 249 | path, p_2_a, p_2_b, n2 = top_beam_2.Proxy.path_a_b_n 250 | arr = [[p_1_a, p_2_a], 251 | [p_1_a, p_2_b], 252 | [p_1_b, p_2_a], 253 | [p_1_b, p_2_b]] 254 | 255 | l_arr = list(map(norm, arr)) 256 | min_val = min(l_arr) 257 | min_item = l_arr.index(min_val) 258 | 259 | if min_item in [0, 1]: 260 | n1 *= -1 261 | if min_item in [0, 2]: 262 | n2 *= -1 263 | 264 | bp1, bp2 = arr[min_item] 265 | 266 | # calculating the intersection-point p and face-normal n 267 | t = n1.cross(n2) 268 | np_n1, np_n2, np_t, np_bp1, np_bp2 = list(map(to_np, [n1, n2, t, bp1, bp2])) 269 | np_mat = np.array([np_n1, -np_n2, t]) 270 | np_mat = np_mat.transpose() 271 | np_rhs = np_bp2 - np_bp1 272 | l1, l2, l3 = np.linalg.solve(np_mat, np_rhs) 273 | n = n1 - n2 274 | n.normalize() 275 | p = bp1 + n1 * l1 + t * l3 * 0.5 276 | 277 | pol = top_beam_1.Proxy.project_profile_bound_box(n, p) 278 | face = Part.Face(pol.Wires) 279 | solid = face.extrude(n1 * 100) 280 | return self.Object.this_beam.Shape.cut(solid) 281 | 282 | def face_cut(self, beam, face): 283 | top_beam_1 = self.find_top_beam(self.Object.this_beam) 284 | top_beam_2 = self.find_top_beam(beam) 285 | path, p_1_a, p_1_b, n1 = top_beam_1.Proxy.path_a_b_n 286 | path, p_2_a, p_2_b, n2 = top_beam_2.Proxy.path_a_b_n 287 | arr = [p_1_a, p_1_b] 288 | n = face.normalAt(0, 0) 289 | n.normalize() 290 | 291 | n_dist = lambda p: abs((p - p_2_a).dot(n)) 292 | l_arr = list(map(n_dist, arr)) 293 | min_val = min(l_arr) 294 | min_item = l_arr.index(min_val) 295 | 296 | if min_item in [0]: 297 | n1 *= -1 298 | 299 | # get the face normal and center point 300 | 301 | p = face.CenterOfMass 302 | pol = top_beam_1.Proxy.project_profile_bound_box(n, p) 303 | face = Part.Face(pol.Wires) 304 | solid = face.extrude(n1 * 100) 305 | return self.Object.this_beam.Shape.cut(solid) 306 | 307 | def shape_cut(self, beam): 308 | top_beam = self.find_top_beam(beam) 309 | return self.Object.this_beam.Shape.cut(top_beam.Proxy.outer_shape) 310 | 311 | def __getstate__(self): 312 | return None 313 | 314 | def __setstate__(self, state): 315 | return None 316 | 317 | 318 | class ViewProviderCBeam(ViewProviderBeam): 319 | def __init__(self, obj): 320 | ''' Set this object to the proxy object of the actual view provider ''' 321 | obj.Proxy = self 322 | self.vobj = obj 323 | 324 | def claimChildren(self): 325 | self.vobj.Object.this_beam.ViewObject.Visibility=False 326 | return [self.vobj.Object.this_beam] 327 | 328 | def getIcon(self): 329 | _dir = os.path.dirname(os.path.realpath(__file__)) 330 | return(_dir + "/" + "icons/beam.svg") 331 | 332 | 333 | def norm(p): 334 | return (p[0] - p[1]).Length 335 | 336 | 337 | def sketch_normal(n, normal_info=None): 338 | if normal_info.TypeId == 'Sketcher::SketchObject': 339 | b = App.Vector(0, 0, 1) 340 | return normal_info.Placement.Rotation.multVec(b) 341 | else: 342 | # this is a draft element or something else: 343 | # no information for the normals of the path 344 | if abs(n.x) <= abs(n.y): 345 | if abs(n.x) <= abs(n.z): 346 | a = App.Vector(1, 0, 0) 347 | else: 348 | a = App.Vector(0, 0, 1) 349 | else: 350 | if abs(n.y) <= abs(n.z): 351 | a = App.Vector(0, 1, 0) 352 | else: 353 | a = App.Vector(0, 0, 1) 354 | print(n) 355 | print(a) 356 | b = n.cross(a).normalize() 357 | return b 358 | 359 | 360 | def project_to_plane(plane_p, plane_n, p, n): 361 | '''p + l * n = plane_p + x * plane_n''' 362 | return p + n * (plane_n.dot(plane_p - p) / (n.dot(plane_n))) 363 | 364 | 365 | def to_np(app_vec): 366 | return np.array([app_vec.x, app_vec.y, app_vec.z]) 367 | 368 | -------------------------------------------------------------------------------- /freecad/frametools/icons/beam_plane_cut.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 18 | 38 | 40 | 46 | 51 | 52 | 58 | 63 | 64 | 70 | 75 | 76 | 82 | 85 | 89 | 93 | 97 | 101 | 105 | 109 | 110 | 111 | 117 | 122 | 123 | 129 | 134 | 135 | 136 | 138 | 139 | 141 | image/svg+xml 142 | 144 | 145 | 146 | 147 | 148 | 152 | 156 | 161 | 166 | 171 | 176 | 181 | 186 | 191 | 196 | 201 | 205 | 209 | 213 | 217 | 221 | 226 | 230 | 234 | 238 | 242 | 246 | 250 | 255 | 256 | -------------------------------------------------------------------------------- /freecad/frametools/icons/linked_face.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 18 | 38 | 40 | 46 | 51 | 52 | 58 | 63 | 64 | 70 | 75 | 76 | 82 | 85 | 89 | 93 | 97 | 101 | 105 | 109 | 110 | 111 | 117 | 122 | 123 | 129 | 134 | 135 | 136 | 138 | 139 | 141 | image/svg+xml 142 | 144 | 145 | 146 | 147 | 148 | 152 | 156 | 161 | 166 | 171 | 175 | 179 | 183 | 187 | 191 | 195 | 199 | 203 | 207 | 211 | 215 | 220 | 224 | 228 | 232 | 236 | 240 | 244 | 248 | 254 | 255 | -------------------------------------------------------------------------------- /freecad/frametools/icons/reload.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 25 | 29 | 33 | 37 | 41 | 44 | 48 | 52 | 53 | 60 | 65 | 66 | 73 | 78 | 79 | 86 | 91 | 92 | 99 | 104 | 105 | 112 | 117 | 118 | 125 | 130 | 131 | 138 | 143 | 144 | 151 | 156 | 157 | 164 | 169 | 170 | 177 | 180 | 184 | 188 | 192 | 196 | 200 | 204 | 205 | 206 | 213 | 218 | 219 | 226 | 231 | 232 | 239 | 245 | 246 | 253 | 259 | 260 | 269 | 270 | 292 | 296 | 300 | 304 | 308 | 312 | 316 | 317 | 319 | 320 | 322 | image/svg+xml 323 | 325 | 326 | 327 | 328 | 329 | 333 | 345 | 351 | 363 | 369 | 370 | 371 | -------------------------------------------------------------------------------- /freecad/frametools/icons/beam_miter_cut.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 18 | 38 | 40 | 46 | 51 | 52 | 58 | 63 | 64 | 70 | 75 | 76 | 82 | 85 | 89 | 93 | 97 | 101 | 105 | 109 | 110 | 111 | 117 | 122 | 123 | 129 | 134 | 135 | 136 | 138 | 139 | 141 | image/svg+xml 142 | 144 | 145 | 146 | 147 | 148 | 153 | 158 | 163 | 168 | 173 | 178 | 183 | 188 | 193 | 198 | 203 | 208 | 213 | 218 | 223 | 228 | 233 | 237 | 241 | 245 | 249 | 253 | 257 | 262 | 263 | -------------------------------------------------------------------------------- /freecad/frametools/icons/screw.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 38 | 40 | 42 | 43 | 45 | image/svg+xml 46 | 48 | 49 | 50 | 51 | 52 | 56 | 60 | 64 | 67 | 71 | 75 | 77 | 81 | 85 | 86 | 90 | 94 | 98 | 102 | 106 | 110 | 114 | 118 | 122 | 126 | 130 | 134 | 138 | 142 | 146 | 150 | 154 | 158 | 162 | 166 | 170 | 174 | 178 | 182 | 186 | 190 | 193 | 197 | 201 | 202 | 203 | 204 | -------------------------------------------------------------------------------- /freecad/frametools/icons/beam_shape_cut.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 18 | 38 | 40 | 46 | 51 | 52 | 58 | 63 | 64 | 70 | 75 | 76 | 82 | 85 | 89 | 93 | 97 | 101 | 105 | 109 | 110 | 111 | 117 | 122 | 123 | 129 | 134 | 135 | 136 | 138 | 139 | 141 | image/svg+xml 142 | 144 | 145 | 146 | 147 | 148 | 153 | 158 | 163 | 168 | 173 | 178 | 183 | 188 | 193 | 198 | 203 | 208 | 213 | 218 | 223 | 228 | 233 | 238 | 243 | 248 | 253 | 258 | 263 | 268 | 269 | -------------------------------------------------------------------------------- /freecad/frametools/icons/generic_solver.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 25 | 29 | 33 | 37 | 41 | 44 | 48 | 52 | 53 | 60 | 65 | 66 | 73 | 78 | 79 | 86 | 91 | 92 | 99 | 104 | 105 | 112 | 117 | 118 | 125 | 130 | 131 | 138 | 143 | 144 | 151 | 156 | 157 | 164 | 169 | 170 | 177 | 180 | 184 | 188 | 192 | 196 | 200 | 204 | 205 | 206 | 213 | 218 | 219 | 226 | 231 | 232 | 239 | 245 | 246 | 253 | 259 | 260 | 269 | 270 | 294 | 299 | 304 | 309 | 314 | 319 | 324 | 325 | 327 | 328 | 330 | image/svg+xml 331 | 333 | 334 | 335 | 336 | 337 | 341 | 347 | 354 | 361 | 368 | 375 | 382 | 389 | 395 | 396 | 397 | -------------------------------------------------------------------------------- /freecad/frametools/interaction.py: -------------------------------------------------------------------------------- 1 | from __future__ import division 2 | import FreeCAD as App 3 | import FreeCADGui as Gui 4 | import numpy as np 5 | import os 6 | import Part 7 | from PySide import QtGui 8 | 9 | from . import beamobj 10 | 11 | __all__ = [ 12 | "make_beam", 13 | "make_miter_cut", 14 | "make_plane_cut", 15 | "make_shape_cut"] 16 | 17 | 18 | def refresh(): 19 | reload(beamobj) 20 | 21 | 22 | class make_beam(object): 23 | 24 | def __init__(self, view): 25 | self.profile = None 26 | self.path = None 27 | self.view = view 28 | App.Console.PrintMessage("choose the profile\n") 29 | Gui.Selection.clearSelection() 30 | 31 | self.observer = Gui.Selection.addObserver(self) 32 | 33 | self.form = Gui.PySideUic.loadUi(os.path.join(os.path.dirname(__file__), "make_beam.ui")) 34 | self.form.buttonProfile.setChecked(True) 35 | self.form.buttonProfile.clicked.connect(self.selectProfile) 36 | self.form.buttonPaths.clicked.connect(self.selectPaths) 37 | self.form.buttonCreate.clicked.connect(self.createBeam) 38 | self.form.buttonCreate.setEnabled(False) 39 | 40 | Gui.Control.showDialog(self) 41 | 42 | def selectProfile(self): 43 | if self.form.buttonProfile.isChecked(): 44 | self.form.buttonPaths.setChecked(False) 45 | Gui.Selection.clearSelection() 46 | 47 | def selectPaths(self): 48 | if self.form.buttonPaths.isChecked(): 49 | self.form.buttonProfile.setChecked(False) 50 | Gui.Selection.clearSelection() 51 | 52 | def addSelection(self, document, object, subobject, pos): 53 | if self.form.buttonProfile.isChecked(): 54 | self.profile = App.getDocument(document).getObject(object) 55 | self.form.lineProfile.setText(self.profile.Label) 56 | self.form.buttonProfile.setChecked(False) 57 | self.form.buttonPaths.setChecked(True) 58 | Gui.Selection.clearSelection() 59 | App.Console.PrintMessage("choose_path\n") 60 | else: 61 | if subobject != "": 62 | Gui.Selection.clearSelection() 63 | self.path = (document, object, subobject) 64 | 65 | (document, object, subobject) = self.path 66 | obj = App.getDocument(document).getObject(object) 67 | 68 | if obj != None: 69 | self.form.path.setText("{}.{}".format(obj.Label, subobject)) 70 | 71 | self.form.buttonCreate.setEnabled((self.profile != None) and (self.path != None)) 72 | 73 | def createBeam(self): 74 | (document, object, subobject) = self.path 75 | path_sketch = App.getDocument(document).getObject(object) 76 | 77 | if object != None: 78 | a = App.ActiveDocument.addObject("Part::FeaturePython", "beam") 79 | beamobj.Beam(a, self.profile, path_sketch, subobject) 80 | beamobj.ViewProviderBeam(a.ViewObject) 81 | a.exdent_1 = self.form.extent1.property("value") 82 | a.exdent_2 = self.form.extent2.property("value") 83 | a.Rotation = self.form.rotation.property("value") 84 | 85 | App.ActiveDocument.recompute() 86 | 87 | def getStandardButtons(self): 88 | return int(QtGui.QDialogButtonBox.Close) 89 | 90 | def reject(self): 91 | Gui.Selection.removeObserver(self) 92 | Gui.Control.closeDialog() 93 | 94 | 95 | class make_miter_cut(object): 96 | 97 | def __init__(self, view): 98 | self.view = view 99 | self.beam_1 = None 100 | self.beam_2 = None 101 | 102 | Gui.Selection.addObserver(self) 103 | 104 | self.form = Gui.PySideUic.loadUi(os.path.join(os.path.dirname(__file__), "miter-cut.ui")) 105 | self.form.setProperty("windowTitle", "Miter Cut") 106 | self.form.buttonBeam1.setChecked(True) 107 | self.form.buttonBeam1.clicked.connect(self.selectBeam1) 108 | self.form.buttonBeam2.clicked.connect(self.selectBeam2) 109 | self.form.buttonCut.clicked.connect(self.createCut) 110 | self.form.buttonCut.setEnabled(False) 111 | 112 | items = Gui.Selection.getSelectionEx() 113 | if (len(items) > 0) and (items[0].Object.Proxy != None) \ 114 | and ((items[0].Object.Proxy.__class__.__name__ == 'Beam') or (items[0].Object.Proxy.__class__.__name__ == 'CBeam')): 115 | self.addSelection(items[0].DocumentName, items[0].ObjectName, None, None) 116 | if (len(items) > 1) and (items[1].Object.Proxy != None) \ 117 | and ((items[1].Object.Proxy.__class__.__name__ == 'Beam') or (items[1].Object.Proxy.__class__.__name__ == 'CBeam')): 118 | self.addSelection(items[1].DocumentName, items[1].ObjectName, None, None) 119 | 120 | Gui.Control.showDialog(self) 121 | Gui.Selection.clearSelection() 122 | 123 | def set_miter_cut_obj(self, obj, beam): 124 | if not obj.cut_type: 125 | obj.cut_obj = beam 126 | obj.cut_type = "miter" 127 | return 128 | return False 129 | 130 | def selectBeam1(self): 131 | if self.form.buttonBeam1.isChecked(): 132 | self.form.buttonBeam2.setChecked(False) 133 | Gui.Selection.clearSelection() 134 | 135 | def selectBeam2(self): 136 | if self.form.buttonBeam2.isChecked(): 137 | self.form.buttonBeam1.setChecked(False) 138 | Gui.Selection.clearSelection() 139 | 140 | def addSelection(self, document, object, subobject, pos): 141 | obj = App.getDocument(document).getObject(object) 142 | name = "{}.{}".format(document, obj.Label) 143 | 144 | if self.form.buttonBeam1.isChecked(): 145 | self.beam_1 = obj 146 | self.form.beam1.setText(name) 147 | self.form.buttonBeam2.setChecked(True) 148 | self.selectBeam2() 149 | elif self.form.buttonBeam2.isChecked(): 150 | self.beam_2 = obj 151 | self.form.beam2.setText(name) 152 | 153 | messages = "" 154 | if (self.beam_1 != None) and (self.beam_1.Proxy.__class__.__name__ != "Beam") and (self.beam_1.Proxy.__class__.__name__ != "CBeam"): 155 | messages = messages + "The object {}.{} is not of beam type\n".format(self.beam_1.Document.Name, self.beam_1.Label) 156 | if (self.beam_2 != None) and (self.beam_2.Proxy.__class__.__name__ != "Beam") and (self.beam_2.Proxy.__class__.__name__ != "CBeam"): 157 | messages = messages + "The object {}.{} is not of beam type\n".format(self.beam_2.Document.Name, self.beam_2.Label) 158 | 159 | self.form.validationMessage.setText(messages) 160 | self.form.buttonCut.setEnabled((self.beam_1 != None) and (self.beam_2 != None) and (self.beam_1 != self.beam_2) and (len(messages) == 0)) 161 | 162 | def createCut(self): 163 | a = App.ActiveDocument.addObject( 164 | "Part::FeaturePython", "miter_beam") 165 | beamobj.CBeam(a, self.beam_1) 166 | self.set_miter_cut_obj(a, self.beam_2) 167 | beamobj.ViewProviderCBeam(a.ViewObject) 168 | 169 | a = App.ActiveDocument.addObject( 170 | "Part::FeaturePython", "miter_beam") 171 | beamobj.CBeam(a, self.beam_2) 172 | self.set_miter_cut_obj(a, self.beam_1) 173 | beamobj.ViewProviderCBeam(a.ViewObject) 174 | 175 | self.form.buttonBeam1.setChecked(True) 176 | self.selectBeam1() 177 | App.ActiveDocument.recompute() 178 | 179 | def getStandardButtons(self): 180 | return int(QtGui.QDialogButtonBox.Close) 181 | 182 | def reject(self): 183 | Gui.Selection.removeObserver(self) 184 | Gui.Control.closeDialog() 185 | 186 | 187 | def to_np(app_vec): 188 | return np.array([app_vec.x, app_vec.y, app_vec.z]) 189 | 190 | 191 | class make_plane_cut(object): 192 | 193 | def __init__(self, view): 194 | self.view = view 195 | self.beam = None 196 | self.cut_beam = None 197 | self.cut_face = None 198 | Gui.Selection.addObserver(self) 199 | 200 | self.form = Gui.PySideUic.loadUi(os.path.join(os.path.dirname(__file__), "plane-cut.ui")) 201 | self.form.setProperty("windowTitle", "Plane Cut") 202 | self.form.buttonBeam.setChecked(True) 203 | self.form.buttonBeam.clicked.connect(self.selectBeam) 204 | self.form.buttonFace.clicked.connect(self.selectFace) 205 | self.form.buttonCut.clicked.connect(self.createCut) 206 | self.form.buttonCut.setEnabled(False) 207 | 208 | items = Gui.Selection.getSelectionEx() 209 | if (len(items) > 0) and (items[0].Object.Proxy != None) and ((items[0].Object.Proxy.__class__.__name__ == 'Beam') or (items[0].Object.Proxy.__class__.__name__ == 'CBeam')): 210 | self.addSelection(items[0].DocumentName, items[0].ObjectName, None, None) 211 | if (len(items) > 1) and (items[1].Object.Proxy != None) \ 212 | and ((items[1].Object.Proxy.__class__.__name__ == 'Beam') or (items[1].Object.Proxy.__class__.__name__ == 'CBeam')) \ 213 | and (len(items[1].SubElementNames) > 0): 214 | self.addSelection(items[1].DocumentName, items[1].ObjectName, items[1].SubElementNames[0], None) 215 | 216 | Gui.Control.showDialog(self) 217 | Gui.Selection.clearSelection() 218 | 219 | def set_cut_obj(self, obj, beam, face): 220 | if not obj.cut_type: 221 | obj.cut_obj = beam 222 | obj.cut_type = "cut" 223 | obj.cut_obj_name = face 224 | return 225 | return False 226 | 227 | def selectBeam(self): 228 | if self.form.buttonBeam.isChecked(): 229 | self.form.buttonFace.setChecked(False) 230 | Gui.Selection.clearSelection() 231 | 232 | def selectFace(self): 233 | if self.form.buttonFace.isChecked(): 234 | self.form.buttonBeam.setChecked(False) 235 | Gui.Selection.clearSelection() 236 | 237 | def addSelection(self, document, object, subobject, pos): 238 | obj = App.getDocument(document).getObject(object) 239 | 240 | if self.form.buttonBeam.isChecked(): 241 | self.beam = obj 242 | self.form.beam.setText("{}.{}".format(document, obj.Label)) 243 | self.form.buttonFace.setChecked(True) 244 | self.selectFace() 245 | elif self.form.buttonFace.isChecked() and (subobject != None): 246 | self.cut_beam = obj 247 | self.cut_face = subobject 248 | self.form.face.setText("{}.{}.{}".format(document, obj.Label, subobject)) 249 | 250 | messages = "" 251 | if (self.beam != None) and (self.beam.Proxy.__class__.__name__ != "Beam") and (self.beam.Proxy.__class__.__name__ != "CBeam"): 252 | messages = messages + "The object {}.{} is not of beam type\n".format(self.beam.Document.Name, self.beam.Label) 253 | if (self.cut_beam != None) and (self.cut_beam.Proxy.__class__.__name__ != "Beam") and (self.cut_beam.Proxy.__class__.__name__ != "CBeam"): 254 | messages = messages + "The object {}.{} is not of beam type\n".format(self.cut_beam.Document.Name, self.cut_beam.Name) 255 | 256 | self.form.validationMessage.setText(messages) 257 | self.form.buttonCut.setEnabled((self.beam != None) and (self.cut_beam != None) and (self.beam != self.cut_beam) and (len(messages) == 0)) 258 | 259 | def createCut(self): 260 | a = App.ActiveDocument.addObject( 261 | "Part::FeaturePython", "cut_beam") 262 | beamobj.CBeam(a, self.beam) 263 | self.set_cut_obj(a, self.cut_beam, self.cut_face) 264 | beamobj.ViewProviderCBeam(a.ViewObject) 265 | 266 | self.form.buttonBeam.setChecked(True) 267 | self.selectBeam() 268 | App.ActiveDocument.recompute() 269 | 270 | def getStandardButtons(self): 271 | return int(QtGui.QDialogButtonBox.Close) 272 | 273 | def reject(self): 274 | Gui.Selection.removeObserver(self) 275 | Gui.Control.closeDialog() 276 | 277 | 278 | class make_shape_cut(object): 279 | 280 | def __init__(self, view): 281 | self.view = view 282 | self.beam_1 = None 283 | self.beam_2 = None 284 | 285 | Gui.Selection.addObserver(self) 286 | 287 | self.form = Gui.PySideUic.loadUi(os.path.join(os.path.dirname(__file__), "shape-cut.ui")) 288 | self.form.setProperty("windowTitle", "Shape Cut") 289 | self.form.buttonBeam1.setChecked(True) 290 | self.form.buttonBeam1.clicked.connect(self.selectBeam1) 291 | self.form.buttonBeam2.clicked.connect(self.selectBeam2) 292 | self.form.buttonCut.clicked.connect(self.createCut) 293 | self.form.buttonCut.setEnabled(False) 294 | 295 | items = Gui.Selection.getSelectionEx() 296 | if (len(items) > 0) and (items[0].Object.Proxy != None) \ 297 | and ((items[0].Object.Proxy.__class__.__name__ == 'Beam') or (items[0].Object.Proxy.__class__.__name__ == 'CBeam')): 298 | self.addSelection(items[0].DocumentName, items[0].ObjectName, None, None) 299 | if (len(items) > 1) and (items[1].Object.Proxy != None) \ 300 | and ((items[1].Object.Proxy.__class__.__name__ == 'Beam') or (items[1].Object.Proxy.__class__.__name__ == 'CBeam')): 301 | self.addSelection(items[1].DocumentName, items[1].ObjectName, None, None) 302 | 303 | Gui.Control.showDialog(self) 304 | Gui.Selection.clearSelection() 305 | 306 | def set_cut_obj(self, obj, beam): 307 | if not obj.cut_type: 308 | obj.cut_obj = beam 309 | obj.cut_type = "shape_cut" 310 | return 311 | return False 312 | 313 | def selectBeam1(self): 314 | if self.form.buttonBeam1.isChecked(): 315 | self.form.buttonBeam2.setChecked(False) 316 | Gui.Selection.clearSelection() 317 | 318 | def selectBeam2(self): 319 | if self.form.buttonBeam2.isChecked(): 320 | self.form.buttonBeam1.setChecked(False) 321 | Gui.Selection.clearSelection() 322 | 323 | def addSelection(self, document, object, subobject, pos): 324 | obj = App.getDocument(document).getObject(object) 325 | name = "{}.{}".format(document, obj.Label) 326 | 327 | if self.form.buttonBeam1.isChecked(): 328 | self.beam_1 = obj 329 | self.form.beam1.setText(name) 330 | self.form.buttonBeam2.setChecked(True) 331 | self.selectBeam2() 332 | elif self.form.buttonBeam2.isChecked(): 333 | self.beam_2 = obj 334 | self.form.beam2.setText(name) 335 | 336 | messages = "" 337 | if (self.beam_1 != None) and (self.beam_1.Proxy.__class__.__name__ != "Beam") and (self.beam_1.Proxy.__class__.__name__ != "CBeam"): 338 | messages = messages + "The object {}.{} is not of beam type\n".format(self.beam_1.Document.Name, self.beam_1.Label) 339 | if (self.beam_2 != None) and (self.beam_2.Proxy.__class__.__name__ != "Beam") and (self.beam_2.Proxy.__class__.__name__ != "CBeam"): 340 | messages = messages + "The object {}.{} is not of beam type\n".format(self.beam_2.Document.Name, self.beam_2.Label) 341 | 342 | self.form.validationMessage.setText(messages) 343 | self.form.buttonCut.setEnabled((self.beam_1 != None) and (self.beam_2 != None) and (self.beam_1 != self.beam_2) and (len(messages) == 0)) 344 | 345 | def createCut(self): 346 | a = App.ActiveDocument.addObject( 347 | "Part::FeaturePython", "shape_cut_beam") 348 | beamobj.CBeam(a, self.beam_1) 349 | self.set_cut_obj(a, self.beam_2) 350 | beamobj.ViewProviderCBeam(a.ViewObject) 351 | 352 | Gui.Selection.clearSelection() 353 | self.form.buttonBeam1.setChecked(True) 354 | self.selectBeam1() 355 | App.ActiveDocument.recompute() 356 | 357 | def getStandardButtons(self): 358 | return int(QtGui.QDialogButtonBox.Close) 359 | 360 | def reject(self): 361 | Gui.Selection.removeObserver(self) 362 | Gui.Control.closeDialog() 363 | --------------------------------------------------------------------------------